diff --git a/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol b/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol index 2b09c40b4..c1332e196 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 571568cc5..83bab1411 100644 --- a/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol +++ b/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol @@ -43,6 +43,8 @@ import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {IAccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; +import {MasterVaultFactory} from "../libraries/vault/MasterVaultFactory.sol"; +import {L1ArbitrumGateway} from "./gateway/L1ArbitrumGateway.sol"; /** * @title Layer1 token bridge creator @@ -80,6 +82,15 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { L1OrbitERC20Gateway feeTokenBasedStandardGatewayTemplate; L1OrbitCustomGateway feeTokenBasedCustomGatewayTemplate; IUpgradeExecutor upgradeExecutor; + MasterVaultFactory masterVaultFactory; + } + + struct CreateTokenBridgeArgs { + address inbox; + address rollupOwner; + uint256 maxGasForContracts; + uint256 gasPriceBid; + bool isYieldBearingBridge; } // use separate mapping to allow appending to the struct in the future @@ -190,10 +201,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { * 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 + CreateTokenBridgeArgs calldata args ) external payable { // templates have to be in place if (address(l1Templates.routerTemplate) == address(0)) { @@ -202,10 +210,10 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { // Check that the rollupOwner account has EXECUTOR role // on the upgrade executor which is the owner of the rollup - address upgradeExecutor = IInbox(inbox).bridge().rollup().owner(); + address upgradeExecutor = IInbox(args.inbox).bridge().rollup().owner(); if ( !IAccessControlUpgradeable(upgradeExecutor).hasRole( - UpgradeExecutor(upgradeExecutor).EXECUTOR_ROLE(), rollupOwner + UpgradeExecutor(upgradeExecutor).EXECUTOR_ROLE(), args.rollupOwner ) ) { revert L1AtomicTokenBridgeCreator_RollupOwnershipMisconfig(); @@ -215,9 +223,9 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { // this is useful to recover from expired or out-of-order retryables // in case of resend, we assume L1 contracts already exist and we just need to deploy L2 contracts // deployment mappings should not be updated in case of resend - bool isResend = (inboxToL1Deployment[inbox].router != address(0)); + bool isResend = (inboxToL1Deployment[args.inbox].router != address(0)); - address feeToken = _getFeeToken(inbox); + address feeToken = _getFeeToken(args.inbox); // store L2 addresses before deployments L1DeploymentAddresses memory l1Deployment; @@ -225,12 +233,12 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { // if resend, we use the existing l1 deployment if (isResend) { - l1Deployment = inboxToL1Deployment[inbox]; + l1Deployment = inboxToL1Deployment[args.inbox]; } { // store L2 addresses which are proxies - uint256 chainId = IRollupCore(address(IInbox(inbox).bridge().rollup())).chainId(); + uint256 chainId = IRollupCore(address(IInbox(args.inbox).bridge().rollup())).chainId(); l2Deployment.router = _getProxyAddress(OrbitSalts.L2_ROUTER, chainId); l2Deployment.standardGateway = _getProxyAddress(OrbitSalts.L2_STANDARD_GATEWAY, chainId); l2Deployment.customGateway = _getProxyAddress(OrbitSalts.L2_CUSTOM_GATEWAY, chainId); @@ -248,20 +256,30 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { // deploy L1 side of token bridge // get existing proxy admin and upgrade executor - address proxyAdmin = IInboxProxyAdmin(inbox).getProxyAdmin(); + address proxyAdmin = IInboxProxyAdmin(args.inbox).getProxyAdmin(); if (proxyAdmin == address(0)) { revert L1AtomicTokenBridgeCreator_ProxyAdminNotFound(); } // if resend, we assume L1 contracts already exist if (!isResend) { + if (args.isYieldBearingBridge) { + // deploy master vault factory + l1Deployment.masterVaultFactory = _deployProxyWithSalt( + _getL1Salt(OrbitSalts.MASTER_VAULT_FACTORY, args.inbox), + address(l1Templates.masterVaultFactory), + proxyAdmin + ); + MasterVaultFactory(l1Deployment.masterVaultFactory).initialize(upgradeExecutor); + } + // l1 router deployment block { address routerTemplate = feeToken != address(0) ? address(l1Templates.feeTokenBasedRouterTemplate) : address(l1Templates.routerTemplate); l1Deployment.router = _deployProxyWithSalt( - _getL1Salt(OrbitSalts.L1_ROUTER, inbox), routerTemplate, proxyAdmin + _getL1Salt(OrbitSalts.L1_ROUTER, args.inbox), routerTemplate, proxyAdmin ); } @@ -273,16 +291,17 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { L1ERC20Gateway standardGateway = L1ERC20Gateway( _deployProxyWithSalt( - _getL1Salt(OrbitSalts.L1_STANDARD_GATEWAY, inbox), template, proxyAdmin + _getL1Salt(OrbitSalts.L1_STANDARD_GATEWAY, args.inbox), template, proxyAdmin ) ); standardGateway.initialize( l2Deployment.standardGateway, l1Deployment.router, - inbox, + args.inbox, keccak256(type(ClonableBeaconProxy).creationCode), - l2Deployment.beaconProxyFactory + l2Deployment.beaconProxyFactory, + l1Deployment.masterVaultFactory ); l1Deployment.standardGateway = address(standardGateway); @@ -296,12 +315,12 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { L1CustomGateway customGateway = L1CustomGateway( _deployProxyWithSalt( - _getL1Salt(OrbitSalts.L1_CUSTOM_GATEWAY, inbox), template, proxyAdmin + _getL1Salt(OrbitSalts.L1_CUSTOM_GATEWAY, args.inbox), template, proxyAdmin ) ); customGateway.initialize( - l2Deployment.customGateway, l1Deployment.router, inbox, upgradeExecutor + l2Deployment.customGateway, l1Deployment.router, args.inbox, upgradeExecutor, l1Deployment.masterVaultFactory ); l1Deployment.customGateway = address(customGateway); @@ -312,7 +331,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { L1WethGateway wethGateway = L1WethGateway( payable( _deployProxyWithSalt( - _getL1Salt(OrbitSalts.L1_WETH_GATEWAY, inbox), + _getL1Salt(OrbitSalts.L1_WETH_GATEWAY, args.inbox), address(l1Templates.wethGatewayTemplate), proxyAdmin ) @@ -320,7 +339,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { ); wethGateway.initialize( - l2Deployment.wethGateway, l1Deployment.router, inbox, l1Weth, l2Deployment.weth + l2Deployment.wethGateway, l1Deployment.router, args.inbox, l1Weth, l2Deployment.weth ); l1Deployment.wethGateway = address(wethGateway); @@ -333,30 +352,30 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { l1Deployment.standardGateway, address(0), l2Deployment.router, - inbox + args.inbox ); } // deploy factory and then L2 contracts through L2 factory, using 2 retryables calls // we do not care if it is a resend or not, if the L2 deployment already exists it will simply fail on L2 - _deployL2Factory(inbox, gasPriceBid, feeToken); + _deployL2Factory(args.inbox, args.gasPriceBid, feeToken); RetryableParams memory retryableParams = RetryableParams( - inbox, + args.inbox, canonicalL2FactoryAddress, msg.sender, msg.sender, - maxGasForContracts, - gasPriceBid, + args.maxGasForContracts, + args.gasPriceBid, 0 ); if (feeToken != address(0)) { // transfer fee tokens to inbox to pay for 2nd retryable retryableParams.feeTokenTotalFeeAmount = - _getScaledAmount(feeToken, maxGasForContracts * gasPriceBid); + _getScaledAmount(feeToken, args.maxGasForContracts * args.gasPriceBid); IERC20(feeToken).safeTransferFrom( - msg.sender, inbox, retryableParams.feeTokenTotalFeeAmount + msg.sender, args.inbox, retryableParams.feeTokenTotalFeeAmount ); } @@ -371,9 +390,9 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { ); // alias rollup owner if it is a contract - address l2RollupOwner = rollupOwner.code.length == 0 - ? rollupOwner - : AddressAliasHelper.applyL1ToL2Alias(rollupOwner); + address l2RollupOwner = args.rollupOwner.code.length == 0 + ? args.rollupOwner + : AddressAliasHelper.applyL1ToL2Alias(args.rollupOwner); // sweep the balance to send the retryable and refund the difference // it is known that any eth previously in this contract can be extracted @@ -390,10 +409,10 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { // deployment mappings should not be updated in case of resend if (!isResend) { emit OrbitTokenBridgeCreated( - inbox, rollupOwner, l1Deployment, l2Deployment, proxyAdmin, upgradeExecutor + args.inbox, args.rollupOwner, l1Deployment, l2Deployment, proxyAdmin, upgradeExecutor ); - inboxToL1Deployment[inbox] = l1Deployment; - inboxToL2Deployment[inbox] = l2Deployment; + inboxToL1Deployment[args.inbox] = l1Deployment; + inboxToL2Deployment[args.inbox] = l2Deployment; } } diff --git a/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol b/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol index e98f2fc35..307664421 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 bdef669b4..6a7b26346 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol @@ -29,6 +29,8 @@ import "../../libraries/gateway/GatewayMessageHandler.sol"; import "../../libraries/gateway/TokenGateway.sol"; import "../../libraries/ITransferAndCall.sol"; import "../../libraries/ERC165.sol"; +import { MasterVaultFactory } from "../../libraries/vault/MasterVaultFactory.sol"; +import { IMasterVault } from "../../libraries/vault/IMasterVault.sol"; /** * @title Common interface for gatways on L1 messaging to Arbitrum. @@ -42,7 +44,11 @@ abstract contract L1ArbitrumGateway is using SafeERC20 for IERC20; using Address for address; + error BadVaultFactory(); + error BadVaultCodeHash(); + address public override inbox; + address public masterVaultFactory; event DepositInitiated( address l1Token, @@ -84,13 +90,15 @@ abstract contract L1ArbitrumGateway is 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; + masterVaultFactory = _masterVaultFactory; } /** @@ -142,7 +150,13 @@ abstract contract L1ArbitrumGateway is uint256 _amount ) internal virtual { // this method is virtual since different subclasses can handle escrow differently - IERC20(_l1Token).safeTransfer(_dest, _amount); + 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); + } } /** @@ -301,7 +315,16 @@ abstract contract L1ArbitrumGateway is uint256 prevBalance = IERC20(_l1Token).balanceOf(address(this)); IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount); uint256 postBalance = IERC20(_l1Token).balanceOf(address(this)); - return postBalance - prevBalance; + amountReceived = postBalance - prevBalance; + + 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); + } } function getOutboundCalldata( diff --git a/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol index f5c13c82e..fbb7a04db 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol @@ -96,9 +96,10 @@ contract L1CustomGateway is L1ArbitrumExtendedGateway, ICustomGateway { address _l1Counterpart, address _l1Router, address _inbox, - address _owner + address _owner, + address _masterVaultFactory // todo: document that this switches the contracts behavior between YBB mode and normal mode ) public { - L1ArbitrumGateway._initialize(_l1Counterpart, _l1Router, _inbox); + 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 bbf48bab4..44d8ebe16 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol @@ -86,14 +86,16 @@ contract L1ERC20Gateway is L1ArbitrumExtendedGateway { super.finalizeInboundTransfer(_token, _from, _to, _amount, _data); } + // todo: update initializers for orbit versions of gateways as well function initialize( address _l2Counterpart, address _router, address _inbox, bytes32 _cloneableProxyHash, - address _l2BeaconProxyFactory + address _l2BeaconProxyFactory, + address _masterVaultFactory ) public { - L1ArbitrumGateway._initialize(_l2Counterpart, _router, _inbox); + 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 edc1dd0e1..eaf312d9f 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 8c3630768..df23aa52f 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 new file mode 100644 index 000000000..157fd4fd3 --- /dev/null +++ b/contracts/tokenbridge/libraries/vault/IMasterVault.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +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; + + function getSubVault() external view returns (address); + + function setSubVault(address subVault) external; +} diff --git a/contracts/tokenbridge/libraries/vault/MasterVault.sol b/contracts/tokenbridge/libraries/vault/MasterVault.sol new file mode 100644 index 000000000..a6579dc85 --- /dev/null +++ b/contracts/tokenbridge/libraries/vault/MasterVault.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "./IMasterVault.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +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 +// todo: consider beacon proxy +contract MasterVault is IMasterVault, Ownable { + using SafeERC20 for IERC20; + + error CallerIsNotGateway(); + error ZeroAddress(); + error SubVaultIsNotSet(); + + address public immutable token; + address public immutable gateway; + address public subVault; + + event Deposited(uint256 amount); + event Withdrawn(uint256 amount, address recipient, uint256 shares); + event SubVaultSet(address subVault); + + modifier onlyGateway() { + if (msg.sender != gateway) { + revert CallerIsNotGateway(); + } + _; + } + + // todo: remove gateway and owner params + // factory retains ownership. anyone can call deposit and withdraw since it's close to a standard 4626 vault + constructor(address _token, address _gateway, address _owner) Ownable() { + if (_token == address(0) || _gateway == address(0) || _owner == address(0)) { + revert ZeroAddress(); + } + token = _token; + gateway = _gateway; + transferOwnership(_owner); + } + + function deposit( + uint256 amount + ) external override onlyGateway returns (uint256 amountDeposited) { + if (subVault == address(0)) { + revert SubVaultIsNotSet(); + } + amountDeposited = IERC4626(subVault).deposit(amount, gateway); + emit Deposited(amount); + } + + function withdraw(uint256 amount, address recipient) external override onlyGateway { + if (subVault == address(0)) { + revert SubVaultIsNotSet(); + } + uint256 shares = IERC4626(subVault).withdraw(amount, recipient, gateway); + emit Withdrawn(amount, recipient, shares); + } + + function getSubVault() external view returns (address) { + return subVault; + } + + function setSubVault(address _subVault) external override onlyOwner { + // todo: need to make sure we transfer funds here + subVault = _subVault; + emit SubVaultSet(_subVault); + } +} diff --git a/contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol b/contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol new file mode 100644 index 000000000..79e3c8fba --- /dev/null +++ b/contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "./IMasterVault.sol"; +import "./MasterVault.sol"; + +contract MasterVaultFactory is OwnableUpgradeable { + event VaultDeployed(address indexed token, address indexed vault); + event SubVaultSet(address indexed masterVault, address indexed subVault); + + error ZeroAddress(); + + function initialize(address _owner) public initializer { + _transferOwnership(_owner); + } + + function deployVault(address token) public returns (address vault) { + if (token == address(0)) { + revert ZeroAddress(); + } + + bytes memory bytecode = abi.encodePacked( + type(MasterVault).creationCode, + abi.encode(token) + ); + + vault = Create2.deploy(0, bytes32(0), bytecode); + + emit VaultDeployed(token, vault); + } + + function calculateVaultAddress( + address token + ) public view returns (address) { + bytes32 bytecodeHash = keccak256( + abi.encodePacked(type(MasterVault).creationCode, abi.encode(token)) + ); + return Create2.computeAddress(bytes32(0), bytecodeHash); + } + + function getVault( + address token + ) external returns (address) { + address vault = calculateVaultAddress(token); + if (vault.code.length == 0) { + return deployVault(token); + } + return vault; + } + + // todo: consider a method to enable bridge owner to transfer specific master vault ownership to new address + function setSubVault( + address masterVault, + address subVault + ) external onlyOwner { + IMasterVault(masterVault).setSubVault(subVault); + emit SubVaultSet(masterVault, subVault); + } +} diff --git a/contracts/tokenbridge/libraries/vault/SubVault.sol b/contracts/tokenbridge/libraries/vault/SubVault.sol new file mode 100644 index 000000000..5dec134d9 --- /dev/null +++ b/contracts/tokenbridge/libraries/vault/SubVault.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +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 +/// this vault will issue shares to the master vault +/// @notice should be ERC4626 compatible. TODO: implement & override ERC4626 functions +abstract contract SubVault is ERC20 { + using SafeERC20 for IERC20; + + IERC20 public immutable asset; + + constructor(IERC20 _asset, string memory _name, string memory _symbol) ERC20(_name, _symbol) { + asset = _asset; + } + + modifier onlyMasterVault() { + // TBD let owner set the master vault + _; + } + + /// @dev this function is used to deposit assets into the sub vault + /// @param _amount the amount of assets to deposit + /// @param _receiver the address that deposit the underlying assets and receive shares, + /// the receiver would be the gateway. + function deposit(uint256 _amount, address _receiver) external onlyMasterVault { + // todo: mint shares to receiver + // slither-disable-next-line arbitrary-send-erc20 + asset.safeTransferFrom(_receiver, address(this), _amount); + } + + /// @dev this function is used to withdraw assets from the sub vault + /// @param _amount the amount of assets to withdraw + /// @param _recipient the address that withdraw the underlying assets + /// @param _owner the address that owns the shares + function withdraw(uint256 _amount, address _recipient, address _owner) external onlyMasterVault { + // todo: burn shares from owner + asset.safeTransfer(_recipient, _amount); + } + + // todo: implement ERC4626 functions +} diff --git a/test-foundry/AtomicTokenBridgeFactory.t.sol b/test-foundry/AtomicTokenBridgeFactory.t.sol index 61e834a72..17432ca63 100644 --- a/test-foundry/AtomicTokenBridgeFactory.t.sol +++ b/test-foundry/AtomicTokenBridgeFactory.t.sol @@ -7,6 +7,7 @@ import "forge-std/Test.sol"; import "../contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol"; import "../contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol"; import "../contracts/tokenbridge/libraries/AddressAliasHelper.sol"; +import {MasterVaultFactory} from "../contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol"; import {L1TokenBridgeRetryableSender} from "../contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol"; @@ -138,7 +139,8 @@ contract AtomicTokenBridgeCreatorTest is Test { L1OrbitGatewayRouter(address(new L1OrbitGatewayRouter())), L1OrbitERC20Gateway(address(new L1OrbitERC20Gateway())), L1OrbitCustomGateway(address(new L1OrbitCustomGateway())), - IUpgradeExecutor(address(new UpgradeExecutor())) + IUpgradeExecutor(address(new UpgradeExecutor())), + MasterVaultFactory(address(new MasterVaultFactory())) ); l2TokenBridgeFactoryTemplate = address(new L2AtomicTokenBridgeFactory()); l2RouterTemplate = address(new L2GatewayRouter()); diff --git a/test-foundry/L1AtomicTokenBridgeCreator.t.sol b/test-foundry/L1AtomicTokenBridgeCreator.t.sol index fd4ee0487..d3bf19c8f 100644 --- a/test-foundry/L1AtomicTokenBridgeCreator.t.sol +++ b/test-foundry/L1AtomicTokenBridgeCreator.t.sol @@ -47,6 +47,7 @@ import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {MasterVaultFactory} from "contracts/tokenbridge/libraries/vault/MasterVaultFactory.sol"; contract L1AtomicTokenBridgeCreatorTest is Test { L1AtomicTokenBridgeCreator public l1Creator; @@ -131,7 +132,7 @@ contract L1AtomicTokenBridgeCreatorTest is Test { (address l1RouterAddress, address standardGatewayAddress,,,) = l1Creator.inboxToL1Deployment(address(inbox)); - (L1GatewayRouter routerTemplate,,,,,,,) = l1Creator.l1Templates(); + (L1GatewayRouter routerTemplate,,,,,,,,) = l1Creator.l1Templates(); address expectedL1RouterAddress = Create2.computeAddress( keccak256(abi.encodePacked(bytes("L1R"), address(inbox))), @@ -167,7 +168,7 @@ contract L1AtomicTokenBridgeCreatorTest is Test { (address l1RouterAddress, address l1StandardGatewayAddress,,,) = l1Creator.inboxToL1Deployment(address(inbox)); - (, L1ERC20Gateway standardGatewayTemplate,,,,,,) = l1Creator.l1Templates(); + (, L1ERC20Gateway standardGatewayTemplate,,,,,,,) = l1Creator.l1Templates(); address expectedL1StandardGatewayAddress = Create2.computeAddress( keccak256(abi.encodePacked(bytes("L1SGW"), address(inbox))), @@ -230,7 +231,7 @@ contract L1AtomicTokenBridgeCreatorTest is Test { (address l1RouterAddress,, address l1CustomGatewayAddress,,) = l1Creator.inboxToL1Deployment(address(inbox)); - (,, L1CustomGateway customGatewayTemplate,,,,,) = l1Creator.l1Templates(); + (,, L1CustomGateway customGatewayTemplate,,,,,,) = l1Creator.l1Templates(); address expectedL1CustomGatewayAddress = Create2.computeAddress( keccak256(abi.encodePacked(bytes("L1CGW"), address(inbox))), @@ -272,7 +273,7 @@ contract L1AtomicTokenBridgeCreatorTest is Test { (address l1RouterAddress,,, address l1WethGatewayAddress,) = l1Creator.inboxToL1Deployment(address(inbox)); - (,,, L1WethGateway wethGatewayTemplate,,,,) = l1Creator.l1Templates(); + (,,, L1WethGateway wethGatewayTemplate,,,,,) = l1Creator.l1Templates(); address expectedL1WethGatewayAddress = Create2.computeAddress( keccak256(abi.encodePacked(bytes("L1WGW"), address(inbox))), @@ -744,7 +745,8 @@ contract L1AtomicTokenBridgeCreatorTest is Test { new L1OrbitGatewayRouter(), new L1OrbitERC20Gateway(), new L1OrbitCustomGateway(), - new UpgradeExecutor() + new UpgradeExecutor(), + new MasterVaultFactory() ); vm.expectEmit(true, true, true, true); @@ -773,7 +775,8 @@ contract L1AtomicTokenBridgeCreatorTest is Test { L1OrbitGatewayRouter oRouter, L1OrbitERC20Gateway oGw, L1OrbitCustomGateway oCustomGw, - IUpgradeExecutor executor + IUpgradeExecutor executor, + MasterVaultFactory masterVaultFactory ) = l1Creator.l1Templates(); assertEq(address(router), address(_l1Templates.routerTemplate), "Wrong templates"); assertEq(address(gw), address(_l1Templates.standardGatewayTemplate), "Wrong templates"); @@ -787,6 +790,7 @@ contract L1AtomicTokenBridgeCreatorTest is Test { address(oCustomGw), address(_l1Templates.feeTokenBasedCustomGatewayTemplate), "Wrong gw" ); assertEq(address(executor), address(_l1Templates.upgradeExecutor), "Wrong executor"); + assertEq(address(masterVaultFactory), address(_l1Templates.masterVaultFactory), "Wrong masterVaultFactory"); assertEq( l1Creator.l2TokenBridgeFactoryTemplate(), @@ -820,7 +824,8 @@ contract L1AtomicTokenBridgeCreatorTest is Test { new L1OrbitGatewayRouter(), new L1OrbitERC20Gateway(), new L1OrbitCustomGateway(), - new UpgradeExecutor() + new UpgradeExecutor(), + new MasterVaultFactory() ); vm.expectRevert("Ownable: caller is not the owner"); @@ -849,7 +854,8 @@ contract L1AtomicTokenBridgeCreatorTest is Test { new L1OrbitGatewayRouter(), new L1OrbitERC20Gateway(), new L1OrbitCustomGateway(), - new UpgradeExecutor() + new UpgradeExecutor(), + new MasterVaultFactory() ); address originalL2Factory = makeAddr("originalL2Factory"); @@ -987,7 +993,8 @@ contract L1AtomicTokenBridgeCreatorTest is Test { new L1OrbitGatewayRouter(), new L1OrbitERC20Gateway(), new L1OrbitCustomGateway(), - new UpgradeExecutor() + new UpgradeExecutor(), + new MasterVaultFactory() ); vm.prank(deployer);