From 686b3b5a6e30c74e3bd664bb80146c9aad05eb13 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Mon, 2 Mar 2026 11:31:24 +0100 Subject: [PATCH] Implement VedaAdapter --- src/helpers/VedaAdapter.sol | 353 ++++++++++ src/helpers/interfaces/IVedaTeller.sol | 114 ++++ test/helpers/VedaLending.t.sol | 889 +++++++++++++++++++++++++ 3 files changed, 1356 insertions(+) create mode 100644 src/helpers/VedaAdapter.sol create mode 100644 src/helpers/interfaces/IVedaTeller.sol create mode 100644 test/helpers/VedaLending.t.sol diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol new file mode 100644 index 00000000..23b4ed1f --- /dev/null +++ b/src/helpers/VedaAdapter.sol @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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 } from "../utils/Types.sol"; +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { IVedaTeller } from "./interfaces/IVedaTeller.sol"; + +/** + * @title VedaAdapter + * @notice Adapter contract that enables Veda BoringVault deposit and withdrawal operations through MetaMask's + * delegation framework + * @dev This contract acts as an intermediary between users and Veda's BoringVault, enabling delegation-based + * token operations without requiring direct token approvals. + * + * Architecture: + * - BoringVault: The ERC20 vault share token that also custodies assets. On deposit, the vault pulls + * tokens from the caller via `safeTransferFrom`, so this adapter must approve the BoringVault. + * - Teller: The contract that orchestrates deposits/withdrawals. The adapter calls `teller.bulkDeposit()` + * for deposits (requires SOLVER_ROLE) and `teller.withdraw()` for withdrawals (user-facing, no special + * role needed). + * + * Delegation Flow: + * 1. The user creates an initial delegation to an "operator" address (a DeleGator-upgraded account). + * This delegation includes: + * - A transfer enforcer to control which tokens/shares and amounts can be transferred + * - A redeemer enforcer that restricts redemption to only the VedaAdapter contract + * + * 2. The operator then redelegates to this VedaAdapter contract with additional constraints: + * - Allowed methods enforcer limiting which functions can be called + * - Limited calls enforcer restricting the delegation to a single execution + * + * 3. For deposits: the adapter redeems the delegation chain, transfers tokens from the user to itself, + * approves the BoringVault, and calls `teller.bulkDeposit()` to mint shares to the user. + * For withdrawals: the adapter redeems the delegation chain, transfers vault shares from the user + * to itself, and calls `teller.withdraw()` to burn shares and send underlying assets to the user. + * + * Requirements: + * - VedaAdapter must be granted SOLVER_ROLE (or equivalent auth) on the Teller for deposits + * - VedaAdapter must approve the BoringVault to spend deposit tokens + */ +contract VedaAdapter is Ownable2Step { + using SafeERC20 for IERC20; + using ExecutionLib for bytes; + using ModeLib for ModeCode; + + /** + * @notice Parameters for a single deposit operation in a batch + */ + struct DepositParams { + Delegation[] delegations; + address token; + uint256 amount; + uint256 minimumMint; + } + + /** + * @notice Parameters for a single withdrawal operation in a batch + */ + struct WithdrawParams { + Delegation[] delegations; + address token; + uint256 shareAmount; + uint256 minimumAssets; + } + + ////////////////////////////// Events ////////////////////////////// + + /** + * @notice Emitted when a deposit 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 deposited token + * @param amount Amount of tokens deposited + * @param shares Amount of vault shares minted to the delegator + */ + event DepositExecuted( + address indexed delegator, address indexed delegate, address indexed token, uint256 amount, uint256 shares + ); + + /** + * @notice Emitted when a withdrawal operation is executed via delegation + * @param delegator Address of the share owner (delegator) + * @param delegate Address of the executor (delegate) + * @param token Address of the underlying token withdrawn + * @param shareAmount Amount of vault shares burned + * @param assetsOut Amount of underlying tokens sent to the delegator + */ + event WithdrawExecuted( + address indexed delegator, address indexed delegate, address indexed token, uint256 shareAmount, uint256 assetsOut + ); + + /** + * @notice 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 ////////////////////////////// + + /// @dev Thrown when a zero address is provided for required parameters + error InvalidZeroAddress(); + + /// @dev Thrown when a zero address is provided for the recipient + error InvalidRecipient(); + + /// @dev Thrown when the delegation chain has fewer than 2 delegations + error InvalidDelegationsLength(); + + /// @dev Thrown when the batch array is empty + error InvalidBatchLength(); + + /// @dev Thrown when msg.sender is not the leaf delegator + error NotLeafDelegator(); + + ////////////////////////////// State ////////////////////////////// + + /** + * @notice The DelegationManager contract used to redeem delegations + */ + IDelegationManager public immutable delegationManager; + + /** + * @notice The BoringVault contract (approval target for token transfers) + */ + address public immutable boringVault; + + /** + * @notice The Teller contract for deposit and withdrawal operations + */ + IVedaTeller public immutable teller; + + ////////////////////////////// Constructor ////////////////////////////// + + /** + * @notice Initializes the adapter with delegation manager, BoringVault, and Teller addresses + * @param _owner Address of the contract owner + * @param _delegationManager Address of the delegation manager contract + * @param _boringVault Address of the BoringVault (token approval target) + * @param _teller Address of the Teller contract (deposit entry point) + */ + constructor(address _owner, address _delegationManager, address _boringVault, address _teller) Ownable(_owner) { + if (_delegationManager == address(0) || _boringVault == address(0) || _teller == address(0)) { + revert InvalidZeroAddress(); + } + + delegationManager = IDelegationManager(_delegationManager); + boringVault = _boringVault; + teller = IVedaTeller(_teller); + } + + ////////////////////////////// External Methods ////////////////////////////// + + /** + * @notice Deposits tokens into a Veda BoringVault using delegation-based token transfer + * @dev Redeems the delegation to transfer tokens to this adapter, then calls bulkDeposit + * on the Teller which mints vault shares directly to the original token owner. + * Requires at least 2 delegations forming a chain from user to operator to this adapter. + * @param _delegations Array of Delegation objects, sorted leaf to root + * @param _token Address of the token to deposit + * @param _amount Amount of tokens to deposit + * @param _minimumMint Minimum vault shares the user expects to receive (slippage protection) + */ + function depositByDelegation(Delegation[] memory _delegations, address _token, uint256 _amount, uint256 _minimumMint) external { + _executeDepositByDelegation(_delegations, _token, _amount, _minimumMint, msg.sender); + } + + /** + * @notice Deposits tokens using multiple delegation streams, executed sequentially + * @dev Each element is executed one after the other. The caller must be the delegator + * (first delegate in the chain) for each stream. + * @param _depositStreams Array of deposit parameters + */ + function depositByDelegationBatch(DepositParams[] memory _depositStreams) external { + uint256 streamsLength_ = _depositStreams.length; + if (streamsLength_ == 0) revert InvalidBatchLength(); + + address caller_ = msg.sender; + for (uint256 i = 0; i < streamsLength_;) { + DepositParams memory params_ = _depositStreams[i]; + _executeDepositByDelegation(params_.delegations, params_.token, params_.amount, params_.minimumMint, caller_); + unchecked { + ++i; + } + } + } + + /** + * @notice Withdraws underlying tokens from a Veda BoringVault using delegation-based share transfer + * @dev Redeems the delegation to transfer vault shares to this adapter, then calls withdraw + * on the Teller which burns shares and sends underlying assets directly to the original share owner. + * Requires at least 2 delegations forming a chain from user to operator to this adapter. + * @param _delegations Array of Delegation objects, sorted leaf to root + * @param _token Address of the underlying token to receive + * @param _shareAmount Amount of vault shares to redeem + * @param _minimumAssets Minimum underlying assets the user expects to receive (slippage protection) + */ + function withdrawByDelegation( + Delegation[] memory _delegations, + address _token, + uint256 _shareAmount, + uint256 _minimumAssets + ) + external + { + _executeWithdrawByDelegation(_delegations, _token, _shareAmount, _minimumAssets, msg.sender); + } + + /** + * @notice Withdraws underlying tokens using multiple delegation streams, executed sequentially + * @dev Each element is executed one after the other. The caller must be the delegator + * (first delegate in the chain) for each stream. + * @param _withdrawStreams Array of withdraw parameters + */ + function withdrawByDelegationBatch(WithdrawParams[] memory _withdrawStreams) external { + uint256 streamsLength_ = _withdrawStreams.length; + if (streamsLength_ == 0) revert InvalidBatchLength(); + + address caller_ = msg.sender; + for (uint256 i = 0; i < streamsLength_;) { + WithdrawParams memory params_ = _withdrawStreams[i]; + _executeWithdrawByDelegation(params_.delegations, params_.token, params_.shareAmount, params_.minimumAssets, caller_); + unchecked { + ++i; + } + } + } + + /** + * @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 the BoringVault. + * This function is only for recovering tokens sent to this contract by mistake. + * @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 withdrawEmergency(IERC20 _token, uint256 _amount, address _recipient) external onlyOwner { + if (_recipient == address(0)) revert InvalidRecipient(); + + _token.safeTransfer(_recipient, _amount); + + emit StuckTokensWithdrawn(_token, _recipient, _amount); + } + + ////////////////////////////// Private/Internal Methods ////////////////////////////// + + /** + * @notice Ensures sufficient token allowance for BoringVault to pull tokens + * @dev Checks current allowance and sets exact amount if insufficient, avoiding accumulation + * @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), boringVault); + if (allowance_ < _amount) { + _token.forceApprove(boringVault, _amount); + } + } + + /** + * @notice Internal implementation of deposit by delegation + * @param _delegations Delegation chain, sorted leaf to root + * @param _token Token to deposit + * @param _amount Amount to deposit + * @param _minimumMint Minimum vault shares expected + * @param _caller Authorized caller (must match leaf delegator) + */ + function _executeDepositByDelegation( + Delegation[] memory _delegations, + address _token, + uint256 _amount, + uint256 _minimumMint, + address _caller + ) + internal + { + uint256 length_ = _delegations.length; + if (length_ < 2) revert InvalidDelegationsLength(); + if (_delegations[0].delegator != _caller) revert NotLeafDelegator(); + if (_token == address(0)) revert InvalidZeroAddress(); + + address rootDelegator_ = _delegations[length_ - 1].delegator; + + // Redeem delegation: transfer tokens from user to this adapter + 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(_token, 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + // Approve BoringVault to pull tokens, then deposit via Teller + _ensureAllowance(IERC20(_token), _amount); + uint256 shares_ = teller.bulkDeposit(_token, _amount, _minimumMint, rootDelegator_); + + emit DepositExecuted(rootDelegator_, _caller, _token, _amount, shares_); + } + + /** + * @notice Internal implementation of withdraw by delegation + * @param _delegations Delegation chain, sorted leaf to root + * @param _token Underlying token to receive + * @param _shareAmount Amount of vault shares to redeem + * @param _minimumAssets Minimum underlying assets expected + * @param _caller Authorized caller (must match leaf delegator) + */ + function _executeWithdrawByDelegation( + Delegation[] memory _delegations, + address _token, + uint256 _shareAmount, + uint256 _minimumAssets, + address _caller + ) + internal + { + uint256 length_ = _delegations.length; + if (length_ < 2) revert InvalidDelegationsLength(); + if (_delegations[0].delegator != _caller) revert NotLeafDelegator(); + if (_token == address(0)) revert InvalidZeroAddress(); + + address rootDelegator_ = _delegations[length_ - 1].delegator; + + // Redeem delegation: transfer vault shares from user to this adapter + 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), _shareAmount)); + executionCallDatas_[0] = ExecutionLib.encodeSingle(boringVault, 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + // Withdraw from Teller: burns shares from this adapter, sends underlying to root delegator + uint256 assetsOut_ = teller.withdraw(_token, _shareAmount, _minimumAssets, rootDelegator_); + + emit WithdrawExecuted(rootDelegator_, _caller, _token, _shareAmount, assetsOut_); + } +} diff --git a/src/helpers/interfaces/IVedaTeller.sol b/src/helpers/interfaces/IVedaTeller.sol new file mode 100644 index 00000000..c40daaf5 --- /dev/null +++ b/src/helpers/interfaces/IVedaTeller.sol @@ -0,0 +1,114 @@ +// Based on: +// https://github.com/Se7en-Seas/boring-vault/blob/main/src/base/Roles/TellerWithMultiAssetSupport.sol +// https://github.com/Veda-Labs/boring-vault/blob/dev/oct-2025/src/base/Roles/TellerWithYieldStreaming.sol + +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +/** + * @title IVedaTeller + * @notice Interface for the user-facing functions of Veda's TellerWithMultiAssetSupport. + * @dev Uses `address` for asset parameters to avoid importing Solmate's ERC20. + * The Teller is the entry/exit point for the BoringVault. All functions use `requiresAuth`, + * so callers must be authorized on the Teller's Authority. + */ +interface IVedaTeller { + /** + * @notice Allows users to deposit into the BoringVault, if the contract is not paused. + * @dev Shares are minted to `msg.sender`. A share lock period may apply. + * @param depositAsset The ERC20 token to deposit + * @param depositAmount The amount to deposit + * @param minimumMint The minimum shares the user expects to receive + * @param referralAddress Address used for referral tracking + * @return shares The number of vault shares minted + */ + function deposit( + address depositAsset, + uint256 depositAmount, + uint256 minimumMint, + address referralAddress + ) + external + payable + returns (uint256 shares); + + /** + * @notice Allows users to deposit into the BoringVault using ERC-2612 permit. + * @dev Shares are minted to `msg.sender`. A share lock period may apply. + * @param depositAsset The ERC20 token to deposit + * @param depositAmount The amount to deposit + * @param minimumMint The minimum shares the user expects to receive + * @param deadline The permit deadline timestamp + * @param v The permit signature v value + * @param r The permit signature r value + * @param s The permit signature s value + * @return shares The number of vault shares minted + */ + function depositWithPermit( + address depositAsset, + uint256 depositAmount, + uint256 minimumMint, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) + external + returns (uint256 shares); + + /** + * @notice Allows SOLVER_ROLE to deposit on behalf of a recipient. + * @dev Tokens are pulled from `msg.sender`; shares are minted to `to`. + * No share lock period applies to bulk deposits. + * @param depositAsset The ERC20 token to deposit + * @param depositAmount The amount to deposit + * @param minimumMint The minimum shares expected + * @param to The address that will receive the vault shares + * @return shares The number of vault shares minted + */ + function bulkDeposit( + address depositAsset, + uint256 depositAmount, + uint256 minimumMint, + address to + ) + external + returns (uint256 shares); + + /** + * @notice Allows users to withdraw from the BoringVault. + * @dev Available on TellerWithYieldStreaming. Burns shares from `msg.sender` and sends + * underlying assets to `to`. Updates vested yield before withdrawal. + * @param withdrawAsset The ERC20 token to receive + * @param shareAmount The amount of vault shares to burn + * @param minimumAssets The minimum underlying assets expected + * @param to The address that will receive the underlying assets + * @return assetsOut The amount of underlying assets sent + */ + function withdraw( + address withdrawAsset, + uint256 shareAmount, + uint256 minimumAssets, + address to + ) + external + returns (uint256 assetsOut); + + /** + * @notice Allows SOLVER_ROLE to withdraw on behalf of a recipient. + * @dev Shares are burned from `msg.sender`; underlying assets are sent to `to`. + * @param withdrawAsset The ERC20 token to receive + * @param shareAmount The amount of vault shares to burn + * @param minimumAssets The minimum underlying assets expected + * @param to The address that will receive the underlying assets + * @return assetsOut The amount of underlying assets sent + */ + function bulkWithdraw( + address withdrawAsset, + uint256 shareAmount, + uint256 minimumAssets, + address to + ) + external + returns (uint256 assetsOut); +} diff --git a/test/helpers/VedaLending.t.sol b/test/helpers/VedaLending.t.sol new file mode 100644 index 00000000..95993cc0 --- /dev/null +++ b/test/helpers/VedaLending.t.sol @@ -0,0 +1,889 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +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 { EncoderLib } from "../../src/libraries/EncoderLib.sol"; +import { IVedaTeller } from "../../src/helpers/interfaces/IVedaTeller.sol"; +import { BaseTest } from "../utils/BaseTest.t.sol"; +import { Implementation, SignatureType } from "../utils/Types.t.sol"; +import { Execution, Delegation, Caveat, ModeCode, CallType, ExecType } from "../../src/utils/Types.sol"; +import { CALLTYPE_BATCH, EXECTYPE_TRY, MODE_DEFAULT } from "../../src/utils/Constants.sol"; +import { ModePayload } from "@erc7579/lib/ModeLib.sol"; +import { AllowedTargetsEnforcer } from "../../src/enforcers/AllowedTargetsEnforcer.sol"; +import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforcer.sol"; +import { AllowedCalldataEnforcer } from "../../src/enforcers/AllowedCalldataEnforcer.sol"; +import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; +import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; +import { LimitedCallsEnforcer } from "../../src/enforcers/LimitedCallsEnforcer.sol"; +import { LogicalOrWrapperEnforcer } from "../../src/enforcers/LogicalOrWrapperEnforcer.sol"; +import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; +import { IDelegationManager } from "../../src/interfaces/IDelegationManager.sol"; +import { VedaAdapter } from "../../src/helpers/VedaAdapter.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; + +// @dev Do not remove this comment below +/// forge-config: default.evm_version = "shanghai" + +/** + * @title VedaLending Test + * @notice Tests delegation-based lending on Veda BoringVault. + * @dev Uses a forked Ink mainnet environment to test real contract interactions. + * + * Veda BoringVault implements the ERC-4626 standard for tokenized vaults: + * - Users deposit assets (e.g., USDC) and receive vault shares representing proportional ownership + * - Shares are NOT 1:1 with assets - the conversion rate depends on vault's total assets and total supply + * - The vault contract itself is the ERC-20 share token (no separate token contract) + * - Veda uses multiple contracts to manage the flow of funds: + * - We implement Teller for deposits and withdrawals + * - We implement BoringVault for the approval and custody of assets + * - We modify the mainnet deployment to allow public access to the Teller functions for testing (in production, we would get Solver + * role on Adapter) + * - More docs here: https://docs.veda.tech/architecture-and-flow-of-funds + * + * - Security considerations: + * - We need a redelegation with specific amount to the adapter to prevent over withdrawal or deposit. This would not effect the + * user, but could drain the transaction creator wallet. + */ +contract VedaLendingTest is BaseTest { + using ModeLib for ModeCode; + + // Restricted vault - cannot set on behalfOf + IVedaTeller public constant VEDA_TELLER = IVedaTeller(0xc46f2443b3521632E2E2a903D6da8f965B46f6a0); + IERC20 public constant BORING_VAULT = IERC20(0xDbD87325D7b1189Dcc9255c4926076fF4a96A271); + + address public constant ROLES_AUTHORITY = 0x1F53135155d6fF516bCcfDd9424fcdB8AD1eFB77; + address public constant ROLES_AUTHORITY_OWNER = 0x846abf72fE789cf52FDefB0e924bE9E3670667DA; + + IERC20 public constant USDC = IERC20(0x2D270e6886d130D724215A266106e6832161EAEd); + address public constant USDC_WHALE = 0xd3abC2b515345E47D41C0A1Cd64F8493B80d1ad6; + address public owner; + + // Enforcers for delegation restrictions + AllowedTargetsEnforcer public allowedTargetsEnforcer; + AllowedMethodsEnforcer public allowedMethodsEnforcer; + AllowedCalldataEnforcer public allowedCalldataEnforcer; + ValueLteEnforcer public valueLteEnforcer; + LogicalOrWrapperEnforcer public logicalOrWrapperEnforcer; + ERC20TransferAmountEnforcer public erc20TransferAmountEnforcer; + RedeemerEnforcer public redeemerEnforcer; + LimitedCallsEnforcer public limitedCallsEnforcer; + VedaAdapter public vedaAdapter; + + uint256 public constant MAINNET_FORK_BLOCK = 38688994; // Use latest available block + uint256 public constant INITIAL_USD_BALANCE = 10000000000; // 10k USDC + uint256 public constant DEPOSIT_AMOUNT = 1000000000; // 1k USDC + uint256 public constant SHARE_LOCK_SECONDS = 61; // Warp past the 60s share lock period applied by deposit() + + ////////////////////// Setup ////////////////////// + + function setUp() public override { + // Create fork from mainnet at specific block + vm.createSelectFork(vm.envString("INK_RPC_URL"), MAINNET_FORK_BLOCK); + + // Set implementation type + IMPLEMENTATION = Implementation.Hybrid; + SIGNATURE_TYPE = SignatureType.RawP256; + + // Call parent setup to initialize delegation framework + super.setUp(); + + owner = makeAddr("VedaAdapter Owner"); + + // Deploy enforcers + allowedTargetsEnforcer = new AllowedTargetsEnforcer(); + allowedMethodsEnforcer = new AllowedMethodsEnforcer(); + allowedCalldataEnforcer = new AllowedCalldataEnforcer(); + valueLteEnforcer = new ValueLteEnforcer(); + erc20TransferAmountEnforcer = new ERC20TransferAmountEnforcer(); + redeemerEnforcer = new RedeemerEnforcer(); + limitedCallsEnforcer = new LimitedCallsEnforcer(); + logicalOrWrapperEnforcer = new LogicalOrWrapperEnforcer(delegationManager); + vedaAdapter = new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER)); + + 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(vedaAdapter), "VedaAdapter"); + vm.label(address(BORING_VAULT), "Veda BoringVault"); + vm.label(address(VEDA_TELLER), "Veda Teller"); + vm.label(address(USDC), "USDC"); + vm.label(USDC_WHALE, "USDC Whale"); + + vm.deal(address(users.alice.deleGator), 1 ether); + vm.deal(address(users.bob.deleGator), 1 ether); + + vm.prank(USDC_WHALE); + USDC.transfer(address(users.alice.deleGator), INITIAL_USD_BALANCE); // 10k USDC + + // Make solver-gated Teller functions publicly callable on the fork + _enableVedaTellerAccess(); + } + + // ================================================================================== + // Section 1: Direct Protocol Tests (Fork Sanity) + // Validates the forked mainnet environment works before testing adapter logic. + // ================================================================================== + + function test_deposit_direct_usdc() public { + uint256 aliceUSDCInitialBalance_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance_, INITIAL_USD_BALANCE); + + uint256 aliceSharesBefore_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + + vm.prank(address(users.alice.deleGator)); + USDC.approve(address(BORING_VAULT), DEPOSIT_AMOUNT); + vm.prank(address(users.alice.deleGator)); + uint256 sharesMinted_ = VEDA_TELLER.deposit(address(USDC), DEPOSIT_AMOUNT, 0, address(0)); + + uint256 aliceUSDCBalance_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCBalance_, INITIAL_USD_BALANCE - DEPOSIT_AMOUNT); + + uint256 aliceSharesAfter_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertEq(aliceSharesAfter_ - aliceSharesBefore_, sharesMinted_); + } + + function test_withdraw_direct_usdc() public { + _setupLendingState(); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + + uint256 aliceUSDCAfterDeposit_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCAfterDeposit_, INITIAL_USD_BALANCE - DEPOSIT_AMOUNT); + + uint256 aliceShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertGt(aliceShares_, 0, "Alice should have vault shares after deposit"); + + // Withdraw all shares back to USDC + vm.prank(address(users.alice.deleGator)); + BORING_VAULT.approve(address(BORING_VAULT), aliceShares_); + vm.prank(address(users.alice.deleGator)); + uint256 assetsOut_ = VEDA_TELLER.withdraw(address(USDC), aliceShares_, 0, address(users.alice.deleGator)); + + assertGt(assetsOut_, 0, "Should receive assets back"); + + uint256 aliceSharesAfter_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertEq(aliceSharesAfter_, 0, "All shares should be burned"); + + uint256 aliceUSDCFinal_ = USDC.balanceOf(address(users.alice.deleGator)); + assertApproxEqAbs(aliceUSDCFinal_, INITIAL_USD_BALANCE, DEPOSIT_AMOUNT / 100, "USDC balance should be close to initial"); + } + + // ================================================================================== + // Section 2: Adapter Happy-Path Tests (Core Functionality) + // Validates the standard deposit/withdraw flow via the adapter using delegations. + // ================================================================================== + + function test_deposit_viaAdapterDelegation_usdc() public { + uint256 aliceUSDCInitialBalance_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCInitialBalance_, INITIAL_USD_BALANCE); + uint256 aliceSharesInitial_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertEq(aliceSharesInitial_, 0); + + // Alice delegates USDC transfer rights to Bob, redeemable only by the adapter + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max); + + // Bob redelegates to the VedaAdapter with a transfer amount cap + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + + uint256 aliceUSDCFinal_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCFinal_, INITIAL_USD_BALANCE - DEPOSIT_AMOUNT, "USDC balance should decrease"); + + uint256 aliceSharesFinal_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertGt(aliceSharesFinal_, 0, "Shares should be minted to Alice"); + } + + function test_withdraw_viaAdapterDelegation_usdc() public { + _setupLendingState(); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + + uint256 aliceShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertGt(aliceShares_, 0, "Alice should have vault shares"); + uint256 aliceUSDCBefore_ = USDC.balanceOf(address(users.alice.deleGator)); + + // Alice delegates BoringVault share transfer rights to Bob, redeemable only by the adapter + Delegation memory delegation_ = _createTransferDelegation( + address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), type(uint256).max + ); + + // Bob redelegates to the VedaAdapter with a share transfer amount cap + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(BORING_VAULT), aliceShares_); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), aliceShares_, 0); + + uint256 aliceSharesAfter_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertEq(aliceSharesAfter_, 0, "All shares should be burned"); + + uint256 aliceUSDCAfter_ = USDC.balanceOf(address(users.alice.deleGator)); + assertGt(aliceUSDCAfter_, aliceUSDCBefore_, "Alice should receive USDC back"); + assertApproxEqAbs(aliceUSDCAfter_, INITIAL_USD_BALANCE, DEPOSIT_AMOUNT / 100, "USDC balance should be close to initial"); + } + + // ================================================================================== + // Section 3: Constructor Validation Tests + // Ensures the adapter rejects invalid constructor parameters. + // ================================================================================== + + /// @notice Constructor must revert when delegationManager is zero address + function test_constructor_revertsOnZeroDelegationManager() public { + vm.expectRevert(VedaAdapter.InvalidZeroAddress.selector); + new VedaAdapter(owner, address(0), address(BORING_VAULT), address(VEDA_TELLER)); + } + + /// @notice Constructor must revert when boringVault is zero address + function test_constructor_revertsOnZeroBoringVault() public { + vm.expectRevert(VedaAdapter.InvalidZeroAddress.selector); + new VedaAdapter(owner, address(delegationManager), address(0), address(VEDA_TELLER)); + } + + /// @notice Constructor must revert when teller is zero address + function test_constructor_revertsOnZeroTeller() public { + vm.expectRevert(VedaAdapter.InvalidZeroAddress.selector); + new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(0)); + } + + /// @notice Constructor must revert when owner is zero address (OZ Ownable) + function test_constructor_revertsOnZeroOwner() public { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0))); + new VedaAdapter(address(0), address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER)); + } + + /// @notice Constructor must store immutable state correctly with valid inputs + function test_constructor_successWithValidAddresses() public { + VedaAdapter newAdapter_ = new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER)); + + assertEq(address(newAdapter_.delegationManager()), address(delegationManager)); + assertEq(newAdapter_.boringVault(), address(BORING_VAULT)); + assertEq(address(newAdapter_.teller()), address(VEDA_TELLER)); + assertEq(newAdapter_.owner(), owner); + } + + // ================================================================================== + // Section 4: Deposit Input Validation / Revert Tests + // Ensures depositByDelegation rejects invalid inputs before any state changes. + // ================================================================================== + + /// @notice depositByDelegation must revert with 0 delegations + function test_depositByDelegation_revertsOnEmptyDelegations() public { + Delegation[] memory delegations_ = new Delegation[](0); + + vm.expectRevert(VedaAdapter.InvalidDelegationsLength.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + } + + /// @notice depositByDelegation must revert with only 1 delegation (requires >= 2 for redelegation pattern) + function test_depositByDelegation_revertsOnSingleDelegation() public { + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(USDC), DEPOSIT_AMOUNT); + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + vm.expectRevert(VedaAdapter.InvalidDelegationsLength.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + } + + /// @notice depositByDelegation must revert when msg.sender does not match delegations[0].delegator + function test_depositByDelegation_revertsOnUnauthorizedCaller() public { + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + // Alice tries to call but Bob is delegations[0].delegator + vm.expectRevert(VedaAdapter.NotLeafDelegator.selector); + vm.prank(address(users.alice.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + } + + /// @notice depositByDelegation must revert when token address is zero + function test_depositByDelegation_revertsOnZeroTokenAddress() public { + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(USDC), DEPOSIT_AMOUNT); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.expectRevert(VedaAdapter.InvalidZeroAddress.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(0), DEPOSIT_AMOUNT, 0); + } + + /// @notice Depositing more than the delegation's ERC20TransferAmountEnforcer cap must revert + function test_depositByDelegation_revertsOnExcessiveAmount() public { + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + uint256 excessiveAmount_ = DEPOSIT_AMOUNT + 1; + vm.expectRevert(); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), excessiveAmount_, 0); + + // Verify no state change + assertEq(USDC.balanceOf(address(users.alice.deleGator)), INITIAL_USD_BALANCE); + } + + // ================================================================================== + // Section 5: Withdraw Input Validation / Revert Tests + // Ensures withdrawByDelegation rejects invalid inputs before any state changes. + // ================================================================================== + + /// @notice withdrawByDelegation must revert with 0 delegations + function test_withdrawByDelegation_revertsOnEmptyDelegations() public { + _setupLendingState(); + + Delegation[] memory delegations_ = new Delegation[](0); + + vm.expectRevert(VedaAdapter.InvalidDelegationsLength.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + } + + /// @notice withdrawByDelegation must revert with only 1 delegation (requires >= 2 for redelegation pattern) + function test_withdrawByDelegation_revertsOnSingleDelegation() public { + _setupLendingState(); + + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), DEPOSIT_AMOUNT); + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + vm.expectRevert(VedaAdapter.InvalidDelegationsLength.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + } + + /// @notice withdrawByDelegation must revert when msg.sender does not match delegations[0].delegator + function test_withdrawByDelegation_revertsOnUnauthorizedCaller() public { + _setupLendingState(); + + Delegation memory delegation_ = _createTransferDelegation( + address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), type(uint256).max + ); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(BORING_VAULT), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + // Alice tries to call but Bob is delegations[0].delegator + vm.expectRevert(VedaAdapter.NotLeafDelegator.selector); + vm.prank(address(users.alice.deleGator)); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + } + + /// @notice withdrawByDelegation must revert when token address is zero + function test_withdrawByDelegation_revertsOnZeroTokenAddress() public { + _setupLendingState(); + + Delegation memory delegation_ = _createTransferDelegation( + address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), type(uint256).max + ); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(BORING_VAULT), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.expectRevert(VedaAdapter.InvalidZeroAddress.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegation(delegations_, address(0), DEPOSIT_AMOUNT, 0); + } + + // ================================================================================== + // Section 6: Event Emission Tests + // Validates that adapter emits correct events with expected indexed parameters. + // ================================================================================== + + /// @notice depositByDelegation must emit DepositExecuted with correct parameters + function test_depositByDelegation_emitsDepositExecutedEvent() public { + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + // Expect event: check indexed delegator, delegate, and token. Amount and shares are checked via topic4. + vm.expectEmit(true, true, true, false, address(vedaAdapter)); + emit VedaAdapter.DepositExecuted( + address(users.alice.deleGator), address(users.bob.deleGator), address(USDC), DEPOSIT_AMOUNT, 0 + ); + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + } + + /// @notice withdrawByDelegation must emit WithdrawExecuted with correct parameters + function test_withdrawByDelegation_emitsWithdrawExecutedEvent() public { + _setupLendingState(); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + + uint256 aliceShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + + Delegation memory delegation_ = _createTransferDelegation( + address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), type(uint256).max + ); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(BORING_VAULT), aliceShares_); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + // Expect event: check indexed delegator, delegate, and token. shareAmount and assetsOut are checked via topic4. + vm.expectEmit(true, true, true, false, address(vedaAdapter)); + emit VedaAdapter.WithdrawExecuted( + address(users.alice.deleGator), address(users.bob.deleGator), address(USDC), aliceShares_, 0 + ); + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), aliceShares_, 0); + } + + // ================================================================================== + // Section 7: Batch Operation Tests + // Validates depositByDelegationBatch and withdrawByDelegationBatch. + // ================================================================================== + + /// @notice depositByDelegationBatch must revert on empty array + function test_depositByDelegationBatch_revertsOnEmptyArray() public { + VedaAdapter.DepositParams[] memory streams_ = new VedaAdapter.DepositParams[](0); + + vm.expectRevert(VedaAdapter.InvalidBatchLength.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegationBatch(streams_); + } + + /// @notice withdrawByDelegationBatch must revert on empty array + function test_withdrawByDelegationBatch_revertsOnEmptyArray() public { + VedaAdapter.WithdrawParams[] memory streams_ = new VedaAdapter.WithdrawParams[](0); + + vm.expectRevert(VedaAdapter.InvalidBatchLength.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegationBatch(streams_); + } + + /// @notice Batch deposit with 2 independent delegation chains in a single transaction + function test_depositByDelegationBatch_twoDelegationChains() public { + uint256 amount1_ = 300 * 1e6; // 300 USDC + uint256 amount2_ = 400 * 1e6; // 400 USDC + + // Chain 1: Alice -> Bob -> VedaAdapter (salt 0) + Delegation memory delegation1_ = _createTransferDelegationWithSalt( + address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max, 0 + ); + Delegation memory redelegation1_ = + _createAdapterRedelegationWithSalt(EncoderLib._getDelegationHash(delegation1_), address(USDC), amount1_, 0); + Delegation[] memory delegations1_ = new Delegation[](2); + delegations1_[0] = redelegation1_; + delegations1_[1] = delegation1_; + + // Chain 2: Alice -> Bob -> VedaAdapter (salt 1) + Delegation memory delegation2_ = _createTransferDelegationWithSalt( + address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max, 1 + ); + Delegation memory redelegation2_ = + _createAdapterRedelegationWithSalt(EncoderLib._getDelegationHash(delegation2_), address(USDC), amount2_, 1); + Delegation[] memory delegations2_ = new Delegation[](2); + delegations2_[0] = redelegation2_; + delegations2_[1] = delegation2_; + + VedaAdapter.DepositParams[] memory streams_ = new VedaAdapter.DepositParams[](2); + streams_[0] = + VedaAdapter.DepositParams({ delegations: delegations1_, token: address(USDC), amount: amount1_, minimumMint: 0 }); + streams_[1] = + VedaAdapter.DepositParams({ delegations: delegations2_, token: address(USDC), amount: amount2_, minimumMint: 0 }); + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegationBatch(streams_); + + uint256 aliceUSDCFinal_ = USDC.balanceOf(address(users.alice.deleGator)); + assertEq(aliceUSDCFinal_, INITIAL_USD_BALANCE - amount1_ - amount2_, "USDC should decrease by total batch amount"); + + uint256 aliceShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertGt(aliceShares_, 0, "Alice should receive vault shares from batch deposit"); + } + + /// @notice Batch withdraw with 2 independent delegation chains in a single transaction + function test_withdrawByDelegationBatch_twoDelegationChains() public { + // Setup: Deposit via adapter to create shares (bulkDeposit skips share lock) + _depositViaAdapter(DEPOSIT_AMOUNT, 10); + + uint256 totalShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertGt(totalShares_, 0, "Alice should have shares after deposit"); + + uint256 sharesPart1_ = totalShares_ / 2; + uint256 sharesPart2_ = totalShares_ - sharesPart1_; + + VedaAdapter.WithdrawParams[] memory wdStreams_ = new VedaAdapter.WithdrawParams[](2); + wdStreams_[0] = _buildWithdrawParams(sharesPart1_, 20); + wdStreams_[1] = _buildWithdrawParams(sharesPart2_, 21); + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegationBatch(wdStreams_); + + assertEq(BORING_VAULT.balanceOf(address(users.alice.deleGator)), 0, "All shares should be redeemed after batch withdraw"); + assertApproxEqAbs( + USDC.balanceOf(address(users.alice.deleGator)), + INITIAL_USD_BALANCE, + DEPOSIT_AMOUNT / 100, + "USDC should be approximately restored" + ); + } + + // ================================================================================== + // Section 8: Emergency Withdraw Tests + // Validates the owner-only withdrawEmergency function for recovering stuck tokens. + // ================================================================================== + + /// @notice Only the contract owner can call withdrawEmergency + function test_withdrawEmergency_revertsOnNonOwner() public { + BasicERC20 testToken_ = new BasicERC20(owner, "TestToken", "TST", 0); + vm.prank(owner); + testToken_.mint(address(vedaAdapter), 100 ether); + + vm.prank(address(users.alice.deleGator)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(users.alice.deleGator))); + vedaAdapter.withdrawEmergency(testToken_, 50 ether, address(users.alice.deleGator)); + + assertEq(testToken_.balanceOf(address(vedaAdapter)), 100 ether, "Balance should be unchanged"); + } + + /// @notice Owner can recover stuck tokens; emits StuckTokensWithdrawn event + function test_withdrawEmergency_recoverTokens() public { + BasicERC20 testToken_ = new BasicERC20(owner, "TestToken", "TST", 0); + vm.prank(owner); + testToken_.mint(address(vedaAdapter), 100 ether); + + vm.expectEmit(true, true, true, true, address(vedaAdapter)); + emit VedaAdapter.StuckTokensWithdrawn(testToken_, address(users.alice.deleGator), 50 ether); + + vm.prank(owner); + vedaAdapter.withdrawEmergency(testToken_, 50 ether, address(users.alice.deleGator)); + + assertEq(testToken_.balanceOf(address(vedaAdapter)), 50 ether, "Adapter should retain remaining tokens"); + assertEq(testToken_.balanceOf(address(users.alice.deleGator)), 50 ether, "Recipient should receive tokens"); + } + + /// @notice withdrawEmergency must revert when recipient is zero address + function test_withdrawEmergency_revertsOnZeroRecipient() public { + BasicERC20 testToken_ = new BasicERC20(owner, "TestToken", "TST", 0); + vm.prank(owner); + testToken_.mint(address(vedaAdapter), 100 ether); + + vm.expectRevert(VedaAdapter.InvalidRecipient.selector); + vm.prank(owner); + vedaAdapter.withdrawEmergency(testToken_, 50 ether, address(0)); + } + + // ================================================================================== + // Section 9: Edge Cases and Security Validation + // Tests for subtle behaviors, allowance management, chain integrity, and token mismatch. + // ================================================================================== + + /// @notice After a deposit, the adapter must not retain any deposited tokens + function test_adapterDoesNotRetainTokensAfterDeposit() public { + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + + assertEq(USDC.balanceOf(address(vedaAdapter)), 0, "Adapter must not retain any USDC after deposit"); + } + + /// @notice After a withdraw, the adapter must not retain any vault shares + function test_adapterDoesNotRetainSharesAfterWithdraw() public { + _setupLendingState(); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + + uint256 aliceShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + + Delegation memory delegation_ = _createTransferDelegation( + address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), type(uint256).max + ); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(BORING_VAULT), aliceShares_); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), aliceShares_, 0); + + assertEq(BORING_VAULT.balanceOf(address(vedaAdapter)), 0, "Adapter must not retain any vault shares after withdraw"); + } + + /// @notice BoringVault must fully consume the allowance granted by the adapter during bulkDeposit. + /// Verifies that _ensureAllowance does not cause unbounded allowance accumulation. + function test_allowanceFullyConsumedAfterDeposit() public { + assertEq(USDC.allowance(address(vedaAdapter), address(BORING_VAULT)), 0, "Initial allowance should be 0"); + + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(USDC), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + + assertEq( + USDC.allowance(address(vedaAdapter), address(BORING_VAULT)), + 0, + "Allowance must be fully consumed after bulkDeposit -- no residual accumulation" + ); + } + + /// @notice A 3-level delegation chain (Alice -> Carol -> Bob -> Adapter) must correctly resolve + /// rootDelegator as Alice, ensuring shares are minted to the actual token owner. + function test_depositByDelegation_withThreeLevelDelegationChain() public { + vm.deal(address(users.carol.deleGator), 1 ether); + + // Root delegation: Alice -> Carol (with transfer enforcer + redeemer enforcer) + Delegation memory rootDelegation_ = + _createTransferDelegation(address(users.carol.deleGator), address(vedaAdapter), address(USDC), type(uint256).max); + + // Middle delegation: Carol -> Bob (no additional caveats, just extends the chain) + Delegation memory middleDelegation_ = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(users.carol.deleGator), + authority: EncoderLib._getDelegationHash(rootDelegation_), + caveats: new Caveat[](0), + salt: 0, + signature: hex"" + }); + middleDelegation_ = signDelegation(users.carol, middleDelegation_); + + // Leaf delegation: Bob -> VedaAdapter (with transfer amount cap) + Delegation memory adapterDelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(middleDelegation_), address(USDC), DEPOSIT_AMOUNT); + + // Chain order: [leaf, middle, root] + Delegation[] memory delegations_ = new Delegation[](3); + delegations_[0] = adapterDelegation_; + delegations_[1] = middleDelegation_; + delegations_[2] = rootDelegation_; + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + + // rootDelegator_ = delegations[2].delegator = Alice + uint256 aliceShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertGt(aliceShares_, 0, "Shares must be minted to Alice (root delegator), not Carol or Bob"); + + assertEq(BORING_VAULT.balanceOf(address(users.carol.deleGator)), 0, "Carol must not receive shares"); + assertEq(BORING_VAULT.balanceOf(address(users.bob.deleGator)), 0, "Bob must not receive shares"); + } + + /// @notice Passing a token to depositByDelegation that differs from the delegation enforcer's + /// token must revert, because the transfer calldata won't match the enforcer's terms. + function test_depositByDelegation_revertsOnTokenMismatch() public { + // Delegation enforcer is set up for BORING_VAULT (share token), but we try to deposit USDC + Delegation memory delegation_ = _createTransferDelegation( + address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), type(uint256).max + ); + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(BORING_VAULT), DEPOSIT_AMOUNT); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + // The adapter will try to transfer USDC, but the enforcer only allows BORING_VAULT token transfers + vm.expectRevert(); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + } + + // ================================================================================== + // Helper Functions + // ================================================================================== + + /// @notice Pranks into the RolesAuthority owner to make solver-gated Teller functions publicly callable + function _enableVedaTellerAccess() internal { + IRolesAuthority rolesAuthority_ = IRolesAuthority(ROLES_AUTHORITY); + + vm.prank(ROLES_AUTHORITY_OWNER); + rolesAuthority_.setPublicCapability(address(VEDA_TELLER), IVedaTeller.bulkDeposit.selector, true); + + vm.prank(ROLES_AUTHORITY_OWNER); + rolesAuthority_.setPublicCapability(address(VEDA_TELLER), IVedaTeller.bulkWithdraw.selector, true); + } + + /// @notice Sets up initial lending state (Alice deposits USDC to get vault shares) + function _setupLendingState() internal { + vm.prank(address(users.alice.deleGator)); + USDC.approve(address(BORING_VAULT), DEPOSIT_AMOUNT); + vm.prank(address(users.alice.deleGator)); + VEDA_TELLER.deposit(address(USDC), DEPOSIT_AMOUNT, 0, address(0)); + } + + /// @notice Deposits USDC via adapter delegation (helper to reduce stack depth in batch tests) + function _depositViaAdapter(uint256 _amount, uint256 _salt) internal { + Delegation memory delegation_ = _createTransferDelegationWithSalt( + address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max, _salt + ); + Delegation memory redelegation_ = + _createAdapterRedelegationWithSalt(EncoderLib._getDelegationHash(delegation_), address(USDC), _amount, _salt); + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, address(USDC), _amount, 0); + } + + /// @notice Builds a WithdrawParams struct for batch withdraw (helper to reduce stack depth) + function _buildWithdrawParams(uint256 _shareAmount, uint256 _salt) internal view returns (VedaAdapter.WithdrawParams memory) { + Delegation memory wd_ = _createTransferDelegationWithSalt( + address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), type(uint256).max, _salt + ); + Delegation memory rewd_ = + _createAdapterRedelegationWithSalt(EncoderLib._getDelegationHash(wd_), address(BORING_VAULT), _shareAmount, _salt); + Delegation[] memory wdDelegations_ = new Delegation[](2); + wdDelegations_[0] = rewd_; + wdDelegations_[1] = wd_; + + return VedaAdapter.WithdrawParams({ + delegations: wdDelegations_, token: address(USDC), shareAmount: _shareAmount, minimumAssets: 0 + }); + } + + /// @notice Creates a transfer delegation with ERC20TransferAmountEnforcer and RedeemerEnforcer + function _createTransferDelegation( + address _delegate, + address _redeemer, + address _token, + uint256 _amount + ) + internal + view + returns (Delegation memory) + { + return _createTransferDelegationWithSalt(_delegate, _redeemer, _token, _amount, 0); + } + + /// @notice Creates a transfer delegation with a custom salt for unique delegation hashes in batch operations + function _createTransferDelegationWithSalt( + address _delegate, + address _redeemer, + address _token, + uint256 _amount, + uint256 _salt + ) + internal + view + returns (Delegation memory) + { + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = + Caveat({ args: hex"", enforcer: address(erc20TransferAmountEnforcer), terms: abi.encodePacked(_token, _amount) }); + + caveats_[1] = Caveat({ args: hex"", enforcer: address(redeemerEnforcer), terms: abi.encodePacked(_redeemer) }); + + Delegation memory delegation_ = Delegation({ + delegate: _delegate, + delegator: address(users.alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: _salt, + signature: hex"" + }); + + return signDelegation(users.alice, delegation_); + } + + /// @notice Creates an adapter redelegation with ERC20TransferAmountEnforcer + function _createAdapterRedelegation( + bytes32 _authority, + address _token, + uint256 _amount + ) + internal + view + returns (Delegation memory) + { + return _createAdapterRedelegationWithSalt(_authority, _token, _amount, 0); + } + + /// @notice Creates an adapter redelegation with a custom salt for unique delegation hashes in batch operations + function _createAdapterRedelegationWithSalt( + bytes32 _authority, + address _token, + uint256 _amount, + uint256 _salt + ) + internal + view + returns (Delegation memory) + { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: hex"", enforcer: address(erc20TransferAmountEnforcer), terms: abi.encodePacked(_token, _amount) }); + + Delegation memory delegation_ = Delegation({ + delegate: address(vedaAdapter), + delegator: address(users.bob.deleGator), + authority: _authority, + caveats: caveats_, + salt: _salt, + signature: hex"" + }); + + return signDelegation(users.bob, delegation_); + } +} + +interface IRolesAuthority { + function setPublicCapability(address target, bytes4 functionSig, bool enabled) external; +}