Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
83 changes: 51 additions & 32 deletions contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand All @@ -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();
Expand All @@ -215,22 +223,22 @@ 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;
L2DeploymentAddresses memory l2Deployment;

// 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);
Expand All @@ -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
);
}

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -312,15 +331,15 @@ 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
)
)
);

wethGateway.initialize(
l2Deployment.wethGateway, l1Deployment.router, inbox, l1Weth, l2Deployment.weth
l2Deployment.wethGateway, l1Deployment.router, args.inbox, l1Weth, l2Deployment.weth
);

l1Deployment.wethGateway = address(wethGateway);
Expand All @@ -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
);
}

Expand All @@ -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
Expand All @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ struct L1DeploymentAddresses {
address customGateway;
address wethGateway;
address weth;
address masterVaultFactory;
}

struct L2DeploymentAddresses {
Expand Down
29 changes: 26 additions & 3 deletions contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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);
}
}

/**
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 3 additions & 2 deletions contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion contracts/tokenbridge/ethereum/gateway/L1USDCGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions contracts/tokenbridge/libraries/vault/IMasterVault.sol
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading