From 686b3b5a6e30c74e3bd664bb80146c9aad05eb13 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Mon, 2 Mar 2026 11:31:24 +0100 Subject: [PATCH 01/16] 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; +} From 89e74c46329511941c495aec147f3940cd60f491 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Fri, 6 Mar 2026 13:01:29 +0100 Subject: [PATCH 02/16] basic boring on chain queue implementation --- src/helpers/VedaAdapter.sol | 142 +++++++++++++++++- .../interfaces/IBoringOnChainQueue.sol | 31 ++++ test/helpers/VedaLending.t.sol | 60 +++++++- 3 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 src/helpers/interfaces/IBoringOnChainQueue.sol diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index 23b4ed1f..f4c6a874 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -9,6 +9,7 @@ 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"; +import { IBoringOnChainQueue } from "./interfaces/IBoringOnChainQueue.sol"; /** * @title VedaAdapter @@ -68,6 +69,15 @@ contract VedaAdapter is Ownable2Step { uint256 minimumAssets; } + /** + * @notice Parameters for a single queued withdrawal operation in a batch + */ + struct QueueWithdrawParams { + Delegation[] delegations; + address token; + uint128 shareAmount; + } + ////////////////////////////// Events ////////////////////////////// /** @@ -94,6 +104,18 @@ contract VedaAdapter is Ownable2Step { address indexed delegator, address indexed delegate, address indexed token, uint256 shareAmount, uint256 assetsOut ); + /** + * @notice Emitted when a queued withdrawal is created via delegation + * @param delegator Address of the share owner (delegator) + * @param delegate Address of the executor (delegate) + * @param token Address of the underlying token requested + * @param shareAmount Amount of vault shares queued + * @param requestId The queue request identifier + */ + event QueueWithdrawExecuted( + address indexed delegator, address indexed delegate, address indexed token, uint128 shareAmount, bytes32 requestId + ); + /** * @notice Emitted when stuck tokens are withdrawn by owner * @param token Address of the token withdrawn @@ -119,6 +141,11 @@ contract VedaAdapter is Ownable2Step { /// @dev Thrown when msg.sender is not the leaf delegator error NotLeafDelegator(); + ////////////////////////////// Constants ////////////////////////////// + + uint16 public constant QUEUE_DISCOUNT = 0; + uint24 public constant QUEUE_SECONDS_TO_DEADLINE = 864000; // 10 days + ////////////////////////////// State ////////////////////////////// /** @@ -136,23 +163,38 @@ contract VedaAdapter is Ownable2Step { */ IVedaTeller public immutable teller; + /** + * @notice The BoringOnChainQueue contract for queued withdrawals + */ + IBoringOnChainQueue public immutable boringQueue; + ////////////////////////////// Constructor ////////////////////////////// /** - * @notice Initializes the adapter with delegation manager, BoringVault, and Teller addresses + * @notice Initializes the adapter with delegation manager, BoringVault, Teller, and Queue 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) + * @param _boringQueue Address of the BoringOnChainQueue contract (queued withdrawals) */ - constructor(address _owner, address _delegationManager, address _boringVault, address _teller) Ownable(_owner) { - if (_delegationManager == address(0) || _boringVault == address(0) || _teller == address(0)) { + constructor( + address _owner, + address _delegationManager, + address _boringVault, + address _teller, + address _boringQueue + ) + Ownable(_owner) + { + if (_delegationManager == address(0) || _boringVault == address(0) || _teller == address(0) || _boringQueue == address(0)) { revert InvalidZeroAddress(); } delegationManager = IDelegationManager(_delegationManager); boringVault = _boringVault; teller = IVedaTeller(_teller); + boringQueue = IBoringOnChainQueue(_boringQueue); } ////////////////////////////// External Methods ////////////////////////////// @@ -232,6 +274,45 @@ contract VedaAdapter is Ownable2Step { } } + /** + * @notice Queues a withdrawal from the BoringVault on-chain queue using delegation-based share transfer + * @dev Redeems the delegation to transfer vault shares to this adapter, approves the queue, + * then calls requestOnChainWithdraw. Funds are NOT automatically withdrawn; the request + * must be solved/settled separately after maturity. + * @param _delegations Array of Delegation objects, sorted leaf to root + * @param _token Address of the underlying token to receive upon settlement + * @param _shareAmount Amount of vault shares to queue for withdrawal + * @return requestId The queue request identifier + */ + function queueWithdrawByDelegation( + Delegation[] memory _delegations, + address _token, + uint128 _shareAmount + ) + external + returns (bytes32 requestId) + { + return _executeQueueWithdrawByDelegation(_delegations, _token, _shareAmount, msg.sender); + } + + /** + * @notice Queues multiple withdrawals using delegation streams, executed sequentially + * @param _queueWithdrawStreams Array of queue withdraw parameters + */ + function queueWithdrawByDelegationBatch(QueueWithdrawParams[] memory _queueWithdrawStreams) external { + uint256 streamsLength_ = _queueWithdrawStreams.length; + if (streamsLength_ == 0) revert InvalidBatchLength(); + + address caller_ = msg.sender; + for (uint256 i = 0; i < streamsLength_;) { + QueueWithdrawParams memory params_ = _queueWithdrawStreams[i]; + _executeQueueWithdrawByDelegation(params_.delegations, params_.token, params_.shareAmount, 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 @@ -252,15 +333,16 @@ contract VedaAdapter is Ownable2Step { ////////////////////////////// Private/Internal Methods ////////////////////////////// /** - * @notice Ensures sufficient token allowance for BoringVault to pull tokens + * @notice Ensures sufficient token allowance for a spender to pull tokens * @dev Checks current allowance and sets exact amount if insufficient, avoiding accumulation * @param _token Token to manage allowance for + * @param _spender Address that needs to spend the tokens * @param _amount Amount needed for the operation */ - function _ensureAllowance(IERC20 _token, uint256 _amount) private { - uint256 allowance_ = _token.allowance(address(this), boringVault); + function _ensureAllowance(IERC20 _token, address _spender, uint256 _amount) private { + uint256 allowance_ = _token.allowance(address(this), _spender); if (allowance_ < _amount) { - _token.forceApprove(boringVault, _amount); + _token.forceApprove(_spender, _amount); } } @@ -302,7 +384,7 @@ contract VedaAdapter is Ownable2Step { delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); // Approve BoringVault to pull tokens, then deposit via Teller - _ensureAllowance(IERC20(_token), _amount); + _ensureAllowance(IERC20(_token), boringVault, _amount); uint256 shares_ = teller.bulkDeposit(_token, _amount, _minimumMint, rootDelegator_); emit DepositExecuted(rootDelegator_, _caller, _token, _amount, shares_); @@ -350,4 +432,48 @@ contract VedaAdapter is Ownable2Step { emit WithdrawExecuted(rootDelegator_, _caller, _token, _shareAmount, assetsOut_); } + + /** + * @notice Internal implementation of queued withdraw by delegation + * @param _delegations Delegation chain, sorted leaf to root + * @param _token Underlying token to receive upon settlement + * @param _shareAmount Amount of vault shares to queue + * @param _caller Authorized caller (must match leaf delegator) + * @return requestId The queue request identifier + */ + function _executeQueueWithdrawByDelegation( + Delegation[] memory _delegations, + address _token, + uint128 _shareAmount, + address _caller + ) + internal + returns (bytes32 requestId) + { + 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), uint256(_shareAmount))); + executionCallDatas_[0] = ExecutionLib.encodeSingle(boringVault, 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + // Approve queue to pull vault shares, then create the queued withdrawal + _ensureAllowance(IERC20(boringVault), address(boringQueue), uint256(_shareAmount)); + requestId = boringQueue.requestOnChainWithdraw(_token, _shareAmount, QUEUE_DISCOUNT, QUEUE_SECONDS_TO_DEADLINE); + + emit QueueWithdrawExecuted(rootDelegator_, _caller, _token, _shareAmount, requestId); + } } diff --git a/src/helpers/interfaces/IBoringOnChainQueue.sol b/src/helpers/interfaces/IBoringOnChainQueue.sol new file mode 100644 index 00000000..6c7ac951 --- /dev/null +++ b/src/helpers/interfaces/IBoringOnChainQueue.sol @@ -0,0 +1,31 @@ +// Based on: +// https://github.com/Veda-Labs/boring-vault/blob/main/src/base/Roles/BoringQueue/BoringOnChainQueue.sol + +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +/** + * @title IBoringOnChainQueue + * @notice Interface for the BoringOnChainQueue's withdraw-request function. + * @dev Uses native Solidity types to avoid importing Veda-specific dependencies. + */ +interface IBoringOnChainQueue { + /** + * @notice Request an on-chain withdraw from the BoringVault queue. + * @dev The caller must have approved this queue contract to spend `amountOfShares` + * of the BoringVault share token. The queue pulls shares via `safeTransferFrom`. + * @param assetOut The underlying asset the user wants to receive upon maturity + * @param amountOfShares The amount of vault shares to queue for withdrawal + * @param discount The discount to apply in bps (0 = no discount) + * @param secondsToDeadline The time in seconds the request remains valid after maturity + * @return requestId A unique identifier for the queued withdraw request + */ + function requestOnChainWithdraw( + address assetOut, + uint128 amountOfShares, + uint16 discount, + uint24 secondsToDeadline + ) + external + returns (bytes32 requestId); +} diff --git a/test/helpers/VedaLending.t.sol b/test/helpers/VedaLending.t.sol index 95993cc0..f6a2f98a 100644 --- a/test/helpers/VedaLending.t.sol +++ b/test/helpers/VedaLending.t.sol @@ -54,6 +54,7 @@ contract VedaLendingTest is BaseTest { // Restricted vault - cannot set on behalfOf IVedaTeller public constant VEDA_TELLER = IVedaTeller(0xc46f2443b3521632E2E2a903D6da8f965B46f6a0); IERC20 public constant BORING_VAULT = IERC20(0xDbD87325D7b1189Dcc9255c4926076fF4a96A271); + address public constant BORING_QUEUE = 0x406E63323EF5d39D41C6fD895Ef9665AF926184c; address public constant ROLES_AUTHORITY = 0x1F53135155d6fF516bCcfDd9424fcdB8AD1eFB77; address public constant ROLES_AUTHORITY_OWNER = 0x846abf72fE789cf52FDefB0e924bE9E3670667DA; @@ -102,7 +103,7 @@ contract VedaLendingTest is BaseTest { redeemerEnforcer = new RedeemerEnforcer(); limitedCallsEnforcer = new LimitedCallsEnforcer(); logicalOrWrapperEnforcer = new LogicalOrWrapperEnforcer(delegationManager); - vedaAdapter = new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER)); + vedaAdapter = new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER), BORING_QUEUE); vm.label(address(allowedTargetsEnforcer), "AllowedTargetsEnforcer"); vm.label(address(allowedMethodsEnforcer), "AllowedMethodsEnforcer"); @@ -111,6 +112,7 @@ contract VedaLendingTest is BaseTest { vm.label(address(logicalOrWrapperEnforcer), "LogicalOrWrapperEnforcer"); vm.label(address(erc20TransferAmountEnforcer), "ERC20TransferAmountEnforcer"); vm.label(address(vedaAdapter), "VedaAdapter"); + vm.label(BORING_QUEUE, "Veda BoringQueue"); vm.label(address(BORING_VAULT), "Veda BoringVault"); vm.label(address(VEDA_TELLER), "Veda Teller"); vm.label(address(USDC), "USDC"); @@ -247,34 +249,42 @@ contract VedaLendingTest is BaseTest { /// @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)); + new VedaAdapter(owner, address(0), address(BORING_VAULT), address(VEDA_TELLER), BORING_QUEUE); } /// @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)); + new VedaAdapter(owner, address(delegationManager), address(0), address(VEDA_TELLER), BORING_QUEUE); } /// @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)); + new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(0), BORING_QUEUE); + } + + /// @notice Constructor must revert when boringQueue is zero address + function test_constructor_revertsOnZeroBoringQueue() public { + vm.expectRevert(VedaAdapter.InvalidZeroAddress.selector); + new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER), 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)); + new VedaAdapter(address(0), address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER), BORING_QUEUE); } /// @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)); + VedaAdapter newAdapter_ = + new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER), BORING_QUEUE); assertEq(address(newAdapter_.delegationManager()), address(delegationManager)); assertEq(newAdapter_.boringVault(), address(BORING_VAULT)); assertEq(address(newAdapter_.teller()), address(VEDA_TELLER)); + assertEq(address(newAdapter_.boringQueue()), BORING_QUEUE); assertEq(newAdapter_.owner(), owner); } @@ -571,7 +581,40 @@ contract VedaLendingTest is BaseTest { } // ================================================================================== - // Section 8: Emergency Withdraw Tests + // Section 8: Queue Withdraw Tests + // Validates queueWithdrawByDelegation using the deployed BoringOnChainQueue. + // ================================================================================== + + /// @notice Queuing a withdrawal via delegation should transfer shares to the queue and return a valid requestId + function test_queueWithdraw_viaAdapterDelegation() public { + _depositViaAdapter(DEPOSIT_AMOUNT, 50); + + uint256 aliceShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); + assertGt(aliceShares_, 0, "Alice should have vault shares after deposit"); + + uint128 shareAmount_ = uint128(aliceShares_); + uint256 queueSharesBefore_ = BORING_VAULT.balanceOf(BORING_QUEUE); + + 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), uint256(shareAmount_)); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.prank(address(users.bob.deleGator)); + bytes32 requestId_ = vedaAdapter.queueWithdrawByDelegation(delegations_, address(USDC), shareAmount_); + + assertEq(BORING_VAULT.balanceOf(address(users.alice.deleGator)), 0, "Alice should have no shares after queue"); + assertEq(BORING_VAULT.balanceOf(BORING_QUEUE) - queueSharesBefore_, aliceShares_, "Queue should receive the shares"); + assertTrue(requestId_ != bytes32(0), "Request ID should be non-zero"); + } + + // ================================================================================== + // Section 9: Emergency Withdraw Tests // Validates the owner-only withdrawEmergency function for recovering stuck tokens. // ================================================================================== @@ -616,7 +659,7 @@ contract VedaLendingTest is BaseTest { } // ================================================================================== - // Section 9: Edge Cases and Security Validation + // Section 10: Edge Cases and Security Validation // Tests for subtle behaviors, allowance management, chain integrity, and token mismatch. // ================================================================================== @@ -887,3 +930,4 @@ contract VedaLendingTest is BaseTest { interface IRolesAuthority { function setPublicCapability(address target, bytes4 functionSig, bool enabled) external; } + From 7dc22459798c44e476244084162d19f375bb0403 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Mon, 30 Mar 2026 09:29:25 +0200 Subject: [PATCH 03/16] Move to arbitrum mainnet tests, implement new deposit function, remove withdraw via boring queue --- src/helpers/VedaAdapter.sol | 113 +----------------- .../interfaces/IBoringOnChainQueue.sol | 31 ----- src/helpers/interfaces/IVedaTeller.sol | 58 +-------- test/helpers/VedaLending.t.sol | 95 +++------------ 4 files changed, 27 insertions(+), 270 deletions(-) delete mode 100644 src/helpers/interfaces/IBoringOnChainQueue.sol diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index f4c6a874..5006c9dc 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -9,7 +9,6 @@ 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"; -import { IBoringOnChainQueue } from "./interfaces/IBoringOnChainQueue.sol"; /** * @title VedaAdapter @@ -36,12 +35,11 @@ import { IBoringOnChainQueue } from "./interfaces/IBoringOnChainQueue.sol"; * - 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. + * approves the BoringVault, and calls `teller.deposit()` 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 { @@ -141,11 +139,6 @@ contract VedaAdapter is Ownable2Step { /// @dev Thrown when msg.sender is not the leaf delegator error NotLeafDelegator(); - ////////////////////////////// Constants ////////////////////////////// - - uint16 public constant QUEUE_DISCOUNT = 0; - uint24 public constant QUEUE_SECONDS_TO_DEADLINE = 864000; // 10 days - ////////////////////////////// State ////////////////////////////// /** @@ -163,11 +156,6 @@ contract VedaAdapter is Ownable2Step { */ IVedaTeller public immutable teller; - /** - * @notice The BoringOnChainQueue contract for queued withdrawals - */ - IBoringOnChainQueue public immutable boringQueue; - ////////////////////////////// Constructor ////////////////////////////// /** @@ -176,25 +164,15 @@ contract VedaAdapter is Ownable2Step { * @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) - * @param _boringQueue Address of the BoringOnChainQueue contract (queued withdrawals) */ - constructor( - address _owner, - address _delegationManager, - address _boringVault, - address _teller, - address _boringQueue - ) - Ownable(_owner) - { - if (_delegationManager == address(0) || _boringVault == address(0) || _teller == address(0) || _boringQueue == address(0)) { + 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); - boringQueue = IBoringOnChainQueue(_boringQueue); } ////////////////////////////// External Methods ////////////////////////////// @@ -274,45 +252,6 @@ contract VedaAdapter is Ownable2Step { } } - /** - * @notice Queues a withdrawal from the BoringVault on-chain queue using delegation-based share transfer - * @dev Redeems the delegation to transfer vault shares to this adapter, approves the queue, - * then calls requestOnChainWithdraw. Funds are NOT automatically withdrawn; the request - * must be solved/settled separately after maturity. - * @param _delegations Array of Delegation objects, sorted leaf to root - * @param _token Address of the underlying token to receive upon settlement - * @param _shareAmount Amount of vault shares to queue for withdrawal - * @return requestId The queue request identifier - */ - function queueWithdrawByDelegation( - Delegation[] memory _delegations, - address _token, - uint128 _shareAmount - ) - external - returns (bytes32 requestId) - { - return _executeQueueWithdrawByDelegation(_delegations, _token, _shareAmount, msg.sender); - } - - /** - * @notice Queues multiple withdrawals using delegation streams, executed sequentially - * @param _queueWithdrawStreams Array of queue withdraw parameters - */ - function queueWithdrawByDelegationBatch(QueueWithdrawParams[] memory _queueWithdrawStreams) external { - uint256 streamsLength_ = _queueWithdrawStreams.length; - if (streamsLength_ == 0) revert InvalidBatchLength(); - - address caller_ = msg.sender; - for (uint256 i = 0; i < streamsLength_;) { - QueueWithdrawParams memory params_ = _queueWithdrawStreams[i]; - _executeQueueWithdrawByDelegation(params_.delegations, params_.token, params_.shareAmount, 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 @@ -385,7 +324,7 @@ contract VedaAdapter is Ownable2Step { // Approve BoringVault to pull tokens, then deposit via Teller _ensureAllowance(IERC20(_token), boringVault, _amount); - uint256 shares_ = teller.bulkDeposit(_token, _amount, _minimumMint, rootDelegator_); + uint256 shares_ = teller.deposit(_token, _amount, _minimumMint, rootDelegator_, address(0)); emit DepositExecuted(rootDelegator_, _caller, _token, _amount, shares_); } @@ -432,48 +371,4 @@ contract VedaAdapter is Ownable2Step { emit WithdrawExecuted(rootDelegator_, _caller, _token, _shareAmount, assetsOut_); } - - /** - * @notice Internal implementation of queued withdraw by delegation - * @param _delegations Delegation chain, sorted leaf to root - * @param _token Underlying token to receive upon settlement - * @param _shareAmount Amount of vault shares to queue - * @param _caller Authorized caller (must match leaf delegator) - * @return requestId The queue request identifier - */ - function _executeQueueWithdrawByDelegation( - Delegation[] memory _delegations, - address _token, - uint128 _shareAmount, - address _caller - ) - internal - returns (bytes32 requestId) - { - 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), uint256(_shareAmount))); - executionCallDatas_[0] = ExecutionLib.encodeSingle(boringVault, 0, encodedTransfer_); - - delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); - - // Approve queue to pull vault shares, then create the queued withdrawal - _ensureAllowance(IERC20(boringVault), address(boringQueue), uint256(_shareAmount)); - requestId = boringQueue.requestOnChainWithdraw(_token, _shareAmount, QUEUE_DISCOUNT, QUEUE_SECONDS_TO_DEADLINE); - - emit QueueWithdrawExecuted(rootDelegator_, _caller, _token, _shareAmount, requestId); - } } diff --git a/src/helpers/interfaces/IBoringOnChainQueue.sol b/src/helpers/interfaces/IBoringOnChainQueue.sol deleted file mode 100644 index 6c7ac951..00000000 --- a/src/helpers/interfaces/IBoringOnChainQueue.sol +++ /dev/null @@ -1,31 +0,0 @@ -// Based on: -// https://github.com/Veda-Labs/boring-vault/blob/main/src/base/Roles/BoringQueue/BoringOnChainQueue.sol - -// SPDX-License-Identifier: MIT AND Apache-2.0 -pragma solidity 0.8.23; - -/** - * @title IBoringOnChainQueue - * @notice Interface for the BoringOnChainQueue's withdraw-request function. - * @dev Uses native Solidity types to avoid importing Veda-specific dependencies. - */ -interface IBoringOnChainQueue { - /** - * @notice Request an on-chain withdraw from the BoringVault queue. - * @dev The caller must have approved this queue contract to spend `amountOfShares` - * of the BoringVault share token. The queue pulls shares via `safeTransferFrom`. - * @param assetOut The underlying asset the user wants to receive upon maturity - * @param amountOfShares The amount of vault shares to queue for withdrawal - * @param discount The discount to apply in bps (0 = no discount) - * @param secondsToDeadline The time in seconds the request remains valid after maturity - * @return requestId A unique identifier for the queued withdraw request - */ - function requestOnChainWithdraw( - address assetOut, - uint128 amountOfShares, - uint16 discount, - uint24 secondsToDeadline - ) - external - returns (bytes32 requestId); -} diff --git a/src/helpers/interfaces/IVedaTeller.sol b/src/helpers/interfaces/IVedaTeller.sol index c40daaf5..986e84c6 100644 --- a/src/helpers/interfaces/IVedaTeller.sol +++ b/src/helpers/interfaces/IVedaTeller.sol @@ -33,46 +33,18 @@ interface IVedaTeller { 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 + * @notice Allows an authorized caller to deposit into the BoringVault for another address, if this contract is not paused. + * @dev Intended for router-like integrations; this selector should remain role-gated. */ - function bulkDeposit( + function deposit( address depositAsset, uint256 depositAmount, uint256 minimumMint, - address to + address to, + address referralAddress ) external + payable returns (uint256 shares); /** @@ -93,22 +65,4 @@ interface IVedaTeller { ) 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 index f6a2f98a..13ab96c4 100644 --- a/test/helpers/VedaLending.t.sol +++ b/test/helpers/VedaLending.t.sol @@ -31,7 +31,7 @@ import { BasicERC20 } from "../utils/BasicERC20.t.sol"; /** * @title VedaLending Test * @notice Tests delegation-based lending on Veda BoringVault. - * @dev Uses a forked Ink mainnet environment to test real contract interactions. + * @dev Uses a forked Arbitrum 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 @@ -40,8 +40,6 @@ import { BasicERC20 } from "../utils/BasicERC20.t.sol"; * - 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: @@ -52,15 +50,11 @@ 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 BORING_QUEUE = 0x406E63323EF5d39D41C6fD895Ef9665AF926184c; + IVedaTeller public constant VEDA_TELLER = IVedaTeller(0x86821F179eaD9F0b3C79b2f8deF0227eEBFDc9f9); + IERC20 public constant BORING_VAULT = IERC20(0xB5F07d769dD60fE54c97dd53101181073DDf21b2); - 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; + IERC20 public constant USDC = IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831); + address public constant USDC_WHALE = 0xC6962004f452bE9203591991D15f6b388e09E8D0; address public owner; // Enforcers for delegation restrictions @@ -74,7 +68,7 @@ contract VedaLendingTest is BaseTest { LimitedCallsEnforcer public limitedCallsEnforcer; VedaAdapter public vedaAdapter; - uint256 public constant MAINNET_FORK_BLOCK = 38688994; // Use latest available block + uint256 public constant MAINNET_FORK_BLOCK = 447148700; // 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() @@ -83,7 +77,7 @@ contract VedaLendingTest is BaseTest { function setUp() public override { // Create fork from mainnet at specific block - vm.createSelectFork(vm.envString("INK_RPC_URL"), MAINNET_FORK_BLOCK); + vm.createSelectFork(vm.envString("ARBITRUM_RPC_URL"), MAINNET_FORK_BLOCK); // Set implementation type IMPLEMENTATION = Implementation.Hybrid; @@ -103,7 +97,7 @@ contract VedaLendingTest is BaseTest { redeemerEnforcer = new RedeemerEnforcer(); limitedCallsEnforcer = new LimitedCallsEnforcer(); logicalOrWrapperEnforcer = new LogicalOrWrapperEnforcer(delegationManager); - vedaAdapter = new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER), BORING_QUEUE); + vedaAdapter = new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER)); vm.label(address(allowedTargetsEnforcer), "AllowedTargetsEnforcer"); vm.label(address(allowedMethodsEnforcer), "AllowedMethodsEnforcer"); @@ -112,7 +106,6 @@ contract VedaLendingTest is BaseTest { vm.label(address(logicalOrWrapperEnforcer), "LogicalOrWrapperEnforcer"); vm.label(address(erc20TransferAmountEnforcer), "ERC20TransferAmountEnforcer"); vm.label(address(vedaAdapter), "VedaAdapter"); - vm.label(BORING_QUEUE, "Veda BoringQueue"); vm.label(address(BORING_VAULT), "Veda BoringVault"); vm.label(address(VEDA_TELLER), "Veda Teller"); vm.label(address(USDC), "USDC"); @@ -123,9 +116,6 @@ contract VedaLendingTest is BaseTest { 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(); } // ================================================================================== @@ -249,42 +239,34 @@ contract VedaLendingTest is BaseTest { /// @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), BORING_QUEUE); + 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), BORING_QUEUE); + 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), BORING_QUEUE); - } - - /// @notice Constructor must revert when boringQueue is zero address - function test_constructor_revertsOnZeroBoringQueue() public { - vm.expectRevert(VedaAdapter.InvalidZeroAddress.selector); - new VedaAdapter(owner, address(delegationManager), address(BORING_VAULT), address(VEDA_TELLER), address(0)); + 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), BORING_QUEUE); + 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), BORING_QUEUE); + 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(address(newAdapter_.boringQueue()), BORING_QUEUE); assertEq(newAdapter_.owner(), owner); } @@ -555,8 +537,9 @@ contract VedaLendingTest is BaseTest { /// @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) + // Setup: Deposit via adapter to create shares, then warp past the 60s share lock period _depositViaAdapter(DEPOSIT_AMOUNT, 10); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); uint256 totalShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); assertGt(totalShares_, 0, "Alice should have shares after deposit"); @@ -581,40 +564,7 @@ contract VedaLendingTest is BaseTest { } // ================================================================================== - // Section 8: Queue Withdraw Tests - // Validates queueWithdrawByDelegation using the deployed BoringOnChainQueue. - // ================================================================================== - - /// @notice Queuing a withdrawal via delegation should transfer shares to the queue and return a valid requestId - function test_queueWithdraw_viaAdapterDelegation() public { - _depositViaAdapter(DEPOSIT_AMOUNT, 50); - - uint256 aliceShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); - assertGt(aliceShares_, 0, "Alice should have vault shares after deposit"); - - uint128 shareAmount_ = uint128(aliceShares_); - uint256 queueSharesBefore_ = BORING_VAULT.balanceOf(BORING_QUEUE); - - 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), uint256(shareAmount_)); - - Delegation[] memory delegations_ = new Delegation[](2); - delegations_[0] = redelegation_; - delegations_[1] = delegation_; - - vm.prank(address(users.bob.deleGator)); - bytes32 requestId_ = vedaAdapter.queueWithdrawByDelegation(delegations_, address(USDC), shareAmount_); - - assertEq(BORING_VAULT.balanceOf(address(users.alice.deleGator)), 0, "Alice should have no shares after queue"); - assertEq(BORING_VAULT.balanceOf(BORING_QUEUE) - queueSharesBefore_, aliceShares_, "Queue should receive the shares"); - assertTrue(requestId_ != bytes32(0), "Request ID should be non-zero"); - } - - // ================================================================================== - // Section 9: Emergency Withdraw Tests + // Section 8: Emergency Withdraw Tests // Validates the owner-only withdrawEmergency function for recovering stuck tokens. // ================================================================================== @@ -659,7 +609,7 @@ contract VedaLendingTest is BaseTest { } // ================================================================================== - // Section 10: Edge Cases and Security Validation + // Section 9: Edge Cases and Security Validation // Tests for subtle behaviors, allowance management, chain integrity, and token mismatch. // ================================================================================== @@ -792,17 +742,6 @@ contract VedaLendingTest is BaseTest { // 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)); From b0a56ede1e8a2c6d0867e2a6caebca46bd4c76da Mon Sep 17 00:00:00 2001 From: MoMannn Date: Mon, 30 Mar 2026 09:50:27 +0200 Subject: [PATCH 04/16] Clean up docs, add batch events --- src/helpers/VedaAdapter.sol | 41 ++++++++++++++++------------------ test/helpers/VedaLending.t.sol | 17 +++++++------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index 5006c9dc..1cdf226d 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -20,8 +20,8 @@ import { IVedaTeller } from "./interfaces/IVedaTeller.sol"; * 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 + * - Teller: The contract that orchestrates deposits/withdrawals. The adapter calls `teller.deposit()` + * for deposits and `teller.withdraw()` for withdrawals (user-facing, no special * role needed). * * Delegation Flow: @@ -67,15 +67,6 @@ contract VedaAdapter is Ownable2Step { uint256 minimumAssets; } - /** - * @notice Parameters for a single queued withdrawal operation in a batch - */ - struct QueueWithdrawParams { - Delegation[] delegations; - address token; - uint128 shareAmount; - } - ////////////////////////////// Events ////////////////////////////// /** @@ -103,16 +94,18 @@ contract VedaAdapter is Ownable2Step { ); /** - * @notice Emitted when a queued withdrawal is created via delegation - * @param delegator Address of the share owner (delegator) - * @param delegate Address of the executor (delegate) - * @param token Address of the underlying token requested - * @param shareAmount Amount of vault shares queued - * @param requestId The queue request identifier + * @notice Emitted when a batch deposit is completed + * @param caller Address of the batch executor + * @param count Number of deposit streams executed */ - event QueueWithdrawExecuted( - address indexed delegator, address indexed delegate, address indexed token, uint128 shareAmount, bytes32 requestId - ); + event BatchDepositExecuted(address indexed caller, uint256 count); + + /** + * @notice Emitted when a batch withdrawal is completed + * @param caller Address of the batch executor + * @param count Number of withdrawal streams executed + */ + event BatchWithdrawExecuted(address indexed caller, uint256 count); /** * @notice Emitted when stuck tokens are withdrawn by owner @@ -159,7 +152,7 @@ contract VedaAdapter is Ownable2Step { ////////////////////////////// Constructor ////////////////////////////// /** - * @notice Initializes the adapter with delegation manager, BoringVault, Teller, and Queue addresses + * @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) @@ -179,7 +172,7 @@ contract VedaAdapter is Ownable2Step { /** * @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 + * @dev Redeems the delegation to transfer tokens to this adapter, then calls deposit * 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 @@ -209,6 +202,8 @@ contract VedaAdapter is Ownable2Step { ++i; } } + + emit BatchDepositExecuted(caller_, streamsLength_); } /** @@ -250,6 +245,8 @@ contract VedaAdapter is Ownable2Step { ++i; } } + + emit BatchWithdrawExecuted(caller_, streamsLength_); } /** diff --git a/test/helpers/VedaLending.t.sol b/test/helpers/VedaLending.t.sol index 13ab96c4..fb256d1c 100644 --- a/test/helpers/VedaLending.t.sol +++ b/test/helpers/VedaLending.t.sol @@ -153,8 +153,6 @@ contract VedaLendingTest is BaseTest { // 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"); @@ -525,6 +523,9 @@ contract VedaLendingTest is BaseTest { streams_[1] = VedaAdapter.DepositParams({ delegations: delegations2_, token: address(USDC), amount: amount2_, minimumMint: 0 }); + vm.expectEmit(true, true, true, true, address(vedaAdapter)); + emit VedaAdapter.BatchDepositExecuted(address(users.bob.deleGator), 2); + vm.prank(address(users.bob.deleGator)); vedaAdapter.depositByDelegationBatch(streams_); @@ -551,6 +552,9 @@ contract VedaLendingTest is BaseTest { wdStreams_[0] = _buildWithdrawParams(sharesPart1_, 20); wdStreams_[1] = _buildWithdrawParams(sharesPart2_, 21); + vm.expectEmit(true, true, true, true, address(vedaAdapter)); + emit VedaAdapter.BatchWithdrawExecuted(address(users.bob.deleGator), 2); + vm.prank(address(users.bob.deleGator)); vedaAdapter.withdrawByDelegationBatch(wdStreams_); @@ -653,7 +657,7 @@ contract VedaLendingTest is BaseTest { 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. + /// @notice BoringVault must fully consume the allowance granted by the adapter during deposit. /// 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"); @@ -673,7 +677,7 @@ contract VedaLendingTest is BaseTest { assertEq( USDC.allowance(address(vedaAdapter), address(BORING_VAULT)), 0, - "Allowance must be fully consumed after bulkDeposit -- no residual accumulation" + "Allowance must be fully consumed after deposit -- no residual accumulation" ); } @@ -865,8 +869,3 @@ contract VedaLendingTest is BaseTest { return signDelegation(users.bob, delegation_); } } - -interface IRolesAuthority { - function setPublicCapability(address target, bytes4 functionSig, bool enabled) external; -} - From 5af6309bcbc972501fbcac781e78460b7c75e17c Mon Sep 17 00:00:00 2001 From: MoMannn Date: Mon, 30 Mar 2026 09:54:34 +0200 Subject: [PATCH 05/16] add arbitrum rpc to tests --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ba85d68..68d998ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,3 +51,4 @@ jobs: run: forge test -vvv env: LINEA_RPC_URL: ${{ secrets.LINEA_RPC_URL }} + ARBITRUM_RPC_URL: ${{ secrets.ARBITRUM_RPC_URL }} From 0f8ee51147883392fbcad5ed17be1d462ffb7282 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Mon, 30 Mar 2026 10:04:31 +0200 Subject: [PATCH 06/16] set API key in tests --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68d998ad..09fc09bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,3 +52,4 @@ jobs: env: LINEA_RPC_URL: ${{ secrets.LINEA_RPC_URL }} ARBITRUM_RPC_URL: ${{ secrets.ARBITRUM_RPC_URL }} + RPC_API_KEY: ${{ secrets.RPC_API_KEY }} From 25a8c195fde3e3a20eba4367ead6c98ddf4f02c2 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Mon, 30 Mar 2026 13:11:27 +0200 Subject: [PATCH 07/16] add veda deployment script --- script/DeployVedaAdapter.s.sol | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 script/DeployVedaAdapter.s.sol diff --git a/script/DeployVedaAdapter.s.sol b/script/DeployVedaAdapter.s.sol new file mode 100644 index 00000000..cf63862f --- /dev/null +++ b/script/DeployVedaAdapter.s.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; + +import { VedaAdapter } from "../src/helpers/VedaAdapter.sol"; + +/** + * @title DeployVedaAdapter + * @notice Deploys the VedaAdapter contract. + * @dev Update the hardcoded addresses below before deploying. + * @dev Fill the SALT variable in the .env file + * @dev run the script with: + * forge script script/DeployVedaAdapter.s.sol --rpc-url --private-key $PRIVATE_KEY --broadcast + */ +contract DeployVedaAdapter is Script { + // Hardcoded constructor parameters - update these before deploying + address constant OWNER = address(0x0000000000000000000000000000000000000000); + address constant DELEGATION_MANAGER = address(0x0000000000000000000000000000000000000000); + address constant BORING_VAULT = address(0x0000000000000000000000000000000000000000); + address constant VEDA_TELLER = address(0x0000000000000000000000000000000000000000); + + bytes32 salt; + address deployer; + + function setUp() public { + salt = bytes32(abi.encodePacked(vm.envString("SALT"))); + deployer = msg.sender; + console2.log("~~~"); + console2.log("Owner: %s", OWNER); + console2.log("DelegationManager: %s", DELEGATION_MANAGER); + console2.log("BoringVault: %s", BORING_VAULT); + console2.log("VedaTeller: %s", VEDA_TELLER); + console2.log("Deployer: %s", deployer); + console2.log("Salt:"); + console2.logBytes32(salt); + } + + function run() public { + console2.log("~~~"); + vm.startBroadcast(); + + address vedaAdapter = address(new VedaAdapter{ salt: salt }(OWNER, DELEGATION_MANAGER, BORING_VAULT, VEDA_TELLER)); + console2.log("VedaAdapter: %s", vedaAdapter); + + vm.stopBroadcast(); + } +} From 7b3fb1d7abdf4d35161a425424b39f68d9cf042c Mon Sep 17 00:00:00 2001 From: MoMannn Date: Tue, 31 Mar 2026 22:10:39 +0200 Subject: [PATCH 08/16] Remove NotLeafDelegator check and add security consideration --- src/helpers/VedaAdapter.sol | 31 ++++++++++++++++++---------- test/helpers/VedaLending.t.sol | 37 ---------------------------------- 2 files changed, 20 insertions(+), 48 deletions(-) diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index 1cdf226d..765957bc 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -41,6 +41,14 @@ import { IVedaTeller } from "./interfaces/IVedaTeller.sol"; * * Requirements: * - VedaAdapter must approve the BoringVault to spend deposit tokens + * + * @notice Security consideration: Anyone can call `depositByDelegation` and `withdrawByDelegation` — there is no + * caller restriction. Security is enforced entirely through the delegation chain. The redelegation from the + * operator to this adapter MUST include an `ERC20TransferAmountEnforcer` caveat capped to exactly the intended + * deposit or withdrawal amount. Once that amount is transferred the enforcer's running total is exhausted and + * any replay attempt will revert, making the delegation effectively single-use. A delegation without this + * enforcer (or with an amount larger than intended) could be exploited by any caller to transfer more tokens + * than authorised. */ contract VedaAdapter is Ownable2Step { using SafeERC20 for IERC20; @@ -129,9 +137,6 @@ contract VedaAdapter is Ownable2Step { /// @dev Thrown when the batch array is empty error InvalidBatchLength(); - /// @dev Thrown when msg.sender is not the leaf delegator - error NotLeafDelegator(); - ////////////////////////////// State ////////////////////////////// /** @@ -179,6 +184,8 @@ contract VedaAdapter is Ownable2Step { * @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) + * @notice Security consideration: Callable by anyone. The redelegation passed in MUST include an + * `ERC20TransferAmountEnforcer` capped to exactly `_amount` to prevent over-spending or replay. */ function depositByDelegation(Delegation[] memory _delegations, address _token, uint256 _amount, uint256 _minimumMint) external { _executeDepositByDelegation(_delegations, _token, _amount, _minimumMint, msg.sender); @@ -186,9 +193,10 @@ contract VedaAdapter is Ownable2Step { /** * @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. + * @dev Each element is executed one after the other. * @param _depositStreams Array of deposit parameters + * @notice Security consideration: Callable by anyone. Each redelegation in the batch MUST include an + * `ERC20TransferAmountEnforcer` capped to exactly the intended deposit amount to prevent over-spending or replay. */ function depositByDelegationBatch(DepositParams[] memory _depositStreams) external { uint256 streamsLength_ = _depositStreams.length; @@ -215,6 +223,8 @@ contract VedaAdapter is Ownable2Step { * @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) + * @notice Security consideration: Callable by anyone. The redelegation passed in MUST include an + * `ERC20TransferAmountEnforcer` capped to exactly `_shareAmount` to prevent over-spending or replay. */ function withdrawByDelegation( Delegation[] memory _delegations, @@ -229,9 +239,10 @@ contract VedaAdapter is Ownable2Step { /** * @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. + * @dev Each element is executed one after the other. * @param _withdrawStreams Array of withdraw parameters + * @notice Security consideration: Callable by anyone. Each redelegation in the batch MUST include an + * `ERC20TransferAmountEnforcer` capped to exactly the intended share amount to prevent over-spending or replay. */ function withdrawByDelegationBatch(WithdrawParams[] memory _withdrawStreams) external { uint256 streamsLength_ = _withdrawStreams.length; @@ -288,7 +299,7 @@ contract VedaAdapter is Ownable2Step { * @param _token Token to deposit * @param _amount Amount to deposit * @param _minimumMint Minimum vault shares expected - * @param _caller Authorized caller (must match leaf delegator) + * @param _caller Address of the caller, used only for event emission */ function _executeDepositByDelegation( Delegation[] memory _delegations, @@ -301,7 +312,6 @@ contract VedaAdapter is Ownable2Step { { 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; @@ -332,7 +342,7 @@ contract VedaAdapter is Ownable2Step { * @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) + * @param _caller Address of the caller, used only for event emission */ function _executeWithdrawByDelegation( Delegation[] memory _delegations, @@ -345,7 +355,6 @@ contract VedaAdapter is Ownable2Step { { 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; diff --git a/test/helpers/VedaLending.t.sol b/test/helpers/VedaLending.t.sol index fb256d1c..59d3aea9 100644 --- a/test/helpers/VedaLending.t.sol +++ b/test/helpers/VedaLending.t.sol @@ -294,23 +294,6 @@ contract VedaLendingTest is BaseTest { 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_ = @@ -377,26 +360,6 @@ contract VedaLendingTest is BaseTest { 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(); From c02240065ccfd98fbca48ea79d504ac0f49a4704 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Thu, 2 Apr 2026 09:22:48 +0200 Subject: [PATCH 09/16] optimize inputs and add docs regarding senity check inputs --- src/helpers/VedaAdapter.sol | 111 ++++++++++++++++++++++----------- test/helpers/VedaLending.t.sol | 96 ++++++---------------------- 2 files changed, 93 insertions(+), 114 deletions(-) diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index 765957bc..d3a423bd 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -42,6 +42,12 @@ import { IVedaTeller } from "./interfaces/IVedaTeller.sol"; * Requirements: * - VedaAdapter must approve the BoringVault to spend deposit tokens * + * Leaf Caveat Format: + * - The first caveat of the leaf delegation (`_delegations[0].caveats[0]`) must follow the + * ERC20TransferAmountEnforcer terms format: abi.encodePacked(address token, uint256 amount) (52 bytes). + * The adapter parses token and amount directly from these terms instead of accepting them as + * function inputs, ensuring the caller cannot supply values that differ from what the delegator authorised. + * * @notice Security consideration: Anyone can call `depositByDelegation` and `withdrawByDelegation` — there is no * caller restriction. Security is enforced entirely through the delegation chain. The redelegation from the * operator to this adapter MUST include an `ERC20TransferAmountEnforcer` caveat capped to exactly the intended @@ -60,8 +66,6 @@ contract VedaAdapter is Ownable2Step { */ struct DepositParams { Delegation[] delegations; - address token; - uint256 amount; uint256 minimumMint; } @@ -71,7 +75,6 @@ contract VedaAdapter is Ownable2Step { struct WithdrawParams { Delegation[] delegations; address token; - uint256 shareAmount; uint256 minimumAssets; } @@ -180,20 +183,26 @@ contract VedaAdapter is Ownable2Step { * @dev Redeems the delegation to transfer tokens to this adapter, then calls deposit * 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. + * The deposit token and amount are parsed from the first caveat of the leaf delegation + * (`_delegations[0].caveats[0].terms`), which must follow the ERC20TransferAmountEnforcer + * format: abi.encodePacked(address token, uint256 amount). * @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) + * @param _minimumMint Minimum vault shares the caller expects to receive, used as a sanity-check + * bound. The Veda vault conversion is always at fair value; rate drift from yield streaming + * is negligible. A tolerance of 0.1-0.5% is recommended. If this check causes a revert, + * no funds are lost — retry with a fresh quote. * @notice Security consideration: Callable by anyone. The redelegation passed in MUST include an - * `ERC20TransferAmountEnforcer` capped to exactly `_amount` to prevent over-spending or replay. + * `ERC20TransferAmountEnforcer` capped to exactly the intended deposit amount to prevent + * over-spending or replay. */ - function depositByDelegation(Delegation[] memory _delegations, address _token, uint256 _amount, uint256 _minimumMint) external { - _executeDepositByDelegation(_delegations, _token, _amount, _minimumMint, msg.sender); + function depositByDelegation(Delegation[] memory _delegations, uint256 _minimumMint) external { + _executeDepositByDelegation(_delegations, _minimumMint, msg.sender); } /** * @notice Deposits tokens using multiple delegation streams, executed sequentially - * @dev Each element is executed one after the other. + * @dev Each element is executed one after the other. Token and amount for each stream are parsed + * from the first caveat of each stream's leaf delegation. * @param _depositStreams Array of deposit parameters * @notice Security consideration: Callable by anyone. Each redelegation in the batch MUST include an * `ERC20TransferAmountEnforcer` capped to exactly the intended deposit amount to prevent over-spending or replay. @@ -205,7 +214,7 @@ contract VedaAdapter is Ownable2Step { 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_); + _executeDepositByDelegation(params_.delegations, params_.minimumMint, caller_); unchecked { ++i; } @@ -219,27 +228,32 @@ contract VedaAdapter is Ownable2Step { * @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. + * The share amount is parsed from the first caveat of the leaf delegation + * (`_delegations[0].caveats[0].terms`), which must follow the ERC20TransferAmountEnforcer + * format: abi.encodePacked(address boringVault, uint256 shareAmount). * @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) + * @param _token Address of the underlying token to receive from the vault + * @param _minimumAssets Minimum underlying assets the caller expects to receive, used as a + * sanity-check bound. The Veda vault conversion is always at fair value; rate drift from + * yield streaming is negligible. A tolerance of 0.1-0.5% is recommended. If this check + * causes a revert, no funds are lost — retry with a fresh quote. * @notice Security consideration: Callable by anyone. The redelegation passed in MUST include an * `ERC20TransferAmountEnforcer` capped to exactly `_shareAmount` to prevent over-spending or replay. */ function withdrawByDelegation( Delegation[] memory _delegations, address _token, - uint256 _shareAmount, uint256 _minimumAssets ) external { - _executeWithdrawByDelegation(_delegations, _token, _shareAmount, _minimumAssets, msg.sender); + _executeWithdrawByDelegation(_delegations, _token, _minimumAssets, msg.sender); } /** * @notice Withdraws underlying tokens using multiple delegation streams, executed sequentially - * @dev Each element is executed one after the other. + * @dev Each element is executed one after the other. The share amount for each stream is parsed + * from the first caveat of each stream's leaf delegation. * @param _withdrawStreams Array of withdraw parameters * @notice Security consideration: Callable by anyone. Each redelegation in the batch MUST include an * `ERC20TransferAmountEnforcer` capped to exactly the intended share amount to prevent over-spending or replay. @@ -251,7 +265,7 @@ contract VedaAdapter is Ownable2Step { 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_); + _executeWithdrawByDelegation(params_.delegations, params_.token, params_.minimumAssets, caller_); unchecked { ++i; } @@ -295,16 +309,15 @@ contract VedaAdapter is Ownable2Step { /** * @notice Internal implementation of deposit by delegation + * @dev Parses the deposit token and amount from the first caveat of the leaf delegation + * (`_delegations[0].caveats[0].terms`), which must be 52 bytes in + * ERC20TransferAmountEnforcer format: abi.encodePacked(address token, uint256 amount). * @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 _minimumMint Minimum vault shares expected (sanity-check bound) * @param _caller Address of the caller, used only for event emission */ function _executeDepositByDelegation( Delegation[] memory _delegations, - address _token, - uint256 _amount, uint256 _minimumMint, address _caller ) @@ -312,7 +325,21 @@ contract VedaAdapter is Ownable2Step { { uint256 length_ = _delegations.length; if (length_ < 2) revert InvalidDelegationsLength(); - if (_token == address(0)) revert InvalidZeroAddress(); + + // Parse token and amount from the leaf delegation's first caveat terms. + // Terms format (ERC20TransferAmountEnforcer): abi.encodePacked(address token, uint256 amount) = 52 bytes. + // Slice syntax is only available for calldata; use assembly to read from memory bytes. + bytes memory terms_ = _delegations[0].caveats[0].terms; + address token_; + uint256 amount_; + assembly { + // Memory layout of `terms_`: [length (32 bytes)][data ...]. + // `add(terms_, 32)` points to byte 0 of the data. + // The address occupies bytes 0-19 (high 20 bytes of the first 32-byte word). + token_ := shr(96, mload(add(terms_, 32))) + // The uint256 occupies bytes 20-51; load the 32-byte word starting at byte 20. + amount_ := mload(add(terms_, 52)) + } address rootDelegator_ = _delegations[length_ - 1].delegator; @@ -324,30 +351,32 @@ contract VedaAdapter is Ownable2Step { 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_); + 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), boringVault, _amount); - uint256 shares_ = teller.deposit(_token, _amount, _minimumMint, rootDelegator_, address(0)); + _ensureAllowance(IERC20(token_), boringVault, amount_); + uint256 shares_ = teller.deposit(token_, amount_, _minimumMint, rootDelegator_, address(0)); - emit DepositExecuted(rootDelegator_, _caller, _token, _amount, shares_); + emit DepositExecuted(rootDelegator_, _caller, token_, amount_, shares_); } /** * @notice Internal implementation of withdraw by delegation + * @dev Parses the share amount from the first caveat of the leaf delegation + * (`_delegations[0].caveats[0].terms`), which must be 52 bytes in + * ERC20TransferAmountEnforcer format: abi.encodePacked(address boringVault, uint256 shareAmount). + * The transfer target is always the immutable `boringVault` address. * @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 _token Underlying token to receive from the vault (not in the caveat; differs from the share token) + * @param _minimumAssets Minimum underlying assets expected (sanity-check bound) * @param _caller Address of the caller, used only for event emission */ function _executeWithdrawByDelegation( Delegation[] memory _delegations, address _token, - uint256 _shareAmount, uint256 _minimumAssets, address _caller ) @@ -357,6 +386,16 @@ contract VedaAdapter is Ownable2Step { if (length_ < 2) revert InvalidDelegationsLength(); if (_token == address(0)) revert InvalidZeroAddress(); + // Parse share amount from the leaf delegation's first caveat terms. + // Terms format (ERC20TransferAmountEnforcer): abi.encodePacked(address boringVault, uint256 shareAmount) = 52 bytes. + // Slice syntax is only available for calldata; use assembly to read from memory bytes. + bytes memory terms_ = _delegations[0].caveats[0].terms; + uint256 shareAmount_; + assembly { + // The uint256 shareAmount occupies bytes 20-51; load the 32-byte word starting at byte 20. + shareAmount_ := mload(add(terms_, 52)) + } + address rootDelegator_ = _delegations[length_ - 1].delegator; // Redeem delegation: transfer vault shares from user to this adapter @@ -367,14 +406,14 @@ contract VedaAdapter is Ownable2Step { encodedModes_[0] = ModeLib.encodeSimpleSingle(); bytes[] memory executionCallDatas_ = new bytes[](1); - bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), _shareAmount)); + 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_); + uint256 assetsOut_ = teller.withdraw(_token, shareAmount_, _minimumAssets, rootDelegator_); - emit WithdrawExecuted(rootDelegator_, _caller, _token, _shareAmount, assetsOut_); + emit WithdrawExecuted(rootDelegator_, _caller, _token, shareAmount_, assetsOut_); } } diff --git a/test/helpers/VedaLending.t.sol b/test/helpers/VedaLending.t.sol index 59d3aea9..bba13543 100644 --- a/test/helpers/VedaLending.t.sol +++ b/test/helpers/VedaLending.t.sol @@ -188,7 +188,7 @@ contract VedaLendingTest is BaseTest { delegations_[1] = delegation_; vm.prank(address(users.bob.deleGator)); - vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + vedaAdapter.depositByDelegation(delegations_, 0); uint256 aliceUSDCFinal_ = USDC.balanceOf(address(users.alice.deleGator)); assertEq(aliceUSDCFinal_, INITIAL_USD_BALANCE - DEPOSIT_AMOUNT, "USDC balance should decrease"); @@ -219,7 +219,7 @@ contract VedaLendingTest is BaseTest { delegations_[1] = delegation_; vm.prank(address(users.bob.deleGator)); - vedaAdapter.withdrawByDelegation(delegations_, address(USDC), aliceShares_, 0); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), 0); uint256 aliceSharesAfter_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); assertEq(aliceSharesAfter_, 0, "All shares should be burned"); @@ -279,7 +279,7 @@ contract VedaLendingTest is BaseTest { vm.expectRevert(VedaAdapter.InvalidDelegationsLength.selector); vm.prank(address(users.bob.deleGator)); - vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + vedaAdapter.depositByDelegation(delegations_, 0); } /// @notice depositByDelegation must revert with only 1 delegation (requires >= 2 for redelegation pattern) @@ -291,43 +291,7 @@ contract VedaLendingTest is BaseTest { vm.expectRevert(VedaAdapter.InvalidDelegationsLength.selector); vm.prank(address(users.bob.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); + vedaAdapter.depositByDelegation(delegations_, 0); } // ================================================================================== @@ -343,7 +307,7 @@ contract VedaLendingTest is BaseTest { vm.expectRevert(VedaAdapter.InvalidDelegationsLength.selector); vm.prank(address(users.bob.deleGator)); - vedaAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), 0); } /// @notice withdrawByDelegation must revert with only 1 delegation (requires >= 2 for redelegation pattern) @@ -357,10 +321,10 @@ contract VedaLendingTest is BaseTest { vm.expectRevert(VedaAdapter.InvalidDelegationsLength.selector); vm.prank(address(users.bob.deleGator)); - vedaAdapter.withdrawByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), 0); } - /// @notice withdrawByDelegation must revert when token address is zero + /// @notice withdrawByDelegation must revert when underlying token address is zero function test_withdrawByDelegation_revertsOnZeroTokenAddress() public { _setupLendingState(); @@ -376,7 +340,7 @@ contract VedaLendingTest is BaseTest { vm.expectRevert(VedaAdapter.InvalidZeroAddress.selector); vm.prank(address(users.bob.deleGator)); - vedaAdapter.withdrawByDelegation(delegations_, address(0), DEPOSIT_AMOUNT, 0); + vedaAdapter.withdrawByDelegation(delegations_, address(0), 0); } // ================================================================================== @@ -402,7 +366,7 @@ contract VedaLendingTest is BaseTest { ); vm.prank(address(users.bob.deleGator)); - vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + vedaAdapter.depositByDelegation(delegations_, 0); } /// @notice withdrawByDelegation must emit WithdrawExecuted with correct parameters @@ -429,7 +393,7 @@ contract VedaLendingTest is BaseTest { ); vm.prank(address(users.bob.deleGator)); - vedaAdapter.withdrawByDelegation(delegations_, address(USDC), aliceShares_, 0); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), 0); } // ================================================================================== @@ -481,10 +445,8 @@ contract VedaLendingTest is BaseTest { 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 }); + streams_[0] = VedaAdapter.DepositParams({ delegations: delegations1_, minimumMint: 0 }); + streams_[1] = VedaAdapter.DepositParams({ delegations: delegations2_, minimumMint: 0 }); vm.expectEmit(true, true, true, true, address(vedaAdapter)); emit VedaAdapter.BatchDepositExecuted(address(users.bob.deleGator), 2); @@ -592,7 +554,7 @@ contract VedaLendingTest is BaseTest { delegations_[1] = delegation_; vm.prank(address(users.bob.deleGator)); - vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + vedaAdapter.depositByDelegation(delegations_, 0); assertEq(USDC.balanceOf(address(vedaAdapter)), 0, "Adapter must not retain any USDC after deposit"); } @@ -615,7 +577,7 @@ contract VedaLendingTest is BaseTest { delegations_[1] = delegation_; vm.prank(address(users.bob.deleGator)); - vedaAdapter.withdrawByDelegation(delegations_, address(USDC), aliceShares_, 0); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), 0); assertEq(BORING_VAULT.balanceOf(address(vedaAdapter)), 0, "Adapter must not retain any vault shares after withdraw"); } @@ -635,7 +597,7 @@ contract VedaLendingTest is BaseTest { delegations_[1] = delegation_; vm.prank(address(users.bob.deleGator)); - vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + vedaAdapter.depositByDelegation(delegations_, 0); assertEq( USDC.allowance(address(vedaAdapter), address(BORING_VAULT)), @@ -675,7 +637,7 @@ contract VedaLendingTest is BaseTest { delegations_[2] = rootDelegation_; vm.prank(address(users.bob.deleGator)); - vedaAdapter.depositByDelegation(delegations_, address(USDC), DEPOSIT_AMOUNT, 0); + vedaAdapter.depositByDelegation(delegations_, 0); // rootDelegator_ = delegations[2].delegator = Alice uint256 aliceShares_ = BORING_VAULT.balanceOf(address(users.alice.deleGator)); @@ -685,26 +647,6 @@ contract VedaLendingTest is BaseTest { 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 // ================================================================================== @@ -729,7 +671,7 @@ contract VedaLendingTest is BaseTest { delegations_[1] = delegation_; vm.prank(address(users.bob.deleGator)); - vedaAdapter.depositByDelegation(delegations_, address(USDC), _amount, 0); + vedaAdapter.depositByDelegation(delegations_, 0); } /// @notice Builds a WithdrawParams struct for batch withdraw (helper to reduce stack depth) @@ -743,9 +685,7 @@ contract VedaLendingTest is BaseTest { wdDelegations_[0] = rewd_; wdDelegations_[1] = wd_; - return VedaAdapter.WithdrawParams({ - delegations: wdDelegations_, token: address(USDC), shareAmount: _shareAmount, minimumAssets: 0 - }); + return VedaAdapter.WithdrawParams({ delegations: wdDelegations_, token: address(USDC), minimumAssets: 0 }); } /// @notice Creates a transfer delegation with ERC20TransferAmountEnforcer and RedeemerEnforcer From 57120d537aa5c3884d855306a5783295cc7e6087 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Thu, 2 Apr 2026 09:39:27 +0200 Subject: [PATCH 10/16] additional tests and code optimization --- src/helpers/VedaAdapter.sol | 57 ++++---- test/helpers/VedaLending.t.sol | 259 ++++++++++++++++++++++++++++++++- 2 files changed, 278 insertions(+), 38 deletions(-) diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index d3a423bd..512a4e45 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -140,6 +140,9 @@ contract VedaAdapter is Ownable2Step { /// @dev Thrown when the batch array is empty error InvalidBatchLength(); + /// @dev Thrown when the leaf caveat terms are shorter than 52 bytes (ERC20TransferAmountEnforcer format) + error InvalidTermsLength(); + ////////////////////////////// State ////////////////////////////// /** @@ -307,11 +310,26 @@ contract VedaAdapter is Ownable2Step { } } + /** + * @notice Parses ERC20TransferAmountEnforcer terms from memory bytes + * @dev Terms format: abi.encodePacked(address token, uint256 amount) = 52 bytes. + * Slice syntax is only available for calldata; assembly is used to read from memory bytes. + * @param _terms The raw terms bytes from a caveat + * @return token_ The token address encoded in the first 20 bytes + * @return amount_ The uint256 amount encoded in bytes 20-51 + */ + function _parseERC20TransferTerms(bytes memory _terms) private pure returns (address token_, uint256 amount_) { + if (_terms.length < 52) revert InvalidTermsLength(); + assembly { + token_ := shr(96, mload(add(_terms, 32))) + amount_ := mload(add(_terms, 52)) + } + } + /** * @notice Internal implementation of deposit by delegation * @dev Parses the deposit token and amount from the first caveat of the leaf delegation - * (`_delegations[0].caveats[0].terms`), which must be 52 bytes in - * ERC20TransferAmountEnforcer format: abi.encodePacked(address token, uint256 amount). + * via `_parseERC20TransferTerms`. * @param _delegations Delegation chain, sorted leaf to root * @param _minimumMint Minimum vault shares expected (sanity-check bound) * @param _caller Address of the caller, used only for event emission @@ -326,21 +344,7 @@ contract VedaAdapter is Ownable2Step { uint256 length_ = _delegations.length; if (length_ < 2) revert InvalidDelegationsLength(); - // Parse token and amount from the leaf delegation's first caveat terms. - // Terms format (ERC20TransferAmountEnforcer): abi.encodePacked(address token, uint256 amount) = 52 bytes. - // Slice syntax is only available for calldata; use assembly to read from memory bytes. - bytes memory terms_ = _delegations[0].caveats[0].terms; - address token_; - uint256 amount_; - assembly { - // Memory layout of `terms_`: [length (32 bytes)][data ...]. - // `add(terms_, 32)` points to byte 0 of the data. - // The address occupies bytes 0-19 (high 20 bytes of the first 32-byte word). - token_ := shr(96, mload(add(terms_, 32))) - // The uint256 occupies bytes 20-51; load the 32-byte word starting at byte 20. - amount_ := mload(add(terms_, 52)) - } - + (address token_, uint256 amount_) = _parseERC20TransferTerms(_delegations[0].caveats[0].terms); address rootDelegator_ = _delegations[length_ - 1].delegator; // Redeem delegation: transfer tokens from user to this adapter @@ -366,11 +370,11 @@ contract VedaAdapter is Ownable2Step { /** * @notice Internal implementation of withdraw by delegation * @dev Parses the share amount from the first caveat of the leaf delegation - * (`_delegations[0].caveats[0].terms`), which must be 52 bytes in - * ERC20TransferAmountEnforcer format: abi.encodePacked(address boringVault, uint256 shareAmount). - * The transfer target is always the immutable `boringVault` address. + * via `_parseERC20TransferTerms`. The caveat encodes the vault share token and amount; + * `_token` is the desired underlying output asset (e.g. USDC), which differs from the + * share token in the caveat. * @param _delegations Delegation chain, sorted leaf to root - * @param _token Underlying token to receive from the vault (not in the caveat; differs from the share token) + * @param _token Underlying output token to receive from the vault (differs from the share token in the caveat) * @param _minimumAssets Minimum underlying assets expected (sanity-check bound) * @param _caller Address of the caller, used only for event emission */ @@ -386,16 +390,7 @@ contract VedaAdapter is Ownable2Step { if (length_ < 2) revert InvalidDelegationsLength(); if (_token == address(0)) revert InvalidZeroAddress(); - // Parse share amount from the leaf delegation's first caveat terms. - // Terms format (ERC20TransferAmountEnforcer): abi.encodePacked(address boringVault, uint256 shareAmount) = 52 bytes. - // Slice syntax is only available for calldata; use assembly to read from memory bytes. - bytes memory terms_ = _delegations[0].caveats[0].terms; - uint256 shareAmount_; - assembly { - // The uint256 shareAmount occupies bytes 20-51; load the 32-byte word starting at byte 20. - shareAmount_ := mload(add(terms_, 52)) - } - + (, uint256 shareAmount_) = _parseERC20TransferTerms(_delegations[0].caveats[0].terms); address rootDelegator_ = _delegations[length_ - 1].delegator; // Redeem delegation: transfer vault shares from user to this adapter diff --git a/test/helpers/VedaLending.t.sol b/test/helpers/VedaLending.t.sol index bba13543..eaeaa69a 100644 --- a/test/helpers/VedaLending.t.sol +++ b/test/helpers/VedaLending.t.sol @@ -9,7 +9,7 @@ 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 { Implementation, SignatureType, TestUser } 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"; @@ -647,6 +647,220 @@ contract VedaLendingTest is BaseTest { assertEq(BORING_VAULT.balanceOf(address(users.bob.deleGator)), 0, "Bob must not receive shares"); } + // ================================================================================== + // Section 10: Terms Validation Tests + // Ensures the adapter rejects malformed caveat terms before executing. + // ================================================================================== + + /// @notice depositByDelegation must revert when leaf caveat terms are shorter than 52 bytes + function test_depositByDelegation_revertsOnShortTerms() public { + Delegation memory delegation_ = + _createTransferDelegation(address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max); + + Caveat[] memory shortCaveats_ = new Caveat[](1); + shortCaveats_[0] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encodePacked(address(USDC)) // 20 bytes, too short + }); + + Delegation memory redelegation_ = Delegation({ + delegate: address(vedaAdapter), + delegator: address(users.bob.deleGator), + authority: EncoderLib._getDelegationHash(delegation_), + caveats: shortCaveats_, + salt: 0, + signature: hex"" + }); + redelegation_ = signDelegation(users.bob, redelegation_); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.expectRevert(VedaAdapter.InvalidTermsLength.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.depositByDelegation(delegations_, 0); + } + + /// @notice withdrawByDelegation must revert when leaf caveat terms are shorter than 52 bytes + function test_withdrawByDelegation_revertsOnShortTerms() public { + _setupLendingState(); + + Delegation memory delegation_ = _createTransferDelegation( + address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), type(uint256).max + ); + + Caveat[] memory shortCaveats_ = new Caveat[](1); + shortCaveats_[0] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: hex"aabbccdd" // 4 bytes, too short + }); + + Delegation memory redelegation_ = Delegation({ + delegate: address(vedaAdapter), + delegator: address(users.bob.deleGator), + authority: EncoderLib._getDelegationHash(delegation_), + caveats: shortCaveats_, + salt: 0, + signature: hex"" + }); + redelegation_ = signDelegation(users.bob, redelegation_); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.expectRevert(VedaAdapter.InvalidTermsLength.selector); + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), 0); + } + + // ================================================================================== + // Section 11: Replay / Double-Spend Prevention Tests + // Validates that the ERC20TransferAmountEnforcer prevents reuse of the same delegation. + // ================================================================================== + + /// @notice Calling depositByDelegation twice with the same delegation chain must revert on the second call + function test_depositByDelegation_revertsOnReplay() 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_, 0); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(); + vedaAdapter.depositByDelegation(delegations_, 0); + } + + // ================================================================================== + // Section 12: Slippage Protection Tests + // Validates that minimumMint / minimumAssets bounds cause reverts when not met. + // ================================================================================== + + /// @notice depositByDelegation must revert when minimumMint exceeds the actual shares minted + function test_depositByDelegation_revertsOnSlippage() 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)); + vm.expectRevert(); + vedaAdapter.depositByDelegation(delegations_, type(uint256).max); + } + + /// @notice withdrawByDelegation must revert when minimumAssets exceeds the actual assets received + function test_withdrawByDelegation_revertsOnSlippage() 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)); + vm.expectRevert(); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), type(uint256).max); + } + + // ================================================================================== + // Section 13: Alternative Delegator Tests + // Validates the adapter works correctly when Carol (not Alice) is the root delegator. + // ================================================================================== + + /// @notice Deposit via adapter where Carol is the root delegator instead of Alice + function test_depositByDelegation_carolAsRootDelegator() public { + vm.deal(address(users.carol.deleGator), 1 ether); + vm.prank(USDC_WHALE); + USDC.transfer(address(users.carol.deleGator), INITIAL_USD_BALANCE); + + uint256 carolUSDCBefore_ = USDC.balanceOf(address(users.carol.deleGator)); + + // Carol delegates USDC transfer rights to Bob, redeemable only by the adapter + Delegation memory delegation_ = _createTransferDelegationFull( + users.carol, address(users.bob.deleGator), address(vedaAdapter), address(USDC), type(uint256).max, 0 + ); + + // 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_, 0); + + uint256 carolUSDCAfter_ = USDC.balanceOf(address(users.carol.deleGator)); + assertEq(carolUSDCAfter_, carolUSDCBefore_ - DEPOSIT_AMOUNT, "Carol's USDC should decrease"); + + uint256 carolShares_ = BORING_VAULT.balanceOf(address(users.carol.deleGator)); + assertGt(carolShares_, 0, "Shares should be minted to Carol (root delegator)"); + + assertEq(BORING_VAULT.balanceOf(address(users.bob.deleGator)), 0, "Bob must not receive shares"); + assertEq(BORING_VAULT.balanceOf(address(users.alice.deleGator)), 0, "Alice must not receive shares"); + } + + /// @notice Withdraw via adapter where Carol is the root delegator instead of Alice + function test_withdrawByDelegation_carolAsRootDelegator() public { + vm.deal(address(users.carol.deleGator), 1 ether); + vm.prank(USDC_WHALE); + USDC.transfer(address(users.carol.deleGator), INITIAL_USD_BALANCE); + + // Carol deposits directly to get shares + vm.prank(address(users.carol.deleGator)); + USDC.approve(address(BORING_VAULT), DEPOSIT_AMOUNT); + vm.prank(address(users.carol.deleGator)); + VEDA_TELLER.deposit(address(USDC), DEPOSIT_AMOUNT, 0, address(0)); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + + uint256 carolShares_ = BORING_VAULT.balanceOf(address(users.carol.deleGator)); + assertGt(carolShares_, 0, "Carol should have vault shares"); + uint256 carolUSDCBefore_ = USDC.balanceOf(address(users.carol.deleGator)); + + // Carol delegates share transfer rights to Bob, redeemable only by the adapter + Delegation memory delegation_ = _createTransferDelegationFull( + users.carol, address(users.bob.deleGator), address(vedaAdapter), address(BORING_VAULT), type(uint256).max, 0 + ); + + // Bob redelegates to the VedaAdapter with a share transfer amount cap + Delegation memory redelegation_ = + _createAdapterRedelegation(EncoderLib._getDelegationHash(delegation_), address(BORING_VAULT), carolShares_); + + Delegation[] memory delegations_ = new Delegation[](2); + delegations_[0] = redelegation_; + delegations_[1] = delegation_; + + vm.prank(address(users.bob.deleGator)); + vedaAdapter.withdrawByDelegation(delegations_, address(USDC), 0); + + assertEq(BORING_VAULT.balanceOf(address(users.carol.deleGator)), 0, "All shares should be burned"); + uint256 carolUSDCAfter_ = USDC.balanceOf(address(users.carol.deleGator)); + assertGt(carolUSDCAfter_, carolUSDCBefore_, "Carol should receive USDC back"); + } + // ================================================================================== // Helper Functions // ================================================================================== @@ -699,7 +913,7 @@ contract VedaLendingTest is BaseTest { view returns (Delegation memory) { - return _createTransferDelegationWithSalt(_delegate, _redeemer, _token, _amount, 0); + return _createTransferDelegationFull(users.alice, _delegate, _redeemer, _token, _amount, 0); } /// @notice Creates a transfer delegation with a custom salt for unique delegation hashes in batch operations @@ -713,6 +927,22 @@ contract VedaLendingTest is BaseTest { internal view returns (Delegation memory) + { + return _createTransferDelegationFull(users.alice, _delegate, _redeemer, _token, _amount, _salt); + } + + /// @notice Creates a transfer delegation signed by an arbitrary delegator + function _createTransferDelegationFull( + TestUser memory _delegator, + address _delegate, + address _redeemer, + address _token, + uint256 _amount, + uint256 _salt + ) + internal + view + returns (Delegation memory) { Caveat[] memory caveats_ = new Caveat[](2); caveats_[0] = @@ -722,14 +952,14 @@ contract VedaLendingTest is BaseTest { Delegation memory delegation_ = Delegation({ delegate: _delegate, - delegator: address(users.alice.deleGator), + delegator: address(_delegator.deleGator), authority: ROOT_AUTHORITY, caveats: caveats_, salt: _salt, signature: hex"" }); - return signDelegation(users.alice, delegation_); + return signDelegation(_delegator, delegation_); } /// @notice Creates an adapter redelegation with ERC20TransferAmountEnforcer @@ -742,7 +972,7 @@ contract VedaLendingTest is BaseTest { view returns (Delegation memory) { - return _createAdapterRedelegationWithSalt(_authority, _token, _amount, 0); + return _createAdapterRedelegationFull(users.bob, _authority, _token, _amount, 0); } /// @notice Creates an adapter redelegation with a custom salt for unique delegation hashes in batch operations @@ -755,6 +985,21 @@ contract VedaLendingTest is BaseTest { internal view returns (Delegation memory) + { + return _createAdapterRedelegationFull(users.bob, _authority, _token, _amount, _salt); + } + + /// @notice Creates an adapter redelegation signed by an arbitrary operator + function _createAdapterRedelegationFull( + TestUser memory _operator, + bytes32 _authority, + address _token, + uint256 _amount, + uint256 _salt + ) + internal + view + returns (Delegation memory) { Caveat[] memory caveats_ = new Caveat[](1); caveats_[0] = @@ -762,13 +1007,13 @@ contract VedaLendingTest is BaseTest { Delegation memory delegation_ = Delegation({ delegate: address(vedaAdapter), - delegator: address(users.bob.deleGator), + delegator: address(_operator.deleGator), authority: _authority, caveats: caveats_, salt: _salt, signature: hex"" }); - return signDelegation(users.bob, delegation_); + return signDelegation(_operator, delegation_); } } From 4deb901199580a2b4572ce879a13192b1c4afdd1 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Thu, 2 Apr 2026 10:14:33 +0200 Subject: [PATCH 11/16] linter + fetching from latest block --- src/helpers/VedaAdapter.sol | 16 ++-------------- test/helpers/VedaLending.t.sol | 3 +-- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index 512a4e45..402edeff 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -243,13 +243,7 @@ contract VedaAdapter is Ownable2Step { * @notice Security consideration: Callable by anyone. The redelegation passed in MUST include an * `ERC20TransferAmountEnforcer` capped to exactly `_shareAmount` to prevent over-spending or replay. */ - function withdrawByDelegation( - Delegation[] memory _delegations, - address _token, - uint256 _minimumAssets - ) - external - { + function withdrawByDelegation(Delegation[] memory _delegations, address _token, uint256 _minimumAssets) external { _executeWithdrawByDelegation(_delegations, _token, _minimumAssets, msg.sender); } @@ -334,13 +328,7 @@ contract VedaAdapter is Ownable2Step { * @param _minimumMint Minimum vault shares expected (sanity-check bound) * @param _caller Address of the caller, used only for event emission */ - function _executeDepositByDelegation( - Delegation[] memory _delegations, - uint256 _minimumMint, - address _caller - ) - internal - { + function _executeDepositByDelegation(Delegation[] memory _delegations, uint256 _minimumMint, address _caller) internal { uint256 length_ = _delegations.length; if (length_ < 2) revert InvalidDelegationsLength(); diff --git a/test/helpers/VedaLending.t.sol b/test/helpers/VedaLending.t.sol index eaeaa69a..1bd38e43 100644 --- a/test/helpers/VedaLending.t.sol +++ b/test/helpers/VedaLending.t.sol @@ -68,7 +68,6 @@ contract VedaLendingTest is BaseTest { LimitedCallsEnforcer public limitedCallsEnforcer; VedaAdapter public vedaAdapter; - uint256 public constant MAINNET_FORK_BLOCK = 447148700; // 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() @@ -77,7 +76,7 @@ contract VedaLendingTest is BaseTest { function setUp() public override { // Create fork from mainnet at specific block - vm.createSelectFork(vm.envString("ARBITRUM_RPC_URL"), MAINNET_FORK_BLOCK); + vm.createSelectFork(vm.envString("ARBITRUM_RPC_URL")); // Set implementation type IMPLEMENTATION = Implementation.Hybrid; From 57b5b88c10f5a5a64163f084c2c97532a11f63b7 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Sun, 5 Apr 2026 20:05:01 +0200 Subject: [PATCH 12/16] move deploy script to .env variables --- .env.example | 3 +++ script/DeployVedaAdapter.s.sol | 27 ++++++++++++++------------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 390adce3..df0cd20b 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,9 @@ META_SWAP_ADAPTER_OWNER_ADDRESS= METASWAP_ADDRESS= SWAPS_API_SIGNER_ADDRESS= ARGS_EQUALITY_CHECK_ENFORCER_ADDRESS= +VEDA_ADAPTER_OWNER_ADDRESS= +BORING_VAULT_ADDRESS= +VEDA_TELLER_ADDRESS= # Required for verifying contracts ETHERSCAN_API_KEY= diff --git a/script/DeployVedaAdapter.s.sol b/script/DeployVedaAdapter.s.sol index cf63862f..b9b1c145 100644 --- a/script/DeployVedaAdapter.s.sol +++ b/script/DeployVedaAdapter.s.sol @@ -9,29 +9,30 @@ import { VedaAdapter } from "../src/helpers/VedaAdapter.sol"; /** * @title DeployVedaAdapter * @notice Deploys the VedaAdapter contract. - * @dev Update the hardcoded addresses below before deploying. - * @dev Fill the SALT variable in the .env file + * @dev Fill the required variables in the .env file * @dev run the script with: * forge script script/DeployVedaAdapter.s.sol --rpc-url --private-key $PRIVATE_KEY --broadcast */ contract DeployVedaAdapter is Script { - // Hardcoded constructor parameters - update these before deploying - address constant OWNER = address(0x0000000000000000000000000000000000000000); - address constant DELEGATION_MANAGER = address(0x0000000000000000000000000000000000000000); - address constant BORING_VAULT = address(0x0000000000000000000000000000000000000000); - address constant VEDA_TELLER = address(0x0000000000000000000000000000000000000000); - bytes32 salt; address deployer; + address vedaAdapterOwner; + address delegationManager; + address boringVault; + address vedaTeller; function setUp() public { salt = bytes32(abi.encodePacked(vm.envString("SALT"))); + vedaAdapterOwner = vm.envAddress("VEDA_ADAPTER_OWNER_ADDRESS"); + delegationManager = vm.envAddress("DELEGATION_MANAGER_ADDRESS"); + boringVault = vm.envAddress("BORING_VAULT_ADDRESS"); + vedaTeller = vm.envAddress("VEDA_TELLER_ADDRESS"); deployer = msg.sender; console2.log("~~~"); - console2.log("Owner: %s", OWNER); - console2.log("DelegationManager: %s", DELEGATION_MANAGER); - console2.log("BoringVault: %s", BORING_VAULT); - console2.log("VedaTeller: %s", VEDA_TELLER); + console2.log("Owner: %s", vedaAdapterOwner); + console2.log("DelegationManager: %s", delegationManager); + console2.log("BoringVault: %s", boringVault); + console2.log("VedaTeller: %s", vedaTeller); console2.log("Deployer: %s", deployer); console2.log("Salt:"); console2.logBytes32(salt); @@ -41,7 +42,7 @@ contract DeployVedaAdapter is Script { console2.log("~~~"); vm.startBroadcast(); - address vedaAdapter = address(new VedaAdapter{ salt: salt }(OWNER, DELEGATION_MANAGER, BORING_VAULT, VEDA_TELLER)); + address vedaAdapter = address(new VedaAdapter{ salt: salt }(vedaAdapterOwner, delegationManager, boringVault, vedaTeller)); console2.log("VedaAdapter: %s", vedaAdapter); vm.stopBroadcast(); From e412baf643791b38994d83af85034c49cc4259fb Mon Sep 17 00:00:00 2001 From: MoMannn Date: Sun, 5 Apr 2026 20:05:11 +0200 Subject: [PATCH 13/16] fix teller docs --- src/helpers/interfaces/IVedaTeller.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/interfaces/IVedaTeller.sol b/src/helpers/interfaces/IVedaTeller.sol index 986e84c6..ab9a1d8d 100644 --- a/src/helpers/interfaces/IVedaTeller.sol +++ b/src/helpers/interfaces/IVedaTeller.sol @@ -35,6 +35,12 @@ interface IVedaTeller { /** * @notice Allows an authorized caller to deposit into the BoringVault for another address, if this contract is not paused. * @dev Intended for router-like integrations; this selector should remain role-gated. + * @param depositAsset The ERC20 token to deposit + * @param depositAmount The amount to deposit + * @param minimumMint The minimum shares the user expects to receive + * @param to The address that will receive the minted vault shares + * @param referralAddress Address used for referral tracking + * @return shares The number of vault shares minted */ function deposit( address depositAsset, From 801ab0d8b47521dbbd21d69d94530f50acd37afa Mon Sep 17 00:00:00 2001 From: MoMannn Date: Sun, 5 Apr 2026 20:05:26 +0200 Subject: [PATCH 14/16] remove unnecesarry caller variable --- src/helpers/VedaAdapter.sol | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index 402edeff..671b9769 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -199,7 +199,7 @@ contract VedaAdapter is Ownable2Step { * over-spending or replay. */ function depositByDelegation(Delegation[] memory _delegations, uint256 _minimumMint) external { - _executeDepositByDelegation(_delegations, _minimumMint, msg.sender); + _executeDepositByDelegation(_delegations, _minimumMint); } /** @@ -214,16 +214,15 @@ contract VedaAdapter is Ownable2Step { 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_.minimumMint, caller_); + _executeDepositByDelegation(params_.delegations, params_.minimumMint); unchecked { ++i; } } - emit BatchDepositExecuted(caller_, streamsLength_); + emit BatchDepositExecuted(msg.sender, streamsLength_); } /** @@ -244,7 +243,7 @@ contract VedaAdapter is Ownable2Step { * `ERC20TransferAmountEnforcer` capped to exactly `_shareAmount` to prevent over-spending or replay. */ function withdrawByDelegation(Delegation[] memory _delegations, address _token, uint256 _minimumAssets) external { - _executeWithdrawByDelegation(_delegations, _token, _minimumAssets, msg.sender); + _executeWithdrawByDelegation(_delegations, _token, _minimumAssets); } /** @@ -259,16 +258,15 @@ contract VedaAdapter is Ownable2Step { 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_.minimumAssets, caller_); + _executeWithdrawByDelegation(params_.delegations, params_.token, params_.minimumAssets); unchecked { ++i; } } - emit BatchWithdrawExecuted(caller_, streamsLength_); + emit BatchWithdrawExecuted(msg.sender, streamsLength_); } /** @@ -326,9 +324,8 @@ contract VedaAdapter is Ownable2Step { * via `_parseERC20TransferTerms`. * @param _delegations Delegation chain, sorted leaf to root * @param _minimumMint Minimum vault shares expected (sanity-check bound) - * @param _caller Address of the caller, used only for event emission */ - function _executeDepositByDelegation(Delegation[] memory _delegations, uint256 _minimumMint, address _caller) internal { + function _executeDepositByDelegation(Delegation[] memory _delegations, uint256 _minimumMint) internal { uint256 length_ = _delegations.length; if (length_ < 2) revert InvalidDelegationsLength(); @@ -352,7 +349,7 @@ contract VedaAdapter is Ownable2Step { _ensureAllowance(IERC20(token_), boringVault, amount_); uint256 shares_ = teller.deposit(token_, amount_, _minimumMint, rootDelegator_, address(0)); - emit DepositExecuted(rootDelegator_, _caller, token_, amount_, shares_); + emit DepositExecuted(rootDelegator_, msg.sender, token_, amount_, shares_); } /** @@ -364,16 +361,8 @@ contract VedaAdapter is Ownable2Step { * @param _delegations Delegation chain, sorted leaf to root * @param _token Underlying output token to receive from the vault (differs from the share token in the caveat) * @param _minimumAssets Minimum underlying assets expected (sanity-check bound) - * @param _caller Address of the caller, used only for event emission */ - function _executeWithdrawByDelegation( - Delegation[] memory _delegations, - address _token, - uint256 _minimumAssets, - address _caller - ) - internal - { + function _executeWithdrawByDelegation(Delegation[] memory _delegations, address _token, uint256 _minimumAssets) internal { uint256 length_ = _delegations.length; if (length_ < 2) revert InvalidDelegationsLength(); if (_token == address(0)) revert InvalidZeroAddress(); @@ -397,6 +386,6 @@ contract VedaAdapter is Ownable2Step { // 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_); + emit WithdrawExecuted(rootDelegator_, msg.sender, _token, shareAmount_, assetsOut_); } } From 16edf2ab895b8a9d68e656f80114602392864266 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Sun, 5 Apr 2026 20:08:58 +0200 Subject: [PATCH 15/16] update docs --- src/helpers/VedaAdapter.sol | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index 671b9769..6fd84993 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -51,10 +51,11 @@ import { IVedaTeller } from "./interfaces/IVedaTeller.sol"; * @notice Security consideration: Anyone can call `depositByDelegation` and `withdrawByDelegation` — there is no * caller restriction. Security is enforced entirely through the delegation chain. The redelegation from the * operator to this adapter MUST include an `ERC20TransferAmountEnforcer` caveat capped to exactly the intended - * deposit or withdrawal amount. Once that amount is transferred the enforcer's running total is exhausted and - * any replay attempt will revert, making the delegation effectively single-use. A delegation without this - * enforcer (or with an amount larger than intended) could be exploited by any caller to transfer more tokens - * than authorised. + * deposit or withdrawal amount, and it MUST be the first caveat (`caveats[0]`) of that redelegation — the + * adapter reads token and amount directly from `_delegations[0].caveats[0].terms`. Once that amount is + * transferred the enforcer's running total is exhausted and any replay attempt will revert, making the + * delegation effectively single-use. A delegation without this enforcer as the first caveat (or with an amount + * larger than intended) could be exploited by any caller to transfer more tokens than authorised. */ contract VedaAdapter is Ownable2Step { using SafeERC20 for IERC20; @@ -195,8 +196,8 @@ contract VedaAdapter is Ownable2Step { * is negligible. A tolerance of 0.1-0.5% is recommended. If this check causes a revert, * no funds are lost — retry with a fresh quote. * @notice Security consideration: Callable by anyone. The redelegation passed in MUST include an - * `ERC20TransferAmountEnforcer` capped to exactly the intended deposit amount to prevent - * over-spending or replay. + * `ERC20TransferAmountEnforcer` as its first caveat (`caveats[0]`), capped to exactly the intended + * deposit amount, to prevent over-spending or replay. */ function depositByDelegation(Delegation[] memory _delegations, uint256 _minimumMint) external { _executeDepositByDelegation(_delegations, _minimumMint); @@ -208,7 +209,8 @@ contract VedaAdapter is Ownable2Step { * from the first caveat of each stream's leaf delegation. * @param _depositStreams Array of deposit parameters * @notice Security consideration: Callable by anyone. Each redelegation in the batch MUST include an - * `ERC20TransferAmountEnforcer` capped to exactly the intended deposit amount to prevent over-spending or replay. + * `ERC20TransferAmountEnforcer` as its first caveat (`caveats[0]`), capped to exactly the intended + * deposit amount, to prevent over-spending or replay. */ function depositByDelegationBatch(DepositParams[] memory _depositStreams) external { uint256 streamsLength_ = _depositStreams.length; @@ -240,7 +242,8 @@ contract VedaAdapter is Ownable2Step { * yield streaming is negligible. A tolerance of 0.1-0.5% is recommended. If this check * causes a revert, no funds are lost — retry with a fresh quote. * @notice Security consideration: Callable by anyone. The redelegation passed in MUST include an - * `ERC20TransferAmountEnforcer` capped to exactly `_shareAmount` to prevent over-spending or replay. + * `ERC20TransferAmountEnforcer` as its first caveat (`caveats[0]`), capped to exactly `_shareAmount`, + * to prevent over-spending or replay. */ function withdrawByDelegation(Delegation[] memory _delegations, address _token, uint256 _minimumAssets) external { _executeWithdrawByDelegation(_delegations, _token, _minimumAssets); @@ -252,7 +255,8 @@ contract VedaAdapter is Ownable2Step { * from the first caveat of each stream's leaf delegation. * @param _withdrawStreams Array of withdraw parameters * @notice Security consideration: Callable by anyone. Each redelegation in the batch MUST include an - * `ERC20TransferAmountEnforcer` capped to exactly the intended share amount to prevent over-spending or replay. + * `ERC20TransferAmountEnforcer` as its first caveat (`caveats[0]`), capped to exactly the intended + * share amount, to prevent over-spending or replay. */ function withdrawByDelegationBatch(WithdrawParams[] memory _withdrawStreams) external { uint256 streamsLength_ = _withdrawStreams.length; From 51f165496949c9195696149d38f67d467d962314 Mon Sep 17 00:00:00 2001 From: MoMannn Date: Sun, 5 Apr 2026 20:17:04 +0200 Subject: [PATCH 16/16] Update approval logic to unlimited --- src/helpers/VedaAdapter.sol | 4 ++-- test/helpers/VedaLending.t.sol | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/helpers/VedaAdapter.sol b/src/helpers/VedaAdapter.sol index 6fd84993..8dd567ef 100644 --- a/src/helpers/VedaAdapter.sol +++ b/src/helpers/VedaAdapter.sol @@ -294,7 +294,7 @@ contract VedaAdapter is Ownable2Step { /** * @notice Ensures sufficient token allowance for a spender to pull tokens - * @dev Checks current allowance and sets exact amount if insufficient, avoiding accumulation + * @dev Checks current allowance and increases to unlimited if insufficient * @param _token Token to manage allowance for * @param _spender Address that needs to spend the tokens * @param _amount Amount needed for the operation @@ -302,7 +302,7 @@ contract VedaAdapter is Ownable2Step { function _ensureAllowance(IERC20 _token, address _spender, uint256 _amount) private { uint256 allowance_ = _token.allowance(address(this), _spender); if (allowance_ < _amount) { - _token.forceApprove(_spender, _amount); + _token.safeIncreaseAllowance(_spender, type(uint256).max); } } diff --git a/test/helpers/VedaLending.t.sol b/test/helpers/VedaLending.t.sol index 1bd38e43..d502d602 100644 --- a/test/helpers/VedaLending.t.sol +++ b/test/helpers/VedaLending.t.sol @@ -581,8 +581,8 @@ contract VedaLendingTest is BaseTest { 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 deposit. - /// Verifies that _ensureAllowance does not cause unbounded allowance accumulation. + /// @notice After the first deposit, the adapter grants unlimited allowance to BoringVault. + /// Subsequent deposits reuse the existing allowance without issuing a new approval. function test_allowanceFullyConsumedAfterDeposit() public { assertEq(USDC.allowance(address(vedaAdapter), address(BORING_VAULT)), 0, "Initial allowance should be 0"); @@ -600,8 +600,8 @@ contract VedaLendingTest is BaseTest { assertEq( USDC.allowance(address(vedaAdapter), address(BORING_VAULT)), - 0, - "Allowance must be fully consumed after deposit -- no residual accumulation" + type(uint256).max - DEPOSIT_AMOUNT, + "Allowance should be unlimited minus the deposited amount" ); }