From 90fcd86474fc0445b92e3a6b5349eee9b5c90a78 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Fri, 20 Jun 2025 15:06:41 +0200 Subject: [PATCH 1/7] add aave lending deposit and withdrawal with delegation --- test/helpers/AaveLending.t.sol | 283 ++++++++++ test/helpers/interfaces/IAavePool.sol | 722 ++++++++++++++++++++++++++ 2 files changed, 1005 insertions(+) create mode 100644 test/helpers/AaveLending.t.sol create mode 100644 test/helpers/interfaces/IAavePool.sol diff --git a/test/helpers/AaveLending.t.sol b/test/helpers/AaveLending.t.sol new file mode 100644 index 00000000..48d3f1aa --- /dev/null +++ b/test/helpers/AaveLending.t.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Test } from "forge-std/Test.sol"; +import { console2 } from "forge-std/console2.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { BaseTest } from "../utils/BaseTest.t.sol"; +import { Implementation, SignatureType } from "../utils/Types.t.sol"; +import { Execution, Delegation, Caveat, ModeCode } from "../../src/utils/Types.sol"; +import { AllowedTargetsEnforcer } from "../../src/enforcers/AllowedTargetsEnforcer.sol"; +import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforcer.sol"; +import { AllowedCalldataEnforcer } from "../../src/enforcers/AllowedCalldataEnforcer.sol"; +import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; +import { IPool } from "./interfaces/IAavePool.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// @dev Do not remove this comment below +/// forge-config: default.evm_version = "shanghai" + +/** + * @title AaveLending Test + * @notice Tests delegation-based lending on Aave v3. + * @dev Uses a forked Ethereum mainnet environment to test real contract interactions + */ +contract AaveLendingTest is BaseTest { + using ModeLib for ModeCode; + + IPool public constant AAVE_POOL = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2); + IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + address public constant USDC_WHALE = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341; + + // Enforcers for delegation restrictions + AllowedTargetsEnforcer public allowedTargetsEnforcer; + AllowedMethodsEnforcer public allowedMethodsEnforcer; + AllowedCalldataEnforcer public allowedCalldataEnforcer; + ValueLteEnforcer public valueLteEnforcer; + + uint256 public constant MAINNET_FORK_BLOCK = 22734910; // Use latest available block + uint256 public constant INITIAL_USDC_BALANCE = 10000000000; // 10k USDC + uint256 public constant DEPOSIT_AMOUNT = 1000000000; // 1k USDC + + ////////////////////// Setup ////////////////////// + + function setUp() public override { + // Create fork from mainnet at specific block + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), MAINNET_FORK_BLOCK); + + // Set implementation type + IMPLEMENTATION = Implementation.Hybrid; + SIGNATURE_TYPE = SignatureType.RawP256; + + // Call parent setup to initialize delegation framework + super.setUp(); + + // Deploy enforcers + allowedTargetsEnforcer = new AllowedTargetsEnforcer(); + allowedMethodsEnforcer = new AllowedMethodsEnforcer(); + allowedCalldataEnforcer = new AllowedCalldataEnforcer(); + valueLteEnforcer = new ValueLteEnforcer(); + + vm.label(address(allowedTargetsEnforcer), "AllowedTargetsEnforcer"); + vm.label(address(allowedMethodsEnforcer), "AllowedMethodsEnforcer"); + vm.label(address(allowedCalldataEnforcer), "AllowedCalldataEnforcer"); + vm.label(address(valueLteEnforcer), "ValueLteEnforcer"); + vm.label(address(AAVE_POOL), "Aave lending"); + vm.label(address(USDC), "USDC"); + vm.label(USDC_WHALE, "USDC Whale"); + + vm.deal(address(users.alice.deleGator), 1 ether); + + vm.prank(USDC_WHALE); + USDC.transfer(address(users.alice.deleGator), INITIAL_USDC_BALANCE); // 10k USDC + } + + function test_aliceDirectDeposit() public { + uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + + vm.prank(address(users.alice.deleGator)); + USDC.approve(address(AAVE_POOL), DEPOSIT_AMOUNT); + vm.prank(address(users.alice.deleGator)); + AAVE_POOL.supply(address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0); + + uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + + IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); + uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + } + + function test_aliceDirectWithdraw() public { + vm.prank(address(users.alice.deleGator)); + USDC.approve(address(AAVE_POOL), DEPOSIT_AMOUNT); + vm.prank(address(users.alice.deleGator)); + AAVE_POOL.supply(address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0); + + vm.prank(address(users.alice.deleGator)); + AAVE_POOL.withdraw(address(USDC), type(uint256).max, address(users.alice.deleGator)); + + uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE); + } + + function test_aliceDelegatedDeposit() public { + // Check initial balance + uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + + // Create delegation caveats for approving USDC + Caveat[] memory approveCaveats_ = new Caveat[](4); + + // Recommended: Restrict to specific contract + approveCaveats_[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(USDC)) }); + + // Recommended: Restrict to deposit function only + approveCaveats_[1] = + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IERC20.approve.selector) }); + + // Recommended: Restrict approve amount + uint256 paramStart_ = abi.encodeWithSelector(IERC20.approve.selector, address(0)).length; + approveCaveats_[2] = Caveat({ + args: hex"", + enforcer: address(allowedCalldataEnforcer), + terms: abi.encodePacked(paramStart_, DEPOSIT_AMOUNT) + }); + + // Recommended: Set value limit to 0 + approveCaveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); + + // Create delegation for approving USDC + Delegation memory approveDelegation = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: approveCaveats_, + salt: 0, + signature: hex"" + }); + + // Sign delegation + approveDelegation = signDelegation(users.alice, approveDelegation); + + // Create proper execution for approving USDC + Execution memory approveExecution = Execution({ + target: address(USDC), + value: 0, + callData: abi.encodeWithSelector(IERC20.approve.selector, address(AAVE_POOL), DEPOSIT_AMOUNT) + }); + + // Execute approve delegation + Delegation[] memory approveDelegations_ = new Delegation[](1); + approveDelegations_[0] = approveDelegation; + + invokeDelegation_UserOp(users.bob, approveDelegations_, approveExecution); + + // Create delegation caveats for lending + Caveat[] memory lendingCaveats_ = new Caveat[](4); + + // Recommended: Restrict to specific contract + lendingCaveats_[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(AAVE_POOL)) }); + + // Recommended: Restrict to deposit function only + lendingCaveats_[1] = + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IPool.supply.selector) }); + + // Recommended: Restrict supply argument "onBehalfOf" to alice + paramStart_ = abi.encodeWithSelector(IPool.supply.selector, address(0), uint256(0)).length; + lendingCaveats_[2] = Caveat({ + args: hex"", + enforcer: address(allowedCalldataEnforcer), + terms: abi.encode(paramStart_, address(users.alice.deleGator)) + }); + + // Recommended: Set value limit to 0 + lendingCaveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); + + // Create delegation for lending + Delegation memory lendingDelegation = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: lendingCaveats_, + salt: 0, + signature: hex"" + }); + + // Sign delegation + lendingDelegation = signDelegation(users.alice, lendingDelegation); + + // Create execution for lending + Execution memory lendingExecution_ = Execution({ + target: address(AAVE_POOL), + value: 0, + callData: abi.encodeWithSelector(IPool.supply.selector, address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0) + }); + + // Execute delegation + Delegation[] memory lendingDelegations_ = new Delegation[](1); + lendingDelegations_[0] = lendingDelegation; + + invokeDelegation_UserOp(users.bob, lendingDelegations_, lendingExecution_); + + // Check state after delegation + uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + + IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); + uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + } + + function test_aliceDelegatedWithdraw() public { + // Set initial state of alice having deposited 1k USDC + vm.prank(address(users.alice.deleGator)); + USDC.approve(address(AAVE_POOL), DEPOSIT_AMOUNT); + vm.prank(address(users.alice.deleGator)); + AAVE_POOL.supply(address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0); + + // Record initial state + uint256 aliceInitialUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); + IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); + uint256 aliceInitialATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceInitialUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + assertEq(aliceInitialATokenBalance, DEPOSIT_AMOUNT); + + // Create delegation caveats for lending witdrawal + Caveat[] memory lendingWithdrawalCaveats_ = new Caveat[](4); + + // Recommended: Restrict to specific contract + lendingWithdrawalCaveats_[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(AAVE_POOL)) }); + + // Recommended: Restrict to deposit function only + lendingWithdrawalCaveats_[1] = + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IPool.withdraw.selector) }); + + // Recommended: Restrict withdraw argument "to" to alice + uint256 paramStart_ = abi.encodeWithSelector(IPool.withdraw.selector, address(0), uint256(0)).length; + lendingWithdrawalCaveats_[2] = Caveat({ + args: hex"", + enforcer: address(allowedCalldataEnforcer), + terms: abi.encode(paramStart_, address(users.alice.deleGator)) + }); + + // Recommended: Set value limit to 0 + lendingWithdrawalCaveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); + + // Create delegation for lending + Delegation memory lendingWithdrawalDelegation = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: lendingWithdrawalCaveats_, + salt: 0, + signature: hex"" + }); + + // Sign delegation + lendingWithdrawalDelegation = signDelegation(users.alice, lendingWithdrawalDelegation); + + // Create execution for lending + Execution memory lendingWithdrawalExecution_ = Execution({ + target: address(AAVE_POOL), + value: 0, + callData: abi.encodeWithSelector(IPool.withdraw.selector, address(USDC), type(uint256).max, address(users.alice.deleGator)) + }); + + // Execute delegation + Delegation[] memory lendingWithdrawalDelegations_ = new Delegation[](1); + lendingWithdrawalDelegations_[0] = lendingWithdrawalDelegation; + + invokeDelegation_UserOp(users.bob, lendingWithdrawalDelegations_, lendingWithdrawalExecution_); + + // Check state after delegation + uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE); + } +} diff --git a/test/helpers/interfaces/IAavePool.sol b/test/helpers/interfaces/IAavePool.sol new file mode 100644 index 00000000..6fc967a8 --- /dev/null +++ b/test/helpers/interfaces/IAavePool.sol @@ -0,0 +1,722 @@ +// Based on: https://github.com/aave-dao/aave-v3-origin/blob/main/src/contracts/interfaces/IPool.sol + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IPool + * @author Aave + * @notice Defines the basic interface for an Aave Pool. + */ +interface IPool { + /** + * @dev Emitted on mintUnbacked() + * @param reserve The address of the underlying asset of the reserve + * @param user The address initiating the supply + * @param onBehalfOf The beneficiary of the supplied assets, receiving the aTokens + * @param amount The amount of supplied assets + * @param referralCode The referral code used + */ + event MintUnbacked( + address indexed reserve, address user, address indexed onBehalfOf, uint256 amount, uint16 indexed referralCode + ); + + /** + * @dev Emitted on backUnbacked() + * @param reserve The address of the underlying asset of the reserve + * @param backer The address paying for the backing + * @param amount The amount added as backing + * @param fee The amount paid in fees + */ + event BackUnbacked(address indexed reserve, address indexed backer, uint256 amount, uint256 fee); + + /** + * @dev Emitted on supply() + * @param reserve The address of the underlying asset of the reserve + * @param user The address initiating the supply + * @param onBehalfOf The beneficiary of the supply, receiving the aTokens + * @param amount The amount supplied + * @param referralCode The referral code used + */ + event Supply(address indexed reserve, address user, address indexed onBehalfOf, uint256 amount, uint16 indexed referralCode); + + /** + * @dev Emitted on withdraw() + * @param reserve The address of the underlying asset being withdrawn + * @param user The address initiating the withdrawal, owner of aTokens + * @param to The address that will receive the underlying + * @param amount The amount to be withdrawn + */ + event Withdraw(address indexed reserve, address indexed user, address indexed to, uint256 amount); + + /** + * @dev Emitted on repay() + * @param reserve The address of the underlying asset of the reserve + * @param user The beneficiary of the repayment, getting his debt reduced + * @param repayer The address of the user initiating the repay(), providing the funds + * @param amount The amount repaid + * @param useATokens True if the repayment is done using aTokens, `false` if done with underlying asset directly + */ + event Repay(address indexed reserve, address indexed user, address indexed repayer, uint256 amount, bool useATokens); + + /** + * @dev Emitted on borrow(), repay() and liquidationCall() when using isolated assets + * @param asset The address of the underlying asset of the reserve + * @param totalDebt The total isolation mode debt for the reserve + */ + event IsolationModeTotalDebtUpdated(address indexed asset, uint256 totalDebt); + + /** + * @dev Emitted when the user selects a certain asset category for eMode + * @param user The address of the user + * @param categoryId The category id + */ + event UserEModeSet(address indexed user, uint8 categoryId); + + /** + * @dev Emitted on setUserUseReserveAsCollateral() + * @param reserve The address of the underlying asset of the reserve + * @param user The address of the user enabling the usage as collateral + */ + event ReserveUsedAsCollateralEnabled(address indexed reserve, address indexed user); + + /** + * @dev Emitted on setUserUseReserveAsCollateral() + * @param reserve The address of the underlying asset of the reserve + * @param user The address of the user enabling the usage as collateral + */ + event ReserveUsedAsCollateralDisabled(address indexed reserve, address indexed user); + + /** + * @dev Emitted when a borrower is liquidated. + * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation + * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation + * @param user The address of the borrower getting liquidated + * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover + * @param liquidatedCollateralAmount The amount of collateral received by the liquidator + * @param liquidator The address of the liquidator + * @param receiveAToken True if the liquidators wants to receive the collateral aTokens, `false` if he wants + * to receive the underlying collateral asset directly + */ + event LiquidationCall( + address indexed collateralAsset, + address indexed debtAsset, + address indexed user, + uint256 debtToCover, + uint256 liquidatedCollateralAmount, + address liquidator, + bool receiveAToken + ); + + /** + * @dev Emitted when the state of a reserve is updated. + * @param reserve The address of the underlying asset of the reserve + * @param liquidityRate The next liquidity rate + * @param stableBorrowRate The next stable borrow rate @note deprecated on v3.2.0 + * @param variableBorrowRate The next variable borrow rate + * @param liquidityIndex The next liquidity index + * @param variableBorrowIndex The next variable borrow index + */ + event ReserveDataUpdated( + address indexed reserve, + uint256 liquidityRate, + uint256 stableBorrowRate, + uint256 variableBorrowRate, + uint256 liquidityIndex, + uint256 variableBorrowIndex + ); + + /** + * @dev Emitted when the deficit of a reserve is covered. + * @param reserve The address of the underlying asset of the reserve + * @param caller The caller that triggered the DeficitCovered event + * @param amountCovered The amount of deficit covered + */ + event DeficitCovered(address indexed reserve, address caller, uint256 amountCovered); + + /** + * @dev Emitted when the protocol treasury receives minted aTokens from the accrued interest. + * @param reserve The address of the reserve + * @param amountMinted The amount minted to the treasury + */ + event MintedToTreasury(address indexed reserve, uint256 amountMinted); + + /** + * @dev Emitted when deficit is realized on a liquidation. + * @param user The user address where the bad debt will be burned + * @param debtAsset The address of the underlying borrowed asset to be burned + * @param amountCreated The amount of deficit created + */ + event DeficitCreated(address indexed user, address indexed debtAsset, uint256 amountCreated); + + /** + * @notice Mints an `amount` of aTokens to the `onBehalfOf` + * @param asset The address of the underlying asset to mint + * @param amount The amount to mint + * @param onBehalfOf The address that will receive the aTokens + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + */ + function mintUnbacked(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; + + /** + * @notice Back the current unbacked underlying with `amount` and pay `fee`. + * @param asset The address of the underlying asset to back + * @param amount The amount to back + * @param fee The amount paid in fees + * @return The backed amount + */ + function backUnbacked(address asset, uint256 amount, uint256 fee) external returns (uint256); + + /** + * @notice Supplies an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. + * - E.g. User supplies 100 USDC and gets in return 100 aUSDC + * @param asset The address of the underlying asset to supply + * @param amount The amount to be supplied + * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user + * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens + * is a different wallet + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + */ + function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; + + /** + * @notice Supply with transfer approval of asset to be supplied done via permit function + * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 + * @param asset The address of the underlying asset to supply + * @param amount The amount to be supplied + * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user + * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens + * is a different wallet + * @param deadline The deadline timestamp that the permit is valid + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + * @param permitV The V parameter of ERC712 permit sig + * @param permitR The R parameter of ERC712 permit sig + * @param permitS The S parameter of ERC712 permit sig + */ + function supplyWithPermit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) + external; + + /** + * @notice Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned + * E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC + * @param asset The address of the underlying asset to withdraw + * @param amount The underlying amount to be withdrawn + * - Send the value type(uint256).max in order to withdraw the whole aToken balance + * @param to The address that will receive the underlying, same as msg.sender if the user + * wants to receive it on his own wallet, or a different address if the beneficiary is a + * different wallet + * @return The final amount withdrawn + */ + function withdraw(address asset, uint256 amount, address to) external returns (uint256); + + /** + * @notice Allows users to borrow a specific `amount` of the reserve underlying asset, provided that the borrower + * already supplied enough collateral, or he was given enough allowance by a credit delegator on the VariableDebtToken + * - E.g. User borrows 100 USDC passing as `onBehalfOf` his own address, receiving the 100 USDC in his wallet + * and 100 variable debt tokens + * @param asset The address of the underlying asset to borrow + * @param amount The amount to be borrowed + * @param interestRateMode 2 for Variable, 1 is deprecated on v3.2.0 + * @param referralCode The code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + * @param onBehalfOf The address of the user who will receive the debt. Should be the address of the borrower itself + * calling the function if he wants to borrow against his own collateral, or the address of the credit delegator + * if he has been given credit delegation allowance + */ + function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) external; + + /** + * @notice Repays a borrowed `amount` on a specific reserve, burning the equivalent debt tokens owned + * - E.g. User repays 100 USDC, burning 100 variable debt tokens of the `onBehalfOf` address + * @param asset The address of the borrowed underlying asset previously borrowed + * @param amount The amount to repay + * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` + * @param interestRateMode 2 for Variable, 1 is deprecated on v3.2.0 + * @param onBehalfOf The address of the user who will get his debt reduced/removed. Should be the address of the + * user calling the function if he wants to reduce/remove his own debt, or the address of any other + * other borrower whose debt should be removed + * @return The final amount repaid + */ + function repay(address asset, uint256 amount, uint256 interestRateMode, address onBehalfOf) external returns (uint256); + + /** + * @notice Repay with transfer approval of asset to be repaid done via permit function + * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 + * @param asset The address of the borrowed underlying asset previously borrowed + * @param amount The amount to repay + * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` + * @param interestRateMode 2 for Variable, 1 is deprecated on v3.2.0 + * @param onBehalfOf Address of the user who will get his debt reduced/removed. Should be the address of the + * user calling the function if he wants to reduce/remove his own debt, or the address of any other + * other borrower whose debt should be removed + * @param deadline The deadline timestamp that the permit is valid + * @param permitV The V parameter of ERC712 permit sig + * @param permitR The R parameter of ERC712 permit sig + * @param permitS The S parameter of ERC712 permit sig + * @return The final amount repaid + */ + function repayWithPermit( + address asset, + uint256 amount, + uint256 interestRateMode, + address onBehalfOf, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) + external + returns (uint256); + + /** + * @notice Repays a borrowed `amount` on a specific reserve using the reserve aTokens, burning the + * equivalent debt tokens + * - E.g. User repays 100 USDC using 100 aUSDC, burning 100 variable debt tokens + * @dev Passing uint256.max as amount will clean up any residual aToken dust balance, if the user aToken + * balance is not enough to cover the whole debt + * @param asset The address of the borrowed underlying asset previously borrowed + * @param amount The amount to repay + * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` + * @param interestRateMode DEPRECATED in v3.2.0 + * @return The final amount repaid + */ + function repayWithATokens(address asset, uint256 amount, uint256 interestRateMode) external returns (uint256); + + /** + * @notice Allows suppliers to enable/disable a specific supplied asset as collateral + * @param asset The address of the underlying asset supplied + * @param useAsCollateral True if the user wants to use the supply as collateral, false otherwise + */ + function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external; + + /** + * @notice Function to liquidate a non-healthy position collateral-wise, with Health Factor below 1 + * - The caller (liquidator) covers `debtToCover` amount of debt of the user getting liquidated, and receives + * a proportionally amount of the `collateralAsset` plus a bonus to cover market risk + * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation + * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation + * @param user The address of the borrower getting liquidated + * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover + * @param receiveAToken True if the liquidators wants to receive the collateral aTokens, `false` if he wants + * to receive the underlying collateral asset directly + */ + function liquidationCall( + address collateralAsset, + address debtAsset, + address user, + uint256 debtToCover, + bool receiveAToken + ) + external; + + /** + * @notice Allows smartcontracts to access the liquidity of the pool within one transaction, + * as long as the amount taken plus a fee is returned. + * @dev IMPORTANT There are security concerns for developers of flashloan receiver contracts that must be kept + * into consideration. For further details please visit https://docs.aave.com/developers/ + * @param receiverAddress The address of the contract receiving the funds, implementing IFlashLoanReceiver interface + * @param assets The addresses of the assets being flash-borrowed + * @param amounts The amounts of the assets being flash-borrowed + * @param interestRateModes Types of the debt to open if the flash loan is not returned: + * 0 -> Don't open any debt, just revert if funds can't be transferred from the receiver + * 1 -> Deprecated on v3.2.0 + * 2 -> Open debt at variable rate for the value of the amount flash-borrowed to the `onBehalfOf` address + * @param onBehalfOf The address that will receive the debt in the case of using 2 on `modes` + * @param params Variadic packed params to pass to the receiver as extra information + * @param referralCode The code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + */ + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata interestRateModes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) + external; + + /** + * @notice Allows smartcontracts to access the liquidity of the pool within one transaction, + * as long as the amount taken plus a fee is returned. + * @dev IMPORTANT There are security concerns for developers of flashloan receiver contracts that must be kept + * into consideration. For further details please visit https://docs.aave.com/developers/ + * @param receiverAddress The address of the contract receiving the funds, implementing IFlashLoanSimpleReceiver interface + * @param asset The address of the asset being flash-borrowed + * @param amount The amount of the asset being flash-borrowed + * @param params Variadic packed params to pass to the receiver as extra information + * @param referralCode The code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + */ + function flashLoanSimple( + address receiverAddress, + address asset, + uint256 amount, + bytes calldata params, + uint16 referralCode + ) + external; + + /** + * @notice Returns the user account data across all the reserves + * @param user The address of the user + * @return totalCollateralBase The total collateral of the user in the base currency used by the price feed + * @return totalDebtBase The total debt of the user in the base currency used by the price feed + * @return availableBorrowsBase The borrowing power left of the user in the base currency used by the price feed + * @return currentLiquidationThreshold The liquidation threshold of the user + * @return ltv The loan to value of The user + * @return healthFactor The current health factor of the user + */ + function getUserAccountData(address user) + external + view + returns ( + uint256 totalCollateralBase, + uint256 totalDebtBase, + uint256 availableBorrowsBase, + uint256 currentLiquidationThreshold, + uint256 ltv, + uint256 healthFactor + ); + + /** + * @notice Initializes a reserve, activating it, assigning an aToken and debt tokens and an + * interest rate strategy + * @dev Only callable by the PoolConfigurator contract + * @param asset The address of the underlying asset of the reserve + * @param aTokenAddress The address of the aToken that will be assigned to the reserve + * @param variableDebtAddress The address of the VariableDebtToken that will be assigned to the reserve + * @param interestRateStrategyAddress The address of the interest rate strategy contract + */ + function initReserve( + address asset, + address aTokenAddress, + address variableDebtAddress, + address interestRateStrategyAddress + ) + external; + + /** + * @notice Drop a reserve + * @dev Only callable by the PoolConfigurator contract + * @dev Does not reset eMode flags, which must be considered when reusing the same reserve id for a different reserve. + * @param asset The address of the underlying asset of the reserve + */ + function dropReserve(address asset) external; + + /** + * @notice Updates the address of the interest rate strategy contract + * @dev Only callable by the PoolConfigurator contract + * @param asset The address of the underlying asset of the reserve + * @param rateStrategyAddress The address of the interest rate strategy contract + */ + function setReserveInterestRateStrategyAddress(address asset, address rateStrategyAddress) external; + + /** + * @notice Accumulates interest to all indexes of the reserve + * @dev Only callable by the PoolConfigurator contract + * @dev To be used when required by the configurator, for example when updating interest rates strategy data + * @param asset The address of the underlying asset of the reserve + */ + function syncIndexesState(address asset) external; + + /** + * @notice Updates interest rates on the reserve data + * @dev Only callable by the PoolConfigurator contract + * @dev To be used when required by the configurator, for example when updating interest rates strategy data + * @param asset The address of the underlying asset of the reserve + */ + function syncRatesState(address asset) external; + + /** + * @notice Returns the normalized income of the reserve + * @param asset The address of the underlying asset of the reserve + * @return The reserve's normalized income + */ + function getReserveNormalizedIncome(address asset) external view returns (uint256); + + /** + * @notice Returns the normalized variable debt per unit of asset + * @dev WARNING: This function is intended to be used primarily by the protocol itself to get a + * "dynamic" variable index based on time, current stored index and virtual rate at the current + * moment (approx. a borrower would get if opening a position). This means that is always used in + * combination with variable debt supply/balances. + * If using this function externally, consider that is possible to have an increasing normalized + * variable debt that is not equivalent to how the variable debt index would be updated in storage + * (e.g. only updates with non-zero variable debt supply) + * @param asset The address of the underlying asset of the reserve + * @return The reserve normalized variable debt + */ + function getReserveNormalizedVariableDebt(address asset) external view returns (uint256); + + /** + * @notice Returns the virtual underlying balance of the reserve + * @param asset The address of the underlying asset of the reserve + * @return The reserve virtual underlying balance + */ + function getVirtualUnderlyingBalance(address asset) external view returns (uint128); + + /** + * @notice Validates and finalizes an aToken transfer + * @dev Only callable by the overlying aToken of the `asset` + * @param asset The address of the underlying asset of the aToken + * @param from The user from which the aTokens are transferred + * @param to The user receiving the aTokens + * @param amount The amount being transferred/withdrawn + * @param balanceFromBefore The aToken balance of the `from` user before the transfer + * @param balanceToBefore The aToken balance of the `to` user before the transfer + */ + function finalizeTransfer( + address asset, + address from, + address to, + uint256 amount, + uint256 balanceFromBefore, + uint256 balanceToBefore + ) + external; + + /** + * @notice Returns the list of the underlying assets of all the initialized reserves + * @dev It does not include dropped reserves + * @return The addresses of the underlying assets of the initialized reserves + */ + function getReservesList() external view returns (address[] memory); + + /** + * @notice Returns the number of initialized reserves + * @dev It includes dropped reserves + * @return The count + */ + function getReservesCount() external view returns (uint256); + + /** + * @notice Returns the address of the underlying asset of a reserve by the reserve id as stored in the DataTypes.ReserveData + * struct + * @param id The id of the reserve as stored in the DataTypes.ReserveData struct + * @return The address of the reserve associated with id + */ + function getReserveAddressById(uint16 id) external view returns (address); + + /** + * @notice Updates the protocol fee on the bridging + * @param bridgeProtocolFee The part of the premium sent to the protocol treasury + */ + function updateBridgeProtocolFee(uint256 bridgeProtocolFee) external; + + /** + * @notice Updates flash loan premiums. Flash loan premium consists of two parts: + * - A part is sent to aToken holders as extra, one time accumulated interest + * - A part is collected by the protocol treasury + * @dev The total premium is calculated on the total borrowed amount + * @dev The premium to protocol is calculated on the total premium, being a percentage of `flashLoanPremiumTotal` + * @dev Only callable by the PoolConfigurator contract + * @param flashLoanPremiumTotal The total premium, expressed in bps + * @param flashLoanPremiumToProtocol The part of the premium sent to the protocol treasury, expressed in bps + */ + function updateFlashloanPremiums(uint128 flashLoanPremiumTotal, uint128 flashLoanPremiumToProtocol) external; + + /** + * @notice Replaces the current eMode collateralBitmap. + * @param id The id of the category + * @param collateralBitmap The collateralBitmap of the category + */ + function configureEModeCategoryCollateralBitmap(uint8 id, uint128 collateralBitmap) external; + + /** + * @notice Replaces the current eMode borrowableBitmap. + * @param id The id of the category + * @param borrowableBitmap The borrowableBitmap of the category + */ + function configureEModeCategoryBorrowableBitmap(uint8 id, uint128 borrowableBitmap) external; + + /** + * @notice Returns the label of an eMode category + * @param id The id of the category + * @return The label of the category + */ + function getEModeCategoryLabel(uint8 id) external view returns (string memory); + + /** + * @notice Returns the collateralBitmap of an eMode category + * @param id The id of the category + * @return The collateralBitmap of the category + */ + function getEModeCategoryCollateralBitmap(uint8 id) external view returns (uint128); + + /** + * @notice Returns the borrowableBitmap of an eMode category + * @param id The id of the category + * @return The borrowableBitmap of the category + */ + function getEModeCategoryBorrowableBitmap(uint8 id) external view returns (uint128); + + /** + * @notice Allows a user to use the protocol in eMode + * @param categoryId The id of the category + */ + function setUserEMode(uint8 categoryId) external; + + /** + * @notice Returns the eMode the user is using + * @param user The address of the user + * @return The eMode id + */ + function getUserEMode(address user) external view returns (uint256); + + /** + * @notice Resets the isolation mode total debt of the given asset to zero + * @dev It requires the given asset has zero debt ceiling + * @param asset The address of the underlying asset to reset the isolationModeTotalDebt + */ + function resetIsolationModeTotalDebt(address asset) external; + + /** + * @notice Sets the liquidation grace period of the given asset + * @dev To enable a liquidation grace period, a timestamp in the future should be set, + * To disable a liquidation grace period, any timestamp in the past works, like 0 + * @param asset The address of the underlying asset to set the liquidationGracePeriod + * @param until Timestamp when the liquidation grace period will end + * + */ + function setLiquidationGracePeriod(address asset, uint40 until) external; + + /** + * @notice Returns the liquidation grace period of the given asset + * @param asset The address of the underlying asset + * @return Timestamp when the liquidation grace period will end + * + */ + function getLiquidationGracePeriod(address asset) external view returns (uint40); + + /** + * @notice Returns the total fee on flash loans + * @return The total fee on flashloans + */ + function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint128); + + /** + * @notice Returns the part of the bridge fees sent to protocol + * @return The bridge fee sent to the protocol treasury + */ + function BRIDGE_PROTOCOL_FEE() external view returns (uint256); + + /** + * @notice Returns the part of the flashloan fees sent to protocol + * @return The flashloan fee sent to the protocol treasury + */ + function FLASHLOAN_PREMIUM_TO_PROTOCOL() external view returns (uint128); + + /** + * @notice Returns the maximum number of reserves supported to be listed in this Pool + * @return The maximum number of reserves supported + */ + function MAX_NUMBER_RESERVES() external view returns (uint16); + + /** + * @notice Mints the assets accrued through the reserve factor to the treasury in the form of aTokens + * @param assets The list of reserves for which the minting needs to be executed + */ + function mintToTreasury(address[] calldata assets) external; + + /** + * @notice Rescue and transfer tokens locked in this contract + * @param token The address of the token + * @param to The address of the recipient + * @param amount The amount of token to transfer + */ + function rescueTokens(address token, address to, uint256 amount) external; + + /** + * @notice Supplies an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. + * - E.g. User supplies 100 USDC and gets in return 100 aUSDC + * @dev Deprecated: Use the `supply` function instead + * @param asset The address of the underlying asset to supply + * @param amount The amount to be supplied + * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user + * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens + * is a different wallet + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + */ + function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; + + /** + * @notice It covers the deficit of a specified reserve by burning: + * - the equivalent aToken `amount` for assets with virtual accounting enabled + * - the equivalent `amount` of underlying for assets with virtual accounting disabled (e.g. GHO) + * @dev The deficit of a reserve can occur due to situations where borrowed assets are not repaid, leading to bad debt. + * @param asset The address of the underlying asset to cover the deficit. + * @param amount The amount to be covered, in aToken or underlying on non-virtual accounted assets + */ + function eliminateReserveDeficit(address asset, uint256 amount) external; + + /** + * @notice Returns the current deficit of a reserve. + * @param asset The address of the underlying asset of the reserve + * @return The current deficit of the reserve + */ + function getReserveDeficit(address asset) external view returns (uint256); + + /** + * @notice Returns the aToken address of a reserve. + * @param asset The address of the underlying asset of the reserve + * @return The address of the aToken + */ + function getReserveAToken(address asset) external view returns (address); + + /** + * @notice Returns the variableDebtToken address of a reserve. + * @param asset The address of the underlying asset of the reserve + * @return The address of the variableDebtToken + */ + function getReserveVariableDebtToken(address asset) external view returns (address); + + /** + * @notice Gets the address of the external FlashLoanLogic + */ + function getFlashLoanLogic() external view returns (address); + + /** + * @notice Gets the address of the external BorrowLogic + */ + function getBorrowLogic() external view returns (address); + + /** + * @notice Gets the address of the external BridgeLogic + */ + function getBridgeLogic() external view returns (address); + + /** + * @notice Gets the address of the external EModeLogic + */ + function getEModeLogic() external view returns (address); + + /** + * @notice Gets the address of the external LiquidationLogic + */ + function getLiquidationLogic() external view returns (address); + + /** + * @notice Gets the address of the external PoolLogic + */ + function getPoolLogic() external view returns (address); + + /** + * @notice Gets the address of the external SupplyLogic + */ + function getSupplyLogic() external view returns (address); +} From 982b5830617bcb08848196845bca0f1cf076a294 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Mon, 23 Jun 2025 11:37:21 +0200 Subject: [PATCH 2/7] Add test for single delegation with logicalOrEnforcer --- test/helpers/AaveLending.t.sol | 109 +++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/test/helpers/AaveLending.t.sol b/test/helpers/AaveLending.t.sol index 48d3f1aa..5e7045a4 100644 --- a/test/helpers/AaveLending.t.sol +++ b/test/helpers/AaveLending.t.sol @@ -13,6 +13,7 @@ import { AllowedTargetsEnforcer } from "../../src/enforcers/AllowedTargetsEnforc import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforcer.sol"; import { AllowedCalldataEnforcer } from "../../src/enforcers/AllowedCalldataEnforcer.sol"; import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; +import { LogicalOrWrapperEnforcer } from "../../src/enforcers/LogicalOrWrapperEnforcer.sol"; import { IPool } from "./interfaces/IAavePool.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -36,6 +37,7 @@ contract AaveLendingTest is BaseTest { AllowedMethodsEnforcer public allowedMethodsEnforcer; AllowedCalldataEnforcer public allowedCalldataEnforcer; ValueLteEnforcer public valueLteEnforcer; + LogicalOrWrapperEnforcer public logicalOrWrapperEnforcer; uint256 public constant MAINNET_FORK_BLOCK = 22734910; // Use latest available block uint256 public constant INITIAL_USDC_BALANCE = 10000000000; // 10k USDC @@ -59,11 +61,13 @@ contract AaveLendingTest is BaseTest { allowedMethodsEnforcer = new AllowedMethodsEnforcer(); allowedCalldataEnforcer = new AllowedCalldataEnforcer(); valueLteEnforcer = new ValueLteEnforcer(); + logicalOrWrapperEnforcer = new LogicalOrWrapperEnforcer(delegationManager); vm.label(address(allowedTargetsEnforcer), "AllowedTargetsEnforcer"); vm.label(address(allowedMethodsEnforcer), "AllowedMethodsEnforcer"); vm.label(address(allowedCalldataEnforcer), "AllowedCalldataEnforcer"); vm.label(address(valueLteEnforcer), "ValueLteEnforcer"); + vm.label(address(logicalOrWrapperEnforcer), "LogicalOrWrapperEnforcer"); vm.label(address(AAVE_POOL), "Aave lending"); vm.label(address(USDC), "USDC"); vm.label(USDC_WHALE, "USDC Whale"); @@ -280,4 +284,109 @@ contract AaveLendingTest is BaseTest { uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE); } + + function test_aliceDelegatedDepositWithLogicalOrWrapper() public { + // Check initial balance + uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + + LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_ = new LogicalOrWrapperEnforcer.CaveatGroup[](2); + + // Create delegation caveats for approving USDC + Caveat[] memory approveCaveats_ = new Caveat[](4); + + // Recommended: Restrict to specific contract + approveCaveats_[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(USDC)) }); + + // Recommended: Restrict to deposit function only + approveCaveats_[1] = + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IERC20.approve.selector) }); + + // Recommended: Restrict approve amount + uint256 paramStart_ = abi.encodeWithSelector(IERC20.approve.selector, address(0)).length; + approveCaveats_[2] = Caveat({ + args: hex"", + enforcer: address(allowedCalldataEnforcer), + terms: abi.encodePacked(paramStart_, DEPOSIT_AMOUNT) + }); + + // Recommended: Set value limit to 0 + approveCaveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); + + // Create delegation caveats for lending + Caveat[] memory lendingCaveats_ = new Caveat[](4); + + // Recommended: Restrict to specific contract + lendingCaveats_[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(AAVE_POOL)) }); + + // Recommended: Restrict to deposit function only + lendingCaveats_[1] = + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IPool.supply.selector) }); + + // Recommended: Restrict supply argument "onBehalfOf" to alice + paramStart_ = abi.encodeWithSelector(IPool.supply.selector, address(0), uint256(0)).length; + lendingCaveats_[2] = Caveat({ + args: hex"", + enforcer: address(allowedCalldataEnforcer), + terms: abi.encode(paramStart_, address(users.alice.deleGator)) + }); + + // Recommended: Set value limit to 0 + lendingCaveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); + + groups_[0] = LogicalOrWrapperEnforcer.CaveatGroup({ caveats: approveCaveats_ }); + groups_[1] = LogicalOrWrapperEnforcer.CaveatGroup({ caveats: lendingCaveats_ }); + + Caveat[] memory orCaveats_ = new Caveat[](1); + orCaveats_[0] = Caveat({ args: hex"", enforcer: address(logicalOrWrapperEnforcer), terms: abi.encode(groups_) }); + + // Create delegation for lending + Delegation memory delegation = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: orCaveats_, + salt: 0, + signature: hex"" + }); + + // Sign delegation + delegation = signDelegation(users.alice, delegation); + + // Create proper execution for approving USDC + Execution memory approveExecution_ = Execution({ + target: address(USDC), + value: 0, + callData: abi.encodeWithSelector(IERC20.approve.selector, address(AAVE_POOL), DEPOSIT_AMOUNT) + }); + + // Create execution for lending + Execution memory lendingExecution_ = Execution({ + target: address(AAVE_POOL), + value: 0, + callData: abi.encodeWithSelector(IPool.supply.selector, address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0) + }); + + // Execute delegation + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation; + + delegations_[0].caveats[0].args = + abi.encode(LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: 0, caveatArgs: new bytes[](4) })); + invokeDelegation_UserOp(users.bob, delegations_, approveExecution_); + + delegations_[0].caveats[0].args = + abi.encode(LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: 1, caveatArgs: new bytes[](4) })); + invokeDelegation_UserOp(users.bob, delegations_, lendingExecution_); + + // Check state after delegation + uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + + IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); + uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + } } From a0a74990c75eb66dae5c6ca65ff795a700623c94 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Tue, 24 Jun 2025 11:57:19 +0200 Subject: [PATCH 3/7] Add AaveAdapter POC and descriptions --- test/helpers/AaveLending.t.sol | 173 ++++++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/test/helpers/AaveLending.t.sol b/test/helpers/AaveLending.t.sol index 5e7045a4..1cab96dc 100644 --- a/test/helpers/AaveLending.t.sol +++ b/test/helpers/AaveLending.t.sol @@ -14,8 +14,10 @@ import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforc import { AllowedCalldataEnforcer } from "../../src/enforcers/AllowedCalldataEnforcer.sol"; import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; import { LogicalOrWrapperEnforcer } from "../../src/enforcers/LogicalOrWrapperEnforcer.sol"; +import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; import { IPool } from "./interfaces/IAavePool.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; // @dev Do not remove this comment below /// forge-config: default.evm_version = "shanghai" @@ -23,7 +25,15 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** * @title AaveLending Test * @notice Tests delegation-based lending on Aave v3. - * @dev Uses a forked Ethereum mainnet environment to test real contract interactions + * @dev Uses a forked Ethereum mainnet environment to test real contract interactions. + * We are testing 2 different ways of using delegation framework to enable lending on Aave v3. + * 1. Alice delegates token approval on USDC and supply on Aave. This can be done in 2 seperate delegations + * or in a single delegation using LogicalOrWrapperEnforcer. This way the funds and approval are going + * directly from alice to USDC/Aave. But if we want to do rebalancing this means that alice would need to over + * approve tokens so that we can withdraw and deposit again. + * 2. We use a custom contract "AaveAdapter" to which alice delegates with only transfer balance. AaveAdapter + * is then the one that takes care of approving tokens to Aave and doing the lending. This way the adapter is the + * one that over approves tokens. Making it safer. But this introduces a middleware contract. */ contract AaveLendingTest is BaseTest { using ModeLib for ModeCode; @@ -38,6 +48,8 @@ contract AaveLendingTest is BaseTest { AllowedCalldataEnforcer public allowedCalldataEnforcer; ValueLteEnforcer public valueLteEnforcer; LogicalOrWrapperEnforcer public logicalOrWrapperEnforcer; + ERC20TransferAmountEnforcer public erc20TransferAmountEnforcer; + AaveAdapter public aaveAdapter; uint256 public constant MAINNET_FORK_BLOCK = 22734910; // Use latest available block uint256 public constant INITIAL_USDC_BALANCE = 10000000000; // 10k USDC @@ -61,16 +73,21 @@ contract AaveLendingTest is BaseTest { allowedMethodsEnforcer = new AllowedMethodsEnforcer(); allowedCalldataEnforcer = new AllowedCalldataEnforcer(); valueLteEnforcer = new ValueLteEnforcer(); + erc20TransferAmountEnforcer = new ERC20TransferAmountEnforcer(); + logicalOrWrapperEnforcer = new LogicalOrWrapperEnforcer(delegationManager); + aaveAdapter = new AaveAdapter(address(delegationManager), address(AAVE_POOL)); vm.label(address(allowedTargetsEnforcer), "AllowedTargetsEnforcer"); vm.label(address(allowedMethodsEnforcer), "AllowedMethodsEnforcer"); vm.label(address(allowedCalldataEnforcer), "AllowedCalldataEnforcer"); vm.label(address(valueLteEnforcer), "ValueLteEnforcer"); vm.label(address(logicalOrWrapperEnforcer), "LogicalOrWrapperEnforcer"); + vm.label(address(erc20TransferAmountEnforcer), "ERC20TransferAmountEnforcer"); vm.label(address(AAVE_POOL), "Aave lending"); vm.label(address(USDC), "USDC"); vm.label(USDC_WHALE, "USDC Whale"); + vm.label(address(aaveAdapter), "AaveAdapter"); vm.deal(address(users.alice.deleGator), 1 ether); @@ -78,6 +95,7 @@ contract AaveLendingTest is BaseTest { USDC.transfer(address(users.alice.deleGator), INITIAL_USDC_BALANCE); // 10k USDC } + // Testing directly depositing USDC to Aave to see if everything works on the forked mainnet function test_aliceDirectDeposit() public { uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); @@ -95,6 +113,7 @@ contract AaveLendingTest is BaseTest { assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); } + // Testing directly withdrawing USDC from Aave to see if everything works on the forked mainnet function test_aliceDirectWithdraw() public { vm.prank(address(users.alice.deleGator)); USDC.approve(address(AAVE_POOL), DEPOSIT_AMOUNT); @@ -108,6 +127,7 @@ contract AaveLendingTest is BaseTest { assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE); } + // Testing delegating approval and supply functions in 2 separate delegations function test_aliceDelegatedDeposit() public { // Check initial balance uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); @@ -218,6 +238,7 @@ contract AaveLendingTest is BaseTest { assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); } + // Testing delegating withdrawal function in a single delegation function test_aliceDelegatedWithdraw() public { // Set initial state of alice having deposited 1k USDC vm.prank(address(users.alice.deleGator)); @@ -285,6 +306,7 @@ contract AaveLendingTest is BaseTest { assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE); } + // Testing delegating approval and supply functions in a single delegation using LogicalOrWrapperEnforcer function test_aliceDelegatedDepositWithLogicalOrWrapper() public { // Check initial balance uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); @@ -389,4 +411,153 @@ contract AaveLendingTest is BaseTest { uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); } + + // Testing delegating transfer to adapter and then supply by delegation + function test_aliceDelegatedDepositViaAdapter() public { + // Check initial balance + uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + + // Create delegation for transferring USDC to adapter + Caveat[] memory transferCaveats_ = new Caveat[](1); + + transferCaveats_[0] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encodePacked(address(USDC), DEPOSIT_AMOUNT) + }); + + // Create delegation for transfer + Delegation memory delegation = Delegation({ + delegate: address(aaveAdapter), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: transferCaveats_, + salt: 0, + signature: hex"" + }); + + // Sign delegation + delegation = signDelegation(users.alice, delegation); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation; + + vm.prank(address(users.alice.deleGator)); + aaveAdapter.supplyByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + + // Check state after delegation + uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + + IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); + uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + } + + // Testing delegating transfer to adapter and then delegating suppyByDelegation + function test_aliceDelegatedDepositViaAdapterDelegation() public { + // Check initial balance + uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + + // Create delegation for transferring USDC to adapter + Caveat[] memory transferCaveats_ = new Caveat[](1); + + transferCaveats_[0] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encodePacked(address(USDC), DEPOSIT_AMOUNT) + }); + + // Create delegation for transfer + Delegation memory delegation = Delegation({ + delegate: address(aaveAdapter), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: transferCaveats_, + salt: 0, + signature: hex"" + }); + + // Sign delegation + delegation = signDelegation(users.alice, delegation); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation; + + // Create delegation caveats for lending + Caveat[] memory supplyCaveats_ = new Caveat[](1); + + // Recommended: Restrict to specific contract + supplyCaveats_[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(aaveAdapter)) }); + + // Create delegation for supply + Delegation memory supplyDelegation = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: supplyCaveats_, + salt: 0, + signature: hex"" + }); + + // Sign delegation + supplyDelegation = signDelegation(users.alice, supplyDelegation); + + // Create execution for supply + Execution memory supplyExecution_ = Execution({ + target: address(aaveAdapter), + value: 0, + callData: abi.encodeWithSelector(AaveAdapter.supplyByDelegation.selector, delegations_, address(USDC), DEPOSIT_AMOUNT) + }); + + // Execute delegation + Delegation[] memory supplyDelegations_ = new Delegation[](1); + supplyDelegations_[0] = supplyDelegation; + + invokeDelegation_UserOp(users.bob, supplyDelegations_, supplyExecution_); + + // Check state after delegation + uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + + IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); + uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + } +} + +// This is a POC for the AaveAdapter contract. +contract AaveAdapter { + IDelegationManager public immutable delegationManager; + IPool public immutable aavePool; + + constructor(address _delegationManager, address _aavePool) { + delegationManager = IDelegationManager(_delegationManager); + aavePool = IPool(_aavePool); + } + + // This function redeems the delegation then approves and supplies the token to Aave. + function supplyByDelegation(Delegation[] memory _delegations, address _token, uint256 _amount) external { + require(_delegations.length == 1, "Wrong number of delegations"); + require(_delegations[0].delegator == msg.sender, "Not allowed"); + + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(_delegations); + + ModeCode[] memory encodedModes_ = new ModeCode[](1); + encodedModes_[0] = ModeLib.encodeSimpleSingle(); + + bytes[] memory executionCallDatas_ = new bytes[](1); + + bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), _amount)); + executionCallDatas_[0] = ExecutionLib.encodeSingle(address(_token), 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + IERC20(_token).approve(address(aavePool), _amount); + aavePool.supply(_token, _amount, msg.sender, 0); + } } From d49a1f6298c4a2cb7e636502a889f31edb9a38c8 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Tue, 24 Jun 2025 17:03:48 +0200 Subject: [PATCH 4/7] Add open ended supply option to aave adapter and description --- test/helpers/AaveLending.t.sol | 74 +++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/test/helpers/AaveLending.t.sol b/test/helpers/AaveLending.t.sol index 1cab96dc..fb0e3dc2 100644 --- a/test/helpers/AaveLending.t.sol +++ b/test/helpers/AaveLending.t.sol @@ -34,6 +34,15 @@ import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol" * 2. We use a custom contract "AaveAdapter" to which alice delegates with only transfer balance. AaveAdapter * is then the one that takes care of approving tokens to Aave and doing the lending. This way the adapter is the * one that over approves tokens. Making it safer. But this introduces a middleware contract. + * Also there are 2 different ways of using the AaveAdapter: + * - supplyByDelegation - this is a more restrictive way where alice needs to create a transfer delegation to the adapter, + * then another delegation to the executor for him to call supplyByDelegation. This way alice can restrict who can call + * the aaveAdapter. + * - supplyByDelegationOpenEnded - this is less restrictive since anyone can call it as long as they have a valid transfer + * delegation + * to the adapter. + * + * In both cases only the creator of the transfer delegation can receive aTokens. */ contract AaveLendingTest is BaseTest { using ModeLib for ModeCode; @@ -455,7 +464,7 @@ contract AaveLendingTest is BaseTest { assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); } - // Testing delegating transfer to adapter and then delegating suppyByDelegation + // Testing delegating transfer to adapter and then delegating supplyByDelegation function test_aliceDelegatedDepositViaAdapterDelegation() public { // Check initial balance uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); @@ -527,6 +536,48 @@ contract AaveLendingTest is BaseTest { uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); } + + // Testing delegating transfer to adapter and then calling (can be called by anyone) suppyByDelegationOpenEnded + function test_aliceDelegatedDepositViaOpenEndedAdapterDelegation() public { + // Check initial balance + uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + + // Create delegation for transferring USDC to adapter + Caveat[] memory transferCaveats_ = new Caveat[](1); + + transferCaveats_[0] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encodePacked(address(USDC), DEPOSIT_AMOUNT) + }); + + // Create delegation for transfer + Delegation memory delegation = Delegation({ + delegate: address(aaveAdapter), + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: transferCaveats_, + salt: 0, + signature: hex"" + }); + + // Sign delegation + delegation = signDelegation(users.alice, delegation); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation; + + aaveAdapter.supplyByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); + + // Check state after delegation + uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + + IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); + uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + } } // This is a POC for the AaveAdapter contract. @@ -560,4 +611,25 @@ contract AaveAdapter { IERC20(_token).approve(address(aavePool), _amount); aavePool.supply(_token, _amount, msg.sender, 0); } + + // This function redeems the delegation then approves and supplies the token to Aave. + function supplyByDelegationOpenEnded(Delegation[] memory _delegations, address _token, uint256 _amount) external { + require(_delegations.length == 1, "Wrong number of delegations"); + + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(_delegations); + + ModeCode[] memory encodedModes_ = new ModeCode[](1); + encodedModes_[0] = ModeLib.encodeSimpleSingle(); + + bytes[] memory executionCallDatas_ = new bytes[](1); + + bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), _amount)); + executionCallDatas_[0] = ExecutionLib.encodeSingle(address(_token), 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + IERC20(_token).approve(address(aavePool), _amount); + aavePool.supply(_token, _amount, _delegations[0].delegator, 0); + } } From 1a70cc64e52598e8ae162fc80e30a89b3f19cc3b Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Wed, 25 Jun 2025 20:53:04 -0600 Subject: [PATCH 5/7] test: moved aave poc folder, added withdrawals --- src/helpers/AaveAdapter.sol | 147 ++++ .../helpers/interfaces/IAavePool.sol | 8 +- test/helpers/AaveLending.t.sol | 666 +++++++++--------- 3 files changed, 483 insertions(+), 338 deletions(-) create mode 100644 src/helpers/AaveAdapter.sol rename {test => src}/helpers/interfaces/IAavePool.sol (99%) diff --git a/src/helpers/AaveAdapter.sol b/src/helpers/AaveAdapter.sol new file mode 100644 index 00000000..be1922e7 --- /dev/null +++ b/src/helpers/AaveAdapter.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { IAavePool } from "./interfaces/IAavePool.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Delegation, ModeCode, Execution } from "../utils/Types.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +/// @title AaveAdapter +/// @notice Proof of concept adapter contract for Aave lending operations using delegations +/// @dev Handles token transfers and Aave supply operations through delegation-based permissions +contract AaveAdapter { + using SafeERC20 for IERC20; + + IDelegationManager public immutable delegationManager; + IAavePool public immutable aavePool; + + /// @notice Initializes the adapter with delegation manager and Aave pool addresses + /// @param _delegationManager Address of the delegation manager contract + /// @param _aavePool Address of the Aave lending pool contract + constructor(address _delegationManager, address _aavePool) { + delegationManager = IDelegationManager(_delegationManager); + aavePool = IAavePool(_aavePool); + } + + /// @notice Ensures sufficient token allowance for Aave operations + /// @dev Checks current allowance and increases to max if needed + /// @param _token Token to manage allowance for + /// @param _amount Amount needed for the operation + function _ensureAllowance(IERC20 _token, uint256 _amount) private { + uint256 allowance_ = _token.allowance(address(this), address(aavePool)); + if (allowance_ < _amount) { + _token.safeIncreaseAllowance(address(aavePool), type(uint256).max); + } + } + + /// @notice Supplies tokens to Aave using delegation-based token transfer + /// @dev Only the delegator can execute this function, ensuring full control over supply parameters + /// @param _delegations Array containing a single delegation for token transfer + /// @param _token Address of the token to supply to Aave + /// @param _amount Amount of tokens to supply + function supplyByDelegation(Delegation[] memory _delegations, address _token, uint256 _amount) external { + require(_delegations.length == 1, "Wrong number of delegations"); + require(_delegations[0].delegator == msg.sender, "Not allowed"); + + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(_delegations); + + ModeCode[] memory encodedModes_ = new ModeCode[](1); + encodedModes_[0] = ModeLib.encodeSimpleSingle(); + + bytes[] memory executionCallDatas_ = new bytes[](1); + + bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), _amount)); + executionCallDatas_[0] = ExecutionLib.encodeSingle(address(_token), 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + _ensureAllowance(IERC20(_token), _amount); + aavePool.supply(_token, _amount, msg.sender, 0); + } + + /// @notice Supplies tokens to Aave using delegation-based token transfer with open-ended execution + /// @dev Any delegate can execute this function, but aTokens are always credited to the delegator + /// @param _delegations Array containing a single delegation for token transfer + /// @param _token Address of the token to supply to Aave + /// @param _amount Amount of tokens to supply + function supplyByDelegationOpenEnded(Delegation[] memory _delegations, address _token, uint256 _amount) external { + require(_delegations.length == 1, "Wrong number of delegations"); + + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(_delegations); + + ModeCode[] memory encodedModes_ = new ModeCode[](1); + encodedModes_[0] = ModeLib.encodeSimpleSingle(); + + bytes[] memory executionCallDatas_ = new bytes[](1); + + bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), _amount)); + executionCallDatas_[0] = ExecutionLib.encodeSingle(address(_token), 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + _ensureAllowance(IERC20(_token), _amount); + aavePool.supply(_token, _amount, _delegations[0].delegator, 0); + } + + /// @notice Withdraws tokens from Aave using delegation-based aToken transfer + /// @dev Only the delegator can execute this function, ensuring full control over withdrawal parameters + /// @param _delegations Array containing a single delegation for aToken transfer + /// @param _token Address of the underlying token to withdraw from Aave + /// @param _amount Amount of tokens to withdraw (or type(uint256).max for all) + function withdrawByDelegation(Delegation[] memory _delegations, address _token, uint256 _amount) external { + require(_delegations.length == 1, "Wrong number of delegations"); + require(_delegations[0].delegator == msg.sender, "Not allowed"); + + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(_delegations); + + ModeCode[] memory encodedModes_ = new ModeCode[](1); + encodedModes_[0] = ModeLib.encodeSimpleSingle(); + + bytes[] memory executionCallDatas_ = new bytes[](1); + + // Get the aToken address for the underlying token + IERC20 aToken_ = IERC20(aavePool.getReserveAToken(_token)); + + bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), _amount)); + executionCallDatas_[0] = ExecutionLib.encodeSingle(address(aToken_), 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + // Withdraw from Aave directly to the delegator + aavePool.withdraw(_token, _amount, msg.sender); + } + + /// @notice Withdraws tokens from Aave using delegation-based aToken transfer with open-ended execution + /// @dev Any delegate can execute this function, but underlying tokens are always sent to the delegator + /// @param _delegations Array containing a single delegation for aToken transfer + /// @param _token Address of the underlying token to withdraw from Aave + /// @param _amount Amount of tokens to withdraw (or type(uint256).max for all) + function withdrawByDelegationOpenEnded(Delegation[] memory _delegations, address _token, uint256 _amount) external { + require(_delegations.length == 1, "Wrong number of delegations"); + + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(_delegations); + + ModeCode[] memory encodedModes_ = new ModeCode[](1); + encodedModes_[0] = ModeLib.encodeSimpleSingle(); + + bytes[] memory executionCallDatas_ = new bytes[](1); + + // Get the aToken address for the underlying token + IERC20 aToken_ = IERC20(aavePool.getReserveAToken(_token)); + + bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), _amount)); + executionCallDatas_[0] = ExecutionLib.encodeSingle(address(aToken_), 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + // Withdraw from Aave directly to the delegator + aavePool.withdraw(_token, _amount, _delegations[0].delegator); + } +} diff --git a/test/helpers/interfaces/IAavePool.sol b/src/helpers/interfaces/IAavePool.sol similarity index 99% rename from test/helpers/interfaces/IAavePool.sol rename to src/helpers/interfaces/IAavePool.sol index 6fc967a8..04f61830 100644 --- a/test/helpers/interfaces/IAavePool.sol +++ b/src/helpers/interfaces/IAavePool.sol @@ -1,14 +1,14 @@ // Based on: https://github.com/aave-dao/aave-v3-origin/blob/main/src/contracts/interfaces/IPool.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; /** - * @title IPool + * @title IAavePool * @author Aave * @notice Defines the basic interface for an Aave Pool. */ -interface IPool { +interface IAavePool { /** * @dev Emitted on mintUnbacked() * @param reserve The address of the underlying asset of the reserve diff --git a/test/helpers/AaveLending.t.sol b/test/helpers/AaveLending.t.sol index fb0e3dc2..7e76a09a 100644 --- a/test/helpers/AaveLending.t.sol +++ b/test/helpers/AaveLending.t.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.23; import { Test } from "forge-std/Test.sol"; -import { console2 } from "forge-std/console2.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; @@ -15,9 +14,10 @@ import { AllowedCalldataEnforcer } from "../../src/enforcers/AllowedCalldataEnfo import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; import { LogicalOrWrapperEnforcer } from "../../src/enforcers/LogicalOrWrapperEnforcer.sol"; import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; -import { IPool } from "./interfaces/IAavePool.sol"; +import { IAavePool } from "../../src/helpers/interfaces/IAavePool.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; +import { AaveAdapter } from "../../src/helpers/AaveAdapter.sol"; // @dev Do not remove this comment below /// forge-config: default.evm_version = "shanghai" @@ -26,29 +26,32 @@ import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol" * @title AaveLending Test * @notice Tests delegation-based lending on Aave v3. * @dev Uses a forked Ethereum mainnet environment to test real contract interactions. - * We are testing 2 different ways of using delegation framework to enable lending on Aave v3. - * 1. Alice delegates token approval on USDC and supply on Aave. This can be done in 2 seperate delegations - * or in a single delegation using LogicalOrWrapperEnforcer. This way the funds and approval are going - * directly from alice to USDC/Aave. But if we want to do rebalancing this means that alice would need to over - * approve tokens so that we can withdraw and deposit again. - * 2. We use a custom contract "AaveAdapter" to which alice delegates with only transfer balance. AaveAdapter - * is then the one that takes care of approving tokens to Aave and doing the lending. This way the adapter is the - * one that over approves tokens. Making it safer. But this introduces a middleware contract. - * Also there are 2 different ways of using the AaveAdapter: - * - supplyByDelegation - this is a more restrictive way where alice needs to create a transfer delegation to the adapter, - * then another delegation to the executor for him to call supplyByDelegation. This way alice can restrict who can call - * the aaveAdapter. - * - supplyByDelegationOpenEnded - this is less restrictive since anyone can call it as long as they have a valid transfer - * delegation - * to the adapter. * - * In both cases only the creator of the transfer delegation can receive aTokens. + * We are testing 2 different approaches to using the delegation framework for Aave v3 lending: + * + * 1. Direct delegation: Alice delegates token approval and supply operations on Aave. This can be done with + * either 2 separate delegations or a single delegation using LogicalOrWrapperEnforcer. Funds and approvals + * flow directly from Alice to USDC/Aave. However, for rebalancing scenarios, Alice would need to over-approve + * tokens to allow withdrawals and re-deposits. + * + * 2. Adapter pattern: We use a custom "AaveAdapter" contract where Alice delegates only transfer permissions. + * The AaveAdapter handles token approvals to Aave and executes lending operations. This approach centralizes + * over-approval in the adapter, making it safer, but introduces a middleware contract. + * + * The AaveAdapter supports two usage patterns: + * - supplyByDelegation: More restrictive - Alice creates a transfer delegation to the adapter, then another + * delegation to the executor for calling supplyByDelegation. This allows Alice to control who can call the adapter. + * - supplyByDelegationOpenEnded: Less restrictive - Anyone can call it as long as they have a valid transfer + * delegation to the adapter. + * + * In both adapter cases, only the creator of the transfer delegation receives aTokens. */ contract AaveLendingTest is BaseTest { using ModeLib for ModeCode; - IPool public constant AAVE_POOL = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2); + IAavePool public constant AAVE_POOL = IAavePool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2); IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + IERC20 public aUSDC; address public constant USDC_WHALE = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341; // Enforcers for delegation restrictions @@ -102,28 +105,29 @@ contract AaveLendingTest is BaseTest { vm.prank(USDC_WHALE); USDC.transfer(address(users.alice.deleGator), INITIAL_USDC_BALANCE); // 10k USDC + + aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); } // Testing directly depositing USDC to Aave to see if everything works on the forked mainnet - function test_aliceDirectDeposit() public { - uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + function test_deposit_direct() public { + uint256 aliceUSDCInitialBalance_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance_, INITIAL_USDC_BALANCE); vm.prank(address(users.alice.deleGator)); USDC.approve(address(AAVE_POOL), DEPOSIT_AMOUNT); vm.prank(address(users.alice.deleGator)); AAVE_POOL.supply(address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0); - uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + uint256 aliceUSDCBalance_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance_, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); - IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); - uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + uint256 aliceATokenBalance_ = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceATokenBalance_, DEPOSIT_AMOUNT); } // Testing directly withdrawing USDC from Aave to see if everything works on the forked mainnet - function test_aliceDirectWithdraw() public { + function test_withdraw_direct() public { vm.prank(address(users.alice.deleGator)); USDC.approve(address(AAVE_POOL), DEPOSIT_AMOUNT); vm.prank(address(users.alice.deleGator)); @@ -132,199 +136,78 @@ contract AaveLendingTest is BaseTest { vm.prank(address(users.alice.deleGator)); AAVE_POOL.withdraw(address(USDC), type(uint256).max, address(users.alice.deleGator)); - uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE); + _assertBalances(INITIAL_USDC_BALANCE, 0); } // Testing delegating approval and supply functions in 2 separate delegations - function test_aliceDelegatedDeposit() public { - // Check initial balance - uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); - - // Create delegation caveats for approving USDC - Caveat[] memory approveCaveats_ = new Caveat[](4); + function test_deposit_viaDelegation() public { + _assertBalances(INITIAL_USDC_BALANCE, 0); - // Recommended: Restrict to specific contract - approveCaveats_[0] = - Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(USDC)) }); + // Create and execute approval delegation + Delegation memory approveDelegation = + _createApprovalDelegation(address(users.bob.deleGator), address(AAVE_POOL), DEPOSIT_AMOUNT); - // Recommended: Restrict to deposit function only - approveCaveats_[1] = - Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IERC20.approve.selector) }); - - // Recommended: Restrict approve amount - uint256 paramStart_ = abi.encodeWithSelector(IERC20.approve.selector, address(0)).length; - approveCaveats_[2] = Caveat({ - args: hex"", - enforcer: address(allowedCalldataEnforcer), - terms: abi.encodePacked(paramStart_, DEPOSIT_AMOUNT) - }); - - // Recommended: Set value limit to 0 - approveCaveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); - - // Create delegation for approving USDC - Delegation memory approveDelegation = Delegation({ - delegate: address(users.bob.deleGator), - delegator: address(users.alice.deleGator), - authority: ROOT_AUTHORITY, - caveats: approveCaveats_, - salt: 0, - signature: hex"" - }); - - // Sign delegation - approveDelegation = signDelegation(users.alice, approveDelegation); - - // Create proper execution for approving USDC Execution memory approveExecution = Execution({ target: address(USDC), value: 0, callData: abi.encodeWithSelector(IERC20.approve.selector, address(AAVE_POOL), DEPOSIT_AMOUNT) }); - // Execute approve delegation - Delegation[] memory approveDelegations_ = new Delegation[](1); - approveDelegations_[0] = approveDelegation; - - invokeDelegation_UserOp(users.bob, approveDelegations_, approveExecution); - - // Create delegation caveats for lending - Caveat[] memory lendingCaveats_ = new Caveat[](4); - - // Recommended: Restrict to specific contract - lendingCaveats_[0] = - Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(AAVE_POOL)) }); - - // Recommended: Restrict to deposit function only - lendingCaveats_[1] = - Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IPool.supply.selector) }); - - // Recommended: Restrict supply argument "onBehalfOf" to alice - paramStart_ = abi.encodeWithSelector(IPool.supply.selector, address(0), uint256(0)).length; - lendingCaveats_[2] = Caveat({ - args: hex"", - enforcer: address(allowedCalldataEnforcer), - terms: abi.encode(paramStart_, address(users.alice.deleGator)) - }); - - // Recommended: Set value limit to 0 - lendingCaveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); - - // Create delegation for lending - Delegation memory lendingDelegation = Delegation({ - delegate: address(users.bob.deleGator), - delegator: address(users.alice.deleGator), - authority: ROOT_AUTHORITY, - caveats: lendingCaveats_, - salt: 0, - signature: hex"" - }); + Delegation[] memory approveDelegations = new Delegation[](1); + approveDelegations[0] = approveDelegation; + invokeDelegation_UserOp(users.bob, approveDelegations, approveExecution); - // Sign delegation - lendingDelegation = signDelegation(users.alice, lendingDelegation); + // Create and execute supply delegation + Delegation memory supplyDelegation = _createSupplyDelegation(address(users.bob.deleGator)); - // Create execution for lending - Execution memory lendingExecution_ = Execution({ + Execution memory supplyExecution = Execution({ target: address(AAVE_POOL), value: 0, - callData: abi.encodeWithSelector(IPool.supply.selector, address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0) + callData: abi.encodeWithSelector( + IAavePool.supply.selector, address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0 + ) }); - // Execute delegation - Delegation[] memory lendingDelegations_ = new Delegation[](1); - lendingDelegations_[0] = lendingDelegation; - - invokeDelegation_UserOp(users.bob, lendingDelegations_, lendingExecution_); - - // Check state after delegation - uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + Delegation[] memory supplyDelegations = new Delegation[](1); + supplyDelegations[0] = supplyDelegation; + invokeDelegation_UserOp(users.bob, supplyDelegations, supplyExecution); - IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); - uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); } // Testing delegating withdrawal function in a single delegation - function test_aliceDelegatedWithdraw() public { - // Set initial state of alice having deposited 1k USDC - vm.prank(address(users.alice.deleGator)); - USDC.approve(address(AAVE_POOL), DEPOSIT_AMOUNT); - vm.prank(address(users.alice.deleGator)); - AAVE_POOL.supply(address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0); - - // Record initial state - uint256 aliceInitialUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); - IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); - uint256 aliceInitialATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceInitialUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); - assertEq(aliceInitialATokenBalance, DEPOSIT_AMOUNT); - - // Create delegation caveats for lending witdrawal - Caveat[] memory lendingWithdrawalCaveats_ = new Caveat[](4); + function test_withdraw_viaDelegation() public { + _setupLendingState(); + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); - // Recommended: Restrict to specific contract - lendingWithdrawalCaveats_[0] = - Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(AAVE_POOL)) }); + // Create and execute withdrawal delegation + Delegation memory withdrawDelegation = _createWithdrawDelegation(address(users.bob.deleGator)); - // Recommended: Restrict to deposit function only - lendingWithdrawalCaveats_[1] = - Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IPool.withdraw.selector) }); - - // Recommended: Restrict withdraw argument "to" to alice - uint256 paramStart_ = abi.encodeWithSelector(IPool.withdraw.selector, address(0), uint256(0)).length; - lendingWithdrawalCaveats_[2] = Caveat({ - args: hex"", - enforcer: address(allowedCalldataEnforcer), - terms: abi.encode(paramStart_, address(users.alice.deleGator)) - }); - - // Recommended: Set value limit to 0 - lendingWithdrawalCaveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); - - // Create delegation for lending - Delegation memory lendingWithdrawalDelegation = Delegation({ - delegate: address(users.bob.deleGator), - delegator: address(users.alice.deleGator), - authority: ROOT_AUTHORITY, - caveats: lendingWithdrawalCaveats_, - salt: 0, - signature: hex"" - }); - - // Sign delegation - lendingWithdrawalDelegation = signDelegation(users.alice, lendingWithdrawalDelegation); - - // Create execution for lending - Execution memory lendingWithdrawalExecution_ = Execution({ + Execution memory withdrawExecution = Execution({ target: address(AAVE_POOL), value: 0, - callData: abi.encodeWithSelector(IPool.withdraw.selector, address(USDC), type(uint256).max, address(users.alice.deleGator)) + callData: abi.encodeWithSelector( + IAavePool.withdraw.selector, address(USDC), type(uint256).max, address(users.alice.deleGator) + ) }); - // Execute delegation - Delegation[] memory lendingWithdrawalDelegations_ = new Delegation[](1); - lendingWithdrawalDelegations_[0] = lendingWithdrawalDelegation; + Delegation[] memory withdrawDelegations = new Delegation[](1); + withdrawDelegations[0] = withdrawDelegation; + invokeDelegation_UserOp(users.bob, withdrawDelegations, withdrawExecution); - invokeDelegation_UserOp(users.bob, lendingWithdrawalDelegations_, lendingWithdrawalExecution_); - - // Check state after delegation - uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE); + _assertBalances(INITIAL_USDC_BALANCE, 0); } // Testing delegating approval and supply functions in a single delegation using LogicalOrWrapperEnforcer - function test_aliceDelegatedDepositWithLogicalOrWrapper() public { + function test_deposit_viaDelegationWithLogicalOrWrapper() public { // Check initial balance - uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + uint256 aliceUSDCInitialBalance_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance_, INITIAL_USDC_BALANCE); LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_ = new LogicalOrWrapperEnforcer.CaveatGroup[](2); // Create delegation caveats for approving USDC - Caveat[] memory approveCaveats_ = new Caveat[](4); + Caveat[] memory approveCaveats_ = new Caveat[](5); // Recommended: Restrict to specific contract approveCaveats_[0] = @@ -342,8 +225,13 @@ contract AaveLendingTest is BaseTest { terms: abi.encodePacked(paramStart_, DEPOSIT_AMOUNT) }); + // Recommended: Restrict approve recipient + paramStart_ = abi.encodeWithSelector(IERC20.approve.selector).length; + approveCaveats_[3] = + Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), terms: abi.encode(paramStart_, address(AAVE_POOL)) }); + // Recommended: Set value limit to 0 - approveCaveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); + approveCaveats_[4] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); // Create delegation caveats for lending Caveat[] memory lendingCaveats_ = new Caveat[](4); @@ -354,10 +242,10 @@ contract AaveLendingTest is BaseTest { // Recommended: Restrict to deposit function only lendingCaveats_[1] = - Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IPool.supply.selector) }); + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IAavePool.supply.selector) }); // Recommended: Restrict supply argument "onBehalfOf" to alice - paramStart_ = abi.encodeWithSelector(IPool.supply.selector, address(0), uint256(0)).length; + paramStart_ = abi.encodeWithSelector(IAavePool.supply.selector, address(0), uint256(0)).length; lendingCaveats_[2] = Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), @@ -374,7 +262,7 @@ contract AaveLendingTest is BaseTest { orCaveats_[0] = Caveat({ args: hex"", enforcer: address(logicalOrWrapperEnforcer), terms: abi.encode(groups_) }); // Create delegation for lending - Delegation memory delegation = Delegation({ + Delegation memory delegation_ = Delegation({ delegate: address(users.bob.deleGator), delegator: address(users.alice.deleGator), authority: ROOT_AUTHORITY, @@ -384,7 +272,7 @@ contract AaveLendingTest is BaseTest { }); // Sign delegation - delegation = signDelegation(users.alice, delegation); + delegation_ = signDelegation(users.alice, delegation_); // Create proper execution for approving USDC Execution memory approveExecution_ = Execution({ @@ -397,15 +285,17 @@ contract AaveLendingTest is BaseTest { Execution memory lendingExecution_ = Execution({ target: address(AAVE_POOL), value: 0, - callData: abi.encodeWithSelector(IPool.supply.selector, address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0) + callData: abi.encodeWithSelector( + IAavePool.supply.selector, address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0 + ) }); // Execute delegation Delegation[] memory delegations_ = new Delegation[](1); - delegations_[0] = delegation; + delegations_[0] = delegation_; delegations_[0].caveats[0].args = - abi.encode(LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: 0, caveatArgs: new bytes[](4) })); + abi.encode(LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: 0, caveatArgs: new bytes[](5) })); invokeDelegation_UserOp(users.bob, delegations_, approveExecution_); delegations_[0].caveats[0].args = @@ -413,223 +303,331 @@ contract AaveLendingTest is BaseTest { invokeDelegation_UserOp(users.bob, delegations_, lendingExecution_); // Check state after delegation - uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + uint256 aliceUSDCBalance_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance_, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); - IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); - uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + uint256 aliceATokenBalance_ = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceATokenBalance_, DEPOSIT_AMOUNT); } // Testing delegating transfer to adapter and then supply by delegation - function test_aliceDelegatedDepositViaAdapter() public { - // Check initial balance - uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + function test_deposit_viaAdapter() public { + _assertBalances(INITIAL_USDC_BALANCE, 0); - // Create delegation for transferring USDC to adapter - Caveat[] memory transferCaveats_ = new Caveat[](1); - - transferCaveats_[0] = Caveat({ - args: hex"", - enforcer: address(erc20TransferAmountEnforcer), - terms: abi.encodePacked(address(USDC), DEPOSIT_AMOUNT) - }); - - // Create delegation for transfer - Delegation memory delegation = Delegation({ - delegate: address(aaveAdapter), - delegator: address(users.alice.deleGator), - authority: ROOT_AUTHORITY, - caveats: transferCaveats_, - salt: 0, - signature: hex"" - }); - - // Sign delegation - delegation = signDelegation(users.alice, delegation); + // Create transfer delegation to adapter + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); Delegation[] memory delegations_ = new Delegation[](1); - delegations_[0] = delegation; + delegations_[0] = delegation_; vm.prank(address(users.alice.deleGator)); aaveAdapter.supplyByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); - // Check state after delegation - uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); - - IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); - uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); } // Testing delegating transfer to adapter and then delegating supplyByDelegation - function test_aliceDelegatedDepositViaAdapterDelegation() public { - // Check initial balance - uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + function test_deposit_viaAdapterDelegation() public { + _assertBalances(INITIAL_USDC_BALANCE, 0); - // Create delegation for transferring USDC to adapter - Caveat[] memory transferCaveats_ = new Caveat[](1); + // Create transfer delegation to adapter and target delegation for adapter call + Delegation memory transferDelegation_ = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + Delegation memory adapterDelegation_ = _createTargetRestrictedDelegation(address(users.bob.deleGator), address(aaveAdapter)); - transferCaveats_[0] = Caveat({ - args: hex"", - enforcer: address(erc20TransferAmountEnforcer), - terms: abi.encodePacked(address(USDC), DEPOSIT_AMOUNT) - }); + Delegation[] memory transferDelegations_ = new Delegation[](1); + transferDelegations_[0] = transferDelegation_; - // Create delegation for transfer - Delegation memory delegation = Delegation({ - delegate: address(aaveAdapter), - delegator: address(users.alice.deleGator), - authority: ROOT_AUTHORITY, - caveats: transferCaveats_, - salt: 0, - signature: hex"" + // Create execution for supply via adapter + Execution memory supplyExecution_ = Execution({ + target: address(aaveAdapter), + value: 0, + callData: abi.encodeWithSelector( + AaveAdapter.supplyByDelegation.selector, transferDelegations_, address(USDC), DEPOSIT_AMOUNT + ) }); - // Sign delegation - delegation = signDelegation(users.alice, delegation); + Delegation[] memory adapterDelegations_ = new Delegation[](1); + adapterDelegations_[0] = adapterDelegation_; + + invokeDelegation_UserOp(users.bob, adapterDelegations_, supplyExecution_); + + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + } + + // Testing delegating transfer to adapter and then calling (can be called by anyone) suppyByDelegationOpenEnded + function test_deposit_viaOpenEndedAdapterDelegation() public { + _assertBalances(INITIAL_USDC_BALANCE, 0); + + // Create transfer delegation to adapter + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); Delegation[] memory delegations_ = new Delegation[](1); - delegations_[0] = delegation; + delegations_[0] = delegation_; - // Create delegation caveats for lending - Caveat[] memory supplyCaveats_ = new Caveat[](1); + aaveAdapter.supplyByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); + + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + } + + // Testing delegating transfer of aTokens to adapter and then withdraw by delegation + function test_withdraw_viaAdapter() public { + _setupLendingState(); + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + + // Create transfer delegation to adapter + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + vm.prank(address(users.alice.deleGator)); + aaveAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + + _assertBalances(INITIAL_USDC_BALANCE, 0); + } + + // Testing delegating transfer of aTokens to adapter and then delegating withdrawByDelegation + function test_withdraw_viaAdapterDelegation() public { + _setupLendingState(); + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // Create delegation caveats for withdrawal + Caveat[] memory withdrawalCaveats_ = new Caveat[](1); // Recommended: Restrict to specific contract - supplyCaveats_[0] = + withdrawalCaveats_[0] = Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(aaveAdapter)) }); - // Create delegation for supply - Delegation memory supplyDelegation = Delegation({ + // Create delegation for withdrawal + Delegation memory withdrawalDelegation_ = Delegation({ delegate: address(users.bob.deleGator), delegator: address(users.alice.deleGator), authority: ROOT_AUTHORITY, - caveats: supplyCaveats_, + caveats: withdrawalCaveats_, salt: 0, signature: hex"" }); // Sign delegation - supplyDelegation = signDelegation(users.alice, supplyDelegation); + withdrawalDelegation_ = signDelegation(users.alice, withdrawalDelegation_); - // Create execution for supply - Execution memory supplyExecution_ = Execution({ + // Create execution for withdrawal + Execution memory withdrawalExecution_ = Execution({ target: address(aaveAdapter), value: 0, - callData: abi.encodeWithSelector(AaveAdapter.supplyByDelegation.selector, delegations_, address(USDC), DEPOSIT_AMOUNT) + callData: abi.encodeWithSelector(AaveAdapter.withdrawByDelegation.selector, delegations_, address(USDC), DEPOSIT_AMOUNT) }); // Execute delegation - Delegation[] memory supplyDelegations_ = new Delegation[](1); - supplyDelegations_[0] = supplyDelegation; + Delegation[] memory withdrawalDelegations_ = new Delegation[](1); + withdrawalDelegations_[0] = withdrawalDelegation_; - invokeDelegation_UserOp(users.bob, supplyDelegations_, supplyExecution_); + invokeDelegation_UserOp(users.bob, withdrawalDelegations_, withdrawalExecution_); - // Check state after delegation - uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + _assertBalances(INITIAL_USDC_BALANCE, 0); + } + + // Testing delegating transfer of aTokens to adapter and then calling (can be called by anyone) withdrawByDelegationOpenEnded + function test_withdraw_viaOpenEndedAdapterDelegation() public { + _setupLendingState(); + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + aaveAdapter.withdrawByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); - IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); - uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); + _assertBalances(INITIAL_USDC_BALANCE, 0); } - // Testing delegating transfer to adapter and then calling (can be called by anyone) suppyByDelegationOpenEnded - function test_aliceDelegatedDepositViaOpenEndedAdapterDelegation() public { - // Check initial balance - uint256 aliceUSDCInitialBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCInitialBalance, INITIAL_USDC_BALANCE); + ////////////////////// Helpers ////////////////////// - // Create delegation for transferring USDC to adapter - Caveat[] memory transferCaveats_ = new Caveat[](1); + /// @notice Creates a transfer delegation with ERC20TransferAmountEnforcer + /// @param delegate Address that can execute the delegation + /// @param token Token to transfer + /// @param amount Amount to transfer + /// @return Signed delegation ready for execution + function _createTransferDelegation(address delegate, address token, uint256 amount) internal view returns (Delegation memory) { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: hex"", enforcer: address(erc20TransferAmountEnforcer), terms: abi.encodePacked(token, amount) }); - transferCaveats_[0] = Caveat({ - args: hex"", - enforcer: address(erc20TransferAmountEnforcer), - terms: abi.encodePacked(address(USDC), DEPOSIT_AMOUNT) + Delegation memory delegation_ = Delegation({ + delegate: delegate, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" }); - // Create delegation for transfer - Delegation memory delegation = Delegation({ - delegate: address(aaveAdapter), + return signDelegation(users.alice, delegation_); + } + + /// @notice Creates a target-restricted delegation + /// @param delegate Address that can execute the delegation + /// @param target Allowed target address + /// @return Signed delegation ready for execution + function _createTargetRestrictedDelegation(address delegate, address target) internal view returns (Delegation memory) { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(target) }); + + Delegation memory delegation_ = Delegation({ + delegate: delegate, delegator: address(users.alice.deleGator), authority: ROOT_AUTHORITY, - caveats: transferCaveats_, + caveats: caveats_, salt: 0, signature: hex"" }); - // Sign delegation - delegation = signDelegation(users.alice, delegation); + return signDelegation(users.alice, delegation_); + } - Delegation[] memory delegations_ = new Delegation[](1); - delegations_[0] = delegation; + /// @notice Creates a comprehensive approval delegation with all security restrictions + /// @param delegate Address that can execute the delegation + /// @param spender Address to approve + /// @param amount Amount to approve + /// @return Signed delegation ready for execution + function _createApprovalDelegation( + address delegate, + address spender, + uint256 amount + ) + internal + view + returns (Delegation memory) + { + Caveat[] memory caveats_ = new Caveat[](5); + + // Restrict to USDC contract + caveats_[0] = Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(USDC)) }); + + // Restrict to approve function + caveats_[1] = + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IERC20.approve.selector) }); - aaveAdapter.supplyByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); + // Restrict approve recipient + uint256 paramStart_ = abi.encodeWithSelector(IERC20.approve.selector).length; + caveats_[2] = Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), terms: abi.encode(paramStart_, spender) }); - // Check state after delegation - uint256 aliceUSDCBalance = USDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceUSDCBalance, INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT); + // Restrict approve amount + paramStart_ = abi.encodeWithSelector(IERC20.approve.selector, address(0)).length; + caveats_[3] = + Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), terms: abi.encodePacked(paramStart_, amount) }); - IERC20 aUSDC = IERC20(AAVE_POOL.getReserveAToken(address(USDC))); - uint256 aliceATokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); - assertEq(aliceATokenBalance, DEPOSIT_AMOUNT); - } -} + // Set value limit to 0 + caveats_[4] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); -// This is a POC for the AaveAdapter contract. -contract AaveAdapter { - IDelegationManager public immutable delegationManager; - IPool public immutable aavePool; + Delegation memory delegation_ = Delegation({ + delegate: delegate, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); - constructor(address _delegationManager, address _aavePool) { - delegationManager = IDelegationManager(_delegationManager); - aavePool = IPool(_aavePool); + return signDelegation(users.alice, delegation_); } - // This function redeems the delegation then approves and supplies the token to Aave. - function supplyByDelegation(Delegation[] memory _delegations, address _token, uint256 _amount) external { - require(_delegations.length == 1, "Wrong number of delegations"); - require(_delegations[0].delegator == msg.sender, "Not allowed"); + /// @notice Creates a supply delegation with all security restrictions + /// @param delegate Address that can execute the delegation + /// @return Signed delegation ready for execution + function _createSupplyDelegation(address delegate) internal view returns (Delegation memory) { + Caveat[] memory caveats_ = new Caveat[](4); - bytes[] memory permissionContexts_ = new bytes[](1); - permissionContexts_[0] = abi.encode(_delegations); + // Restrict to Aave pool + caveats_[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(AAVE_POOL)) }); - ModeCode[] memory encodedModes_ = new ModeCode[](1); - encodedModes_[0] = ModeLib.encodeSimpleSingle(); + // Restrict to supply function + caveats_[1] = + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IAavePool.supply.selector) }); - bytes[] memory executionCallDatas_ = new bytes[](1); + // Restrict onBehalfOf to Alice + uint256 paramStart_ = abi.encodeWithSelector(IAavePool.supply.selector, address(0), uint256(0)).length; + caveats_[2] = Caveat({ + args: hex"", + enforcer: address(allowedCalldataEnforcer), + terms: abi.encode(paramStart_, address(users.alice.deleGator)) + }); - bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), _amount)); - executionCallDatas_[0] = ExecutionLib.encodeSingle(address(_token), 0, encodedTransfer_); + // Set value limit to 0 + caveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); - delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + Delegation memory delegation_ = Delegation({ + delegate: delegate, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); - IERC20(_token).approve(address(aavePool), _amount); - aavePool.supply(_token, _amount, msg.sender, 0); + return signDelegation(users.alice, delegation_); } - // This function redeems the delegation then approves and supplies the token to Aave. - function supplyByDelegationOpenEnded(Delegation[] memory _delegations, address _token, uint256 _amount) external { - require(_delegations.length == 1, "Wrong number of delegations"); + /// @notice Creates a withdraw delegation with all security restrictions + /// @param delegate Address that can execute the delegation + /// @return Signed delegation ready for execution + function _createWithdrawDelegation(address delegate) internal view returns (Delegation memory) { + Caveat[] memory caveats_ = new Caveat[](4); + + // Restrict to Aave pool + caveats_[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(AAVE_POOL)) }); + + // Restrict to withdraw function + caveats_[1] = + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IAavePool.withdraw.selector) }); + + // Restrict to parameter to Alice + uint256 paramStart_ = abi.encodeWithSelector(IAavePool.withdraw.selector, address(0), uint256(0)).length; + caveats_[2] = Caveat({ + args: hex"", + enforcer: address(allowedCalldataEnforcer), + terms: abi.encode(paramStart_, address(users.alice.deleGator)) + }); - bytes[] memory permissionContexts_ = new bytes[](1); - permissionContexts_[0] = abi.encode(_delegations); + // Set value limit to 0 + caveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); - ModeCode[] memory encodedModes_ = new ModeCode[](1); - encodedModes_[0] = ModeLib.encodeSimpleSingle(); + Delegation memory delegation_ = Delegation({ + delegate: delegate, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); - bytes[] memory executionCallDatas_ = new bytes[](1); + return signDelegation(users.alice, delegation_); + } - bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), _amount)); - executionCallDatas_[0] = ExecutionLib.encodeSingle(address(_token), 0, encodedTransfer_); + /// @notice Sets up initial lending state (Alice deposits USDC to get aTokens) + function _setupLendingState() internal { + vm.prank(address(users.alice.deleGator)); + USDC.approve(address(AAVE_POOL), DEPOSIT_AMOUNT); + vm.prank(address(users.alice.deleGator)); + AAVE_POOL.supply(address(USDC), DEPOSIT_AMOUNT, address(users.alice.deleGator), 0); + } - delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + /// @notice Asserts Alice's USDC and aUSDC balances + /// @param expectedUSDC Expected USDC balance + /// @param expectedAUSDC Expected aUSDC balance + function _assertBalances(uint256 expectedUSDC, uint256 expectedAUSDC) internal { + uint256 aliceUSDCBalance_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance_, expectedUSDC, "USDC balance mismatch"); - IERC20(_token).approve(address(aavePool), _amount); - aavePool.supply(_token, _amount, _delegations[0].delegator, 0); + uint256 aliceATokenBalance_ = aUSDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceATokenBalance_, expectedAUSDC, "aUSDC balance mismatch"); } } From 8de913848f3acc0b2524ce78a2fc113f3e353fb9 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Wed, 2 Jul 2025 09:12:40 -0600 Subject: [PATCH 6/7] chore: added tests, docs, coverage fix --- README.md | 6 + documents/Adapters.md | 17 ++ script/coverage.sh | 96 ++++++++- src/helpers/AaveAdapter.sol | 61 +++++- test/helpers/AaveLending.t.sol | 356 +++++++++++++++++++++++++++++++++ 5 files changed, 526 insertions(+), 10 deletions(-) create mode 100644 documents/Adapters.md diff --git a/README.md b/README.md index 0a4f1364..3ca4ae00 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,12 @@ Developers can build new Caveat Enforcers for their own use cases, and the possi [Read more on "Caveats" ->](/documents/DelegationManager.md#Caveats) +### Delegation Adapters + +Delegation Adapters are specialized contracts that bridge the gap between the delegation framework and external protocols that don't natively support delegations. + +[Read more on "Delegation Adapters" ->](/documents/Adapters.md) + ## Development ### Third Party Developers diff --git a/documents/Adapters.md b/documents/Adapters.md new file mode 100644 index 00000000..18e73fae --- /dev/null +++ b/documents/Adapters.md @@ -0,0 +1,17 @@ +# Delegation Adapters + +Delegation adapters are specialized contracts that simplify integration between the delegation framework and external protocols that don't natively support delegations. Many DeFi protocols require users to perform multi-step operations—typically providing ERC-20 approvals followed by specific function calls—which adapters combine into single, atomic, delegatable executions. This enables users to delegate complex protocol interactions while ensuring outputs are delivered to the root delegator or their specified recipients. Adapters are particularly valuable for enabling non-DeleGator accounts to redeem delegations and meet delegator-specified requirements. Since most existing protocols haven't yet implemented native delegation support, adapters serve as an essential bridge layer that converts delegation-based permissions into protocol-compatible interactions while enforcing proper restrictions and safeguards during redemption. + +## Current Adapters + +### DelegationMetaSwapAdapter + +Facilitates token swaps through DEX aggregators by leveraging ERC-20 delegations with enforced outcome validation. This adapter integrates with the MetaSwap aggregator to execute optimal token swaps while maintaining delegation-based access control. It enables users to delegate ERC-20 token permissions and add conditions for enforced outcomes by the end of redemption. The adapter creates a self-redemption mechanism that atomically executes both the ERC-20 transfer and swap during the execution phase, followed by outcome validation in the afterAllHooks phase. It supports configurable token whitelisting through caveat enforcers and includes signature validation with expiration timestamps for secure API integration. + +### LiquidStakingAdapter + +Manages stETH withdrawal operations through Lido's withdrawal queue using delegation-based permissions. This adapter specifically focuses on withdrawal functions that require ERC-20 approvals, while deposit operations are excluded since they only require ETH and do not need ERC-20 approval mechanisms. The adapter facilitates stETH withdrawal requests by using delegations to transfer stETH tokens and supports both delegation-based transfers and ERC-20 permit signatures for gasless approvals. It has been designed to enhance the delegation experience by allowing users to enforce stricter delegation restrictions related to the Lido protocol, ensuring that redeemers must send withdrawal request ownership and any resulting tokens directly to the root delegator. + +### AaveAdapter + +Enables delegation-based lending and borrowing operations on the Aave protocol through secure token transfer delegations. This adapter facilitates both supply and withdrawal operations while maintaining full delegator control over asset custody and destination. The adapter supports flexible execution models including delegator-only operations for maximum control and open-ended execution where authorized delegates can perform operations on behalf of the delegator. Supply operations use ERC-20 token delegations to transfer underlying tokens to the adapter, which then supplies them to Aave with aTokens always being credited to the original delegator. Withdrawal operations leverage aToken delegations to transfer yield-bearing tokens to the adapter, which then redeems them from Aave with underlying tokens always being sent back to the delegator. The adapter eliminates the need for traditional ERC-20 approvals by using delegation-based token transfers, providing more granular control and enhanced security for DeFi lending interactions. diff --git a/script/coverage.sh b/script/coverage.sh index f9bed604..8314de1b 100755 --- a/script/coverage.sh +++ b/script/coverage.sh @@ -7,17 +7,105 @@ cd .. folder_path="coverage" if [ ! -d "$folder_path" ]; then - # If not, create the folder mkdir -p "$folder_path" echo "Folder created at: $folder_path" else echo "Folder already exists at: $folder_path" fi +# Configuration: Define test files for different EVM versions +declare -a SHANGHAI_TESTS=( + "test/helpers/AaveLending.t.sol" + # Add more shanghai tests here in the future + # "test/helpers/AnotherShanghaiTest.t.sol" +) +declare -a CANCUN_TESTS=( + # Add cancun tests here when needed + # "test/helpers/CancunTest.t.sol" +) -# Generates lcov.info -forge coverage --report lcov --skip scripts --report-file "$folder_path/lcov.info" +# Function to build match patterns for forge coverage +build_match_patterns() { + local tests=("$@") + local patterns="" + + for test in "${tests[@]}"; do + if [[ -n "$patterns" ]]; then + patterns="$patterns --match-path *$(basename "$test")" + else + patterns="--match-path *$(basename "$test")" + fi + done + + echo "$patterns" +} + +# Function to build no-match patterns for forge coverage +build_no_match_patterns() { + local tests=("$@") + local patterns="" + + for test in "${tests[@]}"; do + if [[ -n "$patterns" ]]; then + patterns="$patterns --no-match-path *$(basename "$test")" + else + patterns="--no-match-path *$(basename "$test")" + fi + done + + echo "$patterns" +} + +echo "Running coverage with inline EVM version flags..." +echo "-----------------------------------------------" + +# Build list of all special EVM tests to exclude from default London run +ALL_SPECIAL_EVM_TESTS=("${SHANGHAI_TESTS[@]}" "${CANCUN_TESTS[@]}") +LONDON_NO_MATCH_PATTERNS=$(build_no_match_patterns "${ALL_SPECIAL_EVM_TESTS[@]}") + +# Generate coverage for London EVM (default) - exclude special EVM tests +if [[ -n "$LONDON_NO_MATCH_PATTERNS" ]]; then + echo "Running coverage for London EVM..." + echo "Excluding: ${ALL_SPECIAL_EVM_TESTS[*]}" + forge coverage --evm-version london --report lcov --skip scripts $LONDON_NO_MATCH_PATTERNS --report-file "$folder_path/lcov-london.info" +else + echo "Running coverage for London EVM - no exclusions..." + forge coverage --evm-version london --report lcov --skip scripts --report-file "$folder_path/lcov-london.info" +fi + +# Generate coverage for Shanghai EVM tests if any exist +if [ ${#SHANGHAI_TESTS[@]} -gt 0 ]; then + echo "Running coverage for Shanghai EVM..." + echo "Including: ${SHANGHAI_TESTS[*]}" + SHANGHAI_MATCH_PATTERNS=$(build_match_patterns "${SHANGHAI_TESTS[@]}") + forge coverage --evm-version shanghai --report lcov --skip scripts $SHANGHAI_MATCH_PATTERNS --report-file "$folder_path/lcov-shanghai.info" +fi + +# Generate coverage for Cancun EVM tests if any exist +if [ ${#CANCUN_TESTS[@]} -gt 0 ]; then + echo "Running coverage for Cancun EVM..." + echo "Including: ${CANCUN_TESTS[*]}" + CANCUN_MATCH_PATTERNS=$(build_match_patterns "${CANCUN_TESTS[@]}") + forge coverage --evm-version cancun --report lcov --skip scripts $CANCUN_MATCH_PATTERNS --report-file "$folder_path/lcov-cancun.info" +fi + +# Build the list of coverage files to merge +COVERAGE_FILES=("$folder_path/lcov-london.info") +if [ ${#SHANGHAI_TESTS[@]} -gt 0 ]; then + COVERAGE_FILES+=("$folder_path/lcov-shanghai.info") +fi +if [ ${#CANCUN_TESTS[@]} -gt 0 ]; then + COVERAGE_FILES+=("$folder_path/lcov-cancun.info") +fi + +# Merge the lcov files +echo "Merging coverage reports..." +echo "Files to merge: ${COVERAGE_FILES[*]}" +lcov \ + --rc branch_coverage=1 \ + $(printf -- "--add-tracefile %s " "${COVERAGE_FILES[@]}") \ + --output-file "$folder_path/lcov.info" # Filter out test, mock, and script files lcov \ @@ -39,4 +127,4 @@ then --output-directory "$folder_path" \ "$folder_path/filtered-lcov.info" open "$folder_path/index.html" -fi \ No newline at end of file +fi \ No newline at end of file diff --git a/src/helpers/AaveAdapter.sol b/src/helpers/AaveAdapter.sol index be1922e7..0410803b 100644 --- a/src/helpers/AaveAdapter.sol +++ b/src/helpers/AaveAdapter.sol @@ -15,17 +15,52 @@ import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; contract AaveAdapter { using SafeERC20 for IERC20; + ////////////////////// Events ////////////////////// + + /// @notice Emitted when a supply operation is executed via delegation + /// @param delegator Address of the token owner (delegator) + /// @param delegate Address of the executor (delegate) + /// @param token Address of the supplied token + /// @param amount Amount of tokens supplied + event SupplyExecuted(address indexed delegator, address indexed delegate, address indexed token, uint256 amount); + + /// @notice Emitted when a withdrawal operation is executed via delegation + /// @param delegator Address of the token owner (delegator) + /// @param delegate Address of the executor (delegate) + /// @param token Address of the withdrawn token + /// @param amount Amount of tokens withdrawn + event WithdrawExecuted(address indexed delegator, address indexed delegate, address indexed token, uint256 amount); + + ////////////////////// Errors ////////////////////// + + /// @notice Thrown when a zero address is provided for required parameters + error InvalidZeroAddress(); + + /// @notice Thrown when the number of delegations provided is not exactly one + error InvalidDelegationsLength(); + + /// @notice Thrown when the caller is not the delegator for restricted functions + error UnauthorizedCaller(); + + ////////////////////// State ////////////////////// + IDelegationManager public immutable delegationManager; IAavePool public immutable aavePool; + ////////////////////// Constructor ////////////////////// + /// @notice Initializes the adapter with delegation manager and Aave pool addresses /// @param _delegationManager Address of the delegation manager contract /// @param _aavePool Address of the Aave lending pool contract constructor(address _delegationManager, address _aavePool) { + if (_delegationManager == address(0) || _aavePool == address(0)) revert InvalidZeroAddress(); + delegationManager = IDelegationManager(_delegationManager); aavePool = IAavePool(_aavePool); } + ////////////////////// Private Functions ////////////////////// + /// @notice Ensures sufficient token allowance for Aave operations /// @dev Checks current allowance and increases to max if needed /// @param _token Token to manage allowance for @@ -37,14 +72,17 @@ contract AaveAdapter { } } + ////////////////////// Public Functions ////////////////////// + /// @notice Supplies tokens to Aave using delegation-based token transfer /// @dev Only the delegator can execute this function, ensuring full control over supply parameters /// @param _delegations Array containing a single delegation for token transfer /// @param _token Address of the token to supply to Aave /// @param _amount Amount of tokens to supply function supplyByDelegation(Delegation[] memory _delegations, address _token, uint256 _amount) external { - require(_delegations.length == 1, "Wrong number of delegations"); - require(_delegations[0].delegator == msg.sender, "Not allowed"); + if (_delegations.length != 1) revert InvalidDelegationsLength(); + if (_delegations[0].delegator != msg.sender) revert UnauthorizedCaller(); + if (_token == address(0)) revert InvalidZeroAddress(); bytes[] memory permissionContexts_ = new bytes[](1); permissionContexts_[0] = abi.encode(_delegations); @@ -61,6 +99,8 @@ contract AaveAdapter { _ensureAllowance(IERC20(_token), _amount); aavePool.supply(_token, _amount, msg.sender, 0); + + emit SupplyExecuted(msg.sender, msg.sender, _token, _amount); } /// @notice Supplies tokens to Aave using delegation-based token transfer with open-ended execution @@ -69,7 +109,8 @@ contract AaveAdapter { /// @param _token Address of the token to supply to Aave /// @param _amount Amount of tokens to supply function supplyByDelegationOpenEnded(Delegation[] memory _delegations, address _token, uint256 _amount) external { - require(_delegations.length == 1, "Wrong number of delegations"); + if (_delegations.length != 1) revert InvalidDelegationsLength(); + if (_token == address(0)) revert InvalidZeroAddress(); bytes[] memory permissionContexts_ = new bytes[](1); permissionContexts_[0] = abi.encode(_delegations); @@ -86,6 +127,8 @@ contract AaveAdapter { _ensureAllowance(IERC20(_token), _amount); aavePool.supply(_token, _amount, _delegations[0].delegator, 0); + + emit SupplyExecuted(_delegations[0].delegator, msg.sender, _token, _amount); } /// @notice Withdraws tokens from Aave using delegation-based aToken transfer @@ -94,8 +137,9 @@ contract AaveAdapter { /// @param _token Address of the underlying token to withdraw from Aave /// @param _amount Amount of tokens to withdraw (or type(uint256).max for all) function withdrawByDelegation(Delegation[] memory _delegations, address _token, uint256 _amount) external { - require(_delegations.length == 1, "Wrong number of delegations"); - require(_delegations[0].delegator == msg.sender, "Not allowed"); + if (_delegations.length != 1) revert InvalidDelegationsLength(); + if (_delegations[0].delegator != msg.sender) revert UnauthorizedCaller(); + if (_token == address(0)) revert InvalidZeroAddress(); bytes[] memory permissionContexts_ = new bytes[](1); permissionContexts_[0] = abi.encode(_delegations); @@ -115,6 +159,8 @@ contract AaveAdapter { // Withdraw from Aave directly to the delegator aavePool.withdraw(_token, _amount, msg.sender); + + emit WithdrawExecuted(msg.sender, msg.sender, _token, _amount); } /// @notice Withdraws tokens from Aave using delegation-based aToken transfer with open-ended execution @@ -123,7 +169,8 @@ contract AaveAdapter { /// @param _token Address of the underlying token to withdraw from Aave /// @param _amount Amount of tokens to withdraw (or type(uint256).max for all) function withdrawByDelegationOpenEnded(Delegation[] memory _delegations, address _token, uint256 _amount) external { - require(_delegations.length == 1, "Wrong number of delegations"); + if (_delegations.length != 1) revert InvalidDelegationsLength(); + if (_token == address(0)) revert InvalidZeroAddress(); bytes[] memory permissionContexts_ = new bytes[](1); permissionContexts_[0] = abi.encode(_delegations); @@ -143,5 +190,7 @@ contract AaveAdapter { // Withdraw from Aave directly to the delegator aavePool.withdraw(_token, _amount, _delegations[0].delegator); + + emit WithdrawExecuted(_delegations[0].delegator, msg.sender, _token, _amount); } } diff --git a/test/helpers/AaveLending.t.sol b/test/helpers/AaveLending.t.sol index 7e76a09a..c0db503b 100644 --- a/test/helpers/AaveLending.t.sol +++ b/test/helpers/AaveLending.t.sol @@ -447,6 +447,362 @@ contract AaveLendingTest is BaseTest { _assertBalances(INITIAL_USDC_BALANCE, 0); } + ////////////////////// Event Tests ////////////////////// + + /// @notice Tests that verify events are properly emitted by AaveAdapter functions + function test_supplyByDelegation_emitsSupplyExecutedEvent() public { + _assertBalances(INITIAL_USDC_BALANCE, 0); + + // Create transfer delegation to adapter + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // Expect the SupplyExecuted event to be emitted + vm.expectEmit(true, true, true, true, address(aaveAdapter)); + emit AaveAdapter.SupplyExecuted( + address(users.alice.deleGator), address(users.alice.deleGator), address(USDC), DEPOSIT_AMOUNT + ); + + vm.prank(address(users.alice.deleGator)); + aaveAdapter.supplyByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + } + + /// @notice Tests that supplyByDelegationOpenEnded emits the correct event when bob supplies on alice's behalf + function test_supplyByDelegationOpenEnded_emitsSupplyExecutedEvent() public { + _assertBalances(INITIAL_USDC_BALANCE, 0); + + // Create transfer delegation to adapter + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // Expect the SupplyExecuted event to be emitted with different delegate (bob calling on alice's behalf) + vm.expectEmit(true, true, true, true, address(aaveAdapter)); + emit AaveAdapter.SupplyExecuted(address(users.alice.deleGator), address(users.bob.deleGator), address(USDC), DEPOSIT_AMOUNT); + + vm.prank(address(users.bob.deleGator)); + aaveAdapter.supplyByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); + + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + } + + /// @notice Tests that withdrawByDelegation emits the correct event when alice withdraws her own funds + function test_withdrawByDelegation_emitsWithdrawExecutedEvent() public { + _setupLendingState(); + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + + // Create transfer delegation to adapter + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // Expect the WithdrawExecuted event to be emitted + vm.expectEmit(true, true, true, true, address(aaveAdapter)); + emit AaveAdapter.WithdrawExecuted( + address(users.alice.deleGator), address(users.alice.deleGator), address(USDC), DEPOSIT_AMOUNT + ); + + vm.prank(address(users.alice.deleGator)); + aaveAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + + _assertBalances(INITIAL_USDC_BALANCE, 0); + } + + /// @notice Tests that withdrawByDelegationOpenEnded emits the correct event when bob withdraws on alice's behalf + function test_withdrawByDelegationOpenEnded_emitsWithdrawExecutedEvent() public { + _setupLendingState(); + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // Expect the WithdrawExecuted event to be emitted with different delegate (bob calling on alice's behalf) + vm.expectEmit(true, true, true, true, address(aaveAdapter)); + emit AaveAdapter.WithdrawExecuted( + address(users.alice.deleGator), address(users.bob.deleGator), address(USDC), DEPOSIT_AMOUNT + ); + + vm.prank(address(users.bob.deleGator)); + aaveAdapter.withdrawByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); + + _assertBalances(INITIAL_USDC_BALANCE, 0); + } + + ////////////////////// Error Tests ////////////////////// + + /// @notice Tests that verify the custom errors are properly thrown by AaveAdapter functions + function test_supplyByDelegation_revertsOnInvalidDelegationsLength() public { + // Create empty delegations array + Delegation[] memory delegations_ = new Delegation[](0); + + vm.expectRevert(AaveAdapter.InvalidDelegationsLength.selector); + vm.prank(address(users.alice.deleGator)); + aaveAdapter.supplyByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + + // Create delegations array with 2 elements + delegations_ = new Delegation[](2); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + delegations_[1] = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.InvalidDelegationsLength.selector); + vm.prank(address(users.alice.deleGator)); + aaveAdapter.supplyByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + } + + /// @notice Tests that supplyByDelegation reverts when called by an unauthorized caller (not the delegator) + function test_supplyByDelegation_revertsOnUnauthorizedCaller() public { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.UnauthorizedCaller.selector); + vm.prank(address(users.bob.deleGator)); + aaveAdapter.supplyByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + } + + /// @notice Tests that supplyByDelegation reverts when token is zero address + function test_supplyByDelegation_revertsOnInvalidZeroAddress() public { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); + vm.prank(address(users.alice.deleGator)); + aaveAdapter.supplyByDelegation(delegations_, address(0), DEPOSIT_AMOUNT); + } + + /// @notice Tests that supplyByDelegationOpenEnded reverts when delegations array length is not exactly 1 + function test_supplyByDelegationOpenEnded_revertsOnInvalidDelegationsLength() public { + // Create empty delegations array + Delegation[] memory delegations_ = new Delegation[](0); + + vm.expectRevert(AaveAdapter.InvalidDelegationsLength.selector); + aaveAdapter.supplyByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); + + // Create delegations array with 2 elements + delegations_ = new Delegation[](2); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + delegations_[1] = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.InvalidDelegationsLength.selector); + aaveAdapter.supplyByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); + } + + /// @notice Tests that supplyByDelegationOpenEnded reverts when token is zero address + function test_supplyByDelegationOpenEnded_revertsOnInvalidZeroAddress() public { + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); + aaveAdapter.supplyByDelegationOpenEnded(delegations_, address(0), DEPOSIT_AMOUNT); + } + + /// @notice Tests that withdrawByDelegation reverts when delegations array length is not exactly 1 + function test_withdrawByDelegation_revertsOnInvalidDelegationsLength() public { + _setupLendingState(); + + // Create empty delegations array + Delegation[] memory delegations_ = new Delegation[](0); + + vm.expectRevert(AaveAdapter.InvalidDelegationsLength.selector); + vm.prank(address(users.alice.deleGator)); + aaveAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + + // Create delegations array with 2 elements + delegations_ = new Delegation[](2); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + delegations_[1] = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.InvalidDelegationsLength.selector); + vm.prank(address(users.alice.deleGator)); + aaveAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + } + + /// @notice Tests that withdrawByDelegation reverts when called by an unauthorized caller (not the delegator) + function test_withdrawByDelegation_revertsOnUnauthorizedCaller() public { + _setupLendingState(); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.UnauthorizedCaller.selector); + vm.prank(address(users.bob.deleGator)); + aaveAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + } + + /// @notice Tests that withdrawByDelegation reverts when token is zero address + function test_withdrawByDelegation_revertsOnInvalidZeroAddress() public { + _setupLendingState(); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); + vm.prank(address(users.alice.deleGator)); + aaveAdapter.withdrawByDelegation(delegations_, address(0), DEPOSIT_AMOUNT); + } + + /// @notice Tests that withdrawByDelegationOpenEnded reverts when delegations array length is not exactly 1 + function test_withdrawByDelegationOpenEnded_revertsOnInvalidDelegationsLength() public { + _setupLendingState(); + + // Create empty delegations array + Delegation[] memory delegations_ = new Delegation[](0); + + vm.expectRevert(AaveAdapter.InvalidDelegationsLength.selector); + aaveAdapter.withdrawByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); + + // Create delegations array with 2 elements + delegations_ = new Delegation[](2); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + delegations_[1] = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.InvalidDelegationsLength.selector); + aaveAdapter.withdrawByDelegationOpenEnded(delegations_, address(USDC), DEPOSIT_AMOUNT); + } + + /// @notice Tests that withdrawByDelegationOpenEnded reverts when token is zero address + function test_withdrawByDelegationOpenEnded_revertsOnInvalidZeroAddress() public { + _setupLendingState(); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = _createTransferDelegation(address(aaveAdapter), address(aUSDC), DEPOSIT_AMOUNT); + + vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); + aaveAdapter.withdrawByDelegationOpenEnded(delegations_, address(0), DEPOSIT_AMOUNT); + } + + ////////////////////// Constructor Error Tests ////////////////////// + + /// @notice Tests that constructor reverts when delegation manager is zero address + function test_constructor_revertsOnZeroDelegationManager() public { + vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); + new AaveAdapter(address(0), address(AAVE_POOL)); + } + + /// @notice Tests that constructor reverts when Aave pool is zero address + function test_constructor_revertsOnZeroAavePool() public { + vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); + new AaveAdapter(address(delegationManager), address(0)); + } + + /// @notice Tests that constructor reverts when both delegation manager and Aave pool are zero addresses + function test_constructor_revertsOnBothZeroAddresses() public { + vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); + new AaveAdapter(address(0), address(0)); + } + + /// @notice Tests successful constructor with valid addresses + function test_constructor_successWithValidAddresses() public { + AaveAdapter newAdapter = new AaveAdapter(address(delegationManager), address(AAVE_POOL)); + + assertEq(address(newAdapter.delegationManager()), address(delegationManager)); + assertEq(address(newAdapter.aavePool()), address(AAVE_POOL)); + } + + ////////////////////// Edge Case Tests ////////////////////// + + /// @notice Tests supplyByDelegation with maximum uint256 amount + function test_supplyByDelegation_withMaxAmount() public { + // First, we need to ensure Alice has sufficient balance for a reasonable test + uint256 testAmount = INITIAL_USDC_BALANCE; // Use all her balance + + _assertBalances(INITIAL_USDC_BALANCE, 0); + + // Create transfer delegation to adapter + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(USDC), testAmount); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + vm.prank(address(users.alice.deleGator)); + aaveAdapter.supplyByDelegation(delegations_, address(USDC), testAmount); + + _assertBalances(0, testAmount); + } + + /// @notice Tests withdrawByDelegation with maximum uint256 amount (withdraw all) + function test_withdrawByDelegation_withMaxAmount() public { + _setupLendingState(); + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + + // Get Alice's actual aToken balance and use a high limit for delegation + uint256 aTokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); + + // Create transfer delegation to adapter with very high allowance for max amount withdrawal + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(aUSDC), type(uint128).max); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + vm.prank(address(users.alice.deleGator)); + aaveAdapter.withdrawByDelegation(delegations_, address(USDC), aTokenBalance); + + _assertBalances(INITIAL_USDC_BALANCE, 0); + } + + /// @notice Tests that adapter properly handles allowance management + function test_ensureAllowance_increasesWhenNeeded() public { + _assertBalances(INITIAL_USDC_BALANCE, 0); + + // Create transfer delegation to adapter + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + // Check initial allowance (should be 0) + uint256 initialAllowance = USDC.allowance(address(aaveAdapter), address(AAVE_POOL)); + assertEq(initialAllowance, 0); + + vm.prank(address(users.alice.deleGator)); + aaveAdapter.supplyByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT); + + // Check that allowance was increased and then decreased by the transfer amount + uint256 finalAllowance = USDC.allowance(address(aaveAdapter), address(AAVE_POOL)); + assertEq(finalAllowance, type(uint256).max - DEPOSIT_AMOUNT); + + _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + } + + /// @notice Tests multiple supplies with existing allowance (should not increase again) + function test_ensureAllowance_doesNotIncreaseWhenSufficient() public { + _assertBalances(INITIAL_USDC_BALANCE, 0); + + // First supply to set allowance with higher amount limit to allow multiple uses + Delegation memory delegation1_ = _createTransferDelegation(address(aaveAdapter), address(USDC), 2 * DEPOSIT_AMOUNT); + Delegation[] memory delegations1_ = new Delegation[](1); + delegations1_[0] = delegation1_; + + vm.prank(address(users.alice.deleGator)); + aaveAdapter.supplyByDelegation(delegations1_, address(USDC), DEPOSIT_AMOUNT); + + uint256 allowanceAfterFirst = USDC.allowance(address(aaveAdapter), address(AAVE_POOL)); + assertEq(allowanceAfterFirst, type(uint256).max - DEPOSIT_AMOUNT); + + // Second supply should not change allowance (reuse same delegation) + vm.prank(address(users.alice.deleGator)); + aaveAdapter.supplyByDelegation(delegations1_, address(USDC), DEPOSIT_AMOUNT); + + uint256 allowanceAfterSecond = USDC.allowance(address(aaveAdapter), address(AAVE_POOL)); + assertEq(allowanceAfterSecond, type(uint256).max - (2 * DEPOSIT_AMOUNT)); + + // Check USDC balance + uint256 aliceUSDCBalance_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance_, INITIAL_USDC_BALANCE - (2 * DEPOSIT_AMOUNT), "USDC balance mismatch"); + + // Check aUSDC balance (allow for small interest accrual) + uint256 aliceATokenBalance_ = aUSDC.balanceOf(address(users.alice.deleGator)); + assertGe(aliceATokenBalance_, 2 * DEPOSIT_AMOUNT, "aUSDC balance should be at least 2x deposit amount"); + assertLe(aliceATokenBalance_, 2 * DEPOSIT_AMOUNT + 10, "aUSDC balance should not exceed deposit + small interest"); + } + ////////////////////// Helpers ////////////////////// /// @notice Creates a transfer delegation with ERC20TransferAmountEnforcer From 67a37fb105f924a12a857f0cd0130d00771c6e15 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Wed, 2 Jul 2025 11:08:20 -0600 Subject: [PATCH 7/7] chore: docs for contract, more test coverage --- src/helpers/AaveAdapter.sol | 56 +++++++++-- test/helpers/AaveLending.t.sol | 171 ++++++++++++++++++++++++--------- 2 files changed, 176 insertions(+), 51 deletions(-) diff --git a/src/helpers/AaveAdapter.sol b/src/helpers/AaveAdapter.sol index 0410803b..f86c6978 100644 --- a/src/helpers/AaveAdapter.sol +++ b/src/helpers/AaveAdapter.sol @@ -1,18 +1,37 @@ // SPDX-License-Identifier: MIT AND Apache-2.0 pragma solidity 0.8.23; -import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; -import { IAavePool } from "./interfaces/IAavePool.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { Delegation, ModeCode, Execution } from "../utils/Types.sol"; +import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { Delegation, ModeCode, Execution } from "../utils/Types.sol"; +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { IAavePool } from "./interfaces/IAavePool.sol"; + /// @title AaveAdapter -/// @notice Proof of concept adapter contract for Aave lending operations using delegations -/// @dev Handles token transfers and Aave supply operations through delegation-based permissions -contract AaveAdapter { +/// @notice Adapter contract that enables Aave lending operations through MetaMask's delegation framework +/// @dev This contract acts as an intermediary between users and Aave, enabling delegation-based token operations +/// without requiring direct token approvals. It supports both supply and withdrawal operations with two +/// execution models: +/// +/// 1. Delegator-only execution: Only the token owner (delegator) can execute operations, providing +/// maximum control over when and how their tokens are used in Aave operations. +/// +/// 2. Open-ended execution: Any authorized delegate can execute operations on behalf of the delegator, +/// enabling automated or third-party execution while ensuring benefits always flow to the delegator. +/// +/// The contract leverages ERC-7710 delegations to transfer tokens from users to itself, then interacts +/// with Aave pools on their behalf. This eliminates the need for traditional ERC20 approvals and enables +/// more flexible, intent-based interactions with DeFi protocols. +/// +/// Ownable functionality is implemented for emergency administration: +/// - Recovery of tokens accidentally sent directly to the contract (bypassing delegation flow) +/// - The contract is designed to never hold tokens during normal operation, making owner functions +/// purely for exceptional circumstances +contract AaveAdapter is Ownable2Step { using SafeERC20 for IERC20; ////////////////////// Events ////////////////////// @@ -31,6 +50,12 @@ contract AaveAdapter { /// @param amount Amount of tokens withdrawn event WithdrawExecuted(address indexed delegator, address indexed delegate, address indexed token, uint256 amount); + /// @notice Event emitted when stuck tokens are withdrawn by owner + /// @param token Address of the token withdrawn + /// @param recipient Address of the recipient + /// @param amount Amount of tokens withdrawn + event StuckTokensWithdrawn(IERC20 indexed token, address indexed recipient, uint256 amount); + ////////////////////// Errors ////////////////////// /// @notice Thrown when a zero address is provided for required parameters @@ -50,9 +75,10 @@ contract AaveAdapter { ////////////////////// Constructor ////////////////////// /// @notice Initializes the adapter with delegation manager and Aave pool addresses + /// @param _owner Address of the contract owner /// @param _delegationManager Address of the delegation manager contract /// @param _aavePool Address of the Aave lending pool contract - constructor(address _delegationManager, address _aavePool) { + constructor(address _owner, address _delegationManager, address _aavePool) Ownable(_owner) { if (_delegationManager == address(0) || _aavePool == address(0)) revert InvalidZeroAddress(); delegationManager = IDelegationManager(_delegationManager); @@ -193,4 +219,20 @@ contract AaveAdapter { emit WithdrawExecuted(_delegations[0].delegator, msg.sender, _token, _amount); } + + /** + * @notice Emergency function to recover tokens accidentally sent to this contract. + * @dev This contract should never hold ERC20 tokens as all token operations are handled + * through delegation-based transfers that move tokens directly between users and Aave. + * This function is only for recovering tokens that users may have sent to this contract + * by mistake (e.g., direct transfers instead of using delegation functions). + * @param _token The token to be recovered. + * @param _amount The amount of tokens to recover. + * @param _recipient The address to receive the recovered tokens. + */ + function withdraw(IERC20 _token, uint256 _amount, address _recipient) external onlyOwner { + IERC20(_token).safeTransfer(_recipient, _amount); + + emit StuckTokensWithdrawn(_token, _recipient, _amount); + } } diff --git a/test/helpers/AaveLending.t.sol b/test/helpers/AaveLending.t.sol index c0db503b..79e01f9e 100644 --- a/test/helpers/AaveLending.t.sol +++ b/test/helpers/AaveLending.t.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.23; import { Test } from "forge-std/Test.sol"; import { ModeLib } from "@erc7579/lib/ModeLib.sol"; import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { BaseTest } from "../utils/BaseTest.t.sol"; import { Implementation, SignatureType } from "../utils/Types.t.sol"; @@ -15,9 +17,9 @@ import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; import { LogicalOrWrapperEnforcer } from "../../src/enforcers/LogicalOrWrapperEnforcer.sol"; import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; import { IAavePool } from "../../src/helpers/interfaces/IAavePool.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; import { AaveAdapter } from "../../src/helpers/AaveAdapter.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; // @dev Do not remove this comment below /// forge-config: default.evm_version = "shanghai" @@ -53,6 +55,7 @@ contract AaveLendingTest is BaseTest { IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); IERC20 public aUSDC; address public constant USDC_WHALE = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341; + address public owner; // Enforcers for delegation restrictions AllowedTargetsEnforcer public allowedTargetsEnforcer; @@ -80,6 +83,8 @@ contract AaveLendingTest is BaseTest { // Call parent setup to initialize delegation framework super.setUp(); + owner = makeAddr("AaveAdapter Owner"); + // Deploy enforcers allowedTargetsEnforcer = new AllowedTargetsEnforcer(); allowedMethodsEnforcer = new AllowedMethodsEnforcer(); @@ -88,7 +93,7 @@ contract AaveLendingTest is BaseTest { erc20TransferAmountEnforcer = new ERC20TransferAmountEnforcer(); logicalOrWrapperEnforcer = new LogicalOrWrapperEnforcer(delegationManager); - aaveAdapter = new AaveAdapter(address(delegationManager), address(AAVE_POOL)); + aaveAdapter = new AaveAdapter(owner, address(delegationManager), address(AAVE_POOL)); vm.label(address(allowedTargetsEnforcer), "AllowedTargetsEnforcer"); vm.label(address(allowedMethodsEnforcer), "AllowedMethodsEnforcer"); @@ -683,27 +688,27 @@ contract AaveLendingTest is BaseTest { /// @notice Tests that constructor reverts when delegation manager is zero address function test_constructor_revertsOnZeroDelegationManager() public { vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); - new AaveAdapter(address(0), address(AAVE_POOL)); + new AaveAdapter(owner, address(0), address(AAVE_POOL)); } /// @notice Tests that constructor reverts when Aave pool is zero address function test_constructor_revertsOnZeroAavePool() public { vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); - new AaveAdapter(address(delegationManager), address(0)); + new AaveAdapter(owner, address(delegationManager), address(0)); } /// @notice Tests that constructor reverts when both delegation manager and Aave pool are zero addresses function test_constructor_revertsOnBothZeroAddresses() public { vm.expectRevert(AaveAdapter.InvalidZeroAddress.selector); - new AaveAdapter(address(0), address(0)); + new AaveAdapter(owner, address(0), address(0)); } /// @notice Tests successful constructor with valid addresses function test_constructor_successWithValidAddresses() public { - AaveAdapter newAdapter = new AaveAdapter(address(delegationManager), address(AAVE_POOL)); + AaveAdapter newAdapter_ = new AaveAdapter(owner, address(delegationManager), address(AAVE_POOL)); - assertEq(address(newAdapter.delegationManager()), address(delegationManager)); - assertEq(address(newAdapter.aavePool()), address(AAVE_POOL)); + assertEq(address(newAdapter_.delegationManager()), address(delegationManager)); + assertEq(address(newAdapter_.aavePool()), address(AAVE_POOL)); } ////////////////////// Edge Case Tests ////////////////////// @@ -711,20 +716,20 @@ contract AaveLendingTest is BaseTest { /// @notice Tests supplyByDelegation with maximum uint256 amount function test_supplyByDelegation_withMaxAmount() public { // First, we need to ensure Alice has sufficient balance for a reasonable test - uint256 testAmount = INITIAL_USDC_BALANCE; // Use all her balance + uint256 testAmount_ = INITIAL_USDC_BALANCE; // Use all her balance _assertBalances(INITIAL_USDC_BALANCE, 0); // Create transfer delegation to adapter - Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(USDC), testAmount); + Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(USDC), testAmount_); Delegation[] memory delegations_ = new Delegation[](1); delegations_[0] = delegation_; vm.prank(address(users.alice.deleGator)); - aaveAdapter.supplyByDelegation(delegations_, address(USDC), testAmount); + aaveAdapter.supplyByDelegation(delegations_, address(USDC), testAmount_); - _assertBalances(0, testAmount); + _assertBalances(0, testAmount_); } /// @notice Tests withdrawByDelegation with maximum uint256 amount (withdraw all) @@ -733,7 +738,7 @@ contract AaveLendingTest is BaseTest { _assertBalances(INITIAL_USDC_BALANCE - DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); // Get Alice's actual aToken balance and use a high limit for delegation - uint256 aTokenBalance = aUSDC.balanceOf(address(users.alice.deleGator)); + uint256 aTokenBalance_ = aUSDC.balanceOf(address(users.alice.deleGator)); // Create transfer delegation to adapter with very high allowance for max amount withdrawal Delegation memory delegation_ = _createTransferDelegation(address(aaveAdapter), address(aUSDC), type(uint128).max); @@ -742,7 +747,7 @@ contract AaveLendingTest is BaseTest { delegations_[0] = delegation_; vm.prank(address(users.alice.deleGator)); - aaveAdapter.withdrawByDelegation(delegations_, address(USDC), aTokenBalance); + aaveAdapter.withdrawByDelegation(delegations_, address(USDC), aTokenBalance_); _assertBalances(INITIAL_USDC_BALANCE, 0); } @@ -783,15 +788,15 @@ contract AaveLendingTest is BaseTest { vm.prank(address(users.alice.deleGator)); aaveAdapter.supplyByDelegation(delegations1_, address(USDC), DEPOSIT_AMOUNT); - uint256 allowanceAfterFirst = USDC.allowance(address(aaveAdapter), address(AAVE_POOL)); - assertEq(allowanceAfterFirst, type(uint256).max - DEPOSIT_AMOUNT); + uint256 allowanceAfterFirst_ = USDC.allowance(address(aaveAdapter), address(AAVE_POOL)); + assertEq(allowanceAfterFirst_, type(uint256).max - DEPOSIT_AMOUNT); // Second supply should not change allowance (reuse same delegation) vm.prank(address(users.alice.deleGator)); aaveAdapter.supplyByDelegation(delegations1_, address(USDC), DEPOSIT_AMOUNT); - uint256 allowanceAfterSecond = USDC.allowance(address(aaveAdapter), address(AAVE_POOL)); - assertEq(allowanceAfterSecond, type(uint256).max - (2 * DEPOSIT_AMOUNT)); + uint256 allowanceAfterSecond_ = USDC.allowance(address(aaveAdapter), address(AAVE_POOL)); + assertEq(allowanceAfterSecond_, type(uint256).max - (2 * DEPOSIT_AMOUNT)); // Check USDC balance uint256 aliceUSDCBalance_ = USDC.balanceOf(address(users.alice.deleGator)); @@ -803,20 +808,98 @@ contract AaveLendingTest is BaseTest { assertLe(aliceATokenBalance_, 2 * DEPOSIT_AMOUNT + 10, "aUSDC balance should not exceed deposit + small interest"); } + ////////////////////// Withdraw Function Tests ////////////////////// + + /// @notice Tests that only owner can call withdraw function + function test_withdraw_onlyOwner() public { + // Create test token and give some balance to the adapter + BasicERC20 testToken_ = new BasicERC20(owner, "TestToken", "TST", 1000 ether); + + // Mint some tokens to the adapter + vm.prank(owner); + testToken_.mint(address(aaveAdapter), 100 ether); + + // Verify adapter has tokens + assertEq(testToken_.balanceOf(address(aaveAdapter)), 100 ether); + + // Try to call withdraw from non-owner address (should fail) + vm.prank(address(users.alice.deleGator)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(users.alice.deleGator))); + aaveAdapter.withdraw(testToken_, 50 ether, address(users.alice.deleGator)); + + // Verify balance hasn't changed + assertEq(testToken_.balanceOf(address(aaveAdapter)), 100 ether); + } + + /// @notice Tests that withdraw function works correctly when called by owner + function test_withdraw_functionality() public { + // Create test token and give some balance to the adapter + BasicERC20 testToken_ = new BasicERC20(owner, "TestToken", "TST", 1000 ether); + + // Mint some tokens to the adapter + vm.prank(owner); + testToken_.mint(address(aaveAdapter), 100 ether); + + // Verify initial balances + assertEq(testToken_.balanceOf(address(aaveAdapter)), 100 ether); + assertEq(testToken_.balanceOf(address(users.alice.deleGator)), 0); + + // Expect the event to be emitted + vm.expectEmit(true, true, true, true, address(aaveAdapter)); + emit AaveAdapter.StuckTokensWithdrawn(testToken_, address(users.alice.deleGator), 50 ether); + + // Call withdraw as owner + vm.prank(owner); + aaveAdapter.withdraw(testToken_, 50 ether, address(users.alice.deleGator)); + + // Verify balances after withdrawal + assertEq(testToken_.balanceOf(address(aaveAdapter)), 50 ether); + assertEq(testToken_.balanceOf(address(users.alice.deleGator)), 50 ether); + } + + /// @notice Tests withdraw function with full balance + function test_withdraw_fullBalance() public { + // Create test token and give some balance to the adapter + BasicERC20 testToken_ = new BasicERC20(owner, "TestToken", "TST", 1000 ether); + + // Mint some tokens to the adapter + vm.prank(owner); + testToken_.mint(address(aaveAdapter), 100 ether); + + // Verify initial balance + assertEq(testToken_.balanceOf(address(aaveAdapter)), 100 ether); + + // Withdraw full balance + vm.prank(owner); + aaveAdapter.withdraw(testToken_, 100 ether, address(users.bob.deleGator)); + + // Verify all tokens were withdrawn + assertEq(testToken_.balanceOf(address(aaveAdapter)), 0); + assertEq(testToken_.balanceOf(address(users.bob.deleGator)), 100 ether); + } + ////////////////////// Helpers ////////////////////// /// @notice Creates a transfer delegation with ERC20TransferAmountEnforcer - /// @param delegate Address that can execute the delegation - /// @param token Token to transfer - /// @param amount Amount to transfer + /// @param _delegate Address that can execute the delegation + /// @param _token Token to transfer + /// @param _amount Amount to transfer /// @return Signed delegation ready for execution - function _createTransferDelegation(address delegate, address token, uint256 amount) internal view returns (Delegation memory) { + function _createTransferDelegation( + address _delegate, + address _token, + uint256 _amount + ) + internal + view + returns (Delegation memory) + { Caveat[] memory caveats_ = new Caveat[](1); caveats_[0] = - Caveat({ args: hex"", enforcer: address(erc20TransferAmountEnforcer), terms: abi.encodePacked(token, amount) }); + Caveat({ args: hex"", enforcer: address(erc20TransferAmountEnforcer), terms: abi.encodePacked(_token, _amount) }); Delegation memory delegation_ = Delegation({ - delegate: delegate, + delegate: _delegate, delegator: address(users.alice.deleGator), authority: ROOT_AUTHORITY, caveats: caveats_, @@ -828,15 +911,15 @@ contract AaveLendingTest is BaseTest { } /// @notice Creates a target-restricted delegation - /// @param delegate Address that can execute the delegation - /// @param target Allowed target address + /// @param _delegate Address that can execute the delegation + /// @param _target Allowed target address /// @return Signed delegation ready for execution - function _createTargetRestrictedDelegation(address delegate, address target) internal view returns (Delegation memory) { + function _createTargetRestrictedDelegation(address _delegate, address _target) internal view returns (Delegation memory) { Caveat[] memory caveats_ = new Caveat[](1); - caveats_[0] = Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(target) }); + caveats_[0] = Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(_target) }); Delegation memory delegation_ = Delegation({ - delegate: delegate, + delegate: _delegate, delegator: address(users.alice.deleGator), authority: ROOT_AUTHORITY, caveats: caveats_, @@ -848,14 +931,14 @@ contract AaveLendingTest is BaseTest { } /// @notice Creates a comprehensive approval delegation with all security restrictions - /// @param delegate Address that can execute the delegation - /// @param spender Address to approve - /// @param amount Amount to approve + /// @param _delegate Address that can execute the delegation + /// @param _spender Address to approve + /// @param _amount Amount to approve /// @return Signed delegation ready for execution function _createApprovalDelegation( - address delegate, - address spender, - uint256 amount + address _delegate, + address _spender, + uint256 _amount ) internal view @@ -872,18 +955,18 @@ contract AaveLendingTest is BaseTest { // Restrict approve recipient uint256 paramStart_ = abi.encodeWithSelector(IERC20.approve.selector).length; - caveats_[2] = Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), terms: abi.encode(paramStart_, spender) }); + caveats_[2] = Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), terms: abi.encode(paramStart_, _spender) }); // Restrict approve amount paramStart_ = abi.encodeWithSelector(IERC20.approve.selector, address(0)).length; caveats_[3] = - Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), terms: abi.encodePacked(paramStart_, amount) }); + Caveat({ args: hex"", enforcer: address(allowedCalldataEnforcer), terms: abi.encodePacked(paramStart_, _amount) }); // Set value limit to 0 caveats_[4] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); Delegation memory delegation_ = Delegation({ - delegate: delegate, + delegate: _delegate, delegator: address(users.alice.deleGator), authority: ROOT_AUTHORITY, caveats: caveats_, @@ -895,9 +978,9 @@ contract AaveLendingTest is BaseTest { } /// @notice Creates a supply delegation with all security restrictions - /// @param delegate Address that can execute the delegation + /// @param _delegate Address that can execute the delegation /// @return Signed delegation ready for execution - function _createSupplyDelegation(address delegate) internal view returns (Delegation memory) { + function _createSupplyDelegation(address _delegate) internal view returns (Delegation memory) { Caveat[] memory caveats_ = new Caveat[](4); // Restrict to Aave pool @@ -920,7 +1003,7 @@ contract AaveLendingTest is BaseTest { caveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); Delegation memory delegation_ = Delegation({ - delegate: delegate, + delegate: _delegate, delegator: address(users.alice.deleGator), authority: ROOT_AUTHORITY, caveats: caveats_, @@ -932,9 +1015,9 @@ contract AaveLendingTest is BaseTest { } /// @notice Creates a withdraw delegation with all security restrictions - /// @param delegate Address that can execute the delegation + /// @param _delegate Address that can execute the delegation /// @return Signed delegation ready for execution - function _createWithdrawDelegation(address delegate) internal view returns (Delegation memory) { + function _createWithdrawDelegation(address _delegate) internal view returns (Delegation memory) { Caveat[] memory caveats_ = new Caveat[](4); // Restrict to Aave pool @@ -957,7 +1040,7 @@ contract AaveLendingTest is BaseTest { caveats_[3] = Caveat({ args: hex"", enforcer: address(valueLteEnforcer), terms: abi.encode(0) }); Delegation memory delegation_ = Delegation({ - delegate: delegate, + delegate: _delegate, delegator: address(users.alice.deleGator), authority: ROOT_AUTHORITY, caveats: caveats_,