Some checks failed
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Solidity Contracts (push) Has been cancelled
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
Validation / validate-genesis (push) Has started running
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
Extends AaveQuotePushFlashReceiver with before/after swap collateral steps, env-driven run scripts, forkproof parity, and scoped forge tests for supply/toggle callback ordering. Co-authored-by: Cursor <cursoragent@cursor.com>
376 lines
14 KiB
Solidity
376 lines
14 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
|
|
interface IAavePoolLike {
|
|
/// @notice Standard V3 flash loan (single-asset via length-1 arrays). Use this on Aave V3.2+ pools
|
|
/// where `flashLoanSimple` may revert with `NotActivated()`.
|
|
function flashLoan(
|
|
address receiverAddress,
|
|
address[] calldata assets,
|
|
uint256[] calldata amounts,
|
|
uint256[] calldata interestRateModes,
|
|
address onBehalfOf,
|
|
bytes calldata params,
|
|
uint16 referralCode
|
|
) external;
|
|
|
|
/// @dev Retained for compatibility with older pool deployments; prefer `flashLoan` for new integrations.
|
|
function flashLoanSimple(
|
|
address receiverAddress,
|
|
address asset,
|
|
uint256 amount,
|
|
bytes calldata params,
|
|
uint16 referralCode
|
|
) external;
|
|
|
|
function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
|
|
|
|
function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external;
|
|
}
|
|
|
|
/// @dev Callback for `flashLoan` (multi-asset API).
|
|
interface IAaveFlashLoanReceiver {
|
|
function executeOperation(
|
|
address[] calldata assets,
|
|
uint256[] calldata amounts,
|
|
uint256[] calldata premiums,
|
|
address initiator,
|
|
bytes calldata params
|
|
) external returns (bool);
|
|
}
|
|
|
|
interface IAaveFlashLoanSimpleReceiver {
|
|
function executeOperation(
|
|
address asset,
|
|
uint256 amount,
|
|
uint256 premium,
|
|
address initiator,
|
|
bytes calldata params
|
|
) external returns (bool);
|
|
}
|
|
|
|
interface IAaveDODOQuotePushSwapExactIn {
|
|
function swapExactIn(address pool, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
|
external
|
|
returns (uint256 amountOut);
|
|
}
|
|
|
|
interface IAaveExternalUnwinder {
|
|
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata data)
|
|
external
|
|
returns (uint256 amountOut);
|
|
}
|
|
|
|
interface IAaveAtomicBridgeCoordinator {
|
|
struct CreateIntentParams {
|
|
uint64 sourceChain;
|
|
uint64 destinationChain;
|
|
address assetIn;
|
|
address assetOut;
|
|
uint256 amountIn;
|
|
uint256 minAmountOut;
|
|
address recipient;
|
|
uint256 deadline;
|
|
bytes32 routeId;
|
|
}
|
|
|
|
function createIntent(CreateIntentParams calldata p) external returns (bytes32 obligationId);
|
|
|
|
function submitCommitment(bytes32 obligationId, bytes32 settlementMode) external;
|
|
|
|
function obligationEscrow() external view returns (address);
|
|
}
|
|
|
|
/**
|
|
* @title AaveQuotePushFlashReceiver
|
|
* @notice Aave V3 flash-loan receiver for the quote-push workflow:
|
|
* flash borrow quote (`flashLoan` single-asset) -> optional Aave collateral supply/toggle hooks ->
|
|
* buy PMM base -> optional atomic bridge -> unwind base externally -> repay lender, retaining any surplus.
|
|
* @dev Collateral hooks apply to **this contract's** Aave position (`onBehalfOf` = address(this)). Pre-fund the
|
|
* receiver with collateral tokens before `supplyBeforeSwap` steps, or supply from flash proceeds in later txs.
|
|
*/
|
|
contract AaveQuotePushFlashReceiver is IAaveFlashLoanSimpleReceiver, IAaveFlashLoanReceiver, Ownable {
|
|
using SafeERC20 for IERC20;
|
|
|
|
address public immutable pool;
|
|
|
|
enum CollateralHook {
|
|
BeforeSwap,
|
|
AfterSwap,
|
|
BeforeUnwind
|
|
}
|
|
|
|
struct CollateralSupplyStep {
|
|
address asset;
|
|
uint256 amount;
|
|
}
|
|
|
|
struct CollateralToggleStep {
|
|
address asset;
|
|
bool useAsCollateral;
|
|
}
|
|
|
|
struct CollateralParams {
|
|
CollateralSupplyStep[] supplyBeforeSwap;
|
|
CollateralToggleStep[] toggleBeforeSwap;
|
|
CollateralToggleStep[] toggleAfterSwap;
|
|
CollateralToggleStep[] toggleBeforeUnwind;
|
|
}
|
|
|
|
struct QuotePushParams {
|
|
address integration;
|
|
address pmmPool;
|
|
address baseToken;
|
|
address externalUnwinder;
|
|
uint256 minOutPmm;
|
|
uint256 minOutUnwind;
|
|
bytes unwindData;
|
|
AtomicBridgeParams atomicBridge;
|
|
CollateralParams collateral;
|
|
}
|
|
|
|
struct AtomicBridgeParams {
|
|
address coordinator;
|
|
uint64 sourceChain;
|
|
uint64 destinationChain;
|
|
address destinationAsset;
|
|
uint256 bridgeAmount;
|
|
uint256 minDestinationAmount;
|
|
address destinationRecipient;
|
|
uint256 destinationDeadline;
|
|
bytes32 routeId;
|
|
bytes32 settlementMode;
|
|
bool submitCommitment;
|
|
}
|
|
|
|
error UntrustedPool();
|
|
error UntrustedInitiator();
|
|
error BadParams();
|
|
error InsufficientToRepay();
|
|
error InvalidAtomicBridge();
|
|
error NothingToSweep();
|
|
|
|
event QuotePushExecuted(
|
|
address indexed quoteToken,
|
|
address indexed baseToken,
|
|
uint256 borrowedAmount,
|
|
uint256 premium,
|
|
uint256 baseOut,
|
|
uint256 unwindOut,
|
|
uint256 surplus
|
|
);
|
|
event AtomicBridgeTriggered(
|
|
bytes32 indexed obligationId,
|
|
address indexed coordinator,
|
|
address indexed destinationRecipient,
|
|
uint256 bridgeAmount,
|
|
uint256 minDestinationAmount
|
|
);
|
|
event TokenSwept(address indexed token, address indexed to, uint256 amount);
|
|
event SurplusSwept(address indexed token, address indexed to, uint256 amount, uint256 reserveRetained);
|
|
event CollateralSupplied(address indexed asset, uint256 amount);
|
|
event CollateralToggled(address indexed asset, bool useAsCollateral, CollateralHook hook);
|
|
|
|
/// @notice Empty `collateral` block for quote-push scripts that do not use Aave position hooks.
|
|
function emptyCollateralParams() external pure returns (CollateralParams memory params) {
|
|
return params;
|
|
}
|
|
|
|
/// @notice Non-zero when this deployment supports `QuotePushParams.collateral` atomic hooks.
|
|
function collateralHooksVersion() external pure returns (uint256) {
|
|
return 1;
|
|
}
|
|
|
|
constructor(address pool_, address initialOwner) Ownable(initialOwner) {
|
|
if (pool_ == address(0) || initialOwner == address(0)) revert BadParams();
|
|
pool = pool_;
|
|
}
|
|
|
|
function flashQuotePush(address asset, uint256 amount, QuotePushParams calldata params) external {
|
|
address[] memory assets = new address[](1);
|
|
uint256[] memory amts = new uint256[](1);
|
|
uint256[] memory modes = new uint256[](1);
|
|
assets[0] = asset;
|
|
amts[0] = amount;
|
|
modes[0] = 0;
|
|
// `onBehalfOf` must not be zero on some Aave V3.2+ deployments (mapping lookups revert NotActivated).
|
|
IAavePoolLike(pool).flashLoan(
|
|
address(this), assets, amts, modes, address(this), abi.encode(address(this), params), 0
|
|
);
|
|
}
|
|
|
|
function quoteSurplusBalance(address quoteToken, uint256 reserveRetained) public view returns (uint256 surplus) {
|
|
uint256 quoteBal = IERC20(quoteToken).balanceOf(address(this));
|
|
if (quoteBal > reserveRetained) {
|
|
surplus = quoteBal - reserveRetained;
|
|
}
|
|
}
|
|
|
|
function sweepQuoteSurplus(address quoteToken, address to, uint256 reserveRetained)
|
|
external
|
|
onlyOwner
|
|
returns (uint256 amount)
|
|
{
|
|
if (quoteToken == address(0) || to == address(0)) revert BadParams();
|
|
amount = quoteSurplusBalance(quoteToken, reserveRetained);
|
|
if (amount == 0) revert NothingToSweep();
|
|
IERC20(quoteToken).safeTransfer(to, amount);
|
|
emit SurplusSwept(quoteToken, to, amount, reserveRetained);
|
|
}
|
|
|
|
function sweepToken(address token, address to, uint256 amount) external onlyOwner {
|
|
if (token == address(0) || to == address(0) || amount == 0) revert BadParams();
|
|
IERC20(token).safeTransfer(to, amount);
|
|
emit TokenSwept(token, to, amount);
|
|
}
|
|
|
|
function executeOperation(
|
|
address[] calldata assets,
|
|
uint256[] calldata amounts,
|
|
uint256[] calldata premiums,
|
|
address initiator,
|
|
bytes calldata params
|
|
) external returns (bool) {
|
|
if (msg.sender != pool) revert UntrustedPool();
|
|
if (assets.length != 1 || amounts.length != 1 || premiums.length != 1) revert BadParams();
|
|
_executeQuotePush(assets[0], amounts[0], premiums[0], initiator, params);
|
|
return true;
|
|
}
|
|
|
|
function executeOperation(
|
|
address asset,
|
|
uint256 amount,
|
|
uint256 premium,
|
|
address initiator,
|
|
bytes calldata params
|
|
) external returns (bool) {
|
|
if (msg.sender != pool) revert UntrustedPool();
|
|
_executeQuotePush(asset, amount, premium, initiator, params);
|
|
return true;
|
|
}
|
|
|
|
function _executeQuotePush(
|
|
address asset,
|
|
uint256 amount,
|
|
uint256 premium,
|
|
address initiator,
|
|
bytes calldata params
|
|
) internal {
|
|
(address expectedInitiator, QuotePushParams memory p) = abi.decode(params, (address, QuotePushParams));
|
|
if (initiator != expectedInitiator) revert UntrustedInitiator();
|
|
if (
|
|
p.integration == address(0) || p.pmmPool == address(0) || p.baseToken == address(0)
|
|
|| p.externalUnwinder == address(0)
|
|
) revert BadParams();
|
|
if (p.baseToken == asset) revert BadParams();
|
|
|
|
_applyCollateralSupplies(p.collateral.supplyBeforeSwap);
|
|
_applyCollateralToggles(p.collateral.toggleBeforeSwap, CollateralHook.BeforeSwap);
|
|
|
|
uint256 baseOut = _swapQuoteForBase(asset, amount, p.integration, p.pmmPool, p.minOutPmm);
|
|
|
|
_applyCollateralToggles(p.collateral.toggleAfterSwap, CollateralHook.AfterSwap);
|
|
|
|
uint256 baseBal = IERC20(p.baseToken).balanceOf(address(this));
|
|
if (p.atomicBridge.coordinator != address(0)) {
|
|
_triggerAtomicBridge(p.baseToken, baseBal, p.atomicBridge);
|
|
}
|
|
|
|
_applyCollateralToggles(p.collateral.toggleBeforeUnwind, CollateralHook.BeforeUnwind);
|
|
|
|
uint256 unwindOut = _unwindBaseIntoQuote(p.baseToken, asset, p.externalUnwinder, p.minOutUnwind, p.unwindData);
|
|
uint256 surplus = _approveRepayment(asset, amount + premium);
|
|
emit QuotePushExecuted(asset, p.baseToken, amount, premium, baseOut, unwindOut, surplus);
|
|
}
|
|
|
|
function _swapQuoteForBase(address asset, uint256 amount, address integration, address pmmPool, uint256 minOutPmm)
|
|
internal
|
|
returns (uint256 baseOut)
|
|
{
|
|
IERC20(asset).forceApprove(integration, amount);
|
|
baseOut = IAaveDODOQuotePushSwapExactIn(integration).swapExactIn(pmmPool, asset, amount, minOutPmm);
|
|
}
|
|
|
|
function _unwindBaseIntoQuote(
|
|
address baseToken,
|
|
address quoteToken,
|
|
address externalUnwinder,
|
|
uint256 minOutUnwind,
|
|
bytes memory unwindData
|
|
) internal returns (uint256 unwindOut) {
|
|
uint256 remainingBase = IERC20(baseToken).balanceOf(address(this));
|
|
IERC20(baseToken).forceApprove(externalUnwinder, remainingBase);
|
|
unwindOut =
|
|
IAaveExternalUnwinder(externalUnwinder).unwind(baseToken, quoteToken, remainingBase, minOutUnwind, unwindData);
|
|
}
|
|
|
|
function _applyCollateralSupplies(CollateralSupplyStep[] memory steps) internal {
|
|
uint256 n = steps.length;
|
|
for (uint256 i = 0; i < n; ++i) {
|
|
CollateralSupplyStep memory step = steps[i];
|
|
if (step.asset == address(0) || step.amount == 0) continue;
|
|
IERC20(step.asset).forceApprove(pool, step.amount);
|
|
IAavePoolLike(pool).supply(step.asset, step.amount, address(this), 0);
|
|
emit CollateralSupplied(step.asset, step.amount);
|
|
}
|
|
}
|
|
|
|
function _applyCollateralToggles(CollateralToggleStep[] memory steps, CollateralHook hook) internal {
|
|
uint256 n = steps.length;
|
|
for (uint256 i = 0; i < n; ++i) {
|
|
CollateralToggleStep memory step = steps[i];
|
|
if (step.asset == address(0)) continue;
|
|
IAavePoolLike(pool).setUserUseReserveAsCollateral(step.asset, step.useAsCollateral);
|
|
emit CollateralToggled(step.asset, step.useAsCollateral, hook);
|
|
}
|
|
}
|
|
|
|
function _approveRepayment(address quoteToken, uint256 need) internal returns (uint256 surplus) {
|
|
IERC20 quote = IERC20(quoteToken);
|
|
uint256 quoteBal = quote.balanceOf(address(this));
|
|
if (quoteBal < need) revert InsufficientToRepay();
|
|
surplus = quoteBal - need;
|
|
quote.forceApprove(pool, need);
|
|
}
|
|
|
|
function _triggerAtomicBridge(address baseToken, uint256 baseBal, AtomicBridgeParams memory atomicBridge) internal {
|
|
if (
|
|
atomicBridge.destinationAsset == address(0) || atomicBridge.destinationRecipient == address(0)
|
|
|| atomicBridge.bridgeAmount == 0 || atomicBridge.bridgeAmount > baseBal
|
|
|| atomicBridge.destinationDeadline <= block.timestamp
|
|
) revert InvalidAtomicBridge();
|
|
|
|
address escrowAddress = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).obligationEscrow();
|
|
IERC20(baseToken).forceApprove(escrowAddress, atomicBridge.bridgeAmount);
|
|
bytes32 obligationId = IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).createIntent(
|
|
IAaveAtomicBridgeCoordinator.CreateIntentParams({
|
|
sourceChain: atomicBridge.sourceChain,
|
|
destinationChain: atomicBridge.destinationChain,
|
|
assetIn: baseToken,
|
|
assetOut: atomicBridge.destinationAsset,
|
|
amountIn: atomicBridge.bridgeAmount,
|
|
minAmountOut: atomicBridge.minDestinationAmount,
|
|
recipient: atomicBridge.destinationRecipient,
|
|
deadline: atomicBridge.destinationDeadline,
|
|
routeId: atomicBridge.routeId
|
|
})
|
|
);
|
|
if (atomicBridge.submitCommitment) {
|
|
IAaveAtomicBridgeCoordinator(atomicBridge.coordinator).submitCommitment(
|
|
obligationId, atomicBridge.settlementMode
|
|
);
|
|
}
|
|
emit AtomicBridgeTriggered(
|
|
obligationId,
|
|
atomicBridge.coordinator,
|
|
atomicBridge.destinationRecipient,
|
|
atomicBridge.bridgeAmount,
|
|
atomicBridge.minDestinationAmount
|
|
);
|
|
}
|
|
}
|