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>
248 lines
9.0 KiB
Solidity
248 lines
9.0 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import {
|
|
AaveQuotePushFlashReceiver,
|
|
IAaveDODOQuotePushSwapExactIn,
|
|
IAaveExternalUnwinder,
|
|
IAavePoolLike
|
|
} from "../../contracts/flash/AaveQuotePushFlashReceiver.sol";
|
|
|
|
contract MockQuoteToken is ERC20 {
|
|
constructor() ERC20("Mock Quote", "MQ") {}
|
|
|
|
function mint(address to, uint256 amount) external {
|
|
_mint(to, amount);
|
|
}
|
|
}
|
|
|
|
contract MockBaseToken is ERC20 {
|
|
constructor() ERC20("Mock Base", "MB") {}
|
|
|
|
function mint(address to, uint256 amount) external {
|
|
_mint(to, amount);
|
|
}
|
|
}
|
|
|
|
contract MockPmmIntegration is IAaveDODOQuotePushSwapExactIn {
|
|
MockQuoteToken internal immutable quote;
|
|
MockBaseToken internal immutable base;
|
|
|
|
constructor(MockQuoteToken quote_, MockBaseToken base_) {
|
|
quote = quote_;
|
|
base = base_;
|
|
}
|
|
|
|
function swapExactIn(address, address tokenIn, uint256 amountIn, uint256 minAmountOut)
|
|
external
|
|
override
|
|
returns (uint256 amountOut)
|
|
{
|
|
require(tokenIn == address(quote), "quote only");
|
|
quote.transferFrom(msg.sender, address(this), amountIn);
|
|
amountOut = amountIn;
|
|
require(amountOut >= minAmountOut, "minOut");
|
|
base.mint(msg.sender, amountOut);
|
|
}
|
|
}
|
|
|
|
contract MockUnwinder is IAaveExternalUnwinder {
|
|
MockQuoteToken internal immutable quote;
|
|
MockBaseToken internal immutable base;
|
|
|
|
constructor(MockQuoteToken quote_, MockBaseToken base_) {
|
|
quote = quote_;
|
|
base = base_;
|
|
}
|
|
|
|
function unwind(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes calldata)
|
|
external
|
|
override
|
|
returns (uint256 amountOut)
|
|
{
|
|
require(tokenIn == address(base), "base only");
|
|
require(tokenOut == address(quote), "quote only");
|
|
base.transferFrom(msg.sender, address(this), amountIn);
|
|
amountOut = amountIn;
|
|
require(amountOut >= minAmountOut, "min unwind");
|
|
quote.mint(msg.sender, amountOut);
|
|
}
|
|
}
|
|
|
|
contract MockAavePool is IAavePoolLike {
|
|
struct ToggleCall {
|
|
address asset;
|
|
bool useAsCollateral;
|
|
}
|
|
|
|
uint256 public premiumBps = 5;
|
|
uint256 public toggleCallCount;
|
|
ToggleCall[] internal _toggleCalls;
|
|
uint256 public supplyCount;
|
|
|
|
function flashLoan(
|
|
address receiverAddress,
|
|
address[] calldata assets,
|
|
uint256[] calldata amounts,
|
|
uint256[] calldata,
|
|
address,
|
|
bytes calldata params,
|
|
uint16
|
|
) external override {
|
|
uint256 premium = amounts[0] * premiumBps / 10_000;
|
|
uint256[] memory premiums = new uint256[](1);
|
|
premiums[0] = premium;
|
|
IERC20(assets[0]).transfer(receiverAddress, amounts[0]);
|
|
bool ok = IAaveFlashLoanReceiver(receiverAddress).executeOperation(
|
|
assets, amounts, premiums, receiverAddress, params
|
|
);
|
|
require(ok, "callback failed");
|
|
IERC20(assets[0]).transferFrom(receiverAddress, address(this), amounts[0] + premium);
|
|
}
|
|
|
|
function flashLoanSimple(address, address, uint256, bytes calldata, uint16) external pure override {
|
|
revert("use flashLoan");
|
|
}
|
|
|
|
function supply(address asset, uint256 amount, address onBehalfOf, uint16) external override {
|
|
IERC20(asset).transferFrom(msg.sender, address(this), amount);
|
|
supplyCount++;
|
|
onBehalfOf;
|
|
}
|
|
|
|
function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external override {
|
|
_toggleCalls.push(ToggleCall({asset: asset, useAsCollateral: useAsCollateral}));
|
|
toggleCallCount++;
|
|
}
|
|
|
|
function toggleCallAt(uint256 index) external view returns (address asset, bool useAsCollateral) {
|
|
ToggleCall memory t = _toggleCalls[index];
|
|
return (t.asset, t.useAsCollateral);
|
|
}
|
|
}
|
|
|
|
interface IAaveFlashLoanReceiver {
|
|
function executeOperation(
|
|
address[] calldata assets,
|
|
uint256[] calldata amounts,
|
|
uint256[] calldata premiums,
|
|
address initiator,
|
|
bytes calldata params
|
|
) external returns (bool);
|
|
}
|
|
|
|
contract AaveQuotePushCollateralHooksTest is Test {
|
|
MockQuoteToken internal quote;
|
|
MockBaseToken internal base;
|
|
MockPmmIntegration internal integration;
|
|
MockUnwinder internal unwinder;
|
|
MockAavePool internal pool;
|
|
AaveQuotePushFlashReceiver internal receiver;
|
|
|
|
address internal constant WETH = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
|
|
|
function setUp() public {
|
|
quote = new MockQuoteToken();
|
|
base = new MockBaseToken();
|
|
integration = new MockPmmIntegration(quote, base);
|
|
unwinder = new MockUnwinder(quote, base);
|
|
pool = new MockAavePool();
|
|
receiver = new AaveQuotePushFlashReceiver(address(pool), address(this));
|
|
quote.mint(address(pool), 10_000_000);
|
|
quote.mint(address(receiver), 1_000_000);
|
|
}
|
|
|
|
function testCollateralToggleRunsInFlashCallbackBeforeAndAfterSwap() public {
|
|
AaveQuotePushFlashReceiver.CollateralToggleStep[] memory beforeSwap =
|
|
new AaveQuotePushFlashReceiver.CollateralToggleStep[](1);
|
|
beforeSwap[0] = AaveQuotePushFlashReceiver.CollateralToggleStep({asset: WETH, useAsCollateral: false});
|
|
|
|
AaveQuotePushFlashReceiver.CollateralToggleStep[] memory afterSwap =
|
|
new AaveQuotePushFlashReceiver.CollateralToggleStep[](1);
|
|
afterSwap[0] = AaveQuotePushFlashReceiver.CollateralToggleStep({asset: WETH, useAsCollateral: true});
|
|
|
|
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
|
integration: address(integration),
|
|
pmmPool: address(0xBEEF),
|
|
baseToken: address(base),
|
|
externalUnwinder: address(unwinder),
|
|
minOutPmm: 1,
|
|
minOutUnwind: 1,
|
|
unwindData: "",
|
|
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
|
coordinator: address(0),
|
|
sourceChain: 0,
|
|
destinationChain: 0,
|
|
destinationAsset: address(0),
|
|
bridgeAmount: 0,
|
|
minDestinationAmount: 0,
|
|
destinationRecipient: address(0),
|
|
destinationDeadline: 0,
|
|
routeId: bytes32(0),
|
|
settlementMode: bytes32(0),
|
|
submitCommitment: false
|
|
}),
|
|
collateral: AaveQuotePushFlashReceiver.CollateralParams({
|
|
supplyBeforeSwap: new AaveQuotePushFlashReceiver.CollateralSupplyStep[](0),
|
|
toggleBeforeSwap: beforeSwap,
|
|
toggleAfterSwap: afterSwap,
|
|
toggleBeforeUnwind: new AaveQuotePushFlashReceiver.CollateralToggleStep[](0)
|
|
})
|
|
});
|
|
|
|
uint256 borrow = 1_000_000;
|
|
receiver.flashQuotePush(address(quote), borrow, p);
|
|
|
|
assertEq(pool.toggleCallCount(), 2, "two toggles");
|
|
(address asset0, bool use0) = pool.toggleCallAt(0);
|
|
(address asset1, bool use1) = pool.toggleCallAt(1);
|
|
assertEq(asset0, WETH);
|
|
assertFalse(use0);
|
|
assertEq(asset1, WETH);
|
|
assertTrue(use1);
|
|
}
|
|
|
|
function testCollateralSupplyBeforeSwapUsesReceiverBalance() public {
|
|
quote.mint(address(receiver), 500_000);
|
|
|
|
AaveQuotePushFlashReceiver.CollateralSupplyStep[] memory supplies =
|
|
new AaveQuotePushFlashReceiver.CollateralSupplyStep[](1);
|
|
supplies[0] = AaveQuotePushFlashReceiver.CollateralSupplyStep({asset: address(quote), amount: 100_000});
|
|
|
|
AaveQuotePushFlashReceiver.QuotePushParams memory p = AaveQuotePushFlashReceiver.QuotePushParams({
|
|
integration: address(integration),
|
|
pmmPool: address(0xBEEF),
|
|
baseToken: address(base),
|
|
externalUnwinder: address(unwinder),
|
|
minOutPmm: 1,
|
|
minOutUnwind: 1,
|
|
unwindData: "",
|
|
atomicBridge: AaveQuotePushFlashReceiver.AtomicBridgeParams({
|
|
coordinator: address(0),
|
|
sourceChain: 0,
|
|
destinationChain: 0,
|
|
destinationAsset: address(0),
|
|
bridgeAmount: 0,
|
|
minDestinationAmount: 0,
|
|
destinationRecipient: address(0),
|
|
destinationDeadline: 0,
|
|
routeId: bytes32(0),
|
|
settlementMode: bytes32(0),
|
|
submitCommitment: false
|
|
}),
|
|
collateral: AaveQuotePushFlashReceiver.CollateralParams({
|
|
supplyBeforeSwap: supplies,
|
|
toggleBeforeSwap: new AaveQuotePushFlashReceiver.CollateralToggleStep[](0),
|
|
toggleAfterSwap: new AaveQuotePushFlashReceiver.CollateralToggleStep[](0),
|
|
toggleBeforeUnwind: new AaveQuotePushFlashReceiver.CollateralToggleStep[](0)
|
|
})
|
|
});
|
|
|
|
receiver.flashQuotePush(address(quote), 1_000_000, p);
|
|
assertEq(pool.supplyCount(), 1, "supply called once");
|
|
}
|
|
}
|