// 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"); } }