From 0beefc77942ef2fcb1f6241e1955c301b5ccc4f2 Mon Sep 17 00:00:00 2001 From: Henry <11198460+godzillaba@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:00:27 -0700 Subject: [PATCH 1/3] many vaults per gw and gwr, simplify initializers and creator, refactor MVF, add comments/todos/questions --- .../arbitrum/L2AtomicTokenBridgeFactory.sol | 1 + .../ethereum/L1AtomicTokenBridgeCreator.sol | 99 ++++--------------- .../ethereum/L1TokenBridgeRetryableSender.sol | 1 + .../ethereum/gateway/L1ArbitrumGateway.sol | 49 +++------ .../ethereum/gateway/L1CustomGateway.sol | 19 +--- .../ethereum/gateway/L1ERC20Gateway.sol | 24 +---- .../ethereum/gateway/L1USDCGateway.sol | 4 +- .../ethereum/gateway/L1WethGateway.sol | 4 +- .../libraries/vault/IMasterVault.sol | 1 + .../libraries/vault/MasterVaultFactory.sol | 43 +++----- .../tokenbridge/libraries/vault/SubVault.sol | 2 + 11 files changed, 65 insertions(+), 182 deletions(-) diff --git a/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol b/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol index 2b09c40b44..c1332e196f 100644 --- a/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol +++ b/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol @@ -305,6 +305,7 @@ library OrbitSalts { bytes internal constant L1_STANDARD_GATEWAY = bytes("L1SGW"); bytes internal constant L1_CUSTOM_GATEWAY = bytes("L1CGW"); bytes internal constant L1_WETH_GATEWAY = bytes("L1WGW"); + bytes internal constant MASTER_VAULT_FACTORY = bytes("MVF"); bytes internal constant L2_PROXY_ADMIN = bytes("L2PA"); bytes internal constant L2_ROUTER = bytes("L2R"); diff --git a/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol b/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol index bd5fc049cc..3a5ba91d18 100644 --- a/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol +++ b/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol @@ -85,11 +85,6 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { MasterVaultFactory masterVaultFactory; } - struct YieldBearingGatewayConfig { - bool enableYieldBearing; - address underlyingToken; - } - // use separate mapping to allow appending to the struct in the future // and workaround some stack too deep issues mapping(address => L1DeploymentAddresses) public inboxToL1Deployment; @@ -197,33 +192,13 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { * Warning: Due to asynchronous communication between parent and child chain, always check child chain contracts are * fully deployed and initialized before sending tokens to the bridge. Otherwise tokens might be permanently lost. */ - function createTokenBridge( - address inbox, - address rollupOwner, - uint256 maxGasForContracts, - uint256 gasPriceBid - ) external payable { - YieldBearingGatewayConfig memory emptyConfig; - _createTokenBridge(inbox, rollupOwner, maxGasForContracts, gasPriceBid, emptyConfig); - } - function createTokenBridge( address inbox, address rollupOwner, uint256 maxGasForContracts, uint256 gasPriceBid, - YieldBearingGatewayConfig memory yieldBearingConfig + bool isYieldBearingBridge ) external payable { - _createTokenBridge(inbox, rollupOwner, maxGasForContracts, gasPriceBid, yieldBearingConfig); - } - - function _createTokenBridge( - address inbox, - address rollupOwner, - uint256 maxGasForContracts, - uint256 gasPriceBid, - YieldBearingGatewayConfig memory yieldBearingConfig - ) internal { // templates have to be in place if (address(l1Templates.routerTemplate) == address(0)) { revert L1AtomicTokenBridgeCreator_TemplatesNotSet(); @@ -284,6 +259,15 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { // if resend, we assume L1 contracts already exist if (!isResend) { + if (isYieldBearingBridge) { + // deploy master vault factory + l1Deployment.masterVaultFactory = _deployProxyWithSalt( + _getL1Salt(OrbitSalts.MASTER_VAULT_FACTORY, inbox), + address(l1Templates.masterVaultFactory), + proxyAdmin + ); + } + // l1 router deployment block { address routerTemplate = feeToken != address(0) @@ -306,33 +290,14 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { ) ); - if ( - yieldBearingConfig.enableYieldBearing && - yieldBearingConfig.underlyingToken != address(0) - ) { - L1ArbitrumGateway.YieldBearingConfig memory config = L1ArbitrumGateway - .YieldBearingConfig({ - token: yieldBearingConfig.underlyingToken, - masterVaultFactory: address(l1Templates.masterVaultFactory), - isYieldBearingGateway: true - }); - standardGateway.initialize( - l2Deployment.standardGateway, - l1Deployment.router, - inbox, - keccak256(type(ClonableBeaconProxy).creationCode), - l2Deployment.beaconProxyFactory, - config - ); - } else { - standardGateway.initialize( - l2Deployment.standardGateway, - l1Deployment.router, - inbox, - keccak256(type(ClonableBeaconProxy).creationCode), - l2Deployment.beaconProxyFactory - ); - } + standardGateway.initialize( + l2Deployment.standardGateway, + l1Deployment.router, + inbox, + keccak256(type(ClonableBeaconProxy).creationCode), + l2Deployment.beaconProxyFactory, + l1Deployment.masterVaultFactory + ); l1Deployment.standardGateway = address(standardGateway); } @@ -349,31 +314,9 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { ) ); - if ( - yieldBearingConfig.enableYieldBearing && - yieldBearingConfig.underlyingToken != address(0) - ) { - L1ArbitrumGateway.YieldBearingConfig memory config = L1ArbitrumGateway - .YieldBearingConfig({ - token: yieldBearingConfig.underlyingToken, - masterVaultFactory: address(l1Templates.masterVaultFactory), - isYieldBearingGateway: true - }); - customGateway.initialize( - l2Deployment.customGateway, - l1Deployment.router, - inbox, - upgradeExecutor, - config - ); - } else { - customGateway.initialize( - l2Deployment.customGateway, - l1Deployment.router, - inbox, - upgradeExecutor - ); - } + customGateway.initialize( + l2Deployment.customGateway, l1Deployment.router, inbox, upgradeExecutor, l1Deployment.masterVaultFactory + ); l1Deployment.customGateway = address(customGateway); } diff --git a/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol b/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol index e98f2fc35b..3076644219 100644 --- a/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol +++ b/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol @@ -224,6 +224,7 @@ struct L1DeploymentAddresses { address customGateway; address wethGateway; address weth; + address masterVaultFactory; } struct L2DeploymentAddresses { diff --git a/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol index 0f60ef6781..6a7b26346f 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol @@ -47,15 +47,8 @@ abstract contract L1ArbitrumGateway is error BadVaultFactory(); error BadVaultCodeHash(); - // considering moving this struct to different common file - struct YieldBearingConfig { - address token; - address masterVaultFactory; - bool isYieldBearingGateway; // could be redundant - } - address public override inbox; - address public masterVault; + address public masterVaultFactory; event DepositInitiated( address l1Token, @@ -94,36 +87,18 @@ abstract contract L1ArbitrumGateway is // this has no other logic since the current upgrade doesn't require this logic } - function _initialize( address _l2Counterpart, address _router, - address _inbox + address _inbox, + address _masterVaultFactory ) internal { TokenGateway._initialize(_l2Counterpart, _router); // L1 gateway must have a router require(_router != address(0), "BAD_ROUTER"); require(_inbox != address(0), "BAD_INBOX"); inbox = _inbox; - } - - function _initialize( - address _l2Counterpart, - address _router, - address _inbox, - YieldBearingConfig memory _yieldBearingConfig - ) internal { - _initialize(_l2Counterpart, _router, _inbox); - _initializeYieldBearing(_yieldBearingConfig); - } - - function _initializeYieldBearing(YieldBearingConfig memory _config) internal { - if (_config.isYieldBearingGateway) { - if (_config.masterVaultFactory == address(0)) revert BadVaultFactory(); - if (_config.token == address(0)) revert BadVaultCodeHash(); - - masterVault = MasterVaultFactory(_config.masterVaultFactory).deployVault(_config.token); - } + masterVaultFactory = _masterVaultFactory; } /** @@ -175,9 +150,10 @@ abstract contract L1ArbitrumGateway is uint256 _amount ) internal virtual { // this method is virtual since different subclasses can handle escrow differently - if (masterVault != address(0)) { - // todo: approve shares to master vault - IMasterVault(masterVault).withdraw(_amount, _dest); + address _masterVaultFactory = masterVaultFactory; + if (_masterVaultFactory != address(0)) { + // todo: do we want to unwrap here or just transfer vault shares? + address masterVault = MasterVaultFactory(masterVaultFactory).getVault(_l1Token); } else { IERC20(_l1Token).safeTransfer(_dest, _amount); } @@ -341,9 +317,12 @@ abstract contract L1ArbitrumGateway is uint256 postBalance = IERC20(_l1Token).balanceOf(address(this)); amountReceived = postBalance - prevBalance; - if (masterVault != address(0)) { - address subVault = IMasterVault(masterVault).getSubVault(); - IERC20(_l1Token).safeApprove(subVault, amountReceived); + address _masterVaultFactory = masterVaultFactory; + if (_masterVaultFactory != address(0)) { + address masterVault = MasterVaultFactory(masterVaultFactory).getVault(_l1Token); + // todo: decide whether we want the master vault to act like its own vault, or whether it is just a pointer to the real vault + // this affects which address gets approved + // somewhat related to deciding whether we want to auto unwrap on withdrawals amountReceived = IMasterVault(masterVault).deposit(amountReceived); } } diff --git a/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol index 76a8c48403..fbb7a04db0 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol @@ -92,29 +92,14 @@ contract L1CustomGateway is L1ArbitrumExtendedGateway, ICustomGateway { super.finalizeInboundTransfer(_token, _from, _to, _amount, _data); } - // todo: have only one initialize function - function initialize( - address _l1Counterpart, - address _l1Router, - address _inbox, - address _owner - ) public { - L1ArbitrumGateway._initialize(_l1Counterpart, _l1Router, _inbox); - owner = _owner; - // disable whitelist by default - whitelist = address(0); - // reentrancy guard - _status = _NOT_ENTERED; - } - function initialize( address _l1Counterpart, address _l1Router, address _inbox, address _owner, - YieldBearingConfig memory _yieldBearingConfig + address _masterVaultFactory // todo: document that this switches the contracts behavior between YBB mode and normal mode ) public { - L1ArbitrumGateway._initialize(_l1Counterpart, _l1Router, _inbox, _yieldBearingConfig); + L1ArbitrumGateway._initialize(_l1Counterpart, _l1Router, _inbox, _masterVaultFactory); owner = _owner; // disable whitelist by default whitelist = address(0); diff --git a/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol b/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol index f6cf364af2..44d8ebe16f 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol @@ -86,34 +86,16 @@ contract L1ERC20Gateway is L1ArbitrumExtendedGateway { super.finalizeInboundTransfer(_token, _from, _to, _amount, _data); } - // todo: have only one initialize function - function initialize( - address _l2Counterpart, - address _router, - address _inbox, - bytes32 _cloneableProxyHash, - address _l2BeaconProxyFactory - ) public { - L1ArbitrumGateway._initialize(_l2Counterpart, _router, _inbox); - require(_cloneableProxyHash != bytes32(0), "INVALID_PROXYHASH"); - require(_l2BeaconProxyFactory != address(0), "INVALID_BEACON"); - cloneableProxyHash = _cloneableProxyHash; - l2BeaconProxyFactory = _l2BeaconProxyFactory; - // disable whitelist by default - whitelist = address(0); - // reentrancy guard - _status = _NOT_ENTERED; - } - + // todo: update initializers for orbit versions of gateways as well function initialize( address _l2Counterpart, address _router, address _inbox, bytes32 _cloneableProxyHash, address _l2BeaconProxyFactory, - YieldBearingConfig memory _yieldBearingConfig + address _masterVaultFactory ) public { - L1ArbitrumGateway._initialize(_l2Counterpart, _router, _inbox, _yieldBearingConfig); + L1ArbitrumGateway._initialize(_l2Counterpart, _router, _inbox, _masterVaultFactory); require(_cloneableProxyHash != bytes32(0), "INVALID_PROXYHASH"); require(_l2BeaconProxyFactory != address(0), "INVALID_BEACON"); cloneableProxyHash = _cloneableProxyHash; diff --git a/contracts/tokenbridge/ethereum/gateway/L1USDCGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1USDCGateway.sol index edc1dd0e19..eaf312d9f8 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1USDCGateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1USDCGateway.sol @@ -82,7 +82,9 @@ contract L1USDCGateway is L1ArbitrumExtendedGateway { if (_owner == address(0)) { revert L1USDCGateway_InvalidOwner(); } - L1ArbitrumGateway._initialize(_l2Counterpart, _l1Router, _inbox); + // address(0) master vault factory indicates no YBB functionality + // todo: ensure this is what we want here + L1ArbitrumGateway._initialize(_l2Counterpart, _l1Router, _inbox, address(0)); l1USDC = _l1USDC; l2USDC = _l2USDC; owner = _owner; diff --git a/contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol index 8c36307686..df23aa52f5 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol @@ -38,7 +38,9 @@ contract L1WethGateway is L1ArbitrumExtendedGateway { address _l1Weth, address _l2Weth ) public { - L1ArbitrumGateway._initialize(_l2Counterpart, _l1Router, _inbox); + // address(0) master vault factory disables YBB functionality + // YBB is not relevant to this gateway since the asset is escrowed as ETH in the main Bridge contract + L1ArbitrumGateway._initialize(_l2Counterpart, _l1Router, _inbox, address(0)); require(_l1Weth != address(0), "INVALID_L1WETH"); require(_l2Weth != address(0), "INVALID_L2WETH"); l1Weth = _l1Weth; diff --git a/contracts/tokenbridge/libraries/vault/IMasterVault.sol b/contracts/tokenbridge/libraries/vault/IMasterVault.sol index 251be77345..157fd4fd3b 100644 --- a/contracts/tokenbridge/libraries/vault/IMasterVault.sol +++ b/contracts/tokenbridge/libraries/vault/IMasterVault.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; interface IMasterVault { + // todo: add bytes param for slippage etc function deposit(uint256 amount) external returns (uint256); function withdraw(uint256 amount, address recipient) external; diff --git a/contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol b/contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol index 87711574ce..7564a37f09 100644 --- a/contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol +++ b/contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol @@ -12,54 +12,39 @@ contract MasterVaultFactory { error VaultDeploymentFailed(); error ZeroAddress(); - function deployVault(address token) external returns (address vault) { + function deployVault(address token) public returns (address vault) { if (token == address(0)) { revert ZeroAddress(); } address gateway = msg.sender; - bytes32 salt = _getSalt(token, gateway); bytes memory bytecode = abi.encodePacked( type(MasterVault).creationCode, - abi.encode(token, gateway, gateway) + abi.encode(token) ); - vault = Create2.deploy(0, salt, bytecode); - - if (vault == address(0)) { - revert VaultDeploymentFailed(); - } + vault = Create2.deploy(0, bytes32(0), bytecode); emit VaultDeployed(token, gateway, vault); } function calculateVaultAddress( - address token, - address gateway - ) external view returns (address) { - bytes32 salt = _getSalt(token, gateway); + address token + ) public view returns (address) { bytes32 bytecodeHash = keccak256( - abi.encodePacked(type(MasterVault).creationCode, abi.encode(token, gateway, gateway)) + abi.encodePacked(type(MasterVault).creationCode, abi.encode(token)) ); - return Create2.computeAddress(salt, bytecodeHash); + return Create2.computeAddress(bytes32(0), bytecodeHash); } function getVault( - address token, - address gateway - ) external view returns (address) { - bytes32 salt = _getSalt(token, gateway); - bytes32 bytecodeHash = keccak256( - abi.encodePacked(type(MasterVault).creationCode, abi.encode(token, gateway, gateway)) - ); - return Create2.computeAddress(salt, bytecodeHash); - } - - function _getSalt( - address token, - address gateway - ) internal view returns (bytes32) { - return keccak256(abi.encodePacked(token, gateway, gateway, block.chainid)); + address token + ) external returns (address) { + address vault = calculateVaultAddress(token); + if (vault.code.length == 0) { + return deployVault(token); + } + return vault; } } diff --git a/contracts/tokenbridge/libraries/vault/SubVault.sol b/contracts/tokenbridge/libraries/vault/SubVault.sol index 41da61e91a..5dec134d91 100644 --- a/contracts/tokenbridge/libraries/vault/SubVault.sol +++ b/contracts/tokenbridge/libraries/vault/SubVault.sol @@ -6,6 +6,8 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// todo: we may want to leave this contract out of the feature and push it off to the chain owners to implement /// @dev this is an `abstruct` contract that is used to create a sub vault by bridge owner /// bridge owner must set the master vault address /// it is up to the owner to implement sub-strategies for this vault From 791cb87f3e64298652877924c0ebd56ff8dff6a3 Mon Sep 17 00:00:00 2001 From: Henry <11198460+godzillaba@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:15:48 -0700 Subject: [PATCH 2/3] note about transferring funds --- contracts/tokenbridge/libraries/vault/MasterVault.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/tokenbridge/libraries/vault/MasterVault.sol b/contracts/tokenbridge/libraries/vault/MasterVault.sol index 294666c90f..a342d35973 100644 --- a/contracts/tokenbridge/libraries/vault/MasterVault.sol +++ b/contracts/tokenbridge/libraries/vault/MasterVault.sol @@ -62,6 +62,7 @@ contract MasterVault is IMasterVault, Ownable { } function setSubVault(address _subVault) external override onlyOwner { + // todo: need to make sure we transfer funds here subVault = _subVault; emit SubVaultSet(_subVault); } From 2d661d983769bbb2f8f5113c11d77730f37edc2c Mon Sep 17 00:00:00 2001 From: Henry <11198460+godzillaba@users.noreply.github.com> Date: Fri, 19 Sep 2025 07:18:58 -0700 Subject: [PATCH 3/3] comments --- contracts/tokenbridge/libraries/vault/MasterVault.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/tokenbridge/libraries/vault/MasterVault.sol b/contracts/tokenbridge/libraries/vault/MasterVault.sol index a342d35973..41d3cb73a5 100644 --- a/contracts/tokenbridge/libraries/vault/MasterVault.sol +++ b/contracts/tokenbridge/libraries/vault/MasterVault.sol @@ -8,6 +8,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC4626 } from "lib/forge-std/src/interfaces/IERC4626.sol"; +// todo: make this more like a 4626 vault, erc20 shares + deposit + withdraw contract MasterVault is IMasterVault, Ownable { using SafeERC20 for IERC20; @@ -30,6 +31,8 @@ contract MasterVault is IMasterVault, Ownable { _; } + // todo: remove gateway and owner params + // factory can transfer ownership to upgrade executor constructor(address _token, address _gateway, address _owner) Ownable() { if (_token == address(0) || _gateway == address(0) || _owner == address(0)) { revert ZeroAddress();