Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
686b3b5
Implement VedaAdapter
MoMannn Mar 2, 2026
89e74c4
basic boring on chain queue implementation
MoMannn Mar 6, 2026
7dc2245
Move to arbitrum mainnet tests, implement new deposit function, remov…
MoMannn Mar 30, 2026
b0a56ed
Clean up docs, add batch events
MoMannn Mar 30, 2026
5af6309
add arbitrum rpc to tests
MoMannn Mar 30, 2026
0f8ee51
set API key in tests
MoMannn Mar 30, 2026
25a8c19
add veda deployment script
MoMannn Mar 30, 2026
7b3fb1d
Remove NotLeafDelegator check and add security consideration
MoMannn Mar 31, 2026
c022400
optimize inputs and add docs regarding senity check inputs
MoMannn Apr 2, 2026
57120d5
additional tests and code optimization
MoMannn Apr 2, 2026
4deb901
linter + fetching from latest block
MoMannn Apr 2, 2026
d3338bb
Merge branch 'main' into chore-veda-adapter
MoMannn Apr 2, 2026
57b5b88
move deploy script to .env variables
MoMannn Apr 5, 2026
e412baf
fix teller docs
MoMannn Apr 5, 2026
801ab0d
remove unnecesarry caller variable
MoMannn Apr 5, 2026
16edf2a
update docs
MoMannn Apr 5, 2026
51f1654
Update approval logic to unlimited
MoMannn Apr 5, 2026
01be197
Merge branch 'chore-veda-adapter' of https://github.com/MetaMask/dele…
MoMannn Apr 5, 2026
1f1182e
Use calldata instead of memory for external function array parameters
MoMannn Apr 9, 2026
adb6c64
Eliminate single-use encodedTransfer_ local variable
MoMannn Apr 9, 2026
231e1de
Fix _ensureAllowance
MoMannn Apr 9, 2026
aba8aa5
fixate veda adapter deposit / withdrawal token in constructor
MoMannn Apr 9, 2026
6301f8c
Merge branch 'main' of https://github.com/MetaMask/delegation-framewo…
MoMannn Apr 20, 2026
077acc4
add veda adapter audit
MoMannn Apr 21, 2026
edf55c3
gas optimization - max approve in constructor
MoMannn Apr 21, 2026
f0c77d1
add monad specific deployment changes
MoMannn May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 353 additions & 0 deletions src/helpers/VedaAdapter.sol
Original file line number Diff line number Diff line change
@@ -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);
Comment thread
MoMannn marked this conversation as resolved.
Outdated
}

/**
* @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;
Comment thread
MoMannn marked this conversation as resolved.
Outdated
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
Comment thread
MoMannn marked this conversation as resolved.
Outdated
)
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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm im thinking we get rid of this function. It doesnt seem possible for tokens to be left in this contract without the tx reverting, since it's atomic. If we remove, it really makes the adapter clean and lightweight, removes the need for owner / state.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in case of someone sending tokens directly to the contract (mistaking it for EOA).

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) {
Comment thread
MoMannn marked this conversation as resolved.
Outdated
_token.forceApprove(boringVault, _amount);
}
}
Comment thread
MoMannn marked this conversation as resolved.

/**
* @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_);
Comment thread
MoMannn marked this conversation as resolved.
Outdated

emit WithdrawExecuted(rootDelegator_, _caller, _token, _shareAmount, assetsOut_);
}
}
Loading
Loading