diff --git a/.env-sample b/.env-sample index 9e3413c0b9..0fd2718b05 100644 --- a/.env-sample +++ b/.env-sample @@ -1,4 +1,13 @@ -## Goerli - Deploy L1TokenBridgeCreator -ARB_GOERLI_RPC="" -ARB_GOERLI_DEPLOYER_KEY="" -ORBIT_RPC="" \ No newline at end of file +## Rollup on top of which token bridge will be created +ROLLUP_ADDRESS="" +ROLLUP_OWNER="" +L1_TOKEN_BRIDGE_CREATOR="" +# needed for verification +L1_RETRYABLE_SENDER="" + +## RPC endpoints +BASECHAIN_RPC="" +ORBIT_RPC="" + +## Deployer key used for deploying creator and creating token bridge +BASECHAIN_DEPLOYER_KEY="" diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index ad682ff676..a4b78860c1 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -6,6 +6,67 @@ on: jobs: test-unit: + name: Test unit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Setup node/yarn + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + cache-dependency-path: '**/yarn.lock' + + - name: Install packages + run: yarn + + - name: Run tests + run: yarn test:unit + + test-only-doesnt-exist: + name: No .only + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Check if .only exists in integration test + run: fgrep .only -R test/ && exit 1 || exit 0 + + test-hardhat: + name: Test hardhat + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Setup node/yarn + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + cache-dependency-path: '**/yarn.lock' + + - name: Install packages + run: yarn + + - name: Compile contracts + run: yarn build + + - name: Run integration tests + run: yarn test + + test-storage: name: Test storage layout runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 6b2ee8afd5..ae5e00a019 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,20 @@ +.gitignore .env node_modules +.vscode/ #Hardhat files cache build artifacts deployment.json + +#Foundry files +out/ +forge-cache/ + +#Storage layout test files test/storage/*-old.dot + +# local deployment files network.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..e19917d6cd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,7 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/nitro-contracts"] + path = lib/nitro-contracts + url = git@github.com:OffchainLabs/nitro-contracts.git + branch = feature-orbit-bridge diff --git a/.solhint.json b/.solhint.json index b1167a3c90..d2c46a0f97 100644 --- a/.solhint.json +++ b/.solhint.json @@ -4,6 +4,6 @@ "rules": { "mark-callable-contracts": "none", "prettier/prettier": "error", - "compiler-version": ["error", "^0.6.11"] + "compiler-version": ["error", "^0.8.0"] } } diff --git a/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol b/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol index 7bd3d7d674..4859535812 100644 --- a/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol +++ b/contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol @@ -6,6 +6,7 @@ import {L2ERC20Gateway} from "./gateway/L2ERC20Gateway.sol"; import {L2CustomGateway} from "./gateway/L2CustomGateway.sol"; import {L2WethGateway} from "./gateway/L2WethGateway.sol"; import {StandardArbERC20} from "./StandardArbERC20.sol"; +import {IUpgradeExecutor} from "@offchainlabs/upgrade-executor/src/IUpgradeExecutor.sol"; import {BeaconProxyFactory} from "../libraries/ClonableBeaconProxy.sol"; import {aeWETH} from "../libraries/aeWETH.sol"; import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; @@ -32,25 +33,74 @@ contract L2AtomicTokenBridgeFactory { address l1WethGateway, address l1Weth, address l2StandardGatewayCanonicalAddress, - address rollupOwner + address rollupOwner, + address aliasedL1UpgradeExecutor ) external { - // create proxyAdmin which will be used for all contracts. Revert if canonical deployment already exists - address proxyAdminAddress = Create2.computeAddress( - _getL2Salt(OrbitSalts.L2_PROXY_ADMIN), keccak256(type(ProxyAdmin).creationCode), address(this) - ); - if (proxyAdminAddress.code.length > 0) { - revert L2AtomicTokenBridgeFactory_AlreadyExists(); + // Create proxyAdmin which will be used for all contracts. Revert if canonical deployment already exists + { + address proxyAdminAddress = Create2.computeAddress( + _getL2Salt(OrbitSalts.L2_PROXY_ADMIN), + keccak256(type(ProxyAdmin).creationCode), + address(this) + ); + if (proxyAdminAddress.code.length > 0) { + revert L2AtomicTokenBridgeFactory_AlreadyExists(); + } } - address proxyAdmin = address(new ProxyAdmin{ salt: _getL2Salt(OrbitSalts.L2_PROXY_ADMIN) }()); + address proxyAdmin = + address(new ProxyAdmin{ salt: _getL2Salt(OrbitSalts.L2_PROXY_ADMIN) }()); - // deploy router/gateways - address router = _deployRouter(l2Code.router, l1Router, l2StandardGatewayCanonicalAddress, proxyAdmin); - _deployStandardGateway(l2Code.standardGateway, l1StandardGateway, router, proxyAdmin); + // deploy router/gateways/executor + address upgradeExecutor = _deployUpgradeExecutor( + l2Code.upgradeExecutor, rollupOwner, proxyAdmin, aliasedL1UpgradeExecutor + ); + address router = + _deployRouter(l2Code.router, l1Router, l2StandardGatewayCanonicalAddress, proxyAdmin); + _deployStandardGateway( + l2Code.standardGateway, l1StandardGateway, router, proxyAdmin, upgradeExecutor + ); _deployCustomGateway(l2Code.customGateway, l1CustomGateway, router, proxyAdmin); - _deployWethGateway(l2Code.wethGateway, l2Code.aeWeth, l1WethGateway, l1Weth, router, proxyAdmin); - // transfer ownership to rollup's owner - ProxyAdmin(proxyAdmin).transferOwnership(rollupOwner); + // fee token based creator will provide address(0) as WETH is not used in ERC20-based chains + if (l1WethGateway != address(0)) { + _deployWethGateway( + l2Code.wethGateway, l2Code.aeWeth, l1WethGateway, l1Weth, router, proxyAdmin + ); + } + + // deploy multicall + Create2.deploy(0, _getL2Salt(OrbitSalts.L2_MULTICALL), _creationCodeFor(l2Code.multicall)); + + // transfer ownership to L2 upgradeExecutor + ProxyAdmin(proxyAdmin).transferOwnership(upgradeExecutor); + } + + function _deployUpgradeExecutor( + bytes calldata runtimeCode, + address rollupOwner, + address proxyAdmin, + address aliasedL1UpgradeExecutor + ) internal returns (address) { + // canonical L2 upgrade executor with dummy logic + address canonicalUpgradeExecutor = _deploySeedProxy( + proxyAdmin, _getL2Salt(OrbitSalts.L2_EXECUTOR), _getL2Salt(OrbitSalts.L2_EXECUTOR_LOGIC) + ); + + // create UpgradeExecutor logic and upgrade to it + address upExecutorLogic = Create2.deploy( + 0, _getL2Salt(OrbitSalts.L2_EXECUTOR_LOGIC), _creationCodeFor(runtimeCode) + ); + ProxyAdmin(proxyAdmin).upgrade( + ITransparentUpgradeableProxy(canonicalUpgradeExecutor), upExecutorLogic + ); + + // init upgrade executor + address[] memory executors = new address[](2); + executors[0] = rollupOwner; + executors[1] = aliasedL1UpgradeExecutor; + IUpgradeExecutor(canonicalUpgradeExecutor).initialize(canonicalUpgradeExecutor, executors); + + return canonicalUpgradeExecutor; } function _deployRouter( @@ -60,11 +110,13 @@ contract L2AtomicTokenBridgeFactory { address proxyAdmin ) internal returns (address) { // canonical L2 router with dummy logic - address canonicalRouter = - _deploySeedProxy(proxyAdmin, _getL2Salt(OrbitSalts.L2_ROUTER), _getL2Salt(OrbitSalts.L2_ROUTER_LOGIC)); + address canonicalRouter = _deploySeedProxy( + proxyAdmin, _getL2Salt(OrbitSalts.L2_ROUTER), _getL2Salt(OrbitSalts.L2_ROUTER_LOGIC) + ); // create L2 router logic and upgrade - address routerLogic = Create2.deploy(0, _getL2Salt(OrbitSalts.L2_ROUTER_LOGIC), _creationCodeFor(runtimeCode)); + address routerLogic = + Create2.deploy(0, _getL2Salt(OrbitSalts.L2_ROUTER_LOGIC), _creationCodeFor(runtimeCode)); ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(canonicalRouter), routerLogic); // init @@ -77,17 +129,23 @@ contract L2AtomicTokenBridgeFactory { bytes calldata runtimeCode, address l1StandardGateway, address router, - address proxyAdmin + address proxyAdmin, + address upgradeExecutor ) internal { // canonical L2 standard gateway with dummy logic address canonicalStdGateway = _deploySeedProxy( - proxyAdmin, _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY), _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY_LOGIC) + proxyAdmin, + _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY), + _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY_LOGIC) ); // create L2 standard gateway logic and upgrade - address stdGatewayLogic = - Create2.deploy(0, _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY_LOGIC), _creationCodeFor(runtimeCode)); - ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(canonicalStdGateway), stdGatewayLogic); + address stdGatewayLogic = Create2.deploy( + 0, _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY_LOGIC), _creationCodeFor(runtimeCode) + ); + ProxyAdmin(proxyAdmin).upgrade( + ITransparentUpgradeableProxy(canonicalStdGateway), stdGatewayLogic + ); // create beacon StandardArbERC20 standardArbERC20 = new StandardArbERC20{ @@ -102,7 +160,12 @@ contract L2AtomicTokenBridgeFactory { // init contracts beaconProxyFactory.initialize(address(beacon)); - L2ERC20Gateway(canonicalStdGateway).initialize(l1StandardGateway, router, address(beaconProxyFactory)); + L2ERC20Gateway(canonicalStdGateway).initialize( + l1StandardGateway, router, address(beaconProxyFactory) + ); + + // make L2 executor the beacon owner + beacon.transferOwnership(upgradeExecutor); } function _deployCustomGateway( @@ -113,13 +176,18 @@ contract L2AtomicTokenBridgeFactory { ) internal { // canonical L2 custom gateway with dummy logic address canonicalCustomGateway = _deploySeedProxy( - proxyAdmin, _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY), _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY_LOGIC) + proxyAdmin, + _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY), + _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY_LOGIC) ); // create L2 custom gateway logic and upgrade - address customGatewayLogicAddress = - Create2.deploy(0, _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY_LOGIC), _creationCodeFor(runtimeCode)); - ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(canonicalCustomGateway), customGatewayLogicAddress); + address customGatewayLogicAddress = Create2.deploy( + 0, _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY_LOGIC), _creationCodeFor(runtimeCode) + ); + ProxyAdmin(proxyAdmin).upgrade( + ITransparentUpgradeableProxy(canonicalCustomGateway), customGatewayLogicAddress + ); // init L2GatewayRouter(canonicalCustomGateway).initialize(l1CustomGateway, router); @@ -134,23 +202,32 @@ contract L2AtomicTokenBridgeFactory { address proxyAdmin ) internal { // canonical L2 WETH with dummy logic - address canonicalL2Weth = - _deploySeedProxy(proxyAdmin, _getL2Salt(OrbitSalts.L2_WETH), _getL2Salt(OrbitSalts.L2_WETH_LOGIC)); + address canonicalL2Weth = _deploySeedProxy( + proxyAdmin, _getL2Salt(OrbitSalts.L2_WETH), _getL2Salt(OrbitSalts.L2_WETH_LOGIC) + ); // create L2WETH logic and upgrade - address l2WethLogic = - Create2.deploy(0, _getL2Salt(OrbitSalts.L2_WETH_LOGIC), _creationCodeFor(aeWethRuntimeCode)); + address l2WethLogic = Create2.deploy( + 0, _getL2Salt(OrbitSalts.L2_WETH_LOGIC), _creationCodeFor(aeWethRuntimeCode) + ); ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(canonicalL2Weth), l2WethLogic); // canonical L2 WETH gateway with dummy logic address canonicalL2WethGateway = _deploySeedProxy( - proxyAdmin, _getL2Salt(OrbitSalts.L2_WETH_GATEWAY), _getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC) + proxyAdmin, + _getL2Salt(OrbitSalts.L2_WETH_GATEWAY), + _getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC) ); // create L2WETH gateway logic and upgrade - address l2WethGatewayLogic = - Create2.deploy(0, _getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC), _creationCodeFor(wethGatewayRuntimeCode)); - ProxyAdmin(proxyAdmin).upgrade(ITransparentUpgradeableProxy(canonicalL2WethGateway), l2WethGatewayLogic); + address l2WethGatewayLogic = Create2.deploy( + 0, + _getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC), + _creationCodeFor(wethGatewayRuntimeCode) + ); + ProxyAdmin(proxyAdmin).upgrade( + ITransparentUpgradeableProxy(canonicalL2WethGateway), l2WethGatewayLogic + ); // init gateway L2WethGateway(payable(canonicalL2WethGateway)).initialize( @@ -158,22 +235,27 @@ contract L2AtomicTokenBridgeFactory { ); // init L2Weth - aeWETH(payable(canonicalL2Weth)).initialize("WETH", "WETH", 18, canonicalL2WethGateway, l1Weth); + aeWETH(payable(canonicalL2Weth)).initialize( + "WETH", "WETH", 18, canonicalL2WethGateway, l1Weth + ); } /** - * In addition to hard-coded prefix, salt for L2 contracts depends on msg.sender. Deploying L2 token bridge contracts is - * permissionless. By making msg.sender part of the salt we know exactly which set of contracts is the "canonical" one, + * In addition to hard-coded prefix, salt for L2 contracts depends on msg.sender and the chainId. Deploying L2 token bridge contracts is + * permissionless. By making msg.sender part of the salt we know exactly which set of contracts is the "canonical" one for given chain, * deployed by L1TokenBridgeRetryableSender via retryable ticket. */ - function _getL2Salt(bytes32 prefix) internal view returns (bytes32) { - return keccak256(abi.encodePacked(prefix, msg.sender)); + function _getL2Salt(bytes memory prefix) internal view returns (bytes32) { + return keccak256(abi.encodePacked(prefix, block.chainid, msg.sender)); } /** * Deploys a proxy with empty logic contract in order to get deterministic address which does not depend on actual logic contract. */ - function _deploySeedProxy(address proxyAdmin, bytes32 proxySalt, bytes32 logicSalt) internal returns (address) { + function _deploySeedProxy(address proxyAdmin, bytes32 proxySalt, bytes32 logicSalt) + internal + returns (address) + { return address( new TransparentUpgradeableProxy{ salt: proxySalt }( address(new CanonicalAddressSeed{ salt: logicSalt}()), @@ -219,30 +301,35 @@ struct L2RuntimeCode { bytes customGateway; bytes wethGateway; bytes aeWeth; + bytes upgradeExecutor; + bytes multicall; } /** * Collection of salts used in CREATE2 deployment of L2 token bridge contracts. */ library OrbitSalts { - bytes32 public constant L1_PROXY_ADMIN = keccak256(bytes("OrbitL1ProxyAdmin")); - bytes32 public constant L1_ROUTER = keccak256(bytes("OrbitL1GatewayRouterProxy")); - bytes32 public constant L1_STANDARD_GATEWAY = keccak256(bytes("OrbitL1StandardGatewayProxy")); - bytes32 public constant L1_CUSTOM_GATEWAY = keccak256(bytes("OrbitL1CustomGatewayProxy")); - bytes32 public constant L1_WETH_GATEWAY = keccak256(bytes("OrbitL1WethGatewayProxy")); - - bytes32 public constant L2_PROXY_ADMIN = keccak256(bytes("OrbitL2ProxyAdmin")); - bytes32 public constant L2_ROUTER_LOGIC = keccak256(bytes("OrbitL2GatewayRouterLogic")); - bytes32 public constant L2_ROUTER = keccak256(bytes("OrbitL2GatewayRouterProxy")); - bytes32 public constant L2_STANDARD_GATEWAY_LOGIC = keccak256(bytes("OrbitL2StandardGatewayLogic")); - bytes32 public constant L2_STANDARD_GATEWAY = keccak256(bytes("OrbitL2StandardGatewayProxy")); - bytes32 public constant L2_CUSTOM_GATEWAY_LOGIC = keccak256(bytes("OrbitL2CustomGatewayLogic")); - bytes32 public constant L2_CUSTOM_GATEWAY = keccak256(bytes("OrbitL2CustomGatewayProxy")); - bytes32 public constant L2_WETH_GATEWAY_LOGIC = keccak256(bytes("OrbitL2WethGatewayLogic")); - bytes32 public constant L2_WETH_GATEWAY = keccak256(bytes("OrbitL2WethGatewayProxy")); - bytes32 public constant L2_WETH_LOGIC = keccak256(bytes("OrbitL2WETH")); - bytes32 public constant L2_WETH = keccak256(bytes("OrbitL2WETHProxy")); - bytes32 public constant L2_STANDARD_ERC20 = keccak256(bytes("OrbitStandardArbERC20")); - bytes32 public constant UPGRADEABLE_BEACON = keccak256(bytes("OrbitUpgradeableBeacon")); - bytes32 public constant BEACON_PROXY_FACTORY = keccak256(bytes("OrbitBeaconProxyFactory")); + bytes public constant L1_PROXY_ADMIN = bytes("OrbitL1ProxyAdmin"); + bytes public constant L1_ROUTER = bytes("OrbitL1GatewayRouterProxy"); + bytes public constant L1_STANDARD_GATEWAY = bytes("OrbitL1StandardGatewayProxy"); + bytes public constant L1_CUSTOM_GATEWAY = bytes("OrbitL1CustomGatewayProxy"); + bytes public constant L1_WETH_GATEWAY = bytes("OrbitL1WethGatewayProxy"); + + bytes public constant L2_PROXY_ADMIN = bytes("OrbitL2ProxyAdmin"); + bytes public constant L2_ROUTER_LOGIC = bytes("OrbitL2GatewayRouterLogic"); + bytes public constant L2_ROUTER = bytes("OrbitL2GatewayRouterProxy"); + bytes public constant L2_STANDARD_GATEWAY_LOGIC = bytes("OrbitL2StandardGatewayLogic"); + bytes public constant L2_STANDARD_GATEWAY = bytes("OrbitL2StandardGatewayProxy"); + bytes public constant L2_CUSTOM_GATEWAY_LOGIC = bytes("OrbitL2CustomGatewayLogic"); + bytes public constant L2_CUSTOM_GATEWAY = bytes("OrbitL2CustomGatewayProxy"); + bytes public constant L2_WETH_GATEWAY_LOGIC = bytes("OrbitL2WethGatewayLogic"); + bytes public constant L2_WETH_GATEWAY = bytes("OrbitL2WethGatewayProxy"); + bytes public constant L2_WETH_LOGIC = bytes("OrbitL2WETH"); + bytes public constant L2_WETH = bytes("OrbitL2WETHProxy"); + bytes public constant L2_STANDARD_ERC20 = bytes("OrbitStandardArbERC20"); + bytes public constant UPGRADEABLE_BEACON = bytes("OrbitUpgradeableBeacon"); + bytes public constant BEACON_PROXY_FACTORY = bytes("OrbitBeaconProxyFactory"); + bytes public constant L2_EXECUTOR_LOGIC = bytes("OrbitL2UpgradeExecutorLogic"); + bytes public constant L2_EXECUTOR = bytes("OrbitL2UpgradeExecutorProxy"); + bytes public constant L2_MULTICALL = bytes("OrbitL2Multicall"); } diff --git a/contracts/tokenbridge/ethereum/L1ArbitrumMessenger.sol b/contracts/tokenbridge/ethereum/L1ArbitrumMessenger.sol index d346defadf..f433a706a7 100644 --- a/contracts/tokenbridge/ethereum/L1ArbitrumMessenger.sol +++ b/contracts/tokenbridge/ethereum/L1ArbitrumMessenger.sol @@ -94,12 +94,14 @@ abstract contract L1ArbitrumMessenger { uint256 _gasPriceBid, bytes memory _data ) internal returns (uint256) { - uint256 seqNum = IInbox(_inbox).createRetryableTicket{ value: _l1CallValue }( + uint256 seqNum = _createRetryable( + _inbox, _to, + _refundTo, + _user, + _l1CallValue, _l2CallValue, _maxSubmissionCost, - _refundTo, // only refund excess fee to the custom address - _user, // user can cancel the retryable and receive call value refund _maxGas, _gasPriceBid, _data @@ -146,4 +148,57 @@ abstract contract L1ArbitrumMessenger { require(l2ToL1Sender != address(0), "NO_SENDER"); return l2ToL1Sender; } + + /** + * @notice Calls inbox to create retryable ticket. Default implementation is for standard Eth-based rollup, but it can be overriden to create retryable in ERC20-based rollup. + * @param _inbox address of the rollup's inbox + * @param _to destination L2 contract address + * @param _refundTo refund address for excess fee + * @param _user refund address for callvalue + * @param _totalFeeAmount amount of fees to pay, in Eth or native token, for retryable's execution + * @param _l2CallValue call value for retryable L2 message + * @param _maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee + * @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution + * @param _gasPriceBid price bid for L2 execution + * @param _data ABI encoded data of L2 message + * @return unique message number of the retryable transaction + */ + function _createRetryable( + address _inbox, + address _to, + address _refundTo, + address _user, + uint256 _totalFeeAmount, + uint256 _l2CallValue, + uint256 _maxSubmissionCost, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes memory _data + ) internal virtual returns (uint256) { + return + IInbox(_inbox).createRetryableTicket{ value: _totalFeeAmount }( + _to, + _l2CallValue, + _maxSubmissionCost, + _refundTo, + _user, + _maxGas, + _gasPriceBid, + _data + ); + } } + +interface IERC20Inbox { + function createRetryableTicket( + address to, + uint256 l2CallValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 tokenTotalFeeAmount, + bytes calldata data + ) external returns (uint256); +} \ No newline at end of file diff --git a/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol b/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol index c4dcd97fd9..18688745be 100644 --- a/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol +++ b/contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol @@ -3,15 +3,20 @@ pragma solidity ^0.8.4; import { L1TokenBridgeRetryableSender, - L1Addresses, + L1DeploymentAddresses, RetryableParams, - L2TemplateAddresses + L2TemplateAddresses, + IERC20Inbox, + IERC20, + SafeERC20 } from "./L1TokenBridgeRetryableSender.sol"; import {L1GatewayRouter} from "./gateway/L1GatewayRouter.sol"; import {L1ERC20Gateway} from "./gateway/L1ERC20Gateway.sol"; import {L1CustomGateway} from "./gateway/L1CustomGateway.sol"; import {L1WethGateway} from "./gateway/L1WethGateway.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {L1OrbitGatewayRouter} from "./gateway/L1OrbitGatewayRouter.sol"; +import {L1OrbitERC20Gateway} from "./gateway/L1OrbitERC20Gateway.sol"; +import {L1OrbitCustomGateway} from "./gateway/L1OrbitCustomGateway.sol"; import { L2AtomicTokenBridgeFactory, CanonicalAddressSeed, @@ -19,11 +24,25 @@ import { L2RuntimeCode, ProxyAdmin } from "../arbitrum/L2AtomicTokenBridgeFactory.sol"; +import {BytesLib} from "../libraries/BytesLib.sol"; +import { + IUpgradeExecutor, + UpgradeExecutor +} from "@offchainlabs/upgrade-executor/src/UpgradeExecutor.sol"; +import {AddressAliasHelper} from "../libraries/AddressAliasHelper.sol"; import {IInbox, IBridge, IOwnable} from "@arbitrum/nitro-contracts/src/bridge/IInbox.sol"; import {AddressAliasHelper} from "../libraries/AddressAliasHelper.sol"; -import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {ArbMulticall2} from "../../rpc-utils/MulticallV2.sol"; import {BeaconProxyFactory, ClonableBeaconProxy} from "../libraries/ClonableBeaconProxy.sol"; -import {Initializable, OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import { + Initializable, + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IAccessControlUpgradeable} from + "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; /** * @title Layer1 token bridge creator @@ -31,9 +50,13 @@ import {Initializable, OwnableUpgradeable} from "@openzeppelin/contracts-upgrade * @dev Throughout the contract terms L1 and L2 are used, but those can be considered as base (N) chain and child (N+1) chain */ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { + using SafeERC20 for IERC20; + error L1AtomicTokenBridgeCreator_OnlyRollupOwner(); error L1AtomicTokenBridgeCreator_InvalidRouterAddr(); error L1AtomicTokenBridgeCreator_TemplatesNotSet(); + error L1AtomicTokenBridgeCreator_RollupOwnershipMisconfig(); + error L1AtomicTokenBridgeCreator_ProxyAdminNotFound(); event OrbitTokenBridgeCreated( address indexed inbox, @@ -42,11 +65,23 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { address standardGateway, address customGateway, address wethGateway, - address proxyAdmin + address proxyAdmin, + address upgradeExecutor ); event OrbitTokenBridgeTemplatesUpdated(); event NonCanonicalRouterSet(address indexed inbox, address indexed router); + struct L1Templates { + L1GatewayRouter routerTemplate; + L1ERC20Gateway standardGatewayTemplate; + L1CustomGateway customGatewayTemplate; + L1WethGateway wethGatewayTemplate; + L1OrbitGatewayRouter feeTokenBasedRouterTemplate; + L1OrbitERC20Gateway feeTokenBasedStandardGatewayTemplate; + L1OrbitCustomGateway feeTokenBasedCustomGatewayTemplate; + IUpgradeExecutor upgradeExecutor; + } + // non-canonical router registry mapping(address => address) public inboxToNonCanonicalRouter; @@ -59,10 +94,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { L1TokenBridgeRetryableSender public retryableSender; // L1 logic contracts shared by all token bridges - L1GatewayRouter public routerTemplate; - L1ERC20Gateway public standardGatewayTemplate; - L1CustomGateway public customGatewayTemplate; - L1WethGateway public wethGatewayTemplate; + L1Templates public l1Templates; // L2 contracts deployed to L1 as bytecode placeholders address public l2TokenBridgeFactoryTemplate; @@ -71,46 +103,28 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { address public l2CustomGatewayTemplate; address public l2WethGatewayTemplate; address public l2WethTemplate; + address public l2MulticallTemplate; // WETH address on L1 address public l1Weth; - // immutable canonical addresses for L2 contracts + // immutable canonical address for L2 factory // other canonical addresses (dependent on L2 template implementations) can be fetched through `getCanonicalL2***Address` functions address public canonicalL2FactoryAddress; - address public canonicalL2ProxyAdminAddress; - address public canonicalL2BeaconProxyFactoryAddress; constructor() { _disableInitializers(); } - function initialize() public initializer { + function initialize(L1TokenBridgeRetryableSender _retryableSender) public initializer { __Ownable_init(); - // deploy retryableSender only once - its address will be part of salt for L2 contracts - if (address(retryableSender) == address(0)) { - retryableSender = L1TokenBridgeRetryableSender( - address( - new TransparentUpgradeableProxy( - address(new L1TokenBridgeRetryableSender()), - msg.sender, - bytes("") - ) - ) - ); - retryableSender.initialize(); - } + // store retryable sender and initialize it. This contract will be set as owner + retryableSender = _retryableSender; + retryableSender.initialize(); - canonicalL2FactoryAddress = _computeAddress(AddressAliasHelper.applyL1ToL2Alias(address(this)), 0); - canonicalL2ProxyAdminAddress = Create2.computeAddress( - _getL2Salt(OrbitSalts.L2_PROXY_ADMIN), keccak256(type(ProxyAdmin).creationCode), canonicalL2FactoryAddress - ); - canonicalL2BeaconProxyFactoryAddress = Create2.computeAddress( - _getL2Salt(OrbitSalts.BEACON_PROXY_FACTORY), - keccak256(type(BeaconProxyFactory).creationCode), - canonicalL2FactoryAddress - ); + canonicalL2FactoryAddress = + _computeAddress(AddressAliasHelper.applyL1ToL2Alias(address(this)), 0); } /** @@ -119,23 +133,18 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { * payload used to deploy contracts on L2 side. */ function setTemplates( - L1GatewayRouter _router, - L1ERC20Gateway _standardGateway, - L1CustomGateway _customGateway, - L1WethGateway _wethGatewayTemplate, + L1Templates calldata _l1Templates, address _l2TokenBridgeFactoryTemplate, address _l2RouterTemplate, address _l2StandardGatewayTemplate, address _l2CustomGatewayTemplate, address _l2WethGatewayTemplate, address _l2WethTemplate, + address _l2MulticallTemplate, address _l1Weth, uint256 _gasLimitForL2FactoryDeployment ) external onlyOwner { - routerTemplate = _router; - standardGatewayTemplate = _standardGateway; - customGatewayTemplate = _customGateway; - wethGatewayTemplate = _wethGatewayTemplate; + l1Templates = _l1Templates; l2TokenBridgeFactoryTemplate = _l2TokenBridgeFactoryTemplate; l2RouterTemplate = _l2RouterTemplate; @@ -143,6 +152,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { l2CustomGatewayTemplate = _l2CustomGatewayTemplate; l2WethGatewayTemplate = _l2WethGatewayTemplate; l2WethTemplate = _l2WethTemplate; + l2MulticallTemplate = _l2MulticallTemplate; l1Weth = _l1Weth; @@ -162,22 +172,61 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { * because L1 salts are already used at that point and L1 contracts are already deployed at canonical addresses * for that inbox. */ - function createTokenBridge(address inbox, uint256 maxGasForContracts, uint256 gasPriceBid) external payable { - if (address(routerTemplate) == address(0)) { + function createTokenBridge( + address inbox, + address rollupOwner, + uint256 maxGasForContracts, + uint256 gasPriceBid + ) external payable { + // templates have to be in place + if (address(l1Templates.routerTemplate) == address(0)) { revert L1AtomicTokenBridgeCreator_TemplatesNotSet(); } - // deploy L1 side of token bridge - address owner = _getRollupOwner(inbox); - (address router, address standardGateway, address customGateway, address wethGateway) = - _deployL1Contracts(inbox, owner); + // 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(); + if ( + !IAccessControlUpgradeable(upgradeExecutor).hasRole( + UpgradeExecutor(upgradeExecutor).EXECUTOR_ROLE(), rollupOwner + ) + ) { + revert L1AtomicTokenBridgeCreator_RollupOwnershipMisconfig(); + } + + uint256 rollupChainId = IRollupCore(address(IInbox(inbox).bridge().rollup())).chainId(); + + /// deploy L1 side of token bridge + bool isUsingFeeToken = _getFeeToken(inbox) != address(0); + L1DeploymentAddresses memory l1DeploymentAddresses = + _deployL1Contracts(inbox, rollupOwner, upgradeExecutor, isUsingFeeToken, rollupChainId); /// deploy factory and then L2 contracts through L2 factory, using 2 retryables calls - uint256 valueSpentForFactory = _deployL2Factory(inbox, gasPriceBid); - uint256 fundsRemaining = msg.value - valueSpentForFactory; - _deployL2Contracts( - router, standardGateway, customGateway, wethGateway, inbox, maxGasForContracts, gasPriceBid, fundsRemaining - ); + if (isUsingFeeToken) { + _deployL2Factory(inbox, gasPriceBid, isUsingFeeToken); + _deployL2ContractsUsingFeeToken( + l1DeploymentAddresses, + inbox, + maxGasForContracts, + gasPriceBid, + rollupOwner, + upgradeExecutor, + rollupChainId + ); + } else { + uint256 valueSpentForFactory = _deployL2Factory(inbox, gasPriceBid, isUsingFeeToken); + uint256 fundsRemaining = msg.value - valueSpentForFactory; + _deployL2ContractsUsingEth( + l1DeploymentAddresses, + inbox, + maxGasForContracts, + gasPriceBid, + fundsRemaining, + rollupOwner, + upgradeExecutor, + rollupChainId + ); + } } /** @@ -185,7 +234,7 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { * @dev Non-canonical router can be unregistered by re-setting it to address(0) - it makes canonical router the valid one. */ function setNonCanonicalRouter(address inbox, address nonCanonicalRouter) external { - if (msg.sender != _getRollupOwner(inbox)) { + if (msg.sender != IInbox(inbox).bridge().rollup().owner()) { revert L1AtomicTokenBridgeCreator_OnlyRollupOwner(); } if (nonCanonicalRouter == getCanonicalL1RouterAddress(inbox)) { @@ -206,175 +255,361 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { return getCanonicalL1RouterAddress(inbox); } - function _deployL1Contracts(address inbox, address owner) - internal - returns (address router, address standardGateway, address customGateway, address wethGateway) - { - address proxyAdmin = address(new ProxyAdmin{ salt: _getL1Salt(OrbitSalts.L1_PROXY_ADMIN, inbox) }()); + function _deployL1Contracts( + address inbox, + address rollupOwner, + address upgradeExecutor, + bool isUsingFeeToken, + uint256 chainId + ) internal returns (L1DeploymentAddresses memory l1Addresses) { + // get existing proxy admin and upgrade executor + address proxyAdmin = IInbox_ProxyAdmin(inbox).getProxyAdmin(); + if (proxyAdmin == address(0)) { + revert L1AtomicTokenBridgeCreator_ProxyAdminNotFound(); + } // deploy router - router = address( + address routerTemplate = isUsingFeeToken + ? address(l1Templates.feeTokenBasedRouterTemplate) + : address(l1Templates.routerTemplate); + l1Addresses.router = address( new TransparentUpgradeableProxy{ salt: _getL1Salt(OrbitSalts.L1_ROUTER, inbox) }( - address(routerTemplate), + routerTemplate, proxyAdmin, bytes("") ) ); // deploy and init gateways - standardGateway = _deployL1StandardGateway(proxyAdmin, router, inbox); - customGateway = _deployL1CustomGateway(proxyAdmin, router, inbox, owner); - wethGateway = _deployL1WethGateway(proxyAdmin, router, inbox); + l1Addresses.standardGateway = _deployL1StandardGateway( + proxyAdmin, l1Addresses.router, inbox, isUsingFeeToken, chainId + ); + l1Addresses.customGateway = _deployL1CustomGateway( + proxyAdmin, l1Addresses.router, inbox, upgradeExecutor, isUsingFeeToken, chainId + ); + l1Addresses.wethGateway = isUsingFeeToken + ? address(0) + : _deployL1WethGateway(proxyAdmin, l1Addresses.router, inbox, chainId); + l1Addresses.weth = isUsingFeeToken ? address(0) : l1Weth; // init router - L1GatewayRouter(router).initialize( - owner, address(standardGateway), address(0), getCanonicalL2RouterAddress(), inbox + L1GatewayRouter(l1Addresses.router).initialize( + upgradeExecutor, + l1Addresses.standardGateway, + address(0), + getCanonicalL2RouterAddress(chainId), + inbox ); - // transfer ownership to owner - ProxyAdmin(proxyAdmin).transferOwnership(owner); - // emit it - emit OrbitTokenBridgeCreated(inbox, owner, router, standardGateway, customGateway, wethGateway, proxyAdmin); + emit OrbitTokenBridgeCreated( + inbox, + rollupOwner, + l1Addresses.router, + l1Addresses.standardGateway, + l1Addresses.customGateway, + l1Addresses.wethGateway, + proxyAdmin, + upgradeExecutor + ); } - function _deployL1StandardGateway(address proxyAdmin, address router, address inbox) internal returns (address) { + function _deployL1StandardGateway( + address proxyAdmin, + address router, + address inbox, + bool isUsingFeeToken, + uint256 chainId + ) internal returns (address) { + address template = isUsingFeeToken + ? address(l1Templates.feeTokenBasedStandardGatewayTemplate) + : address(l1Templates.standardGatewayTemplate); + L1ERC20Gateway standardGateway = L1ERC20Gateway( address( new TransparentUpgradeableProxy{ salt: _getL1Salt(OrbitSalts.L1_STANDARD_GATEWAY, inbox) - }(address(standardGatewayTemplate), proxyAdmin, bytes("")) + }(template, proxyAdmin, bytes("")) ) ); standardGateway.initialize( - getCanonicalL2StandardGatewayAddress(), + getCanonicalL2StandardGatewayAddress(chainId), router, inbox, keccak256(type(ClonableBeaconProxy).creationCode), - canonicalL2BeaconProxyFactoryAddress + getCanonicalL2BeaconProxyFactoryAddress(chainId) ); return address(standardGateway); } - function _deployL1CustomGateway(address proxyAdmin, address router, address inbox, address owner) - internal - returns (address) - { + function _deployL1CustomGateway( + address proxyAdmin, + address router, + address inbox, + address upgradeExecutor, + bool isUsingFeeToken, + uint256 chainId + ) internal returns (address) { + address template = isUsingFeeToken + ? address(l1Templates.feeTokenBasedCustomGatewayTemplate) + : address(l1Templates.customGatewayTemplate); + L1CustomGateway customGateway = L1CustomGateway( address( new TransparentUpgradeableProxy{ salt: _getL1Salt(OrbitSalts.L1_CUSTOM_GATEWAY, inbox) - }(address(customGatewayTemplate), proxyAdmin, bytes("")) + }(template, proxyAdmin, bytes("")) ) ); - customGateway.initialize(getCanonicalL2CustomGatewayAddress(), router, inbox, owner); + customGateway.initialize( + getCanonicalL2CustomGatewayAddress(chainId), router, inbox, upgradeExecutor + ); return address(customGateway); } - function _deployL1WethGateway(address proxyAdmin, address router, address inbox) internal returns (address) { + function _deployL1WethGateway( + address proxyAdmin, + address router, + address inbox, + uint256 chainId + ) internal returns (address) { L1WethGateway wethGateway = L1WethGateway( payable( address( new TransparentUpgradeableProxy{ salt: _getL1Salt(OrbitSalts.L1_WETH_GATEWAY, inbox) - }(address(wethGatewayTemplate), proxyAdmin, bytes("")) + }(address(l1Templates.wethGatewayTemplate), proxyAdmin, bytes("")) ) ) ); - wethGateway.initialize(getCanonicalL2WethGatewayAddress(), router, inbox, l1Weth, getCanonicalL2WethAddress()); + wethGateway.initialize( + getCanonicalL2WethGatewayAddress(chainId), + router, + inbox, + l1Weth, + getCanonicalL2WethAddress(chainId) + ); return address(wethGateway); } - function _deployL2Factory(address inbox, uint256 gasPriceBid) internal returns (uint256) { + function _deployL2Factory(address inbox, uint256 gasPriceBid, bool isUsingFeeToken) + internal + returns (uint256) + { // encode L2 factory bytecode bytes memory deploymentData = _creationCodeFor(l2TokenBridgeFactoryTemplate.code); - uint256 maxSubmissionCost = IInbox(inbox).calculateRetryableSubmissionFee(deploymentData.length, 0); - uint256 value = maxSubmissionCost + gasLimitForL2FactoryDeployment * gasPriceBid; - IInbox(inbox).createRetryableTicket{value: value}( - address(0), - 0, - maxSubmissionCost, - msg.sender, + if (isUsingFeeToken) { + // transfer fee tokens to inbox to pay for 1st retryable + address feeToken = _getFeeToken(inbox); + uint256 retryableFee = gasLimitForL2FactoryDeployment * gasPriceBid; + IERC20(feeToken).safeTransferFrom(msg.sender, inbox, retryableFee); + + IERC20Inbox(inbox).createRetryableTicket( + address(0), + 0, + 0, + msg.sender, + msg.sender, + gasLimitForL2FactoryDeployment, + gasPriceBid, + retryableFee, + deploymentData + ); + return 0; + } else { + uint256 maxSubmissionCost = + IInbox(inbox).calculateRetryableSubmissionFee(deploymentData.length, 0); + uint256 retryableFee = maxSubmissionCost + gasLimitForL2FactoryDeployment * gasPriceBid; + + IInbox(inbox).createRetryableTicket{value: retryableFee}( + address(0), + 0, + maxSubmissionCost, + msg.sender, + msg.sender, + gasLimitForL2FactoryDeployment, + gasPriceBid, + deploymentData + ); + return retryableFee; + } + } + + function _deployL2ContractsUsingEth( + L1DeploymentAddresses memory l1Addresses, + address inbox, + uint256 maxGas, + uint256 gasPriceBid, + uint256 availableFunds, + address rollupOwner, + address upgradeExecutor, + uint256 chainId + ) internal { + retryableSender.sendRetryableUsingEth{value: availableFunds}( + RetryableParams( + inbox, canonicalL2FactoryAddress, msg.sender, msg.sender, maxGas, gasPriceBid + ), + L2TemplateAddresses( + l2RouterTemplate, + l2StandardGatewayTemplate, + l2CustomGatewayTemplate, + l2WethGatewayTemplate, + l2WethTemplate, + address(l1Templates.upgradeExecutor), + l2MulticallTemplate + ), + l1Addresses, + getCanonicalL2StandardGatewayAddress(chainId), + rollupOwner, msg.sender, - gasLimitForL2FactoryDeployment, - gasPriceBid, - deploymentData + AddressAliasHelper.applyL1ToL2Alias(upgradeExecutor) ); - - return value; } - function _deployL2Contracts( - address l1Router, - address l1StandardGateway, - address l1CustomGateway, - address l1WethGateway, + function _deployL2ContractsUsingFeeToken( + L1DeploymentAddresses memory l1Addresses, address inbox, uint256 maxGas, uint256 gasPriceBid, - uint256 availableFunds + address rollupOwner, + address upgradeExecutor, + uint256 chainId ) internal { - retryableSender.sendRetryable{value: availableFunds}( - RetryableParams(inbox, canonicalL2FactoryAddress, msg.sender, msg.sender, maxGas, gasPriceBid), + // transfer fee tokens to inbox to pay for 2nd retryable + address feeToken = _getFeeToken(inbox); + uint256 fee = maxGas * gasPriceBid; + IERC20(feeToken).safeTransferFrom(msg.sender, inbox, fee); + + retryableSender.sendRetryableUsingFeeToken( + RetryableParams( + inbox, canonicalL2FactoryAddress, msg.sender, msg.sender, maxGas, gasPriceBid + ), L2TemplateAddresses( l2RouterTemplate, l2StandardGatewayTemplate, l2CustomGatewayTemplate, - l2WethGatewayTemplate, - l2WethTemplate + address(0), + address(0), + address(l1Templates.upgradeExecutor), + l2MulticallTemplate ), - L1Addresses(l1Router, l1StandardGateway, l1CustomGateway, l1WethGateway, l1Weth), - getCanonicalL2StandardGatewayAddress(), - _getRollupOwner(inbox), - msg.sender + l1Addresses, + getCanonicalL2StandardGatewayAddress(chainId), + rollupOwner, + AddressAliasHelper.applyL1ToL2Alias(upgradeExecutor) ); } function getCanonicalL1RouterAddress(address inbox) public view returns (address) { address expectedL1ProxyAdminAddress = Create2.computeAddress( - _getL1Salt(OrbitSalts.L1_PROXY_ADMIN, inbox), keccak256(type(ProxyAdmin).creationCode), address(this) + _getL1Salt(OrbitSalts.L1_PROXY_ADMIN, inbox), + keccak256(type(ProxyAdmin).creationCode), + address(this) ); + bool isUsingFeeToken = _getFeeToken(inbox) != address(0); + address template = isUsingFeeToken + ? address(l1Templates.feeTokenBasedRouterTemplate) + : address(l1Templates.routerTemplate); + return Create2.computeAddress( _getL1Salt(OrbitSalts.L1_ROUTER, inbox), keccak256( abi.encodePacked( type(TransparentUpgradeableProxy).creationCode, - abi.encode(routerTemplate, expectedL1ProxyAdminAddress, bytes("")) + abi.encode(template, expectedL1ProxyAdminAddress, bytes("")) ) ), address(this) ); } - function getCanonicalL2RouterAddress() public view returns (address) { - return _getProxyAddress(_getL2Salt(OrbitSalts.L2_ROUTER_LOGIC), _getL2Salt(OrbitSalts.L2_ROUTER)); + function getCanonicalL2RouterAddress(uint256 chainId) public view returns (address) { + return _getProxyAddress( + _getL2Salt(OrbitSalts.L2_ROUTER_LOGIC, chainId), + _getL2Salt(OrbitSalts.L2_ROUTER, chainId), + chainId + ); } - function getCanonicalL2StandardGatewayAddress() public view returns (address) { + function getCanonicalL2StandardGatewayAddress(uint256 chainId) public view returns (address) { return _getProxyAddress( - _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY_LOGIC), _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY) + _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY_LOGIC, chainId), + _getL2Salt(OrbitSalts.L2_STANDARD_GATEWAY, chainId), + chainId ); } - function getCanonicalL2CustomGatewayAddress() public view returns (address) { - return - _getProxyAddress(_getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY_LOGIC), _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY)); + function getCanonicalL2CustomGatewayAddress(uint256 chainId) public view returns (address) { + return _getProxyAddress( + _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY_LOGIC, chainId), + _getL2Salt(OrbitSalts.L2_CUSTOM_GATEWAY, chainId), + chainId + ); } - function getCanonicalL2WethGatewayAddress() public view returns (address) { - return _getProxyAddress(_getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC), _getL2Salt(OrbitSalts.L2_WETH_GATEWAY)); + function getCanonicalL2WethGatewayAddress(uint256 chainId) public view returns (address) { + return _getProxyAddress( + _getL2Salt(OrbitSalts.L2_WETH_GATEWAY_LOGIC, chainId), + _getL2Salt(OrbitSalts.L2_WETH_GATEWAY, chainId), + chainId + ); } - function getCanonicalL2WethAddress() public view returns (address) { - return _getProxyAddress(_getL2Salt(OrbitSalts.L2_WETH_LOGIC), _getL2Salt(OrbitSalts.L2_WETH)); + function getCanonicalL2WethAddress(uint256 chainId) public view returns (address) { + return _getProxyAddress( + _getL2Salt(OrbitSalts.L2_WETH_LOGIC, chainId), + _getL2Salt(OrbitSalts.L2_WETH, chainId), + chainId + ); + } + + function getCanonicalL2ProxyAdminAddress(uint256 chainId) public view returns (address) { + return Create2.computeAddress( + _getL2Salt(OrbitSalts.L2_PROXY_ADMIN, chainId), + keccak256(type(ProxyAdmin).creationCode), + canonicalL2FactoryAddress + ); + } + + function getCanonicalL2BeaconProxyFactoryAddress(uint256 chainId) + public + view + returns (address) + { + return Create2.computeAddress( + _getL2Salt(OrbitSalts.BEACON_PROXY_FACTORY, chainId), + keccak256(type(BeaconProxyFactory).creationCode), + canonicalL2FactoryAddress + ); + } + + function getCanonicalL2UpgradeExecutorAddress(uint256 chainId) public view returns (address) { + return _getProxyAddress( + _getL2Salt(OrbitSalts.L2_EXECUTOR_LOGIC, chainId), + _getL2Salt(OrbitSalts.L2_EXECUTOR, chainId), + chainId + ); + } + + function _getFeeToken(address inbox) internal view returns (address) { + address bridge = address(IInbox(inbox).bridge()); + + (bool success, bytes memory feeTokenAddressData) = + bridge.staticcall(abi.encodeWithSelector(IERC20Bridge.nativeToken.selector)); + + if (!success || feeTokenAddressData.length < 32) { + return address(0); + } + + return BytesLib.toAddress(feeTokenAddressData, 12); } /** @@ -429,15 +664,15 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { return abi.encodePacked(hex"63", uint32(code.length), hex"80600E6000396000F3", code); } - function _getRollupOwner(address inbox) internal view returns (address) { - return IInbox(inbox).bridge().rollup().owner(); - } - /** * @notice L2 contracts are deployed as proxy with dummy seed logic contracts using CREATE2. That enables * us to upfront calculate the expected canonical addresses. */ - function _getProxyAddress(bytes32 logicSalt, bytes32 proxySalt) internal view returns (address) { + function _getProxyAddress(bytes32 logicSalt, bytes32 proxySalt, uint256 chainId) + internal + view + returns (address) + { address logicSeedAddress = Create2.computeAddress( logicSalt, keccak256(type(CanonicalAddressSeed).creationCode), canonicalL2FactoryAddress ); @@ -447,7 +682,9 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { keccak256( abi.encodePacked( type(TransparentUpgradeableProxy).creationCode, - abi.encode(logicSeedAddress, canonicalL2ProxyAdminAddress, bytes("")) + abi.encode( + logicSeedAddress, getCanonicalL2ProxyAdminAddress(chainId), bytes("") + ) ) ), canonicalL2FactoryAddress @@ -459,17 +696,33 @@ contract L1AtomicTokenBridgeCreator is Initializable, OwnableUpgradeable { * reason we make rollup's inbox address part of the salt. It prevents deploying more than one * token bridge. */ - function _getL1Salt(bytes32 prefix, address inbox) internal pure returns (bytes32) { + function _getL1Salt(bytes memory prefix, address inbox) internal pure returns (bytes32) { return keccak256(abi.encodePacked(prefix, inbox)); } /** - * @notice Salt for L2 token bridge contracts depends on the caller's address. Canonical token bridge + * @notice Salt for L2 token bridge contracts depends on the chainId and caller's address. Canonical token bridge * will be deployed by retryable ticket which is created by `retryableSender` contract. That * means `retryableSender`'s alias will be used on L2 side to calculate the salt for deploying - * L2 contracts (_getL2Salt function in L2AtomicTokenBridgeFactory). + * L2 contracts, in addition to chainId (_getL2Salt function in L2AtomicTokenBridgeFactory). */ - function _getL2Salt(bytes32 prefix) internal view returns (bytes32) { - return keccak256(abi.encodePacked(prefix, AddressAliasHelper.applyL1ToL2Alias(address(retryableSender)))); + function _getL2Salt(bytes memory prefix, uint256 chainId) internal view returns (bytes32) { + return keccak256( + abi.encodePacked( + prefix, chainId, AddressAliasHelper.applyL1ToL2Alias(address(retryableSender)) + ) + ); } } + +interface IERC20Bridge { + function nativeToken() external view returns (address); +} + +interface IInbox_ProxyAdmin { + function getProxyAdmin() external view returns (address); +} + +interface IRollupCore { + function chainId() external view returns (uint256); +} diff --git a/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol b/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol index cbdc010e2c..bde3de250b 100644 --- a/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol +++ b/contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol @@ -2,8 +2,20 @@ pragma solidity ^0.8.4; import {IInbox} from "@arbitrum/nitro-contracts/src/bridge/IInbox.sol"; -import {L2AtomicTokenBridgeFactory, L2RuntimeCode} from "../arbitrum/L2AtomicTokenBridgeFactory.sol"; -import {Initializable, OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { + L2AtomicTokenBridgeFactory, + L2RuntimeCode, + ProxyAdmin +} from "../arbitrum/L2AtomicTokenBridgeFactory.sol"; +import { + Initializable, + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {TransparentUpgradeableProxy} from + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title Token Bridge Retryable Ticket Sender @@ -26,13 +38,14 @@ contract L1TokenBridgeRetryableSender is Initializable, OwnableUpgradeable { * @dev Function will build retryable data, calculate submission cost and retryable value, create retryable * and then refund the remaining funds to original delpoyer. */ - function sendRetryable( + function sendRetryableUsingEth( RetryableParams calldata retryableParams, L2TemplateAddresses calldata l2, - L1Addresses calldata l1, + L1DeploymentAddresses calldata l1, address l2StandardGatewayAddress, address rollupOwner, - address deployer + address deployer, + address aliasedL1UpgradeExecutor ) external payable onlyOwner { bytes memory data = abi.encodeCall( L2AtomicTokenBridgeFactory.deployL2Contracts, @@ -42,7 +55,9 @@ contract L1TokenBridgeRetryableSender is Initializable, OwnableUpgradeable { l2.standardGatewayTemplate.code, l2.customGatewayTemplate.code, l2.wethGatewayTemplate.code, - l2.wethTemplate.code + l2.wethTemplate.code, + l2.upgradeExecutorTemplate.code, + l2.multicallTemplate.code ), l1.router, l1.standardGateway, @@ -50,13 +65,16 @@ contract L1TokenBridgeRetryableSender is Initializable, OwnableUpgradeable { l1.wethGateway, l1.weth, l2StandardGatewayAddress, - rollupOwner + rollupOwner, + aliasedL1UpgradeExecutor ) ); - uint256 maxSubmissionCost = IInbox(retryableParams.inbox).calculateRetryableSubmissionFee(data.length, 0); - uint256 retryableValue = maxSubmissionCost + retryableParams.maxGas * retryableParams.gasPriceBid; - _createRetryable(retryableParams, maxSubmissionCost, retryableValue, data); + uint256 maxSubmissionCost = + IInbox(retryableParams.inbox).calculateRetryableSubmissionFee(data.length, 0); + uint256 retryableValue = + maxSubmissionCost + retryableParams.maxGas * retryableParams.gasPriceBid; + _createRetryableUsingEth(retryableParams, maxSubmissionCost, retryableValue, data); // refund excess value to the deployer uint256 refund = msg.value - retryableValue; @@ -64,7 +82,48 @@ contract L1TokenBridgeRetryableSender is Initializable, OwnableUpgradeable { if (!success) revert L1TokenBridgeRetryableSender_RefundFailed(); } - function _createRetryable( + /** + * @notice Creates retryable which deploys L2 side of the token bridge. + * @dev Function will build retryable data, calculate submission cost and retryable value, create retryable + * and then refund the remaining funds to original delpoyer. + */ + function sendRetryableUsingFeeToken( + RetryableParams calldata retryableParams, + L2TemplateAddresses calldata l2, + L1DeploymentAddresses calldata l1, + address l2StandardGatewayAddress, + address rollupOwner, + address aliasedL1UpgradeExecutor + ) external payable onlyOwner { + bytes memory data = abi.encodeCall( + L2AtomicTokenBridgeFactory.deployL2Contracts, + ( + L2RuntimeCode( + l2.routerTemplate.code, + l2.standardGatewayTemplate.code, + l2.customGatewayTemplate.code, + "", + "", + l2.upgradeExecutorTemplate.code, + l2.multicallTemplate.code + ), + l1.router, + l1.standardGateway, + l1.customGateway, + address(0), + address(0), + l2StandardGatewayAddress, + rollupOwner, + aliasedL1UpgradeExecutor + ) + ); + + uint256 retryableFee = retryableParams.maxGas * retryableParams.gasPriceBid; + + _createRetryableUsingFeeToken(retryableParams, retryableFee, data); + } + + function _createRetryableUsingEth( RetryableParams calldata retryableParams, uint256 maxSubmissionCost, uint256 value, @@ -81,6 +140,24 @@ contract L1TokenBridgeRetryableSender is Initializable, OwnableUpgradeable { data ); } + + function _createRetryableUsingFeeToken( + RetryableParams calldata retryableParams, + uint256 retryableFee, + bytes memory data + ) internal { + IERC20Inbox(retryableParams.inbox).createRetryableTicket( + retryableParams.target, + 0, + 0, + retryableParams.excessFeeRefundAddress, + retryableParams.callValueRefundAddress, + retryableParams.maxGas, + retryableParams.gasPriceBid, + retryableFee, + data + ); + } } /** @@ -104,15 +181,31 @@ struct L2TemplateAddresses { address customGatewayTemplate; address wethGatewayTemplate; address wethTemplate; + address upgradeExecutorTemplate; + address multicallTemplate; } /** * L1 side of token bridge addresses */ -struct L1Addresses { +struct L1DeploymentAddresses { address router; address standardGateway; address customGateway; address wethGateway; address weth; } + +interface IERC20Inbox { + function createRetryableTicket( + address to, + uint256 l2CallValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 tokenTotalFeeAmount, + bytes calldata data + ) external returns (uint256); +} diff --git a/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol index 4c1047f0ff..8ac88717a4 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol @@ -252,6 +252,7 @@ abstract contract L1ArbitrumGateway is bytes memory extraData; { uint256 _maxSubmissionCost; + uint256 tokenTotalFeeAmount; if (super.isRouter(msg.sender)) { // router encoded (_from, extraData) = GatewayMessageHandler.parseFromRouterToGateway(_data); @@ -259,8 +260,9 @@ abstract contract L1ArbitrumGateway is _from = msg.sender; extraData = _data; } - // user encoded - (_maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); + // unpack user encoded data + (_maxSubmissionCost, extraData, tokenTotalFeeAmount) = _parseUserEncodedData(extraData); + // the inboundEscrowAndCall functionality has been disabled, so no data is allowed require(extraData.length == 0, "EXTRA_DATA_DISABLED"); @@ -273,13 +275,14 @@ abstract contract L1ArbitrumGateway is // we override the res field to save on the stack res = getOutboundCalldata(_l1Token, _from, _to, _amount, extraData); - seqNum = createOutboundTxCustomRefund( + seqNum = _initiateDeposit( _refundTo, _from, _amount, _maxGas, _gasPriceBid, _maxSubmissionCost, + tokenTotalFeeAmount, res ); } @@ -338,4 +341,64 @@ abstract contract L1ArbitrumGateway is interfaceId == this.outboundTransferCustomRefund.selector || super.supportsInterface(interfaceId); } + + /** + * @notice Parse data that was encoded by user and passed into the outbound TX entrypoint + * @dev In case of standard ETH-based rollup, format of encoded data is expected to be: + * - maxSubmissionCost (uint256) + * - callHookData (bytes) + * In case of ERC20-based rollup, format of encoded data is expected to be: + * - maxSubmissionCost (uint256) + * - tokenTotalFeeAmount (uint256) + * - callHookData (bytes) + * @param data data encoded by user + * @return maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee + * @return callHookData Calldata for extra call in inboundEscrowAndCall on L2 + * @return tokenTotalFeeAmount Amount of fees to be deposited in native token to cover for retryable ticket cost (used only in ERC20-based rollups, otherwise 0) + */ + function _parseUserEncodedData(bytes memory data) + internal + pure + virtual + returns ( + uint256 maxSubmissionCost, + bytes memory callHookData, + uint256 tokenTotalFeeAmount + ) + { + (maxSubmissionCost, callHookData) = abi.decode(data, (uint256, bytes)); + } + + /** + * @notice Intermediate internal function that passes on parameters needed to trigger creation of retryable ticket. + * @param _refundTo Account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2 + * @param _from Initiator of deposit + * @param _amount Token amount being deposited + * @param _maxGas Max gas deducted from user's L2 balance to cover L2 execution + * @param _gasPriceBid Gas price for L2 execution + * @param _maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee + * @param _data encoded data from router and user + * @return res abi encoded inbox sequence number + */ + function _initiateDeposit( + address _refundTo, + address _from, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256, // tokenTotalFeeAmount - amount of fees to be deposited in native token to cover for retryable ticket cost (used only in ERC20-based rollups) + bytes memory _data + ) internal virtual returns (uint256) { + return + createOutboundTxCustomRefund( + _refundTo, + _from, + _amount, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + _data + ); + } } diff --git a/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol index ea06568091..f5c13c82ec 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol @@ -113,7 +113,12 @@ contract L1CustomGateway is L1ArbitrumExtendedGateway, ICustomGateway { * @param l1ERC20 address of L1 token * @return L2 address of a bridged ERC20 token */ - function calculateL2TokenAddress(address l1ERC20) public view override(ITokenGateway, TokenGateway) returns (address) { + function calculateL2TokenAddress(address l1ERC20) + public + view + override(ITokenGateway, TokenGateway) + returns (address) + { return l1ToL2Token[l1ERC20]; } @@ -130,7 +135,7 @@ contract L1CustomGateway is L1ArbitrumExtendedGateway, ICustomGateway { uint256 _maxGas, uint256 _gasPriceBid, uint256 _maxSubmissionCost - ) external payable returns (uint256) { + ) external payable virtual returns (uint256) { return registerTokenToL2(_l2Address, _maxGas, _gasPriceBid, _maxSubmissionCost, msg.sender); } @@ -150,15 +155,36 @@ contract L1CustomGateway is L1ArbitrumExtendedGateway, ICustomGateway { uint256 _maxSubmissionCost, address _creditBackAddress ) public payable virtual returns (uint256) { - require( - ArbitrumEnabledToken(msg.sender).isArbitrumEnabled() == uint8(0xb1), - "NOT_ARB_ENABLED" - ); + return + _registerTokenToL2( + _l2Address, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + _creditBackAddress, + msg.value + ); + } + + function _registerTokenToL2( + address _l2Address, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + address _creditBackAddress, + uint256 _feeAmount + ) internal returns (uint256) { + { + require( + ArbitrumEnabledToken(msg.sender).isArbitrumEnabled() == uint8(0xb1), + "NOT_ARB_ENABLED" + ); - address currL2Addr = l1ToL2Token[msg.sender]; - if (currL2Addr != address(0)) { - // if token is already set, don't allow it to set a different L2 address - require(currL2Addr == _l2Address, "NO_UPDATE_TO_DIFFERENT_ADDR"); + address currL2Addr = l1ToL2Token[msg.sender]; + if (currL2Addr != address(0)) { + // if token is already set, don't allow it to set a different L2 address + require(currL2Addr == _l2Address, "NO_UPDATE_TO_DIFFERENT_ADDR"); + } } l1ToL2Token[msg.sender] = _l2Address; @@ -181,7 +207,7 @@ contract L1CustomGateway is L1ArbitrumExtendedGateway, ICustomGateway { inbox, counterpartGateway, _creditBackAddress, - msg.value, + _feeAmount, 0, _maxSubmissionCost, _maxGas, @@ -211,7 +237,26 @@ contract L1CustomGateway is L1ArbitrumExtendedGateway, ICustomGateway { uint256 _maxGas, uint256 _gasPriceBid, uint256 _maxSubmissionCost - ) external payable onlyOwner returns (uint256) { + ) external payable virtual onlyOwner returns (uint256) { + return + _forceRegisterTokenToL2( + _l1Addresses, + _l2Addresses, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + msg.value + ); + } + + function _forceRegisterTokenToL2( + address[] calldata _l1Addresses, + address[] calldata _l2Addresses, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _feeAmount + ) internal returns (uint256) { require(_l1Addresses.length == _l2Addresses.length, "INVALID_LENGTHS"); for (uint256 i = 0; i < _l1Addresses.length; i++) { @@ -232,7 +277,7 @@ contract L1CustomGateway is L1ArbitrumExtendedGateway, ICustomGateway { inbox, counterpartGateway, msg.sender, - msg.value, + _feeAmount, 0, _maxSubmissionCost, _maxGas, diff --git a/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol b/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol index 6c4647cb95..bbf48bab47 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol @@ -62,7 +62,7 @@ contract L1ERC20Gateway is L1ArbitrumExtendedGateway { uint256 _maxGas, uint256 _gasPriceBid, bytes calldata _data - ) public payable override nonReentrant returns (bytes memory res) { + ) public payable virtual override nonReentrant returns (bytes memory res) { return super.outboundTransferCustomRefund( _l1Token, @@ -149,7 +149,12 @@ contract L1ERC20Gateway is L1ArbitrumExtendedGateway { return outboundCalldata; } - function calculateL2TokenAddress(address l1ERC20) public view override(ITokenGateway, TokenGateway) returns (address) { + function calculateL2TokenAddress(address l1ERC20) + public + view + override(ITokenGateway, TokenGateway) + returns (address) + { bytes32 salt = getSalt(l1ERC20); return Create2.computeAddress(salt, cloneableProxyHash, l2BeaconProxyFactory); } diff --git a/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol b/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol index 276c8423e1..f5aef49d97 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol @@ -67,7 +67,24 @@ contract L1GatewayRouter is uint256 _maxGas, uint256 _gasPriceBid, uint256 _maxSubmissionCost - ) external payable onlyOwner returns (uint256) { + ) external payable virtual onlyOwner returns (uint256) { + return + _setDefaultGateway( + newL1DefaultGateway, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + msg.value + ); + } + + function _setDefaultGateway( + address newL1DefaultGateway, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 feeAmount + ) internal returns (uint256) { defaultGateway = newL1DefaultGateway; emit DefaultGatewayUpdated(newL1DefaultGateway); @@ -88,7 +105,7 @@ contract L1GatewayRouter is inbox, counterpartGateway, msg.sender, - msg.value, + feeAmount, 0, L2GasParams({ _maxSubmissionCost: _maxSubmissionCost, @@ -105,54 +122,6 @@ contract L1GatewayRouter is owner = newOwner; } - function _setGateways( - address[] memory _token, - address[] memory _gateway, - uint256 _maxGas, - uint256 _gasPriceBid, - uint256 _maxSubmissionCost, - address _creditBackAddress - ) internal returns (uint256) { - require(_token.length == _gateway.length, "WRONG_LENGTH"); - - for (uint256 i = 0; i < _token.length; i++) { - l1TokenToGateway[_token[i]] = _gateway[i]; - emit GatewaySet(_token[i], _gateway[i]); - // overwrite memory so the L2 router receives the L2 address of each gateway - if (_gateway[i] != address(0) && _gateway[i] != DISABLED) { - // if we are assigning a gateway to the token, the address oracle of the gateway - // must return something other than the 0 address - // this check helps avoid misconfiguring gateways - require( - TokenGateway(_gateway[i]).calculateL2TokenAddress(_token[i]) != address(0), - "TOKEN_NOT_HANDLED_BY_GATEWAY" - ); - _gateway[i] = TokenGateway(_gateway[i]).counterpartGateway(); - } - } - - bytes memory data = abi.encodeWithSelector( - L2GatewayRouter.setGateway.selector, - _token, - _gateway - ); - - return - sendTxToL2( - inbox, - counterpartGateway, - _creditBackAddress, - msg.value, - 0, - L2GasParams({ - _maxSubmissionCost: _maxSubmissionCost, - _maxGas: _maxGas, - _gasPriceBid: _gasPriceBid - }), - data - ); - } - /** * @notice Allows L1 Token contract to trustlessly register its gateway. (other setGateway method allows excess eth recovery from _maxSubmissionCost and is recommended) * @param _gateway l1 gateway address @@ -166,7 +135,7 @@ contract L1GatewayRouter is uint256 _maxGas, uint256 _gasPriceBid, uint256 _maxSubmissionCost - ) external payable override returns (uint256) { + ) external payable virtual override returns (uint256) { return setGateway(_gateway, _maxGas, _gasPriceBid, _maxSubmissionCost, msg.sender); } @@ -185,7 +154,26 @@ contract L1GatewayRouter is uint256 _gasPriceBid, uint256 _maxSubmissionCost, address _creditBackAddress - ) public payable override returns (uint256) { + ) public payable virtual override returns (uint256) { + return + _setGatewayWithCreditBack( + _gateway, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + _creditBackAddress, + msg.value + ); + } + + function _setGatewayWithCreditBack( + address _gateway, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + address _creditBackAddress, + uint256 feeAmount + ) internal returns (uint256) { require( ArbitrumEnabledToken(msg.sender).isArbitrumEnabled() == uint8(0xb1), "NOT_ARB_ENABLED" @@ -212,7 +200,8 @@ contract L1GatewayRouter is _maxGas, _gasPriceBid, _maxSubmissionCost, - _creditBackAddress + _creditBackAddress, + feeAmount ); } @@ -222,11 +211,68 @@ contract L1GatewayRouter is uint256 _maxGas, uint256 _gasPriceBid, uint256 _maxSubmissionCost - ) external payable onlyOwner returns (uint256) { + ) external payable virtual onlyOwner returns (uint256) { // it is assumed that token and gateway are both contracts // require(_token[i].isContract() && _gateway[i].isContract(), "NOT_CONTRACT"); return - _setGateways(_token, _gateway, _maxGas, _gasPriceBid, _maxSubmissionCost, msg.sender); + _setGateways( + _token, + _gateway, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + msg.sender, + msg.value + ); + } + + function _setGateways( + address[] memory _token, + address[] memory _gateway, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + address _creditBackAddress, + uint256 feeAmount + ) internal returns (uint256) { + require(_token.length == _gateway.length, "WRONG_LENGTH"); + + for (uint256 i = 0; i < _token.length; i++) { + l1TokenToGateway[_token[i]] = _gateway[i]; + emit GatewaySet(_token[i], _gateway[i]); + // overwrite memory so the L2 router receives the L2 address of each gateway + if (_gateway[i] != address(0) && _gateway[i] != DISABLED) { + // if we are assigning a gateway to the token, the address oracle of the gateway + // must return something other than the 0 address + // this check helps avoid misconfiguring gateways + require( + TokenGateway(_gateway[i]).calculateL2TokenAddress(_token[i]) != address(0), + "TOKEN_NOT_HANDLED_BY_GATEWAY" + ); + _gateway[i] = TokenGateway(_gateway[i]).counterpartGateway(); + } + } + + bytes memory data = abi.encodeWithSelector( + L2GatewayRouter.setGateway.selector, + _token, + _gateway + ); + + return + sendTxToL2( + inbox, + counterpartGateway, + _creditBackAddress, + feeAmount, + 0, + L2GasParams({ + _maxSubmissionCost: _maxSubmissionCost, + _maxGas: _maxGas, + _gasPriceBid: _gasPriceBid + }), + data + ); } function outboundTransfer( diff --git a/contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol new file mode 100644 index 0000000000..a1851d886b --- /dev/null +++ b/contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1CustomGateway } from "./L1CustomGateway.sol"; +import { IERC20Inbox } from "../L1ArbitrumMessenger.sol"; +import { IERC20Bridge } from "../../libraries/IERC20Bridge.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title Gateway for "custom" bridging functionality in an ERC20-based rollup. + * @notice Adds new entrypoints that have `_feeAmount` as parameter, while entrypoints without that parameter are reverted. + */ +contract L1OrbitCustomGateway is L1CustomGateway { + using SafeERC20 for IERC20; + + /** + * @notice Allows L1 Token contract to trustlessly register its custom L2 counterpart, in an ERC20-based rollup. Retryable costs are paid in native token. + * @param _l2Address counterpart address of L1 token + * @param _maxGas max gas for L2 retryable execution + * @param _gasPriceBid gas price for L2 retryable ticket + * @param _maxSubmissionCost base submission cost for L2 retryable ticket + * @param _feeAmount total amount of fees in native token to cover for retryable ticket costs. This amount will be transferred from user to bridge. + * @return Retryable ticket ID + */ + function registerTokenToL2( + address _l2Address, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _feeAmount + ) external returns (uint256) { + return + registerTokenToL2( + _l2Address, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + msg.sender, + _feeAmount + ); + } + + /** + * @notice Allows L1 Token contract to trustlessly register its custom L2 counterpart, in an ERC20-based rollup. Retryable costs are paid in native token. + * @param _l2Address counterpart address of L1 token + * @param _maxGas max gas for L2 retryable execution + * @param _gasPriceBid gas price for L2 retryable ticket + * @param _maxSubmissionCost base submission cost for L2 retryable ticket + * @param _creditBackAddress address for crediting back overpayment of _maxSubmissionCost + * @param _feeAmount total amount of fees in native token to cover for retryable ticket costs. This amount will be transferred from user to bridge. + * @return Retryable ticket ID + */ + function registerTokenToL2( + address _l2Address, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + address _creditBackAddress, + uint256 _feeAmount + ) public returns (uint256) { + return + _registerTokenToL2( + _l2Address, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + _creditBackAddress, + _feeAmount + ); + } + + /** + * @notice Allows owner to force register a custom L1/L2 token pair. + * @dev _l1Addresses[i] counterpart is assumed to be _l2Addresses[i] + * @param _l1Addresses array of L1 addresses + * @param _l2Addresses array of L2 addresses + * @param _maxGas max gas for L2 retryable execution + * @param _gasPriceBid gas price for L2 retryable ticket + * @param _maxSubmissionCost base submission cost for L2 retryable ticket + * @param _feeAmount total amount of fees in native token to cover for retryable ticket costs. This amount will be transferred from user to bridge. + * @return Retryable ticket ID + */ + function forceRegisterTokenToL2( + address[] calldata _l1Addresses, + address[] calldata _l2Addresses, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _feeAmount + ) external onlyOwner returns (uint256) { + return + _forceRegisterTokenToL2( + _l1Addresses, + _l2Addresses, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + _feeAmount + ); + } + + /** + * @notice Revert 'registerTokenToL2' entrypoint which doesn't have total amount of token fees as an argument. + */ + function registerTokenToL2( + address, + uint256, + uint256, + uint256, + address + ) public payable override returns (uint256) { + revert("NOT_SUPPORTED_IN_ORBIT"); + } + + /** + * @notice Revert 'registerTokenToL2' entrypoint which doesn't have total amount of token fees as an argument. + */ + function registerTokenToL2( + address, + uint256, + uint256, + uint256 + ) external payable override returns (uint256) { + revert("NOT_SUPPORTED_IN_ORBIT"); + } + + /** + * @notice Revert 'forceRegisterTokenToL2' entrypoint which doesn't have total amount of token fees as an argument. + */ + function forceRegisterTokenToL2( + address[] calldata, + address[] calldata, + uint256, + uint256, + uint256 + ) external payable override onlyOwner returns (uint256) { + revert("NOT_SUPPORTED_IN_ORBIT"); + } + + function _parseUserEncodedData(bytes memory data) + internal + pure + override + returns ( + uint256 maxSubmissionCost, + bytes memory callHookData, + uint256 tokenTotalFeeAmount + ) + { + (maxSubmissionCost, callHookData, tokenTotalFeeAmount) = abi.decode( + data, + (uint256, bytes, uint256) + ); + } + + function _initiateDeposit( + address _refundTo, + address _from, + uint256, // _amount, this info is already contained in _data + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 tokenTotalFeeAmount, + bytes memory _data + ) internal override returns (uint256) { + return + sendTxToL2CustomRefund( + inbox, + counterpartGateway, + _refundTo, + _from, + tokenTotalFeeAmount, + 0, + L2GasParams({ + _maxSubmissionCost: _maxSubmissionCost, + _maxGas: _maxGas, + _gasPriceBid: _gasPriceBid + }), + _data + ); + } + + function _createRetryable( + address _inbox, + address _to, + address _refundTo, + address _user, + uint256 _totalFeeAmount, + uint256 _l2CallValue, + uint256 _maxSubmissionCost, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes memory _data + ) internal override returns (uint256) { + { + // Transfer native token amount needed to pay for retryable fees to the inbox. + // Fee tokens will be transferred from user who initiated the action - that's `_user` account in + // case call was routed by router, or msg.sender in case gateway's entrypoint was called directly. + address nativeFeeToken = IERC20Bridge(address(getBridge(_inbox))).nativeToken(); + uint256 inboxNativeTokenBalance = IERC20(nativeFeeToken).balanceOf(_inbox); + if (inboxNativeTokenBalance < _totalFeeAmount) { + address transferFrom = isRouter(msg.sender) ? _user : msg.sender; + IERC20(nativeFeeToken).safeTransferFrom( + transferFrom, + _inbox, + _totalFeeAmount - inboxNativeTokenBalance + ); + } + } + + return + IERC20Inbox(_inbox).createRetryableTicket( + _to, + _l2CallValue, + _maxSubmissionCost, + _refundTo, + _user, + _maxGas, + _gasPriceBid, + _totalFeeAmount, + _data + ); + } +} diff --git a/contracts/tokenbridge/ethereum/gateway/L1OrbitERC20Gateway.sol b/contracts/tokenbridge/ethereum/gateway/L1OrbitERC20Gateway.sol new file mode 100644 index 0000000000..ca272641ce --- /dev/null +++ b/contracts/tokenbridge/ethereum/gateway/L1OrbitERC20Gateway.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1ERC20Gateway, IERC20 } from "./L1ERC20Gateway.sol"; +import { IERC20Inbox } from "../L1ArbitrumMessenger.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20Bridge } from "../../libraries/IERC20Bridge.sol"; + +/** + * @title Layer 1 Gateway contract for bridging standard ERC20s in ERC20-based rollup + * @notice This contract handles token deposits, holds the escrowed tokens on layer 1, and (ultimately) finalizes withdrawals. + * @dev Any ERC20 that requires non-standard functionality should use a separate gateway. + * Messages to layer 2 use the inbox's createRetryableTicket method. + */ +contract L1OrbitERC20Gateway is L1ERC20Gateway { + using SafeERC20 for IERC20; + + function outboundTransferCustomRefund( + address _l1Token, + address _refundTo, + address _to, + uint256 _amount, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes calldata _data + ) public payable override returns (bytes memory res) { + // fees are paid in native token, so there is no use for ether + require(msg.value == 0, "NO_VALUE"); + + // We don't allow bridging of native token to avoid having multiple representations of it + // on child chain. Native token can be bridged directly through inbox using depositERC20(). + require(_l1Token != _getNativeFeeToken(), "NOT_ALLOWED_TO_BRIDGE_FEE_TOKEN"); + + return + super.outboundTransferCustomRefund( + _l1Token, + _refundTo, + _to, + _amount, + _maxGas, + _gasPriceBid, + _data + ); + } + + function _parseUserEncodedData(bytes memory data) + internal + pure + override + returns ( + uint256 maxSubmissionCost, + bytes memory callHookData, + uint256 tokenTotalFeeAmount + ) + { + (maxSubmissionCost, callHookData, tokenTotalFeeAmount) = abi.decode( + data, + (uint256, bytes, uint256) + ); + } + + function _initiateDeposit( + address _refundTo, + address _from, + uint256, // _amount, this info is already contained in _data + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 tokenTotalFeeAmount, + bytes memory _data + ) internal override returns (uint256) { + return + sendTxToL2CustomRefund( + inbox, + counterpartGateway, + _refundTo, + _from, + tokenTotalFeeAmount, + 0, + L2GasParams({ + _maxSubmissionCost: _maxSubmissionCost, + _maxGas: _maxGas, + _gasPriceBid: _gasPriceBid + }), + _data + ); + } + + function _createRetryable( + address _inbox, + address _to, + address _refundTo, + address _user, + uint256 _totalFeeAmount, + uint256 _l2CallValue, + uint256 _maxSubmissionCost, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes memory _data + ) internal override returns (uint256) { + { + // Transfer native token amount needed to pay for retryable fees to the inbox. + // Fee tokens will be transferred from user who initiated the action - that's `_user` account in + // case call was routed by router, or msg.sender in case gateway's entrypoint was called directly. + address nativeFeeToken = _getNativeFeeToken(); + uint256 inboxNativeTokenBalance = IERC20(nativeFeeToken).balanceOf(_inbox); + if (inboxNativeTokenBalance < _totalFeeAmount) { + address transferFrom = isRouter(msg.sender) ? _user : msg.sender; + IERC20(nativeFeeToken).safeTransferFrom( + transferFrom, + _inbox, + _totalFeeAmount - inboxNativeTokenBalance + ); + } + } + + return + IERC20Inbox(_inbox).createRetryableTicket( + _to, + _l2CallValue, + _maxSubmissionCost, + _refundTo, + _user, + _maxGas, + _gasPriceBid, + _totalFeeAmount, + _data + ); + } + + /** + * @notice get rollup's native token that's used to pay for fees + */ + function _getNativeFeeToken() internal view returns (address) { + address bridge = address(getBridge(inbox)); + return IERC20Bridge(bridge).nativeToken(); + } +} diff --git a/contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol b/contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol new file mode 100644 index 0000000000..a7322eba4e --- /dev/null +++ b/contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1GatewayRouter } from "./L1GatewayRouter.sol"; +import { IERC20Inbox } from "../L1ArbitrumMessenger.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20Bridge } from "../../libraries/IERC20Bridge.sol"; + +/** + * @title Handles deposits from L1 into L2 in ERC20-based rollups where custom token is used to pay for fees. Tokens are routed to their appropriate L1 gateway. + * @notice Router itself also conforms to the Gateway interface. Router also serves as an L1-L2 token address oracle. + */ +contract L1OrbitGatewayRouter is L1GatewayRouter { + using SafeERC20 for IERC20; + + /** + * @notice Allows owner to register the default gateway. + * @param newL1DefaultGateway default gateway address + * @param _maxGas max gas for L2 retryable execution + * @param _gasPriceBid gas price for L2 retryable ticket + * @param _maxSubmissionCost base submission cost for L2 retryable ticket + * @param _feeAmount total amount of fees in native token to cover for retryable ticket costs. This amount will be transferred from user to bridge. + * @return Retryable ticket ID + */ + function setDefaultGateway( + address newL1DefaultGateway, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _feeAmount + ) external onlyOwner returns (uint256) { + return + _setDefaultGateway( + newL1DefaultGateway, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + _feeAmount + ); + } + + /** + * @notice Allows L1 Token contract to trustlessly register its gateway. + * @dev Other setGateway method allows excess eth recovery from _maxSubmissionCost and is recommended. + * @param _gateway l1 gateway address + * @param _maxGas max gas for L2 retryable execution + * @param _gasPriceBid gas price for L2 retryable ticket + * @param _maxSubmissionCost base submission cost for L2 retryable ticket + * @param _feeAmount total amount of fees in native token to cover for retryable ticket costs. This amount will be transferred from user to bridge. + * @return Retryable ticket ID + */ + function setGateway( + address _gateway, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _feeAmount + ) external returns (uint256) { + return + setGateway(_gateway, _maxGas, _gasPriceBid, _maxSubmissionCost, msg.sender, _feeAmount); + } + + /** + * @notice Allows L1 Token contract to trustlessly register its gateway. + * @dev Other setGateway method allows excess eth recovery from _maxSubmissionCost and is recommended. + * @param _gateway l1 gateway address + * @param _maxGas max gas for L2 retryable execution + * @param _gasPriceBid gas price for L2 retryable ticket + * @param _maxSubmissionCost base submission cost L2 retryable tick3et + * @param _creditBackAddress address for crediting back overpayment of _maxSubmissionCost + * @param _feeAmount total amount of fees in native token to cover for retryable ticket costs. This amount will be transferred from user to bridge. + * @return Retryable ticket ID + */ + function setGateway( + address _gateway, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + address _creditBackAddress, + uint256 _feeAmount + ) public returns (uint256) { + return + _setGatewayWithCreditBack( + _gateway, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + _creditBackAddress, + _feeAmount + ); + } + + /** + * @notice Allows owner to register gateways for specific tokens. + * @param _token list of L1 token addresses + * @param _gateway list of L1 gateway addresses + * @param _maxGas max gas for L2 retryable execution + * @param _gasPriceBid gas price for L2 retryable ticket + * @param _maxSubmissionCost base submission cost for L2 retryable ticket + * @param _feeAmount total amount of fees in native token to cover for retryable ticket costs. This amount will be transferred from user to bridge. + * @return Retryable ticket ID + */ + function setGateways( + address[] memory _token, + address[] memory _gateway, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + uint256 _feeAmount + ) external onlyOwner returns (uint256) { + return + _setGateways( + _token, + _gateway, + _maxGas, + _gasPriceBid, + _maxSubmissionCost, + msg.sender, + _feeAmount + ); + } + + function _createRetryable( + address _inbox, + address _to, + address _refundTo, + address _user, + uint256 _totalFeeAmount, + uint256 _l2CallValue, + uint256 _maxSubmissionCost, + uint256 _maxGas, + uint256 _gasPriceBid, + bytes memory _data + ) internal override returns (uint256) { + { + // Transfer native token amount needed to pay for retryable fees to the inbox. + // Fee tokens will be transferred from msg.sender + address nativeFeeToken = IERC20Bridge(address(getBridge(_inbox))).nativeToken(); + uint256 inboxNativeTokenBalance = IERC20(nativeFeeToken).balanceOf(_inbox); + if (inboxNativeTokenBalance < _totalFeeAmount) { + uint256 diff = _totalFeeAmount - inboxNativeTokenBalance; + IERC20(nativeFeeToken).safeTransferFrom(msg.sender, _inbox, diff); + } + } + + return + IERC20Inbox(_inbox).createRetryableTicket( + _to, + _l2CallValue, + _maxSubmissionCost, + _refundTo, + _user, + _maxGas, + _gasPriceBid, + _totalFeeAmount, + _data + ); + } + + /** + * @notice Revert 'setGateway' entrypoint which doesn't have total amount of token fees as an argument. + */ + function setGateway( + address, + uint256, + uint256, + uint256, + address + ) public payable override returns (uint256) { + revert("NOT_SUPPORTED_IN_ORBIT"); + } + + /** + * @notice Revert 'setDefaultGateway' entrypoint which doesn't have total amount of token fees as an argument. + */ + function setDefaultGateway( + address, + uint256, + uint256, + uint256 + ) external payable override onlyOwner returns (uint256) { + revert("NOT_SUPPORTED_IN_ORBIT"); + } + + /** + * @notice Revert 'setGateway' entrypoint which doesn't have total amount of token fees as an argument. + */ + function setGateway( + address, + uint256, + uint256, + uint256 + ) external payable override returns (uint256) { + revert("NOT_SUPPORTED_IN_ORBIT"); + } + + /** + * @notice Revert 'setGateways' entrypoint which doesn't have total amount of token fees as an argument. + */ + function setGateways( + address[] memory, + address[] memory, + uint256, + uint256, + uint256 + ) external payable override onlyOwner returns (uint256) { + revert("NOT_SUPPORTED_IN_ORBIT"); + } +} diff --git a/contracts/tokenbridge/ethereum/gateway/L1OrbitReverseCustomGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1OrbitReverseCustomGateway.sol new file mode 100644 index 0000000000..da07bb5dd7 --- /dev/null +++ b/contracts/tokenbridge/ethereum/gateway/L1OrbitReverseCustomGateway.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1OrbitCustomGateway } from "./L1OrbitCustomGateway.sol"; +import { IArbToken } from "../../arbitrum/IArbToken.sol"; + +/** + * @title L1 Gateway for reverse "custom" bridging functionality in an ERC20-based rollup. + * @notice Handles some (but not all!) reverse custom Gateway needs. + * Use the reverse custom gateway instead of the normal custom + * gateway if you want total supply to be tracked on the L2 + * rather than the L1. + * @dev The reverse custom gateway burns on the l2 and escrows on the l1 + * which is the opposite of the way the normal custom gateway works + * This means that the total supply L2 isn't affected by bridging, which + * is helpful for observers calculating the total supply especially if + * if minting is also occuring on L2 + */ +contract L1OrbitReverseCustomGateway is L1OrbitCustomGateway { + function inboundEscrowTransfer( + address _l1Address, + address _dest, + uint256 _amount + ) internal virtual override { + IArbToken(_l1Address).bridgeMint(_dest, _amount); + } + + function outboundEscrowTransfer( + address _l1Token, + address _from, + uint256 _amount + ) internal override returns (uint256) { + IArbToken(_l1Token).bridgeBurn(_from, _amount); + // by default we assume that the amount we send to bridgeBurn is the amount burnt + // this might not be the case for every token + return _amount; + } +} diff --git a/contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol b/contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol index 1bbe8ab39f..8c36307686 100644 --- a/contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol +++ b/contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol @@ -101,7 +101,12 @@ contract L1WethGateway is L1ArbitrumExtendedGateway { * @param l1ERC20 address of L1 token * @return L2 address of a bridged ERC20 token */ - function calculateL2TokenAddress(address l1ERC20) public view override(ITokenGateway, TokenGateway) returns (address) { + function calculateL2TokenAddress(address l1ERC20) + public + view + override(ITokenGateway, TokenGateway) + returns (address) + { if (l1ERC20 != l1Weth) { // invalid L1 weth address return address(0); diff --git a/contracts/tokenbridge/libraries/ERC20Upgradeable.sol b/contracts/tokenbridge/libraries/ERC20Upgradeable.sol index a491cd5f2f..16a7042c51 100644 --- a/contracts/tokenbridge/libraries/ERC20Upgradeable.sol +++ b/contracts/tokenbridge/libraries/ERC20Upgradeable.sol @@ -46,10 +46,10 @@ contract ERC20Upgradeable is __ERC20_init_unchained(name_, symbol_); } - function __ERC20_init_unchained( - string memory name_, - string memory symbol_ - ) internal onlyInitializing { + function __ERC20_init_unchained(string memory name_, string memory symbol_) + internal + onlyInitializing + { _name = name_; _symbol = symbol_; _decimals = 18; @@ -118,10 +118,13 @@ contract ERC20Upgradeable is /** * @dev See {IERC20-allowance}. */ - function allowance( - address owner, - address spender - ) public view virtual override returns (uint256) { + function allowance(address owner, address spender) + public + view + virtual + override + returns (uint256) + { return _allowances[owner][spender]; } @@ -200,10 +203,11 @@ contract ERC20Upgradeable is * - `spender` must have allowance for the caller of at least * `subtractedValue`. */ - function decreaseAllowance( - address spender, - uint256 subtractedValue - ) public virtual returns (bool) { + function decreaseAllowance(address spender, uint256 subtractedValue) + public + virtual + returns (bool) + { address owner = _msgSender(); uint256 currentAllowance = allowance(owner, spender); require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); @@ -228,7 +232,11 @@ contract ERC20Upgradeable is * - `to` cannot be the zero address. * - `from` must have a balance of at least `amount`. */ - function _transfer(address from, address to, uint256 amount) internal virtual { + function _transfer( + address from, + address to, + uint256 amount + ) internal virtual { require(from != address(0), "ERC20: transfer from the zero address"); require(to != address(0), "ERC20: transfer to the zero address"); @@ -314,7 +322,11 @@ contract ERC20Upgradeable is * - `owner` cannot be the zero address. * - `spender` cannot be the zero address. */ - function _approve(address owner, address spender, uint256 amount) internal virtual { + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { require(owner != address(0), "ERC20: approve from the zero address"); require(spender != address(0), "ERC20: approve to the zero address"); @@ -341,7 +353,11 @@ contract ERC20Upgradeable is * * Might emit an {Approval} event. */ - function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual { uint256 currentAllowance = allowance(owner, spender); if (currentAllowance != type(uint256).max) { require(currentAllowance >= amount, "ERC20: insufficient allowance"); @@ -365,7 +381,11 @@ contract ERC20Upgradeable is * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} /** * @dev Hook that is called after any transfer of tokens. This includes @@ -381,7 +401,11 @@ contract ERC20Upgradeable is * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} /** * @dev This empty reserved space is put in place to allow future versions to add new diff --git a/contracts/tokenbridge/libraries/IERC20Bridge.sol b/contracts/tokenbridge/libraries/IERC20Bridge.sol new file mode 100644 index 0000000000..d850579311 --- /dev/null +++ b/contracts/tokenbridge/libraries/IERC20Bridge.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 + +// solhint-disable-next-line compiler-version +pragma solidity >=0.6.9 <0.9.0; + +interface IERC20Bridge { + /** + * @dev token that is escrowed in bridge on L1 side and minted on L2 as native currency. Also fees are paid in this token. + */ + function nativeToken() external view returns (address); +} diff --git a/contracts/tokenbridge/test/InboxMock.sol b/contracts/tokenbridge/test/InboxMock.sol index 31cfa1236a..7113840f92 100644 --- a/contracts/tokenbridge/test/InboxMock.sol +++ b/contracts/tokenbridge/test/InboxMock.sol @@ -20,15 +20,34 @@ pragma solidity ^0.8.0; import "@arbitrum/nitro-contracts/src/bridge/IOutbox.sol"; import "@arbitrum/nitro-contracts/src/bridge/IBridge.sol"; -import "@arbitrum/nitro-contracts/src/bridge/IInbox.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -contract InboxMock { - address l2ToL1SenderMock = address(0); +abstract contract AbsInboxMock { + address public l2ToL1SenderMock = address(0); + uint256 public seqNum = 0; event TicketData(uint256 maxSubmissionCost); event RefundAddresses(address excessFeeRefundAddress, address callValueRefundAddress); event InboxRetryableTicket(address from, address to, uint256 value, uint256 maxGas, bytes data); + function bridge() external view returns (IBridge) { + return IBridge(address(this)); + } + + function activeOutbox() external view returns (address) { + return address(this); + } + + function setL2ToL1Sender(address sender) external { + l2ToL1SenderMock = sender; + } + + function l2ToL1Sender() external view returns (address) { + return l2ToL1SenderMock; + } +} + +contract InboxMock is AbsInboxMock { function createRetryableTicket( address to, uint256 l2CallValue, @@ -45,22 +64,61 @@ contract InboxMock { emit TicketData(maxSubmissionCost); emit RefundAddresses(excessFeeRefundAddress, callValueRefundAddress); emit InboxRetryableTicket(msg.sender, to, l2CallValue, gasLimit, data); - return 0; + return seqNum++; } +} - function bridge() external view returns (IBridge) { - return IBridge(address(this)); - } +contract ERC20InboxMock is AbsInboxMock { + address public nativeToken; - function activeOutbox() external view returns (address) { - return address(this); - } + event ERC20InboxRetryableTicket( + address from, + address to, + uint256 l2CallValue, + uint256 maxGas, + uint256 gasPrice, + uint256 tokenTotalFeeAmount, + bytes data + ); - function setL2ToL1Sender(address sender) external { - l2ToL1SenderMock = sender; + function createRetryableTicket( + address to, + uint256 l2CallValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 tokenTotalFeeAmount, + bytes calldata data + ) external returns (uint256) { + // ensure the user's deposit alone will make submission succeed + if (tokenTotalFeeAmount < (maxSubmissionCost + l2CallValue + gasLimit * maxFeePerGas)) { + revert("WRONG_TOKEN_VALUE"); + } + + emit TicketData(maxSubmissionCost); + emit RefundAddresses(excessFeeRefundAddress, callValueRefundAddress); + emit ERC20InboxRetryableTicket( + msg.sender, + to, + l2CallValue, + gasLimit, + maxFeePerGas, + tokenTotalFeeAmount, + data + ); + + // transfer out received native tokens (to simulate bridge spending those funds) + uint256 balance = IERC20(address(nativeToken)).balanceOf(address(this)); + if (balance > 0) { + IERC20(address(nativeToken)).transfer(address(1), balance); + } + + return seqNum++; } - function l2ToL1Sender() external view returns (address) { - return l2ToL1SenderMock; + function setMockNativeToken(address _nativeToken) external { + nativeToken = _nativeToken; } } diff --git a/contracts/tokenbridge/test/TestArbCustomToken.sol b/contracts/tokenbridge/test/TestArbCustomToken.sol index d5342dac69..e5dcee79e8 100644 --- a/contracts/tokenbridge/test/TestArbCustomToken.sol +++ b/contracts/tokenbridge/test/TestArbCustomToken.sol @@ -49,7 +49,9 @@ contract TestArbCustomToken is aeERC20, IArbToken { } contract MintableTestArbCustomToken is TestArbCustomToken { - constructor(address _l2Gateway, address _l1Address) TestArbCustomToken(_l2Gateway, _l1Address){} + constructor(address _l2Gateway, address _l1Address) + TestArbCustomToken(_l2Gateway, _l1Address) + {} function userMint(address account, uint256 amount) external { _mint(account, amount); diff --git a/contracts/tokenbridge/test/TestArbCustomTokenBurnFee.sol b/contracts/tokenbridge/test/TestArbCustomTokenBurnFee.sol index 8361f2ba39..3ca3f52df8 100644 --- a/contracts/tokenbridge/test/TestArbCustomTokenBurnFee.sol +++ b/contracts/tokenbridge/test/TestArbCustomTokenBurnFee.sol @@ -21,10 +21,9 @@ pragma solidity ^0.8.0; import "./TestArbCustomToken.sol"; contract TestArbCustomTokenBurnFee is TestArbCustomToken { - constructor( - address _l2Gateway, - address _l1Address - ) TestArbCustomToken(_l2Gateway, _l1Address) {} + constructor(address _l2Gateway, address _l1Address) + TestArbCustomToken(_l2Gateway, _l1Address) + {} // this token transfer extra 1 wei from the sender as fee when it burn token // alternatively, it can also be a callback that pass execution to the user diff --git a/contracts/tokenbridge/test/TestCustomTokenL1.sol b/contracts/tokenbridge/test/TestCustomTokenL1.sol index 359eef1d7f..cc2cc33e51 100644 --- a/contracts/tokenbridge/test/TestCustomTokenL1.sol +++ b/contracts/tokenbridge/test/TestCustomTokenL1.sol @@ -6,8 +6,10 @@ import "../libraries/aeERC20.sol"; import "../ethereum/ICustomToken.sol"; import "../ethereum/gateway/L1CustomGateway.sol"; import "../ethereum/gateway/L1GatewayRouter.sol"; +import { IERC20Bridge } from "../libraries/IERC20Bridge.sol"; import "@openzeppelin/contracts/utils/Context.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface IL1CustomGateway { function registerTokenToL2( @@ -19,6 +21,17 @@ interface IL1CustomGateway { ) external payable returns (uint256); } +interface IL1OrbitCustomGateway { + function registerTokenToL2( + address _l2Address, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + address _creditBackAddress, + uint256 _feeAmount + ) external returns (uint256); +} + interface IGatewayRouter2 { function setGateway( address _gateway, @@ -29,10 +42,23 @@ interface IGatewayRouter2 { ) external payable returns (uint256); } +interface IOrbitGatewayRouter { + function setGateway( + address _gateway, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost, + address _creditBackAddress, + uint256 _feeAmount + ) external returns (uint256); + + function inbox() external returns (address); +} + contract TestCustomTokenL1 is aeERC20, ICustomToken { address public gateway; address public router; - bool private shouldRegisterGateway; + bool internal shouldRegisterGateway; constructor(address _gateway, address _router) { gateway = _gateway; @@ -52,7 +78,9 @@ contract TestCustomTokenL1 is aeERC20, ICustomToken { return ERC20Upgradeable.transferFrom(sender, recipient, amount); } - function balanceOf(address account) + function balanceOf( + address account + ) public view virtual @@ -78,7 +106,7 @@ contract TestCustomTokenL1 is aeERC20, ICustomToken { uint256 valueForGateway, uint256 valueForRouter, address creditBackAddress - ) public payable override { + ) public payable virtual override { // we temporarily set `shouldRegisterGateway` to true for the callback in registerTokenToL2 to succeed bool prev = shouldRegisterGateway; shouldRegisterGateway = true; @@ -103,6 +131,72 @@ contract TestCustomTokenL1 is aeERC20, ICustomToken { } } +contract TestOrbitCustomTokenL1 is TestCustomTokenL1 { + using SafeERC20 for IERC20; + + constructor(address _gateway, address _router) TestCustomTokenL1(_gateway, _router) {} + + function registerTokenOnL2( + address l2CustomTokenAddress, + uint256 maxSubmissionCostForCustomGateway, + uint256 maxSubmissionCostForRouter, + uint256 maxGasForCustomGateway, + uint256 maxGasForRouter, + uint256 gasPriceBid, + uint256 valueForGateway, + uint256 valueForRouter, + address creditBackAddress + ) public payable override { + // we temporarily set `shouldRegisterGateway` to true for the callback in registerTokenToL2 to succeed + bool prev = shouldRegisterGateway; + shouldRegisterGateway = true; + + address inbox = IOrbitGatewayRouter(router).inbox(); + address bridge = address(IInbox(inbox).bridge()); + + // transfer fees from user to here, and approve router to use it + { + address nativeToken = IERC20Bridge(bridge).nativeToken(); + + IERC20(nativeToken).safeTransferFrom( + msg.sender, + address(this), + valueForGateway + valueForRouter + ); + IERC20(nativeToken).approve(router, valueForRouter); + IERC20(nativeToken).approve(gateway, valueForGateway); + } + + IL1OrbitCustomGateway(gateway).registerTokenToL2( + l2CustomTokenAddress, + maxGasForCustomGateway, + gasPriceBid, + maxSubmissionCostForCustomGateway, + creditBackAddress, + valueForGateway + ); + + IOrbitGatewayRouter(router).setGateway( + gateway, + maxGasForRouter, + gasPriceBid, + maxSubmissionCostForRouter, + creditBackAddress, + valueForRouter + ); + + // reset allowance back to 0 in case not all approved native tokens are spent + { + address nativeToken = IERC20Bridge(bridge).nativeToken(); + + IERC20(nativeToken).approve(router, 0); + IERC20(nativeToken).approve(gateway, 0); + } + + shouldRegisterGateway = prev; + } +} + contract MintableTestCustomTokenL1 is L1MintableToken, TestCustomTokenL1 { constructor(address _gateway, address _router) TestCustomTokenL1(_gateway, _router) {} @@ -111,21 +205,16 @@ contract MintableTestCustomTokenL1 is L1MintableToken, TestCustomTokenL1 { _; } - function bridgeMint(address account, uint256 amount) - public - override(L1MintableToken) - onlyGateway - { + function bridgeMint( + address account, + uint256 amount + ) public override(L1MintableToken) onlyGateway { _mint(account, amount); } - function balanceOf(address account) - public - view - virtual - override(TestCustomTokenL1, ICustomToken) - returns (uint256 amount) - { + function balanceOf( + address account + ) public view virtual override(TestCustomTokenL1, ICustomToken) returns (uint256 amount) { return super.balanceOf(account); } @@ -141,20 +230,16 @@ contract MintableTestCustomTokenL1 is L1MintableToken, TestCustomTokenL1 { contract ReverseTestCustomTokenL1 is L1ReverseToken, MintableTestCustomTokenL1 { constructor(address _gateway, address _router) MintableTestCustomTokenL1(_gateway, _router) {} - function bridgeBurn(address account, uint256 amount) - public - override(L1ReverseToken) - onlyGateway - { + function bridgeBurn( + address account, + uint256 amount + ) public override(L1ReverseToken) onlyGateway { _burn(account, amount); } - function balanceOf(address account) - public - view - override(MintableTestCustomTokenL1, ICustomToken) - returns (uint256 amount) - { + function balanceOf( + address account + ) public view override(MintableTestCustomTokenL1, ICustomToken) returns (uint256 amount) { return super.balanceOf(account); } diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000000..ba6691c9fe --- /dev/null +++ b/foundry.toml @@ -0,0 +1,14 @@ +[profile.default] +src = 'contracts' +out = 'out' +libs = ["node_modules", "lib"] +test = 'test-foundry' +cache_path = 'forge-cache' +optimizer = true +optimizer_runs = 100 +via_ir = false + +[fmt] +number_underscore = 'thousands' +line_length = 100 +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index e641a55748..b718396056 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -36,7 +36,7 @@ const config = { }, }, { - version: '0.8.17', + version: '0.8.16', settings: { optimizer: { enabled: true, diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000000..2b58ecbcf3 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 2b58ecbcf3dfde7a75959dc7b4eb3d0670278de6 diff --git a/lib/nitro-contracts b/lib/nitro-contracts new file mode 160000 index 0000000000..d5d33c2b8d --- /dev/null +++ b/lib/nitro-contracts @@ -0,0 +1 @@ +Subproject commit d5d33c2b8d5615563b8c553ca2a1bb936c039924 diff --git a/package.json b/package.json index 75ff6ba7f4..8854504cde 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "test:e2e": "hardhat test test/*.e2e.ts", "test:l1": "hardhat test test/*.l1.ts", "test:l2": "hardhat test test/*.l2.ts", + "test:unit": "forge test", + "test:e2e:local-env": "yarn hardhat test test-e2e/*", "test:storage": "./scripts/storage_layout_test.bash", - "deploy:local:token-bridge": "ts-node ./scripts/local-deployment/deploy.ts", + "deploy:local:token-bridge": "ts-node ./scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts", "deploy:goerli:token-bridge-creator": "ts-node ./scripts/goerli-deployment/deployTokenBridgeCreator.ts", "create:goerli:token-bridge": "ts-node ./scripts/goerli-deployment/createTokenBridge.ts", "test:tokenbridge:deployment": "hardhat test test-e2e/tokenBridgeDeploymentTest.ts", @@ -36,11 +38,12 @@ ], "dependencies": { "@arbitrum/nitro-contracts": "^1.0.0-beta.8", - "@arbitrum/sdk": "^3.1.5", "@openzeppelin/contracts": "4.8.3", - "@openzeppelin/contracts-upgradeable": "4.8.3" + "@openzeppelin/contracts-upgradeable": "4.8.3", + "@offchainlabs/upgrade-executor": "^1.0.0-beta.2" }, "devDependencies": { + "@arbitrum/sdk": "^3.1.3", "@nomiclabs/hardhat-ethers": "^2.0.1", "@nomiclabs/hardhat-etherscan": "^3.1.0", "@nomiclabs/hardhat-waffle": "^2.0.1", diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000000..18d5a0c9ed --- /dev/null +++ b/remappings.txt @@ -0,0 +1,7 @@ +ds-test/=lib/forge-std/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ + + + diff --git a/scripts/atomicTokenBridgeDeployer.ts b/scripts/atomicTokenBridgeDeployer.ts index d3d39f8732..26983ac47c 100644 --- a/scripts/atomicTokenBridgeDeployer.ts +++ b/scripts/atomicTokenBridgeDeployer.ts @@ -14,7 +14,21 @@ import { L1WethGateway__factory, TransparentUpgradeableProxy__factory, ProxyAdmin__factory, + L1TokenBridgeRetryableSender__factory, + L1OrbitERC20Gateway__factory, + L1OrbitCustomGateway__factory, + L1OrbitGatewayRouter__factory, + IInbox__factory, + IERC20Bridge__factory, + IERC20__factory, + ArbMulticall2__factory, + IRollupCore__factory, + IBridge__factory, } from '../build/types' +import { + abi as UpgradeExecutorABI, + bytecode as UpgradeExecutorBytecode, +} from '@offchainlabs/upgrade-executor/build/contracts/src/UpgradeExecutor.sol/UpgradeExecutor.json' import { JsonRpcProvider } from '@ethersproject/providers' import { L1ToL2MessageGasEstimator, @@ -24,6 +38,7 @@ import { import { exit } from 'process' import { getBaseFee } from '@arbitrum/sdk/dist/lib/utils/lib' import { RollupAdminLogic__factory } from '@arbitrum/sdk/dist/lib/abi/factories/RollupAdminLogic__factory' +import { ContractVerifier } from './contractVerifier' /** * Use already deployed L1TokenBridgeCreator to create and init token bridge contracts. @@ -41,7 +56,8 @@ export const createTokenBridge = async ( l1Signer: Signer, l2Provider: ethers.providers.Provider, l1TokenBridgeCreator: L1AtomicTokenBridgeCreator, - rollupAddress: string + rollupAddress: string, + rollupOwnerAddress: string ) => { const gasPrice = await l2Provider.getGasPrice() @@ -50,6 +66,7 @@ export const createTokenBridge = async ( l1Signer, l2Provider ) + const maxGasForFactory = await l1TokenBridgeCreator.gasLimitForL2FactoryDeployment() const maxSubmissionCostForFactory = deployFactoryGasParams.maxSubmissionCost @@ -66,6 +83,8 @@ export const createTokenBridge = async ( customGateway: L2CustomGateway__factory.bytecode, wethGateway: L2WethGateway__factory.bytecode, aeWeth: AeWETH__factory.bytecode, + upgradeExecutor: UpgradeExecutorBytecode, + multicall: ArbMulticall2__factory.bytecode, } const gasEstimateToDeployContracts = await l2FactoryTemplate.estimateGas.deployL2Contracts( @@ -76,13 +95,14 @@ export const createTokenBridge = async ( ethers.Wallet.createRandom().address, ethers.Wallet.createRandom().address, ethers.Wallet.createRandom().address, + ethers.Wallet.createRandom().address, ethers.Wallet.createRandom().address ) const maxGasForContracts = gasEstimateToDeployContracts.mul(2) const maxSubmissionCostForContracts = deployFactoryGasParams.maxSubmissionCost.mul(2) - let retryableValue = maxSubmissionCostForFactory + let retryableFee = maxSubmissionCostForFactory .add(maxSubmissionCostForContracts) .add(maxGasForFactory.mul(gasPrice)) .add(maxGasForContracts.mul(gasPrice)) @@ -93,16 +113,31 @@ export const createTokenBridge = async ( l1Signer.provider! ).inbox() + // if fee token is used approve the fee + const feeToken = await _getFeeToken(inbox, l1Signer.provider!) + if (feeToken != ethers.constants.AddressZero) { + await ( + await IERC20__factory.connect(feeToken, l1Signer).approve( + l1TokenBridgeCreator.address, + retryableFee + ) + ).wait() + retryableFee = BigNumber.from(0) + } + /// do it - create token bridge const receipt = await ( await l1TokenBridgeCreator.createTokenBridge( inbox, + rollupOwnerAddress, maxGasForContracts, gasPrice, - { value: retryableValue } + { value: retryableFee } ) ).wait() + console.log('Deployment TX:', receipt.transactionHash) + /// wait for execution of both tickets const l1TxReceipt = new L1TransactionReceipt(receipt) const messages = await l1TxReceipt.getL1ToL2Messages(l2Provider) @@ -149,22 +184,37 @@ export const createTokenBridge = async ( 'OrbitTokenBridgeCreated' )[0].args + const rollup = await IBridge__factory.connect( + await IInbox__factory.connect(inbox, l1Signer).bridge(), + l1Signer + ).rollup() + const chainId = await IRollupCore__factory.connect(rollup, l1Signer).chainId() + /// pick up L2 contracts - const l2Router = await l1TokenBridgeCreator.getCanonicalL2RouterAddress() + const l2Router = await l1TokenBridgeCreator.getCanonicalL2RouterAddress( + chainId + ) const l2StandardGateway = L2ERC20Gateway__factory.connect( - await l1TokenBridgeCreator.getCanonicalL2StandardGatewayAddress(), + await l1TokenBridgeCreator.getCanonicalL2StandardGatewayAddress(chainId), l2Provider ) const beaconProxyFactory = await l2StandardGateway.beaconProxyFactory() const l2CustomGateway = - await l1TokenBridgeCreator.getCanonicalL2CustomGatewayAddress() - const l2WethGateway = L2WethGateway__factory.connect( - await l1TokenBridgeCreator.getCanonicalL2WethGatewayAddress(), - l2Provider - ) + await l1TokenBridgeCreator.getCanonicalL2CustomGatewayAddress(chainId) + + const isUsingFeeToken = feeToken != ethers.constants.AddressZero + const l2WethGateway = isUsingFeeToken + ? ethers.constants.AddressZero + : L2WethGateway__factory.connect( + await l1TokenBridgeCreator.getCanonicalL2WethGatewayAddress(chainId), + l2Provider + ).address const l1Weth = await l1TokenBridgeCreator.l1Weth() - const l2Weth = await l1TokenBridgeCreator.getCanonicalL2WethAddress() - const l2ProxyAdmin = await l1TokenBridgeCreator.canonicalL2ProxyAdminAddress() + const l2Weth = isUsingFeeToken + ? ethers.constants.AddressZero + : await l1TokenBridgeCreator.getCanonicalL2WethAddress(chainId) + const l2ProxyAdmin = + await l1TokenBridgeCreator.getCanonicalL2ProxyAdminAddress(chainId) return { l1Router, @@ -175,7 +225,7 @@ export const createTokenBridge = async ( l2Router, l2StandardGateway: l2StandardGateway.address, l2CustomGateway, - l2WethGateway: l2WethGateway.address, + l2WethGateway, l1Weth, l2Weth, beaconProxyFactory, @@ -217,7 +267,29 @@ export const deployL1TokenBridgeCreator = async ( l1TokenBridgeCreatorProxy.address, l1Deployer ) - await (await l1TokenBridgeCreator.initialize()).wait() + + /// deploy retryable sender behind proxy + const retryableSenderLogic = await new L1TokenBridgeRetryableSender__factory( + l1Deployer + ).deploy() + await retryableSenderLogic.deployed() + + const retryableSenderProxy = await new TransparentUpgradeableProxy__factory( + l1Deployer + ).deploy( + retryableSenderLogic.address, + l1TokenBridgeCreatorProxyAdmin.address, + '0x' + ) + await retryableSenderProxy.deployed() + + const retryableSender = L1TokenBridgeRetryableSender__factory.connect( + retryableSenderProxy.address, + l1Deployer + ) + + /// init creator + await (await l1TokenBridgeCreator.initialize(retryableSender.address)).wait() /// deploy L1 logic contracts const routerTemplate = await new L1GatewayRouter__factory(l1Deployer).deploy() @@ -238,6 +310,39 @@ export const deployL1TokenBridgeCreator = async ( ).deploy() await wethGatewayTemplate.deployed() + const feeTokenBasedRouterTemplate = await new L1OrbitGatewayRouter__factory( + l1Deployer + ).deploy() + await feeTokenBasedRouterTemplate.deployed() + + const feeTokenBasedStandardGatewayTemplate = + await new L1OrbitERC20Gateway__factory(l1Deployer).deploy() + await feeTokenBasedStandardGatewayTemplate.deployed() + + const feeTokenBasedCustomGatewayTemplate = + await new L1OrbitCustomGateway__factory(l1Deployer).deploy() + await feeTokenBasedCustomGatewayTemplate.deployed() + + const upgradeExecutorFactory = new ethers.ContractFactory( + UpgradeExecutorABI, + UpgradeExecutorBytecode, + l1Deployer + ) + const upgradeExecutor = await upgradeExecutorFactory.deploy() + + const l1Templates = { + routerTemplate: routerTemplate.address, + standardGatewayTemplate: standardGatewayTemplate.address, + customGatewayTemplate: customGatewayTemplate.address, + wethGatewayTemplate: wethGatewayTemplate.address, + feeTokenBasedRouterTemplate: feeTokenBasedRouterTemplate.address, + feeTokenBasedStandardGatewayTemplate: + feeTokenBasedStandardGatewayTemplate.address, + feeTokenBasedCustomGatewayTemplate: + feeTokenBasedCustomGatewayTemplate.address, + upgradeExecutor: upgradeExecutor.address, + } + /// deploy L2 contracts as placeholders on L1 const l2TokenBridgeFactoryOnL1 = @@ -267,6 +372,11 @@ export const deployL1TokenBridgeCreator = async ( const l2WethAddressOnL1 = await new AeWETH__factory(l1Deployer).deploy() await l2WethAddressOnL1.deployed() + const l2MulticallAddressOnL1 = await new ArbMulticall2__factory( + l1Deployer + ).deploy() + await l2MulticallAddressOnL1.deployed() + //// run retryable estimate for deploying L2 factory const deployFactoryGasParams = await getEstimateForDeployingFactory( l1Deployer, @@ -275,22 +385,127 @@ export const deployL1TokenBridgeCreator = async ( await ( await l1TokenBridgeCreator.setTemplates( - routerTemplate.address, - standardGatewayTemplate.address, - customGatewayTemplate.address, - wethGatewayTemplate.address, + l1Templates, l2TokenBridgeFactoryOnL1.address, l2GatewayRouterOnL1.address, l2StandardGatewayAddressOnL1.address, l2CustomGatewayAddressOnL1.address, l2WethGatewayAddressOnL1.address, l2WethAddressOnL1.address, + l2MulticallAddressOnL1.address, l1WethAddress, deployFactoryGasParams.gasLimit ) ).wait() - return l1TokenBridgeCreator + ///// verify contracts + console.log('\n\n Start contract verification \n\n') + const l1Verifier = new ContractVerifier( + (await l1Deployer.provider!.getNetwork()).chainId, + process.env.ARBISCAN_API_KEY! + ) + const abi = ethers.utils.defaultAbiCoder + + await l1Verifier.verifyWithAddress( + 'l1TokenBridgeCreatorProxyAdmin', + l1TokenBridgeCreatorProxyAdmin.address + ) + await l1Verifier.verifyWithAddress( + 'l1TokenBridgeCreatorLogic', + l1TokenBridgeCreatorLogic.address + ) + await l1Verifier.verifyWithAddress( + 'l1TokenBridgeCreatorProxy', + l1TokenBridgeCreatorProxy.address, + abi.encode( + ['address', 'address', 'bytes'], + [ + l1TokenBridgeCreatorLogic.address, + l1TokenBridgeCreatorProxyAdmin.address, + '0x', + ] + ) + ) + await l1Verifier.verifyWithAddress( + 'retryableSenderLogic', + retryableSenderLogic.address + ) + await l1Verifier.verifyWithAddress( + 'retryableSenderProxy', + retryableSenderProxy.address, + abi.encode( + ['address', 'address', 'bytes'], + [ + retryableSenderLogic.address, + l1TokenBridgeCreatorProxyAdmin.address, + '0x', + ] + ) + ) + await l1Verifier.verifyWithAddress('routerTemplate', routerTemplate.address) + await l1Verifier.verifyWithAddress( + 'standardGatewayTemplate', + standardGatewayTemplate.address + ) + await l1Verifier.verifyWithAddress( + 'customGatewayTemplate', + customGatewayTemplate.address + ) + await l1Verifier.verifyWithAddress( + 'wethGatewayTemplate', + wethGatewayTemplate.address + ) + await l1Verifier.verifyWithAddress( + 'feeTokenBasedRouterTemplate', + feeTokenBasedRouterTemplate.address + ) + await l1Verifier.verifyWithAddress( + 'feeTokenBasedStandardGatewayTemplate', + feeTokenBasedStandardGatewayTemplate.address + ) + await l1Verifier.verifyWithAddress( + 'feeTokenBasedCustomGatewayTemplate', + feeTokenBasedCustomGatewayTemplate.address + ) + await l1Verifier.verifyWithAddress( + 'upgradeExecutor', + upgradeExecutor.address, + '', + 20000 + ) + await l1Verifier.verifyWithAddress( + 'l2TokenBridgeFactoryOnL1', + l2TokenBridgeFactoryOnL1.address + ) + await l1Verifier.verifyWithAddress( + 'l2GatewayRouterOnL1', + l2GatewayRouterOnL1.address + ) + await l1Verifier.verifyWithAddress( + 'l2StandardGatewayAddressOnL1', + l2StandardGatewayAddressOnL1.address + ) + await l1Verifier.verifyWithAddress( + 'l2CustomGatewayAddressOnL1', + l2CustomGatewayAddressOnL1.address + ) + await l1Verifier.verifyWithAddress( + 'l2WethGatewayAddressOnL1', + l2WethGatewayAddressOnL1.address + ) + await l1Verifier.verifyWithAddress( + 'l2WethAddressOnL1', + l2WethAddressOnL1.address + ) + await l1Verifier.verifyWithAddress( + 'l2MulticallAddressOnL1', + l2MulticallAddressOnL1.address + ) + + await new Promise(resolve => setTimeout(resolve, 2000)) + console.log('\n\n Contract verification done \n\n') + + return { l1TokenBridgeCreator, retryableSender } } export const getEstimateForDeployingFactory = async ( @@ -337,6 +552,24 @@ export const getParsedLogs = ( return parsedLogs } +const _getFeeToken = async ( + inbox: string, + l1Provider: ethers.providers.Provider +) => { + const bridge = await IInbox__factory.connect(inbox, l1Provider).bridge() + + let feeToken = ethers.constants.AddressZero + + try { + feeToken = await IERC20Bridge__factory.connect( + bridge, + l1Provider + ).nativeToken() + } catch {} + + return feeToken +} + export function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) } diff --git a/scripts/contractVerifier.ts b/scripts/contractVerifier.ts new file mode 100644 index 0000000000..3a5a160569 --- /dev/null +++ b/scripts/contractVerifier.ts @@ -0,0 +1,102 @@ +import { exec } from 'child_process' +import { ethers } from 'ethers' + +export class ContractVerifier { + chainId: number + apiKey: string = '' + + readonly NUM_OF_OPTIMIZATIONS = 100 + readonly COMPILER_VERSION = '0.8.16' + + ///// List of contract addresses and their corresponding source code files + readonly TUP = + 'node_modules/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol:TransparentUpgradeableProxy' + readonly PROXY_ADMIN = + 'node_modules/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol:ProxyAdmin' + readonly EXECUTOR = + 'node_modules/@offchainlabs/upgrade-executor/src/UpgradeExecutor.sol:UpgradeExecutor' + + readonly contractToSource = { + l1TokenBridgeCreatorProxyAdmin: this.PROXY_ADMIN, + l1TokenBridgeCreatorLogic: + 'contracts/tokenbridge/ethereum/L1AtomicTokenBridgeCreator.sol:L1AtomicTokenBridgeCreator', + l1TokenBridgeCreatorProxy: this.TUP, + retryableSenderLogic: + 'contracts/tokenbridge/ethereum/L1TokenBridgeRetryableSender.sol:L1TokenBridgeRetryableSender', + retryableSenderProxy: this.TUP, + routerTemplate: + 'contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol:L1GatewayRouter', + standardGatewayTemplate: + 'contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol:L1ERC20Gateway', + customGatewayTemplate: + 'contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol:L1CustomGateway', + wethGatewayTemplate: + 'contracts/tokenbridge/ethereum/gateway/L1WethGateway.sol:L1WethGateway', + feeTokenBasedRouterTemplate: + 'contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol:L1OrbitGatewayRouter', + feeTokenBasedStandardGatewayTemplate: + 'contracts/tokenbridge/ethereum/gateway/L1OrbitERC20Gateway.sol:L1OrbitERC20Gateway', + feeTokenBasedCustomGatewayTemplate: + 'contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol:L1OrbitCustomGateway', + upgradeExecutor: this.EXECUTOR, + l2TokenBridgeFactoryOnL1: + 'contracts/tokenbridge/arbitrum/L2AtomicTokenBridgeFactory.sol:L2AtomicTokenBridgeFactory', + l2GatewayRouterOnL1: + 'contracts/tokenbridge/arbitrum/gateway/L2GatewayRouter.sol:L2GatewayRouter', + l2StandardGatewayAddressOnL1: + 'contracts/tokenbridge/arbitrum/gateway/L2ERC20Gateway.sol:L2ERC20Gateway', + l2CustomGatewayAddressOnL1: + 'contracts/tokenbridge/arbitrum/gateway/L2CustomGateway.sol:L2CustomGateway', + l2WethGatewayAddressOnL1: + 'contracts/tokenbridge/arbitrum/gateway/L2WethGateway.sol:L2WethGateway', + l2WethAddressOnL1: 'contracts/tokenbridge/libraries/aeWETH.sol:aeWETH', + l2MulticallAddressOnL1: 'contracts/rpc-utils/MulticallV2.sol:ArbMulticall2', + } + + constructor(chainId: number, apiKey: string) { + this.chainId = chainId + if (apiKey) { + this.apiKey = apiKey + } + } + + async verifyWithAddress( + name: string, + contractAddress: string, + constructorArgs?: string, + _numOfOptimization?: number + ) { + // avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 1000)) + + let command = `forge verify-contract --chain-id ${this.chainId} --compiler-version ${this.COMPILER_VERSION}` + + if (_numOfOptimization !== undefined) { + command = `${command} --num-of-optimizations ${_numOfOptimization}` + } else { + command = `${command} --num-of-optimizations ${this.NUM_OF_OPTIMIZATIONS}` + } + + const sourceFile = + this.contractToSource[name as keyof typeof this.contractToSource] + + if (constructorArgs) { + command = `${command} --constructor-args ${constructorArgs}` + } + command = `${command} ${contractAddress} ${sourceFile} --etherscan-api-key ${this.apiKey}` + + exec(command, (err: Error | null, stdout: string, stderr: string) => { + console.log('-----------------') + console.log(command) + if (err) { + console.log( + 'Failed to submit for verification', + contractAddress, + stderr + ) + } else { + console.log('Successfully submitted for verification', contractAddress) + } + }) + } +} diff --git a/scripts/goerli-deployment/createTokenBridge.ts b/scripts/goerli-deployment/createTokenBridge.ts index c5aca52f95..b2a6039300 100644 --- a/scripts/goerli-deployment/createTokenBridge.ts +++ b/scripts/goerli-deployment/createTokenBridge.ts @@ -5,17 +5,19 @@ import { createTokenBridge, getSigner } from '../atomicTokenBridgeDeployer' import dotenv from 'dotenv' import { L1AtomicTokenBridgeCreator__factory } from '../../build/types' import * as fs from 'fs' +import { env } from 'process' dotenv.config() export const envVars = { - baseChainRpc: process.env['ARB_GOERLI_RPC'] as string, - baseChainDeployerKey: process.env['ARB_GOERLI_DEPLOYER_KEY'] as string, + rollupAddress: process.env['ROLLUP_ADDRESS'] as string, + rollupOwner: process.env['ROLLUP_OWNER'] as string, + l1TokenBridgeCreator: process.env['L1_TOKEN_BRIDGE_CREATOR'] as string, + baseChainRpc: process.env['BASECHAIN_RPC'] as string, + baseChainDeployerKey: process.env['BASECHAIN_DEPLOYER_KEY'] as string, childChainRpc: process.env['ORBIT_RPC'] as string, } -const L1_TOKEN_BRIDGE_CREATOR = '0xAd146718D61dc3024428ED7Ed11ade741A37EA38' - /** * Steps: * - read network info from local container and register networks @@ -29,14 +31,22 @@ const L1_TOKEN_BRIDGE_CREATOR = '0xAd146718D61dc3024428ED7Ed11ade741A37EA38' * @param l2Url * @returns */ -export const createTokenBridgeOnGoerli = async (rollupAddress: string) => { +export const createTokenBridgeOnGoerli = async () => { + if (envVars.rollupAddress == undefined) + throw new Error('Missing ROLLUP_ADDRESS in env vars') + if (envVars.rollupOwner == undefined) + throw new Error('Missing ROLLUP_OWNER in env vars') + if (envVars.l1TokenBridgeCreator == undefined) + throw new Error('Missing L1_TOKEN_BRIDGE_CREATOR in env vars') if (envVars.baseChainRpc == undefined) - throw new Error('Missing ARB_GOERLI_RPC in env vars') + throw new Error('Missing BASECHAIN_RPC in env vars') if (envVars.baseChainDeployerKey == undefined) - throw new Error('Missing ARB_GOERLI_DEPLOYER_KEY in env vars') + throw new Error('Missing BASECHAIN_DEPLOYER_KEY in env vars') if (envVars.childChainRpc == undefined) throw new Error('Missing ORBIT_RPC in env vars') + console.log('Creating token bridge for rollup', envVars.rollupAddress) + const l1Provider = new JsonRpcProvider(envVars.baseChainRpc) const l1Deployer = getSigner(l1Provider, envVars.baseChainDeployerKey) const l2Provider = new JsonRpcProvider(envVars.childChainRpc) @@ -44,20 +54,21 @@ export const createTokenBridgeOnGoerli = async (rollupAddress: string) => { const { l1Network, l2Network: corel2Network } = await registerGoerliNetworks( l1Provider, l2Provider, - rollupAddress + envVars.rollupAddress ) - // create token bridge const l1TokenBridgeCreator = L1AtomicTokenBridgeCreator__factory.connect( - L1_TOKEN_BRIDGE_CREATOR, + envVars.l1TokenBridgeCreator, l1Deployer ) + // create token bridge const deployedContracts = await createTokenBridge( l1Deployer, l2Provider, l1TokenBridgeCreator, - rollupAddress + envVars.rollupAddress, + envVars.rollupOwner ) const l2Network = { @@ -159,20 +170,7 @@ const registerGoerliNetworks = async ( } async function main() { - const args = process.argv.slice(2) - if (args.length != 1) { - console.log( - "Please provide exactly 1 argument - rollup address.\nIe. `yarn run create:goerli:token-bridge -- '0xDAB64b6E86035Aa9EB697341B663fb4B46930E60'`" - ) - return - } - - const rollupAddress = args[0] - console.log('Creating token bridge for rollup', rollupAddress) - - const { l1Network, l2Network } = await createTokenBridgeOnGoerli( - rollupAddress - ) + const { l1Network, l2Network } = await createTokenBridgeOnGoerli() const NETWORK_FILE = 'network.json' fs.writeFileSync( NETWORK_FILE, diff --git a/scripts/goerli-deployment/deployTokenBridgeCreator.ts b/scripts/goerli-deployment/deployTokenBridgeCreator.ts index 1c0498ba80..cfbd49a2da 100644 --- a/scripts/goerli-deployment/deployTokenBridgeCreator.ts +++ b/scripts/goerli-deployment/deployTokenBridgeCreator.ts @@ -10,8 +10,8 @@ import dotenv from 'dotenv' dotenv.config() export const envVars = { - baseChainRpc: process.env['ARB_GOERLI_RPC'] as string, - baseChainDeployerKey: process.env['ARB_GOERLI_DEPLOYER_KEY'] as string, + baseChainRpc: process.env['BASECHAIN_RPC'] as string, + baseChainDeployerKey: process.env['BASECHAIN_DEPLOYER_KEY'] as string, childChainRpc: process.env['ORBIT_RPC'] as string, } @@ -32,9 +32,9 @@ const ARB_GOERLI_WETH = '0xEe01c0CD76354C383B8c7B4e65EA88D00B06f36f' */ export const deployTokenBridgeCreator = async (rollupAddress: string) => { if (envVars.baseChainRpc == undefined) - throw new Error('Missing ARB_GOERLI_RPC in env vars') + throw new Error('Missing BASECHAIN_RPC in env vars') if (envVars.baseChainDeployerKey == undefined) - throw new Error('Missing ARB_GOERLI_DEPLOYER_KEY in env vars') + throw new Error('Missing BASECHAIN_DEPLOYER_KEY in env vars') if (envVars.childChainRpc == undefined) throw new Error('Missing ORBIT_RPC in env vars') @@ -45,13 +45,10 @@ export const deployTokenBridgeCreator = async (rollupAddress: string) => { await registerGoerliNetworks(l1Provider, l2Provider, rollupAddress) // deploy L1 creator and set templates - const l1TokenBridgeCreator = await deployL1TokenBridgeCreator( - l1Deployer, - l2Provider, - ARB_GOERLI_WETH - ) + const { l1TokenBridgeCreator, retryableSender } = + await deployL1TokenBridgeCreator(l1Deployer, l2Provider, ARB_GOERLI_WETH) - return l1TokenBridgeCreator + return { l1TokenBridgeCreator, retryableSender } } const registerGoerliNetworks = async ( @@ -127,9 +124,13 @@ const registerGoerliNetworks = async ( async function main() { // this is just random Orbit rollup that will be used to estimate gas needed to deploy L2 token bridge factory via retryable - const rollupAddress = '0xDAB64b6E86035Aa9EB697341B663fb4B46930E60' - const l1TokenBridgeCreator = await deployTokenBridgeCreator(rollupAddress) + const rollupAddress = '0x8223bd899C6643483872ed2A7b105b2aC9C8aBEc' + const { l1TokenBridgeCreator, retryableSender } = + await deployTokenBridgeCreator(rollupAddress) + + console.log('Token bridge creator deployed!') console.log('L1TokenBridgeCreator:', l1TokenBridgeCreator.address) + console.log('L1TokenBridgeRetryableSender:', retryableSender.address, '\n') } main().then(() => console.log('Done.')) diff --git a/scripts/local-deployment/deploy.ts b/scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts similarity index 79% rename from scripts/local-deployment/deploy.ts rename to scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts index 803fe30a9b..543ebf6896 100644 --- a/scripts/local-deployment/deploy.ts +++ b/scripts/local-deployment/deployCreatorAndCreateTokenBridge.ts @@ -9,6 +9,7 @@ import { createTokenBridge, deployL1TokenBridgeCreator, } from '../atomicTokenBridgeDeployer' +import { l2Networks } from '@arbitrum/sdk/dist/lib/dataEntities/networks' /** * Steps: @@ -44,45 +45,47 @@ export const setupTokenBridgeInLocalEnv = async () => { ) // register - needed for retryables - addCustomNetwork({ - customL1Network: l1Network, - customL2Network: { - ...coreL2Network, - tokenBridge: { - l1CustomGateway: '', - l1ERC20Gateway: '', - l1GatewayRouter: '', - l1MultiCall: '', - l1ProxyAdmin: '', - l1Weth: '', - l1WethGateway: '', - - l2CustomGateway: '', - l2ERC20Gateway: '', - l2GatewayRouter: '', - l2Multicall: '', - l2ProxyAdmin: '', - l2Weth: '', - l2WethGateway: '', + const existingL2Network = l2Networks[coreL2Network.chainID.toString()] + if (!existingL2Network) { + addCustomNetwork({ + customL1Network: l1Network, + customL2Network: { + ...coreL2Network, + tokenBridge: { + l1CustomGateway: '', + l1ERC20Gateway: '', + l1GatewayRouter: '', + l1MultiCall: '', + l1ProxyAdmin: '', + l1Weth: '', + l1WethGateway: '', + + l2CustomGateway: '', + l2ERC20Gateway: '', + l2GatewayRouter: '', + l2Multicall: '', + l2ProxyAdmin: '', + l2Weth: '', + l2WethGateway: '', + }, }, - }, - }) + }) + } // prerequisite - deploy L1 creator and set templates const l1Weth = ethers.Wallet.createRandom().address - const l1TokenBridgeCreator = await deployL1TokenBridgeCreator( - l1Deployer, - l2Deployer.provider!, - l1Weth - ) + const { l1TokenBridgeCreator, retryableSender } = + await deployL1TokenBridgeCreator(l1Deployer, l2Deployer.provider!, l1Weth) console.log('L1TokenBridgeCreator', l1TokenBridgeCreator.address) + console.log('L1TokenBridgeRetryableSender', retryableSender.address) // create token bridge const deployedContracts = await createTokenBridge( l1Deployer, l2Deployer.provider!, l1TokenBridgeCreator, - coreL2Network.ethBridge.rollup + coreL2Network.ethBridge.rollup, + l1Deployer.address ) const l2Network: L2Network = { @@ -109,6 +112,8 @@ export const setupTokenBridgeInLocalEnv = async () => { return { l1Network, l2Network, + l1TokenBridgeCreator, + retryableSender, } } @@ -192,12 +197,17 @@ export const getLocalNetworks = async ( } async function main() { - const { l1Network, l2Network } = await setupTokenBridgeInLocalEnv() + const { l1Network, l2Network, l1TokenBridgeCreator, retryableSender } = + await setupTokenBridgeInLocalEnv() const NETWORK_FILE = 'network.json' fs.writeFileSync( NETWORK_FILE, - JSON.stringify({ l1Network, l2Network }, null, 2) + JSON.stringify( + { l1Network, l2Network, l1TokenBridgeCreator, retryableSender }, + null, + 2 + ) ) console.log(NETWORK_FILE + ' updated') } diff --git a/scripts/local-deployment/tokenBridgeDeployer.ts b/scripts/local-deployment/tokenBridgeDeployer.ts deleted file mode 100644 index b52a465b59..0000000000 --- a/scripts/local-deployment/tokenBridgeDeployer.ts +++ /dev/null @@ -1,392 +0,0 @@ -import { - Contract, - ContractFactory, - Signer, - Wallet, - constants, - ethers, -} from 'ethers' -import { - BeaconProxyFactory__factory, - L1CustomGateway__factory, - L1ERC20Gateway__factory, - L1GatewayRouter__factory, - L2CustomGateway__factory, - L2ERC20Gateway__factory, - L2GatewayRouter__factory, - ProxyAdmin__factory, - StandardArbERC20__factory, - TransparentUpgradeableProxy__factory, - UpgradeableBeacon__factory, -} from '../../build/types' -import { JsonRpcProvider } from '@ethersproject/providers' -import { L1Network, L2Network, addCustomNetwork } from '@arbitrum/sdk' -import { execSync } from 'child_process' -import { Bridge__factory } from '@arbitrum/sdk/dist/lib/abi/factories/Bridge__factory' -import { RollupAdminLogic__factory } from '@arbitrum/sdk/dist/lib/abi/factories/RollupAdminLogic__factory' -import * as fs from 'fs' - -export const setupTokenBridge = async ( - l1Deployer: Signer, - l2Deployer: Signer, - l1Url: string, - l2Url: string -) => { - const { l1Network, l2Network: coreL2Network } = await getLocalNetworks( - l1Url, - l2Url - ) - - const { l1: l1Contracts, l2: l2Contracts } = await deployTokenBridgeAndInit( - l1Deployer, - l2Deployer, - coreL2Network.ethBridge.inbox - ) - const l2Network: L2Network = { - ...coreL2Network, - tokenBridge: { - l1CustomGateway: l1Contracts.customGateway.address, - l1ERC20Gateway: l1Contracts.standardGateway.address, - l1GatewayRouter: l1Contracts.router.address, - l1MultiCall: '', - l1ProxyAdmin: l1Contracts.proxyAdmin.address, - l1Weth: '', - l1WethGateway: '', - - l2CustomGateway: l2Contracts.customGateway.address, - l2ERC20Gateway: l2Contracts.standardGateway.address, - l2GatewayRouter: l2Contracts.router.address, - l2Multicall: '', - l2ProxyAdmin: l2Contracts.proxyAdmin.address, - l2Weth: '', - l2WethGateway: '', - }, - } - - addCustomNetwork({ - customL1Network: l1Network, - customL2Network: l2Network, - }) - - return { - l1Network, - l2Network, - } -} - -/** - * Deploy all the L1 and L2 contracts and do the initialization. - * - * @param l1Signer - * @param l2Signer - * @param inboxAddress - * @returns - */ -export const deployTokenBridgeAndInit = async ( - l1Signer: Signer, - l2Signer: Signer, - inboxAddress: string -) => { - console.log('deploying l1 side') - const l1 = await deployTokenBridgeL1Side(l1Signer) - - // fund L2 deployer so contracts can be deployed - await bridgeFundsToL2Deployer(l1Signer, inboxAddress) - - console.log('deploying l2 side') - const l2 = await deployTokenBridgeL2Side(l2Signer) - - console.log('initialising L2') - await l2.router.initialize(l1.router.address, l2.standardGateway.address) - await l2.beaconProxyFactory.initialize(l2.beacon.address) - await ( - await l2.standardGateway.initialize( - l1.standardGateway.address, - l2.router.address, - l2.beaconProxyFactory.address - ) - ).wait() - await ( - await l2.customGateway.initialize( - l1.customGateway.address, - l2.router.address - ) - ).wait() - - console.log('initialising L1') - await ( - await l1.router.initialize( - await l1Signer.getAddress(), - l1.standardGateway.address, - constants.AddressZero, - l2.router.address, - inboxAddress - ) - ).wait() - - await ( - await l1.standardGateway.initialize( - l2.standardGateway.address, - l1.router.address, - inboxAddress, - await l2.beaconProxyFactory.cloneableProxyHash(), - l2.beaconProxyFactory.address - ) - ).wait() - await ( - await l1.customGateway.initialize( - l2.customGateway.address, - l1.router.address, - inboxAddress, - await l1Signer.getAddress() - ) - ).wait() - - return { l1, l2 } -} - -const deployTokenBridgeL1Side = async (deployer: Signer) => { - const proxyAdmin = await new ProxyAdmin__factory(deployer).deploy() - await proxyAdmin.deployed() - console.log('proxyAdmin', proxyAdmin.address) - - const router = await deployContractBehindProxy( - deployer, - L1GatewayRouter__factory, - proxyAdmin.address, - L1GatewayRouter__factory.connect - ) - console.log('router', router.address) - - const standardGateway = await deployContractBehindProxy( - deployer, - L1ERC20Gateway__factory, - proxyAdmin.address, - L1ERC20Gateway__factory.connect - ) - console.log('standardGateway', standardGateway.address) - - const customGateway = await deployContractBehindProxy( - deployer, - L1CustomGateway__factory, - proxyAdmin.address, - L1CustomGateway__factory.connect - ) - console.log('customGateway', standardGateway.address) - - return { - proxyAdmin, - router, - standardGateway, - customGateway, - } -} - -const deployTokenBridgeL2Side = async (deployer: Signer) => { - const proxyAdmin = await new ProxyAdmin__factory(deployer).deploy() - await proxyAdmin.deployed() - - const router = await deployContractBehindProxy( - deployer, - L2GatewayRouter__factory, - proxyAdmin.address, - L2GatewayRouter__factory.connect - ) - - const standardGateway = await deployContractBehindProxy( - deployer, - L2ERC20Gateway__factory, - proxyAdmin.address, - L2ERC20Gateway__factory.connect - ) - - const customGateway = await deployContractBehindProxy( - deployer, - L2CustomGateway__factory, - proxyAdmin.address, - L2CustomGateway__factory.connect - ) - - const standardArbERC20 = await new StandardArbERC20__factory( - deployer - ).deploy() - await standardArbERC20.deployed() - - const beacon = await new UpgradeableBeacon__factory(deployer).deploy( - standardArbERC20.address - ) - await beacon.deployed() - - const beaconProxyFactory = await new BeaconProxyFactory__factory( - deployer - ).deploy() - await beaconProxyFactory.deployed() - - return { - proxyAdmin, - router, - standardGateway, - customGateway, - standardArbERC20, - beacon, - beaconProxyFactory, - } -} - -const bridgeFundsToL2Deployer = async ( - l1Signer: Signer, - inboxAddress: string -) => { - console.log('fund L2 deployer') - - const depositAmount = ethers.utils.parseUnits('3', 'ether') - - // bridge it - const InboxAbi = ['function depositEth() public payable returns (uint256)'] - const Inbox = new Contract(inboxAddress, InboxAbi, l1Signer) - await (await Inbox.depositEth({ value: depositAmount })).wait() - await sleep(30 * 1000) -} - -async function deployContractBehindProxy< - T extends ContractFactory, - U extends Contract ->( - deployer: Signer, - logicFactory: new (deployer: Signer) => T, - proxyAdmin: string, - contractFactory: (address: string, signer: Signer) => U -): Promise { - const logicContract = await new logicFactory(deployer).deploy() - await logicContract.deployed() - - const proxyContract = await new TransparentUpgradeableProxy__factory( - deployer - ).deploy(logicContract.address, proxyAdmin, '0x') - await proxyContract.deployed() - - return contractFactory(proxyContract.address, deployer) -} - -export const getLocalNetworks = async ( - l1Url: string, - l2Url: string -): Promise<{ - l1Network: L1Network - l2Network: Omit -}> => { - const l1Provider = new JsonRpcProvider(l1Url) - const l2Provider = new JsonRpcProvider(l2Url) - let deploymentData: string - try { - deploymentData = execSync( - 'docker exec nitro_testnode_sequencer_1 cat /config/deployment.json' - ).toString() - } catch (e) { - deploymentData = execSync( - 'docker exec nitro-testnode-sequencer-1 cat /config/deployment.json' - ).toString() - } - const parsedDeploymentData = JSON.parse(deploymentData) as { - bridge: string - inbox: string - ['sequencer-inbox']: string - rollup: string - } - - const rollup = RollupAdminLogic__factory.connect( - parsedDeploymentData.rollup, - l1Provider - ) - const confirmPeriodBlocks = await rollup.confirmPeriodBlocks() - - const bridge = Bridge__factory.connect( - parsedDeploymentData.bridge, - l1Provider - ) - const outboxAddr = await bridge.allowedOutboxList(0) - - const l1NetworkInfo = await l1Provider.getNetwork() - const l2NetworkInfo = await l2Provider.getNetwork() - - const l1Network: L1Network = { - blockTime: 10, - chainID: l1NetworkInfo.chainId, - explorerUrl: '', - isCustom: true, - name: 'EthLocal', - partnerChainIDs: [l2NetworkInfo.chainId], - isArbitrum: false, - } - - const l2Network: Omit = { - chainID: l2NetworkInfo.chainId, - confirmPeriodBlocks: confirmPeriodBlocks.toNumber(), - ethBridge: { - bridge: parsedDeploymentData.bridge, - inbox: parsedDeploymentData.inbox, - outbox: outboxAddr, - rollup: parsedDeploymentData.rollup, - sequencerInbox: parsedDeploymentData['sequencer-inbox'], - }, - explorerUrl: '', - isArbitrum: true, - isCustom: true, - name: 'ArbLocal', - partnerChainID: l1NetworkInfo.chainId, - retryableLifetimeSeconds: 7 * 24 * 60 * 60, - nitroGenesisBlock: 0, - nitroGenesisL1Block: 0, - depositTimeout: 900000, - } - return { - l1Network, - l2Network, - } -} - -export const getSigner = (provider: JsonRpcProvider, key?: string) => { - if (!key && !provider) - throw new Error('Provide at least one of key or provider.') - if (key) return new Wallet(key).connect(provider) - else return provider.getSigner(0) -} - -export function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)) -} - -async function main() { - const config = { - arbUrl: 'http://localhost:8547', - ethUrl: 'http://localhost:8545', - } - - const l1Provider = new ethers.providers.JsonRpcProvider(config.ethUrl) - const l2Provider = new ethers.providers.JsonRpcProvider(config.arbUrl) - - const l1DeployerWallet = new ethers.Wallet( - ethers.utils.sha256(ethers.utils.toUtf8Bytes('user_l1user')), - l1Provider - ) - const l2DeployerWallet = new ethers.Wallet( - ethers.utils.sha256(ethers.utils.toUtf8Bytes('user_l1user')), - l2Provider - ) - - const { l1Network, l2Network } = await setupTokenBridge( - l1DeployerWallet, - l2DeployerWallet, - config.ethUrl, - config.arbUrl - ) - - const NETWORK_FILE = 'network.json' - fs.writeFileSync( - NETWORK_FILE, - JSON.stringify({ l1Network, l2Network }, null, 2) - ) - console.log(NETWORK_FILE + ' updated') -} - -main().then(() => console.log('Done.')) diff --git a/test-e2e/orbitTokenBridge.ts b/test-e2e/orbitTokenBridge.ts new file mode 100644 index 0000000000..7dcf601d15 --- /dev/null +++ b/test-e2e/orbitTokenBridge.ts @@ -0,0 +1,358 @@ +import { + L1Network, + L1ToL2MessageGasEstimator, + L1ToL2MessageStatus, + L1TransactionReceipt, + L2Network, + L2TransactionReceipt, +} from '@arbitrum/sdk' +import { getBaseFee } from '@arbitrum/sdk/dist/lib/utils/lib' +import { JsonRpcProvider } from '@ethersproject/providers' +import { expect } from 'chai' +// import { ethers, Wallet } from '@arbitrum/sdk/node_modules/ethers' +import { setupTokenBridgeInLocalEnv } from '../scripts/local-deployment/deployCreatorAndCreateTokenBridge' +import { + ERC20, + ERC20__factory, + IERC20Bridge__factory, + IInbox__factory, + L1OrbitERC20Gateway__factory, + L1OrbitGatewayRouter__factory, + L2GatewayRouter__factory, + TestERC20, + TestERC20__factory, +} from '../build/types' +import { defaultAbiCoder } from 'ethers/lib/utils' +import { BigNumber, Wallet, ethers } from 'ethers' + +const config = { + arbUrl: 'http://localhost:8547', + ethUrl: 'http://localhost:8545', +} + +let l1Provider: JsonRpcProvider +let l2Provider: JsonRpcProvider + +let deployerL1Wallet: Wallet +let deployerL2Wallet: Wallet + +let userL1Wallet: Wallet +let userL2Wallet: Wallet + +let _l1Network: L1Network +let _l2Network: L2Network + +let token: TestERC20 +let l2Token: ERC20 +let nativeToken: ERC20 + +describe('orbitTokenBridge', () => { + // configure orbit token bridge + before(async function () { + l1Provider = new ethers.providers.JsonRpcProvider(config.ethUrl) + l2Provider = new ethers.providers.JsonRpcProvider(config.arbUrl) + + const deployerKey = ethers.utils.sha256( + ethers.utils.toUtf8Bytes('user_l1user') + ) + deployerL1Wallet = new ethers.Wallet(deployerKey, l1Provider) + deployerL2Wallet = new ethers.Wallet(deployerKey, l2Provider) + + console.log('setupOrbitTokenBridge') + const { l1Network, l2Network } = await setupTokenBridgeInLocalEnv() + + _l1Network = l1Network + _l2Network = l2Network + + // create user wallets and fund it + const userKey = ethers.utils.sha256(ethers.utils.toUtf8Bytes('user_wallet')) + userL1Wallet = new ethers.Wallet(userKey, l1Provider) + userL2Wallet = new ethers.Wallet(userKey, l2Provider) + await ( + await deployerL1Wallet.sendTransaction({ + to: userL1Wallet.address, + value: ethers.utils.parseEther('10.0'), + }) + ).wait() + }) + + it('should have deployed token bridge contracts', async function () { + // get router as entry point + const l1Router = L1OrbitGatewayRouter__factory.connect( + _l2Network.tokenBridge.l1GatewayRouter, + l1Provider + ) + + expect((await l1Router.defaultGateway()).toLowerCase()).to.be.eq( + _l2Network.tokenBridge.l1ERC20Gateway.toLowerCase() + ) + }) + + it('can deposit token via default gateway', async function () { + // fund user to be able to pay retryable fees + nativeToken = ERC20__factory.connect( + await getFeeToken(_l2Network.ethBridge.inbox, userL1Wallet), + userL1Wallet + ) + await ( + await nativeToken + .connect(deployerL1Wallet) + .transfer(userL1Wallet.address, ethers.utils.parseEther('1000')) + ).wait() + + // create token to be bridged + const tokenFactory = await new TestERC20__factory(userL1Wallet).deploy() + token = await tokenFactory.deployed() + await (await token.mint()).wait() + + // snapshot state before + + const userTokenBalanceBefore = await token.balanceOf(userL1Wallet.address) + const gatewayTokenBalanceBefore = await token.balanceOf( + _l2Network.tokenBridge.l1ERC20Gateway + ) + const userNativeTokenBalanceBefore = await nativeToken.balanceOf( + userL1Wallet.address + ) + const bridgeNativeTokenBalanceBefore = await nativeToken.balanceOf( + _l2Network.ethBridge.bridge + ) + + // approve token + const depositAmount = 350 + await ( + await token.approve(_l2Network.tokenBridge.l1ERC20Gateway, depositAmount) + ).wait() + + // calculate retryable params + const maxSubmissionCost = 0 + const callhook = '0x' + + const gateway = L1OrbitERC20Gateway__factory.connect( + _l2Network.tokenBridge.l1ERC20Gateway, + userL1Wallet + ) + const outboundCalldata = await gateway.getOutboundCalldata( + token.address, + userL1Wallet.address, + userL2Wallet.address, + depositAmount, + callhook + ) + + const l1ToL2MessageGasEstimate = new L1ToL2MessageGasEstimator(l2Provider) + const retryableParams = await l1ToL2MessageGasEstimate.estimateAll( + { + from: userL1Wallet.address, + to: userL2Wallet.address, + l2CallValue: BigNumber.from(0), + excessFeeRefundAddress: userL1Wallet.address, + callValueRefundAddress: userL1Wallet.address, + data: outboundCalldata, + }, + await getBaseFee(l1Provider), + l1Provider + ) + + const gasLimit = retryableParams.gasLimit.mul(40) + const maxFeePerGas = retryableParams.maxFeePerGas + const tokenTotalFeeAmount = gasLimit.mul(maxFeePerGas).mul(2) + + // approve fee amount + await ( + await nativeToken.approve( + _l2Network.tokenBridge.l1ERC20Gateway, + tokenTotalFeeAmount + ) + ).wait() + + // bridge it + const userEncodedData = defaultAbiCoder.encode( + ['uint256', 'bytes', 'uint256'], + [maxSubmissionCost, callhook, tokenTotalFeeAmount] + ) + + const router = L1OrbitGatewayRouter__factory.connect( + _l2Network.tokenBridge.l1GatewayRouter, + userL1Wallet + ) + + const depositTx = await router.outboundTransferCustomRefund( + token.address, + userL1Wallet.address, + userL2Wallet.address, + depositAmount, + gasLimit, + maxFeePerGas, + userEncodedData + ) + + // wait for L2 msg to be executed + await waitOnL2Msg(depositTx) + + ///// checks + + const l2TokenAddress = await router.calculateL2TokenAddress(token.address) + l2Token = ERC20__factory.connect(l2TokenAddress, l2Provider) + expect(await l2Token.balanceOf(userL2Wallet.address)).to.be.eq( + depositAmount + ) + + const userTokenBalanceAfter = await token.balanceOf(userL1Wallet.address) + expect(userTokenBalanceBefore.sub(userTokenBalanceAfter)).to.be.eq( + depositAmount + ) + + const gatewayTokenBalanceAfter = await token.balanceOf( + _l2Network.tokenBridge.l1ERC20Gateway + ) + expect(gatewayTokenBalanceAfter.sub(gatewayTokenBalanceBefore)).to.be.eq( + depositAmount + ) + + const userNativeTokenBalanceAfter = await nativeToken.balanceOf( + userL1Wallet.address + ) + expect( + userNativeTokenBalanceBefore.sub(userNativeTokenBalanceAfter) + ).to.be.eq(tokenTotalFeeAmount) + + const bridgeNativeTokenBalanceAfter = await nativeToken.balanceOf( + _l2Network.ethBridge.bridge + ) + expect( + bridgeNativeTokenBalanceAfter.sub(bridgeNativeTokenBalanceBefore) + ).to.be.eq(tokenTotalFeeAmount) + }) + + it('can withdraw token via default gateway', async function () { + // fund userL2Wallet so it can pay for L2 withdraw TX + await depositNativeToL2() + + // snapshot state before + const userL1TokenBalanceBefore = await token.balanceOf(userL1Wallet.address) + const userL2TokenBalanceBefore = await l2Token.balanceOf( + userL2Wallet.address + ) + const l1GatewayTokenBalanceBefore = await token.balanceOf( + _l2Network.tokenBridge.l1ERC20Gateway + ) + const l2TokenSupplyBefore = await l2Token.totalSupply() + + // start withdrawal + const withdrawalAmount = 250 + const l2Router = L2GatewayRouter__factory.connect( + _l2Network.tokenBridge.l2GatewayRouter, + userL2Wallet + ) + const withdrawTx = await l2Router[ + 'outboundTransfer(address,address,uint256,bytes)' + ](token.address, userL1Wallet.address, withdrawalAmount, '0x') + const withdrawReceipt = await withdrawTx.wait() + const l2Receipt = new L2TransactionReceipt(withdrawReceipt) + + // wait until dispute period passes and withdrawal is ready for execution + await sleep(5 * 1000) + + const messages = await l2Receipt.getL2ToL1Messages(userL1Wallet) + const l2ToL1Msg = messages[0] + const timeToWaitMs = 1000 + await l2ToL1Msg.waitUntilReadyToExecute(l2Provider, timeToWaitMs) + + // execute on L1 + await (await l2ToL1Msg.execute(l2Provider)).wait() + + //// checks + const userL1TokenBalanceAfter = await token.balanceOf(userL1Wallet.address) + expect(userL1TokenBalanceAfter.sub(userL1TokenBalanceBefore)).to.be.eq( + withdrawalAmount + ) + + const userL2TokenBalanceAfter = await l2Token.balanceOf( + userL2Wallet.address + ) + expect(userL2TokenBalanceBefore.sub(userL2TokenBalanceAfter)).to.be.eq( + withdrawalAmount + ) + + const l1GatewayTokenBalanceAfter = await token.balanceOf( + _l2Network.tokenBridge.l1ERC20Gateway + ) + expect( + l1GatewayTokenBalanceBefore.sub(l1GatewayTokenBalanceAfter) + ).to.be.eq(withdrawalAmount) + + const l2TokenSupplyAfter = await l2Token.totalSupply() + expect(l2TokenSupplyBefore.sub(l2TokenSupplyAfter)).to.be.eq( + withdrawalAmount + ) + }) +}) + +/** + * helper function to fund user wallet on L2 + */ +async function depositNativeToL2() { + /// deposit tokens + const amountToDeposit = ethers.utils.parseEther('2.0') + await ( + await nativeToken + .connect(userL1Wallet) + .approve(_l2Network.ethBridge.inbox, amountToDeposit) + ).wait() + + const depositFuncSig = { + name: 'depositERC20', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { + name: 'amount', + type: 'uint256', + }, + ], + } + const inbox = new ethers.Contract( + _l2Network.ethBridge.inbox, + [depositFuncSig], + userL1Wallet + ) + + const depositTx = await inbox.depositERC20(amountToDeposit) + + // wait for deposit to be processed + const depositRec = await L1TransactionReceipt.monkeyPatchEthDepositWait( + depositTx + ).wait() + await depositRec.waitForL2(l2Provider) +} + +async function waitOnL2Msg(tx: ethers.ContractTransaction) { + const retryableReceipt = await tx.wait() + const l1TxReceipt = new L1TransactionReceipt(retryableReceipt) + const messages = await l1TxReceipt.getL1ToL2Messages(l2Provider) + + // 1 msg expected + const messageResult = await messages[0].waitForStatus() + const status = messageResult.status + expect(status).to.be.eq(L1ToL2MessageStatus.REDEEMED) +} + +const getFeeToken = async (inbox: string, l1Provider: any) => { + const bridge = await IInbox__factory.connect(inbox, l1Provider).bridge() + + let feeToken = ethers.constants.AddressZero + + try { + feeToken = await IERC20Bridge__factory.connect( + bridge, + l1Provider + ).nativeToken() + } catch {} + + return feeToken +} + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/test-e2e/tokenBridgeDeploymentTest.ts b/test-e2e/tokenBridgeDeploymentTest.ts index bc10e4f482..2b4e478554 100644 --- a/test-e2e/tokenBridgeDeploymentTest.ts +++ b/test-e2e/tokenBridgeDeploymentTest.ts @@ -1,9 +1,11 @@ -import { L1Network, L2Network, getL1Network, getL2Network } from '@arbitrum/sdk' -import { JsonRpcProvider } from '@ethersproject/providers' +import { JsonRpcProvider, Provider, Filter } from '@ethersproject/providers' import { BeaconProxyFactory__factory, - IOwnable, + IERC20Bridge__factory, + IInbox__factory, IOwnable__factory, + IRollupCore__factory, + L1AtomicTokenBridgeCreator__factory, L1CustomGateway, L1CustomGateway__factory, L1ERC20Gateway, @@ -20,172 +22,179 @@ import { L2GatewayRouter__factory, L2WethGateway, L2WethGateway__factory, - ProxyAdmin, - ProxyAdmin__factory, } from '../build/types' +import { abi as UpgradeExecutorABI } from '@offchainlabs/upgrade-executor/build/contracts/src/UpgradeExecutor.sol/UpgradeExecutor.json' +import { RollupCore__factory } from '@arbitrum/sdk/dist/lib/abi/factories/RollupCore__factory' +import { applyAlias } from '../test/testhelper' import path from 'path' import fs from 'fs' -import { - addCustomNetwork, - l1Networks, - l2Networks, -} from '@arbitrum/sdk/dist/lib/dataEntities/networks' import { expect } from 'chai' import { ethers } from 'hardhat' +import { Contract } from 'ethers' const config = { + l1Url: process.env.BASECHAIN_RPC || 'http://localhost:8545', l2Url: process.env.ORBIT_RPC || 'http://localhost:8547', - l1Url: process.env.ARB_GOERLI_RPC || 'http://localhost:8545', } -let _l1Network: L1Network -let _l2Network: L2Network - -let _l1Provider: JsonRpcProvider -let _l2Provider: JsonRpcProvider +let l1Provider: JsonRpcProvider +let l2Provider: JsonRpcProvider describe('tokenBridge', () => { it('should have deployed and initialized token bridge contracts', async function () { - const { l1Network, l1Provider, l2Network, l2Provider } = - await getProvidersAndSetupNetworks({ - l1Url: config.l1Url, - l2Url: config.l2Url, - networkFilename: './network.json', - }) - - _l1Network = l1Network - _l2Network = l2Network + l1Provider = new JsonRpcProvider(config.l1Url) + l2Provider = new JsonRpcProvider(config.l2Url) + + /// get rollup, L1 creator and retryable sender as entrypoint, either from env vars or from network.json + let rollupAddress: string + let l1TokenBridgeCreator: string + let l1RetryableSender: string + if (process.env.ROLLUP_ADDRESS && process.env.L1_TOKEN_BRIDGE_CREATOR) { + rollupAddress = process.env.ROLLUP_ADDRESS as string + l1TokenBridgeCreator = process.env.L1_TOKEN_BRIDGE_CREATOR as string + l1RetryableSender = process.env.L1_RETRYABLE_SENDER as string + } else { + const localNetworkFile = path.join(__dirname, '..', 'network.json') + if (fs.existsSync(localNetworkFile)) { + const data = JSON.parse(fs.readFileSync(localNetworkFile).toString()) + rollupAddress = data['l2Network']['ethBridge']['rollup'] + l1TokenBridgeCreator = data['l1TokenBridgeCreator'] + l1RetryableSender = data['retryableSender'] + } else { + throw new Error( + "Can't find rollup address info. Either set ROLLUP_ADDRESS, L1_TOKEN_BRIDGE_CREATOR AND L1_RETRYABLE_SENDER env varS or provide network.json file" + ) + } + } - _l1Provider = l1Provider - _l2Provider = l2Provider + /// get addresses + const { l1, l2 } = await _getTokenBridgeAddresses( + rollupAddress, + l1TokenBridgeCreator + ) //// L1 checks - await checkL1RouterInitialization( - L1GatewayRouter__factory.connect( - _l2Network.tokenBridge.l1GatewayRouter, + // check that setting of retryable sender was not frontrun + const actualRetryableSender = + await L1AtomicTokenBridgeCreator__factory.connect( + l1TokenBridgeCreator, l1Provider - ) + ).retryableSender() + expect(actualRetryableSender.toLowerCase()).to.be.eq( + l1RetryableSender.toLowerCase() + ) + + await checkL1RouterInitialization( + L1GatewayRouter__factory.connect(l1.router, l1Provider), + l1, + l2 ) await checkL1StandardGatewayInitialization( - L1ERC20Gateway__factory.connect( - _l2Network.tokenBridge.l1ERC20Gateway, - l1Provider - ) + L1ERC20Gateway__factory.connect(l1.standardGateway, l1Provider), + l1, + l2 ) await checkL1CustomGatewayInitialization( - L1CustomGateway__factory.connect( - _l2Network.tokenBridge.l1CustomGateway, - l1Provider - ) + L1CustomGateway__factory.connect(l1.customGateway, l1Provider), + l1, + l2 ) - await checkL1WethGatewayInitialization( - L1WethGateway__factory.connect( - _l2Network.tokenBridge.l1WethGateway, - l1Provider + const usingFeeToken = await isUsingFeeToken(l1.inbox, l1Provider) + if (!usingFeeToken) + await checkL1WethGatewayInitialization( + L1WethGateway__factory.connect(l1.wethGateway, l1Provider), + l1, + l2 ) - ) //// L2 checks await checkL2RouterInitialization( - L2GatewayRouter__factory.connect( - _l2Network.tokenBridge.l2GatewayRouter, - l2Provider - ) + L2GatewayRouter__factory.connect(l2.router, l2Provider), + l1, + l2 ) await checkL2StandardGatewayInitialization( - L2ERC20Gateway__factory.connect( - _l2Network.tokenBridge.l2ERC20Gateway, - l2Provider - ) + L2ERC20Gateway__factory.connect(l2.standardGateway, l2Provider), + l1, + l2 ) await checkL2CustomGatewayInitialization( - L2CustomGateway__factory.connect( - _l2Network.tokenBridge.l2CustomGateway, - l2Provider - ) + L2CustomGateway__factory.connect(l2.customGateway, l2Provider), + l1, + l2 ) - const rollupOwner = await IOwnable__factory.connect( - _l2Network.ethBridge.rollup, - l1Provider - ).owner() - await checkOwnership( - rollupOwner.toLowerCase(), - ProxyAdmin__factory.connect( - _l2Network.tokenBridge.l1ProxyAdmin, - l1Provider - ), - ProxyAdmin__factory.connect( - _l2Network.tokenBridge.l2ProxyAdmin, - l2Provider - ), - L1GatewayRouter__factory.connect( - _l2Network.tokenBridge.l1GatewayRouter, - l1Provider - ), - L1CustomGateway__factory.connect( - _l2Network.tokenBridge.l1CustomGateway, - l1Provider + if (!usingFeeToken) { + await checkL2WethGatewayInitialization( + L2WethGateway__factory.connect(l2.wethGateway, l2Provider), + l1, + l2 ) - ) + } - await checkL2WethGatewayInitialization( - L2WethGateway__factory.connect( - _l2Network.tokenBridge.l2WethGateway, - l2Provider - ) + const upgExecutor = new ethers.Contract( + l2.upgradeExecutor, + UpgradeExecutorABI, + l2Provider ) + await checkL2UpgradeExecutorInitialization(upgExecutor, l1) + + await checkL1Ownership(l1) + await checkL2Ownership(l2) }) }) //// L1 contracts -async function checkL1RouterInitialization(l1Router: L1GatewayRouter) { +async function checkL1RouterInitialization( + l1Router: L1GatewayRouter, + l1: L1, + l2: L2 +) { console.log('checkL1RouterInitialization') expect((await l1Router.defaultGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1ERC20Gateway.toLowerCase() + l1.standardGateway.toLowerCase() ) - expect((await l1Router.inbox()).toLowerCase()).to.be.eq( - _l2Network.ethBridge.inbox.toLowerCase() + l1.inbox.toLowerCase() ) - expect((await l1Router.router()).toLowerCase()).to.be.eq( ethers.constants.AddressZero ) - expect((await l1Router.counterpartGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2GatewayRouter.toLowerCase() + l2.router.toLowerCase() ) } async function checkL1StandardGatewayInitialization( - l1ERC20Gateway: L1ERC20Gateway + l1ERC20Gateway: L1ERC20Gateway, + l1: L1, + l2: L2 ) { console.log('checkL1StandardGatewayInitialization') expect((await l1ERC20Gateway.counterpartGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2ERC20Gateway.toLowerCase() + l2.standardGateway.toLowerCase() ) expect((await l1ERC20Gateway.router()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1GatewayRouter.toLowerCase() + l1.router.toLowerCase() ) expect((await l1ERC20Gateway.inbox()).toLowerCase()).to.be.eq( - _l2Network.ethBridge.inbox.toLowerCase() + l1.inbox.toLowerCase() ) expect((await l1ERC20Gateway.l2BeaconProxyFactory()).toLowerCase()).to.be.eq( ( await L2ERC20Gateway__factory.connect( await l1ERC20Gateway.counterpartGateway(), - _l2Provider + l2Provider ).beaconProxyFactory() ).toLowerCase() ) @@ -193,7 +202,7 @@ async function checkL1StandardGatewayInitialization( ( await BeaconProxyFactory__factory.connect( await l1ERC20Gateway.l2BeaconProxyFactory(), - _l2Provider + l2Provider ).cloneableProxyHash() ).toLowerCase() ) @@ -203,61 +212,84 @@ async function checkL1StandardGatewayInitialization( } async function checkL1CustomGatewayInitialization( - l1CustomGateway: L1CustomGateway + l1CustomGateway: L1CustomGateway, + l1: L1, + l2: L2 ) { console.log('checkL1CustomGatewayInitialization') expect((await l1CustomGateway.counterpartGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2CustomGateway.toLowerCase() + l2.customGateway.toLowerCase() ) expect((await l1CustomGateway.router()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1GatewayRouter.toLowerCase() + l1.router.toLowerCase() ) expect((await l1CustomGateway.inbox()).toLowerCase()).to.be.eq( - _l2Network.ethBridge.inbox.toLowerCase() + l1.inbox.toLowerCase() ) - // TODO - // owner check - expect((await l1CustomGateway.whitelist()).toLowerCase()).to.be.eq( ethers.constants.AddressZero ) } -async function checkL1WethGatewayInitialization(l1WethGateway: L1WethGateway) { +async function checkL1WethGatewayInitialization( + l1WethGateway: L1WethGateway, + l1: L1, + l2: L2 +) { console.log('checkL1WethGatewayInitialization') expect((await l1WethGateway.counterpartGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2WethGateway.toLowerCase() + l2.wethGateway.toLowerCase() ) expect((await l1WethGateway.router()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1GatewayRouter.toLowerCase() + l1.router.toLowerCase() ) expect((await l1WethGateway.inbox()).toLowerCase()).to.be.eq( - _l2Network.ethBridge.inbox.toLowerCase() + l1.inbox.toLowerCase() ) - expect((await l1WethGateway.l1Weth()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1Weth.toLowerCase() + expect((await l1WethGateway.l1Weth()).toLowerCase()).to.not.be.eq( + ethers.constants.AddressZero ) - expect((await l1WethGateway.l2Weth()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2Weth.toLowerCase() + expect((await l1WethGateway.l2Weth()).toLowerCase()).to.not.be.eq( + ethers.constants.AddressZero ) } +async function checkL2UpgradeExecutorInitialization( + l2Executor: Contract, + l1: L1 +) { + console.log('checkL2UpgradeExecutorInitialization') + + //// check assigned/revoked roles are correctly set + const adminRole = await l2Executor.ADMIN_ROLE() + const executorRole = await l2Executor.EXECUTOR_ROLE() + + expect(await l2Executor.hasRole(adminRole, l2Executor.address)).to.be.true + expect(await l2Executor.hasRole(executorRole, l1.rollupOwner)).to.be.true + const aliasedL1Executor = applyAlias(l1.upgradeExecutor) + expect(await l2Executor.hasRole(executorRole, aliasedL1Executor)).to.be.true +} + //// L2 contracts -async function checkL2RouterInitialization(l2Router: L2GatewayRouter) { +async function checkL2RouterInitialization( + l2Router: L2GatewayRouter, + l1: L1, + l2: L2 +) { console.log('checkL2RouterInitialization') expect((await l2Router.defaultGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2ERC20Gateway.toLowerCase() + l2.standardGateway.toLowerCase() ) expect((await l2Router.router()).toLowerCase()).to.be.eq( @@ -265,28 +297,30 @@ async function checkL2RouterInitialization(l2Router: L2GatewayRouter) { ) expect((await l2Router.counterpartGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1GatewayRouter.toLowerCase() + l1.router.toLowerCase() ) } async function checkL2StandardGatewayInitialization( - l2ERC20Gateway: L2ERC20Gateway + l2ERC20Gateway: L2ERC20Gateway, + l1: L1, + l2: L2 ) { console.log('checkL2StandardGatewayInitialization') expect((await l2ERC20Gateway.counterpartGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1ERC20Gateway.toLowerCase() + l1.standardGateway.toLowerCase() ) expect((await l2ERC20Gateway.router()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2GatewayRouter.toLowerCase() + l2.router.toLowerCase() ) expect((await l2ERC20Gateway.beaconProxyFactory()).toLowerCase()).to.be.eq( ( await L1ERC20Gateway__factory.connect( await l2ERC20Gateway.counterpartGateway(), - _l1Provider + l1Provider ).l2BeaconProxyFactory() ).toLowerCase() ) @@ -295,112 +329,285 @@ async function checkL2StandardGatewayInitialization( ( await L1ERC20Gateway__factory.connect( await l2ERC20Gateway.counterpartGateway(), - _l1Provider + l1Provider ).cloneableProxyHash() ).toLowerCase() ) } async function checkL2CustomGatewayInitialization( - l2CustomGateway: L2CustomGateway + l2CustomGateway: L2CustomGateway, + l1: L1, + l2: L2 ) { console.log('checkL2CustomGatewayInitialization') expect((await l2CustomGateway.counterpartGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1CustomGateway.toLowerCase() + l1.customGateway.toLowerCase() ) expect((await l2CustomGateway.router()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2GatewayRouter.toLowerCase() + l2.router.toLowerCase() ) } -async function checkL2WethGatewayInitialization(l2WethGateway: L2WethGateway) { +async function checkL2WethGatewayInitialization( + l2WethGateway: L2WethGateway, + l1: L1, + l2: L2 +) { console.log('checkL2WethGatewayInitialization') expect((await l2WethGateway.counterpartGateway()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1WethGateway.toLowerCase() + l1.wethGateway.toLowerCase() ) expect((await l2WethGateway.router()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2GatewayRouter.toLowerCase() + l2.router.toLowerCase() ) - expect((await l2WethGateway.l1Weth()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l1Weth.toLowerCase() + expect((await l2WethGateway.l1Weth()).toLowerCase()).to.not.be.eq( + ethers.constants.AddressZero ) - expect((await l2WethGateway.l2Weth()).toLowerCase()).to.be.eq( - _l2Network.tokenBridge.l2Weth.toLowerCase() + expect((await l2WethGateway.l2Weth()).toLowerCase()).to.not.be.eq( + ethers.constants.AddressZero ) } -async function checkOwnership( - rollupOwner: string, - l1ProxyAdmin: ProxyAdmin, - l2ProxyAdmin: ProxyAdmin, - l1Router: L1GatewayRouter, - l1CustomGateway: L1CustomGateway +async function checkL1Ownership(l1: L1) { + console.log('checkL1Ownership') + + // check proxyAdmins + expect(await _getProxyAdmin(l1.router, l1Provider)).to.be.eq(l1.proxyAdmin) + expect(await _getProxyAdmin(l1.standardGateway, l1Provider)).to.be.eq( + l1.proxyAdmin + ) + expect(await _getProxyAdmin(l1.customGateway, l1Provider)).to.be.eq( + l1.proxyAdmin + ) + if (l1.wethGateway !== ethers.constants.AddressZero) { + expect(await _getProxyAdmin(l1.wethGateway, l1Provider)).to.be.eq( + l1.proxyAdmin + ) + } + expect(await _getProxyAdmin(l1.upgradeExecutor, l1Provider)).to.be.eq( + l1.proxyAdmin + ) + + // check ownables + expect(await _getOwner(l1.proxyAdmin, l1Provider)).to.be.eq( + l1.upgradeExecutor + ) + expect(await _getOwner(l1.router, l1Provider)).to.be.eq(l1.upgradeExecutor) + expect(await _getOwner(l1.customGateway, l1Provider)).to.be.eq( + l1.upgradeExecutor + ) +} + +async function checkL2Ownership(l2: L2) { + console.log('checkL2Ownership') + + const l2ProxyAdmin = await _getProxyAdmin(l2.router, l2Provider) + + // check proxyAdmins + expect(await _getProxyAdmin(l2.router, l2Provider)).to.be.eq(l2ProxyAdmin) + expect(await _getProxyAdmin(l2.standardGateway, l2Provider)).to.be.eq( + l2ProxyAdmin + ) + expect(await _getProxyAdmin(l2.customGateway, l2Provider)).to.be.eq( + l2ProxyAdmin + ) + + if (l2.wethGateway != ethers.constants.AddressZero) { + expect(await _getProxyAdmin(l2.wethGateway, l2Provider)).to.be.eq( + l2ProxyAdmin + ) + } + expect(await _getProxyAdmin(l2.upgradeExecutor, l2Provider)).to.be.eq( + l2ProxyAdmin + ) + + // check ownables + expect(await _getOwner(l2ProxyAdmin, l2Provider)).to.be.eq(l2.upgradeExecutor) +} + +//// utils +async function isUsingFeeToken(inbox: string, l1Provider: JsonRpcProvider) { + const bridge = await IInbox__factory.connect(inbox, l1Provider).bridge() + + try { + await IERC20Bridge__factory.connect(bridge, l1Provider).nativeToken() + } catch { + return false + } + + return true +} + +async function _getTokenBridgeAddresses( + rollupAddress: string, + l1TokenBridgeCreatorAddress: string ) { - console.log('checkL2ProxyAdminInitialization') + const inboxAddress = await RollupCore__factory.connect( + rollupAddress, + l1Provider + ).inbox() + + const l1TokenBridgeCreator = L1AtomicTokenBridgeCreator__factory.connect( + l1TokenBridgeCreatorAddress, + l1Provider + ) - expect(rollupOwner).to.be.eq((await l1ProxyAdmin.owner()).toLowerCase()) - expect(rollupOwner).to.be.eq((await l2ProxyAdmin.owner()).toLowerCase()) - expect(rollupOwner).to.be.eq((await l1Router.owner()).toLowerCase()) - expect(rollupOwner).to.be.eq((await l1CustomGateway.owner()).toLowerCase()) + //// L1 + // find all the events emitted by this address + + const filter: Filter = { + address: l1TokenBridgeCreatorAddress, + topics: [ + ethers.utils.id( + 'OrbitTokenBridgeCreated(address,address,address,address,address,address,address,address)' + ), + ethers.utils.hexZeroPad(inboxAddress, 32), + ], + } + + const currentBlock = await l1Provider.getBlockNumber() + const fromBlock = currentBlock - 100000 // ~last 24h on + const logs = await l1Provider.getLogs({ + ...filter, + fromBlock: fromBlock, + toBlock: 'latest', + }) + + if (logs.length === 0) { + throw new Error( + "Couldn't find any OrbitTokenBridgeCreated events in block range[" + + fromBlock + + ',latest]' + ) + } + + const logData = l1TokenBridgeCreator.interface.parseLog(logs[0]) + + const { + inbox, + owner, + router, + standardGateway, + customGateway, + wethGateway, + proxyAdmin, + upgradeExecutor, + } = logData.args + const l1 = { + inbox: inbox.toLowerCase(), + rollupOwner: owner.toLowerCase(), + router: router.toLowerCase(), + standardGateway: standardGateway.toLowerCase(), + customGateway: customGateway.toLowerCase(), + wethGateway: wethGateway.toLowerCase(), + proxyAdmin: proxyAdmin.toLowerCase(), + upgradeExecutor: upgradeExecutor.toLowerCase(), + } + + const usingFeeToken = await isUsingFeeToken(l1.inbox, l1Provider) + + const chainId = await IRollupCore__factory.connect( + rollupAddress, + l1Provider + ).chainId() + + //// L2 + const l2 = { + router: ( + await l1TokenBridgeCreator.getCanonicalL2RouterAddress(chainId) + ).toLowerCase(), + standardGateway: ( + await l1TokenBridgeCreator.getCanonicalL2StandardGatewayAddress(chainId) + ).toLowerCase(), + customGateway: ( + await l1TokenBridgeCreator.getCanonicalL2CustomGatewayAddress(chainId) + ).toLowerCase(), + wethGateway: (usingFeeToken + ? ethers.constants.AddressZero + : await l1TokenBridgeCreator.getCanonicalL2WethGatewayAddress(chainId) + ).toLowerCase(), + weth: (usingFeeToken + ? ethers.constants.AddressZero + : await l1TokenBridgeCreator.getCanonicalL2WethAddress(chainId) + ).toLowerCase(), + upgradeExecutor: ( + await l1TokenBridgeCreator.getCanonicalL2UpgradeExecutorAddress(chainId) + ).toLowerCase(), + } + + return { + l1, + l2, + } } -export const getProvidersAndSetupNetworks = async (setupConfig: { - l1Url: string - l2Url: string - networkFilename?: string -}): Promise<{ - l1Network: L1Network - l2Network: L2Network - l1Provider: JsonRpcProvider - l2Provider: JsonRpcProvider -}> => { - const l1Provider = new JsonRpcProvider(setupConfig.l1Url) - const l2Provider = new JsonRpcProvider(setupConfig.l2Url) - - if (setupConfig.networkFilename) { - // check if theres an existing network available - const localNetworkFile = path.join( - __dirname, - '..', - setupConfig.networkFilename +async function _getProxyAdmin( + contractAddress: string, + provider: Provider +): Promise { + return ( + await _getAddressAtStorageSlot( + contractAddress, + provider, + '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103' ) - if (fs.existsSync(localNetworkFile)) { - const { l1Network, l2Network } = JSON.parse( - fs.readFileSync(localNetworkFile).toString() - ) as { - l1Network: L1Network - l2Network: L2Network - } + ).toLowerCase() +} - const existingL1Network = l1Networks[l1Network.chainID.toString()] - const existingL2Network = l2Networks[l2Network.chainID.toString()] - if (!existingL2Network) { - addCustomNetwork({ - // dont add the l1 network if it's already been added - customL1Network: existingL1Network ? undefined : l1Network, - customL2Network: l2Network, - }) - } +async function _getOwner( + contractAddress: string, + provider: Provider +): Promise { + return ( + await IOwnable__factory.connect(contractAddress, provider).owner() + ).toLowerCase() +} - return { - l1Network, - l1Provider, - l2Network, - l2Provider, - } - } else throw Error(`Missing file ${localNetworkFile}`) - } else { - return { - l1Network: await getL1Network(l1Provider), - l1Provider, - l2Network: await getL2Network(l2Provider), - l2Provider, - } +async function _getAddressAtStorageSlot( + contractAddress: string, + provider: Provider, + storageSlotBytes: string +): Promise { + const storageValue = await provider.getStorageAt( + contractAddress, + storageSlotBytes + ) + + if (!storageValue) { + return '' } + + // remove excess bytes + const formatAddress = + storageValue.substring(0, 2) + storageValue.substring(26) + + // return address as checksum address + return ethers.utils.getAddress(formatAddress) +} + +interface L1 { + inbox: string + rollupOwner: string + router: string + standardGateway: string + customGateway: string + wethGateway: string + proxyAdmin: string + upgradeExecutor: string +} + +interface L2 { + router: string + standardGateway: string + customGateway: string + wethGateway: string + weth: string + upgradeExecutor: string } diff --git a/test-foundry/GatewayRouter.t.sol b/test-foundry/GatewayRouter.t.sol new file mode 100644 index 0000000000..57d5642143 --- /dev/null +++ b/test-foundry/GatewayRouter.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import { GatewayRouter } from "contracts/tokenbridge/libraries/gateway/GatewayRouter.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +abstract contract GatewayRouterTest is Test { + GatewayRouter public router; + address public defaultGateway; + + // retryable params + uint256 public maxSubmissionCost; + uint256 public maxGas = 1000000000; + uint256 public gasPriceBid = 3; + uint256 public retryableCost; + address public creditBackAddress = makeAddr("creditBackAddress"); + + /* solhint-disable func-name-mixedcase */ + function test_getGateway_DefaultGateway(address token) public { + address gateway = router.getGateway(token); + assertEq(gateway, defaultGateway, "Invalid gateway"); + } + + function test_finalizeInboundTransfer() public { + vm.expectRevert("ONLY_OUTBOUND_ROUTER"); + router.finalizeInboundTransfer(address(1), address(2), address(3), 0, ""); + } +} diff --git a/test-foundry/IOwnable.sol b/test-foundry/IOwnable.sol new file mode 100644 index 0000000000..20735f0dd2 --- /dev/null +++ b/test-foundry/IOwnable.sol @@ -0,0 +1,10 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +// solhint-disable-next-line compiler-version +pragma solidity >=0.4.21 <0.9.0; + +interface IOwnable { + function owner() external view returns (address); +} diff --git a/test-foundry/L1ArbitrumExtendedGateway.t.sol b/test-foundry/L1ArbitrumExtendedGateway.t.sol new file mode 100644 index 0000000000..128e5b86db --- /dev/null +++ b/test-foundry/L1ArbitrumExtendedGateway.t.sol @@ -0,0 +1,469 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "contracts/tokenbridge/ethereum/gateway/L1ArbitrumExtendedGateway.sol"; +import { TestERC20 } from "contracts/tokenbridge/test/TestERC20.sol"; +import { InboxMock } from "contracts/tokenbridge/test/InboxMock.sol"; + +abstract contract L1ArbitrumExtendedGatewayTest is Test { + IL1ArbitrumGateway public l1Gateway; + IERC20 public token; + + address public l2Gateway = makeAddr("l2Gateway"); + address public router = makeAddr("router"); + address public inbox; + address public user = makeAddr("user"); + + // retryable params + uint256 public maxSubmissionCost; + uint256 public maxGas = 1000000000; + uint256 public gasPriceBid = 3; + uint256 public retryableCost; + address public creditBackAddress = makeAddr("creditBackAddress"); + + // fuzzer behaves weirdly when it picks up this address which is used internally for issuing cheatcodes + address internal constant FOUNDRY_CHEATCODE_ADDRESS = + 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + + /* solhint-disable func-name-mixedcase */ + function test_encodeWithdrawal(uint256 exitNum, address dest) public { + bytes32 encodedWithdrawal = L1ArbitrumExtendedGateway(address(l1Gateway)).encodeWithdrawal( + exitNum, + dest + ); + bytes32 expectedEncoding = keccak256(abi.encode(exitNum, dest)); + + assertEq(encodedWithdrawal, expectedEncoding, "Invalid encodeWithdrawal"); + } + + function test_finalizeInboundTransfer() public virtual { + // fund gateway with tokens being withdrawn + vm.prank(address(l1Gateway)); + TestERC20(address(token)).mint(); + + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway)); + + // withdrawal params + address from = address(3000); + uint256 withdrawalAmount = 25; + uint256 exitNum = 7; + bytes memory callHookData = ""; + bytes memory data = abi.encode(exitNum, callHookData); + + InboxMock(address(inbox)).setL2ToL1Sender(l2Gateway); + + // trigger withdrawal + vm.prank(address(IInbox(l1Gateway.inbox()).bridge())); + l1Gateway.finalizeInboundTransfer(address(token), from, user, withdrawalAmount, data); + + // check tokens are properly released + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceAfter - userBalanceBefore, withdrawalAmount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway)); + assertEq( + l1GatewayBalanceBefore - l1GatewayBalanceAfter, + withdrawalAmount, + "Wrong l1 gateway balance" + ); + } + + function test_finalizeInboundTransfer_revert_NotFromBridge() public { + address notBridge = address(300); + vm.prank(notBridge); + vm.expectRevert("NOT_FROM_BRIDGE"); + l1Gateway.finalizeInboundTransfer(address(token), user, user, 100, ""); + } + + function test_finalizeInboundTransfer_revert_OnlyCounterpartGateway() public { + address notCounterPartGateway = address(400); + InboxMock(address(inbox)).setL2ToL1Sender(notCounterPartGateway); + + // trigger withdrawal + vm.prank(address(IInbox(l1Gateway.inbox()).bridge())); + vm.expectRevert("ONLY_COUNTERPART_GATEWAY"); + l1Gateway.finalizeInboundTransfer(address(token), user, user, 100, ""); + } + + function test_getExternalCall( + uint256 exitNum, + address dest, + bytes memory data + ) public { + (address target, bytes memory extData) = L1ArbitrumExtendedGateway(address(l1Gateway)) + .getExternalCall(exitNum, dest, data); + + assertEq(target, dest, "Invalid dest"); + assertEq(extData, data, "Invalid data"); + + bytes32 exitId = keccak256(abi.encode(exitNum, dest)); + (bool isExit, address newTo, bytes memory newData) = L1ArbitrumExtendedGateway( + address(l1Gateway) + ).redirectedExits(exitId); + assertEq(isExit, false, "Invalid isExit"); + assertEq(newTo, address(0), "Invalid _newTo"); + assertEq(newData.length, 0, "Invalid _newData"); + } + + function test_getExternalCall_Redirected( + uint256 exitNum, + address initialDest, + address newDest + ) public { + // redirect + vm.prank(initialDest); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDest, + newDest, + "", + "" + ); + + // check getExternalCall returns new destination + (address target, bytes memory extData) = L1ArbitrumExtendedGateway(address(l1Gateway)) + .getExternalCall(exitNum, initialDest, ""); + assertEq(target, newDest, "Invalid dest"); + assertEq(extData.length, 0, "Invalid data"); + + // check exit redirection is properly stored + bytes32 exitId = keccak256(abi.encode(exitNum, initialDest)); + (bool isExit, address newTo, bytes memory newData) = L1ArbitrumExtendedGateway( + address(l1Gateway) + ).redirectedExits(exitId); + assertEq(isExit, true, "Invalid isExit"); + assertEq(newTo, newDest, "Invalid _newTo"); + assertEq(newData.length, 0, "Invalid _newData"); + } + + function test_outboundTransferCustomRefund_revert_ExtraDataDisabled() public { + bytes memory callHookData = abi.encodeWithSignature("doSomething()"); + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + vm.prank(router); + vm.expectRevert("EXTRA_DATA_DISABLED"); + l1Gateway.outboundTransferCustomRefund( + address(token), + user, + user, + 400, + 0.1 ether, + 0.01 ether, + routerEncodedData + ); + } + + function test_outboundTransferCustomRefund_revert_L1NotContract() public { + address invalidTokenAddress = address(70); + + vm.prank(router); + vm.expectRevert("L1_NOT_CONTRACT"); + l1Gateway.outboundTransferCustomRefund( + address(invalidTokenAddress), + user, + user, + 400, + 0.1 ether, + 0.01 ether, + buildRouterEncodedData("") + ); + } + + function test_outboundTransferCustomRefund_revert_NotFromRouter() public { + vm.expectRevert("NOT_FROM_ROUTER"); + l1Gateway.outboundTransferCustomRefund( + address(token), + user, + user, + 400, + 0.1 ether, + 0.01 ether, + "" + ); + } + + function test_supportsInterface(bytes4 iface) public { + bool expected = false; + if ( + iface == type(IERC165).interfaceId || + iface == IL1ArbitrumGateway.outboundTransferCustomRefund.selector + ) { + expected = true; + } + + assertEq(l1Gateway.supportsInterface(iface), expected, "Interface shouldn't be supported"); + } + + function test_transferExitAndCall_EmptyData_NotRedirected( + uint256 exitNum, + address initialDestination, + address newDestination + ) public { + bytes memory newData; + bytes memory data; + + // check event + vm.expectEmit(true, true, true, true); + emit WithdrawRedirected(initialDestination, newDestination, exitNum, newData, data, false); + + // do it + vm.prank(initialDestination); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDestination, + newDestination, + newData, + data + ); + + // check exit data is properly updated + bytes32 exitId = keccak256(abi.encode(exitNum, initialDestination)); + (bool isExit, address exitTo, bytes memory exitData) = L1ArbitrumExtendedGateway( + address(l1Gateway) + ).redirectedExits(exitId); + assertEq(isExit, true, "Invalid isExit"); + assertEq(exitTo, newDestination, "Invalid exitTo"); + assertEq(exitData.length, 0, "Invalid exitData"); + } + + function test_transferExitAndCall_EmptyData_Redirected( + uint256 exitNum, + address initialDestination + ) public { + bytes memory data; + address intermediateDestination = address(new TestExitReceiver()); + + // transfer exit + vm.prank(initialDestination); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDestination, + intermediateDestination, + "", + data + ); + + address finalDestination = address(new TestExitReceiver()); + vm.prank(intermediateDestination); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDestination, + finalDestination, + "", + data + ); + + // check exit data is properly updated + bytes32 exitId = keccak256(abi.encode(exitNum, initialDestination)); + (bool isExit, address exitTo, bytes memory exitData) = L1ArbitrumExtendedGateway( + address(l1Gateway) + ).redirectedExits(exitId); + assertEq(isExit, true, "Invalid isExit"); + assertEq(exitTo, finalDestination, "Invalid exitTo"); + assertEq(exitData.length, 0, "Invalid exitData"); + } + + function test_transferExitAndCall_NonEmptyData(uint256 exitNum, address initialDestination) + public + { + bytes memory newData; + bytes memory data = abi.encode("fun()"); + address newDestination = address(new TestExitReceiver()); + + // check events + vm.expectEmit(true, true, true, true); + emit ExitHookTriggered(initialDestination, exitNum, data); + + vm.expectEmit(true, true, true, true); + emit WithdrawRedirected(initialDestination, newDestination, exitNum, newData, data, true); + + // do it + vm.prank(initialDestination); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDestination, + newDestination, + newData, + data + ); + + // check exit data is properly updated + bytes32 exitId = keccak256(abi.encode(exitNum, initialDestination)); + (bool isExit, address exitTo, bytes memory exitData) = L1ArbitrumExtendedGateway( + address(l1Gateway) + ).redirectedExits(exitId); + assertEq(isExit, true, "Invalid isExit"); + assertEq(exitTo, newDestination, "Invalid exitTo"); + assertEq(exitData.length, 0, "Invalid exitData"); + } + + function test_transferExitAndCall_NonEmptyData_Redirected( + uint256 exitNum, + address initialDestination + ) public { + bytes memory data = abi.encode("run()"); + address intermediateDestination = address(new TestExitReceiver()); + + // transfer exit + vm.prank(initialDestination); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDestination, + intermediateDestination, + "", + data + ); + + address finalDestination = address(new TestExitReceiver()); + vm.prank(intermediateDestination); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDestination, + finalDestination, + "", + data + ); + + // check exit data is properly updated + bytes32 exitId = keccak256(abi.encode(exitNum, initialDestination)); + (bool isExit, address exitTo, bytes memory exitData) = L1ArbitrumExtendedGateway( + address(l1Gateway) + ).redirectedExits(exitId); + assertEq(isExit, true, "Invalid isExit"); + assertEq(exitTo, finalDestination, "Invalid exitTo"); + assertEq(exitData.length, 0, "Invalid exitData"); + } + + function test_transferExitAndCall_revert_NotExpectedSender() public { + address nonSender = address(800); + vm.expectRevert("NOT_EXPECTED_SENDER"); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + 4, + nonSender, + address(2), + "", + "" + ); + } + + function test_transferExitAndCall_revert_NoDataAllowed() public { + bytes memory nonEmptyData = bytes("abc"); + vm.prank(address(1)); + vm.expectRevert("NO_DATA_ALLOWED"); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + 4, + address(1), + address(2), + nonEmptyData, + "" + ); + } + + function test_transferExitAndCall_revert_ToNotContract(address initialDestination) public { + bytes memory data = abi.encode("execute()"); + address nonContractNewDestination = address(15); + + vm.prank(initialDestination); + vm.expectRevert("TO_NOT_CONTRACT"); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + 4, + initialDestination, + nonContractNewDestination, + "", + data + ); + } + + function test_transferExitAndCall_revert_TransferHookFail( + uint256 exitNum, + address initialDestination + ) public { + bytes memory data = abi.encode("failIt"); + address newDestination = address(new TestExitReceiver()); + + vm.prank(initialDestination); + vm.expectRevert("TRANSFER_HOOK_FAIL"); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDestination, + newDestination, + "", + data + ); + } + + function test_transferExitAndCall_revert_TransferHookFail_Redirected( + uint256 exitNum, + address initialDestination + ) public { + bytes memory data = abi.encode("abc"); + address intermediateDestination = address(new TestExitReceiver()); + + vm.prank(initialDestination); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDestination, + intermediateDestination, + "", + data + ); + + bytes memory failData = abi.encode("failIt"); + address finalDestination = address(new TestExitReceiver()); + + vm.prank(intermediateDestination); + vm.expectRevert("TRANSFER_HOOK_FAIL"); + L1ArbitrumExtendedGateway(address(l1Gateway)).transferExitAndCall( + exitNum, + initialDestination, + finalDestination, + "", + failData + ); + } + + //// + // Helper functions + //// + function buildRouterEncodedData(bytes memory callHookData) + internal + view + virtual + returns (bytes memory) + { + bytes memory userEncodedData = abi.encode(maxSubmissionCost, callHookData); + bytes memory routerEncodedData = abi.encode(user, userEncodedData); + + return routerEncodedData; + } + + ///// + /// Event declarations + ///// + event WithdrawRedirected( + address indexed from, + address indexed to, + uint256 indexed exitNum, + bytes newData, + bytes data, + bool madeExternalCall + ); + event ExitHookTriggered(address sender, uint256 exitNum, bytes data); +} + +contract TestExitReceiver is ITradeableExitReceiver { + event ExitHookTriggered(address sender, uint256 exitNum, bytes data); + + function onExitTransfer( + address sender, + uint256 exitNum, + bytes calldata data + ) external override returns (bool) { + emit ExitHookTriggered(sender, exitNum, data); + return keccak256(data) != keccak256(abi.encode("failIt")); + } +} diff --git a/test-foundry/L1CustomGateway.t.sol b/test-foundry/L1CustomGateway.t.sol new file mode 100644 index 0000000000..ca4f94048d --- /dev/null +++ b/test-foundry/L1CustomGateway.t.sol @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1ArbitrumExtendedGatewayTest } from "./L1ArbitrumExtendedGateway.t.sol"; +import { L1CustomGateway, IInbox, ITokenGateway, IERC165, IL1ArbitrumGateway, IERC20 } from "contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol"; +import { L2CustomGateway } from "contracts/tokenbridge/arbitrum/gateway/L2CustomGateway.sol"; +import { TestERC20 } from "contracts/tokenbridge/test/TestERC20.sol"; +import { InboxMock } from "contracts/tokenbridge/test/InboxMock.sol"; + +contract L1CustomGatewayTest is L1ArbitrumExtendedGatewayTest { + // gateway params + address public owner = makeAddr("owner"); + + function setUp() public virtual { + inbox = address(new InboxMock()); + + l1Gateway = new L1CustomGateway(); + L1CustomGateway(address(l1Gateway)).initialize(l2Gateway, router, inbox, owner); + + token = IERC20(address(new TestERC20())); + + maxSubmissionCost = 20; + retryableCost = maxSubmissionCost + gasPriceBid * maxGas; + + // fund user and router + vm.prank(user); + TestERC20(address(token)).mint(); + vm.deal(router, 100 ether); + vm.deal(address(token), 100 ether); + vm.deal(owner, 100 ether); + } + + /* solhint-disable func-name-mixedcase */ + function test_calculateL2TokenAddress(address l1Token, address l2Token) public virtual { + vm.assume(l1Token != FOUNDRY_CHEATCODE_ADDRESS && l2Token != FOUNDRY_CHEATCODE_ADDRESS); + vm.deal(l1Token, 100 ether); + + // register token to gateway + vm.mockCall( + address(l1Token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(l1Token)); + L1CustomGateway(address(l1Gateway)).registerTokenToL2{ value: retryableCost }( + l2Token, + maxGas, + gasPriceBid, + maxSubmissionCost, + makeAddr("creditBackAddress") + ); + + assertEq(l1Gateway.calculateL2TokenAddress(l1Token), l2Token, "Invalid L2 token address"); + } + + function test_forceRegisterTokenToL2() public virtual { + address[] memory l1Tokens = new address[](2); + l1Tokens[0] = makeAddr("l1Token1"); + l1Tokens[1] = makeAddr("l1Token2"); + address[] memory l2Tokens = new address[](2); + l2Tokens[0] = makeAddr("l2Token1"); + l2Tokens[1] = makeAddr("l2Token2"); + + // expect events + vm.expectEmit(true, true, true, true); + emit TokenSet(l1Tokens[0], l2Tokens[0]); + + vm.expectEmit(true, true, true, true); + emit TokenSet(l1Tokens[1], l2Tokens[1]); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + abi.encodeWithSelector(L2CustomGateway.registerTokenFromL1.selector, l1Tokens, l2Tokens) + ); + + // register token to gateway + vm.prank(owner); + uint256 seqNum = L1CustomGateway(address(l1Gateway)).forceRegisterTokenToL2{ + value: retryableCost + }(l1Tokens, l2Tokens, maxGas, gasPriceBid, maxSubmissionCost); + + ///// checks + assertEq( + L1CustomGateway(address(l1Gateway)).l1ToL2Token(l1Tokens[0]), + l2Tokens[0], + "Invalid L2 token" + ); + + assertEq( + L1CustomGateway(address(l1Gateway)).l1ToL2Token(l1Tokens[1]), + l2Tokens[1], + "Invalid L2 token" + ); + + assertEq(seqNum, 0, "Invalid seqNum"); + } + + function test_forceRegisterTokenToL2_revert_InvalidLength() public virtual { + vm.prank(owner); + vm.expectRevert("INVALID_LENGTHS"); + L1CustomGateway(address(l1Gateway)).forceRegisterTokenToL2{ value: retryableCost }( + new address[](1), + new address[](2), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + } + + function test_forceRegisterTokenToL2_revert_OnlyOwner() public { + vm.expectRevert("ONLY_OWNER"); + L1CustomGateway(address(l1Gateway)).forceRegisterTokenToL2{ value: retryableCost }( + new address[](1), + new address[](1), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + } + + function test_getOutboundCalldata() public { + bytes memory outboundCalldata = l1Gateway.getOutboundCalldata({ + _token: address(token), + _from: user, + _to: address(800), + _amount: 355, + _data: abi.encode("doStuff()") + }); + + bytes memory expectedCalldata = abi.encodeWithSelector( + ITokenGateway.finalizeInboundTransfer.selector, + address(token), + user, + address(800), + 355, + abi.encode("", abi.encode("doStuff()")) + ); + + assertEq(outboundCalldata, expectedCalldata, "Invalid outboundCalldata"); + } + + function test_initialize() public virtual { + L1CustomGateway gateway = new L1CustomGateway(); + gateway.initialize(l2Gateway, router, inbox, owner); + + assertEq(gateway.counterpartGateway(), l2Gateway, "Invalid counterpartGateway"); + assertEq(gateway.router(), router, "Invalid router"); + assertEq(gateway.inbox(), inbox, "Invalid inbox"); + assertEq(gateway.owner(), owner, "Invalid owner"); + assertEq(gateway.whitelist(), address(0), "Invalid whitelist"); + } + + function test_outboundTransfer() public virtual { + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway)); + + uint256 depositAmount = 300; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // register token to gateway + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(token)); + uint256 seqNum0 = L1CustomGateway(address(l1Gateway)).registerTokenToL2{ + value: retryableCost + }(makeAddr("tokenL2Address"), maxGas, gasPriceBid, maxSubmissionCost, creditBackAddress); + + // approve token + vm.prank(user); + token.approve(address(l1Gateway), depositAmount); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(user, user); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + l1Gateway.getOutboundCalldata(address(token), user, user, depositAmount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(token), user, user, 1, depositAmount); + + // trigger deposit + vm.prank(router); + bytes memory seqNum1 = l1Gateway.outboundTransfer{ value: retryableCost }( + address(token), + user, + depositAmount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, depositAmount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + depositAmount, + "Wrong l1 gateway balance" + ); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund() public virtual { + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway)); + + uint256 depositAmount = 450; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // register token to gateway + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(token)); + uint256 seqNum0 = L1CustomGateway(address(l1Gateway)).registerTokenToL2{ + value: retryableCost + }(makeAddr("tokenL2Address"), maxGas, gasPriceBid, maxSubmissionCost, creditBackAddress); + + // approve token + vm.prank(user); + token.approve(address(l1Gateway), depositAmount); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, user); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + l1Gateway.getOutboundCalldata(address(token), user, user, depositAmount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(token), user, user, 1, depositAmount); + + // trigger deposit + vm.prank(router); + bytes memory seqNum1 = l1Gateway.outboundTransferCustomRefund{ value: retryableCost }( + address(token), + creditBackAddress, + user, + depositAmount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, depositAmount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + depositAmount, + "Wrong l1 gateway balance" + ); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund_revert_InsufficientAllowance() public virtual { + uint256 tooManyTokens = 500 ether; + + // register token to gateway + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(token)); + L1CustomGateway(address(l1Gateway)).registerTokenToL2{ value: retryableCost }( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + makeAddr("creditBackAddress") + ); + + vm.prank(router); + vm.expectRevert("ERC20: insufficient allowance"); + l1Gateway.outboundTransferCustomRefund{ value: 1 ether }( + address(token), + user, + user, + tooManyTokens, + 0.1 ether, + 0.01 ether, + buildRouterEncodedData("") + ); + } + + function test_registerTokenToL2(address l1Token, address l2Token) public virtual { + vm.assume(l1Token != FOUNDRY_CHEATCODE_ADDRESS && l2Token != FOUNDRY_CHEATCODE_ADDRESS); + vm.deal(l1Token, 100 ether); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TokenSet(l1Token, l2Token); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(l1Token, l1Token); + + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(l1Token); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = address(l2Token); + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + abi.encodeWithSelector(L2CustomGateway.registerTokenFromL1.selector, l1Tokens, l2Tokens) + ); + + // register token to gateway + vm.mockCall( + address(l1Token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(l1Token)); + L1CustomGateway(address(l1Gateway)).registerTokenToL2{ value: retryableCost }( + l2Token, + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + assertEq( + L1CustomGateway(address(l1Gateway)).l1ToL2Token(l1Token), + l2Token, + "Invalid L2 token" + ); + } + + function test_registerTokenToL2_CustomRefund(address l1Token, address l2Token) public virtual { + vm.assume(l1Token != FOUNDRY_CHEATCODE_ADDRESS && l2Token != FOUNDRY_CHEATCODE_ADDRESS); + vm.deal(l1Token, 100 ether); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TokenSet(l1Token, l2Token); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, creditBackAddress); + + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(l1Token); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = address(l2Token); + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + abi.encodeWithSelector(L2CustomGateway.registerTokenFromL1.selector, l1Tokens, l2Tokens) + ); + + // register token to gateway + vm.mockCall( + address(l1Token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(l1Token)); + L1CustomGateway(address(l1Gateway)).registerTokenToL2{ value: retryableCost }( + l2Token, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + + assertEq( + L1CustomGateway(address(l1Gateway)).l1ToL2Token(l1Token), + l2Token, + "Invalid L2 token" + ); + } + + function test_registerTokenToL2_revert_NotArbEnabled() public virtual { + // wrong answer + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xdd)) + ); + + vm.prank(address(token)); + vm.expectRevert("NOT_ARB_ENABLED"); + L1CustomGateway(address(l1Gateway)).registerTokenToL2{ value: retryableCost }( + address(102), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + } + + function test_registerTokenToL2_revert_NoUpdateToDifferentAddress() public virtual { + // register token to gateway + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + + // set initial address + address initialL2TokenAddress = makeAddr("initial"); + vm.prank(address(token)); + L1CustomGateway(address(l1Gateway)).registerTokenToL2{ value: retryableCost }( + initialL2TokenAddress, + maxGas, + gasPriceBid, + maxSubmissionCost + ); + assertEq( + L1CustomGateway(address(l1Gateway)).l1ToL2Token(address(token)), + initialL2TokenAddress + ); + + // try to set different one + address differentL2TokenAddress = makeAddr("different"); + vm.prank(address(token)); + vm.expectRevert("NO_UPDATE_TO_DIFFERENT_ADDR"); + L1CustomGateway(address(l1Gateway)).registerTokenToL2{ value: retryableCost }( + differentL2TokenAddress, + maxGas, + gasPriceBid, + maxSubmissionCost + ); + } + + function test_setOwner(address newOwner) public { + vm.assume(newOwner != address(0)); + + vm.prank(owner); + L1CustomGateway(address(l1Gateway)).setOwner(newOwner); + + assertEq(L1CustomGateway(address(l1Gateway)).owner(), newOwner, "Invalid owner"); + } + + function test_setOwner_revert_InvalidOwner() public { + address invalidOwner = address(0); + + vm.prank(owner); + vm.expectRevert("INVALID_OWNER"); + L1CustomGateway(address(l1Gateway)).setOwner(invalidOwner); + } + + function test_setOwner_revert_OnlyOwner() public { + address nonOwner = address(250); + + vm.prank(nonOwner); + vm.expectRevert("ONLY_OWNER"); + L1CustomGateway(address(l1Gateway)).setOwner(address(300)); + } + + //// + // Event declarations + //// + event TokenSet(address indexed l1Address, address indexed l2Address); + + event DepositInitiated( + address l1Token, + address indexed _from, + address indexed _to, + uint256 indexed _sequenceNumber, + uint256 _amount + ); + event TicketData(uint256 maxSubmissionCost); + event RefundAddresses(address excessFeeRefundAddress, address callValueRefundAddress); + event InboxRetryableTicket(address from, address to, uint256 value, uint256 maxGas, bytes data); +} diff --git a/test-foundry/L1ERC20Gateway.t.sol b/test-foundry/L1ERC20Gateway.t.sol new file mode 100644 index 0000000000..c6bfb0f7a5 --- /dev/null +++ b/test-foundry/L1ERC20Gateway.t.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1ArbitrumExtendedGatewayTest, InboxMock, TestERC20 } from "./L1ArbitrumExtendedGateway.t.sol"; +import "contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract L1ERC20GatewayTest is L1ArbitrumExtendedGatewayTest { + // gateway params + address public l2BeaconProxyFactory = makeAddr("l2BeaconProxyFactory"); + bytes32 public cloneableProxyHash = + 0x0000000000000000000000000000000000000000000000000000000000000001; + + function setUp() public virtual { + inbox = address(new InboxMock()); + + l1Gateway = new L1ERC20Gateway(); + L1ERC20Gateway(address(l1Gateway)).initialize( + l2Gateway, + router, + inbox, + cloneableProxyHash, + l2BeaconProxyFactory + ); + + token = IERC20(address(new TestERC20())); + + maxSubmissionCost = 70; + retryableCost = maxSubmissionCost + gasPriceBid * maxGas; + + // fund user and router + vm.prank(user); + TestERC20(address(token)).mint(); + vm.deal(router, 100 ether); + } + + /* solhint-disable func-name-mixedcase */ + function test_initialize() public virtual { + L1ERC20Gateway gateway = new L1ERC20Gateway(); + gateway.initialize(l2Gateway, router, inbox, cloneableProxyHash, l2BeaconProxyFactory); + + assertEq(gateway.counterpartGateway(), l2Gateway, "Invalid counterpartGateway"); + assertEq(gateway.router(), router, "Invalid router"); + assertEq(gateway.inbox(), inbox, "Invalid inbox"); + assertEq(gateway.l2BeaconProxyFactory(), l2BeaconProxyFactory, "Invalid beacon"); + assertEq(gateway.whitelist(), address(0), "Invalid whitelist"); + } + + function test_initialize_revert_InvalidProxyHash() public { + L1ERC20Gateway gateway = new L1ERC20Gateway(); + bytes32 invalidProxyHash = bytes32(0); + + vm.expectRevert("INVALID_PROXYHASH"); + gateway.initialize(l2Gateway, router, inbox, invalidProxyHash, l2BeaconProxyFactory); + } + + function test_initialize_revert_InvalidBeacon() public { + L1ERC20Gateway gateway = new L1ERC20Gateway(); + address invalidBeaconProxyFactory = address(0); + + vm.expectRevert("INVALID_BEACON"); + gateway.initialize(l2Gateway, router, inbox, cloneableProxyHash, invalidBeaconProxyFactory); + } + + function test_outboundTransfer() public virtual { + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway)); + + // retryable params + uint256 depositAmount = 300; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // approve token + vm.prank(user); + token.approve(address(l1Gateway), depositAmount); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(user, user); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + l1Gateway.getOutboundCalldata(address(token), user, user, depositAmount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(token), user, user, 0, depositAmount); + + // trigger deposit + vm.prank(router); + l1Gateway.outboundTransfer{ value: retryableCost }( + address(token), + user, + depositAmount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, depositAmount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + depositAmount, + "Wrong l1 gateway balance" + ); + } + + function test_outboundTransferCustomRefund() public virtual { + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway)); + + // retryable params + uint256 depositAmount = 450; + address refundTo = address(2000); + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // approve token + vm.prank(user); + token.approve(address(l1Gateway), depositAmount); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(refundTo, user); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + l1Gateway.getOutboundCalldata(address(token), user, user, depositAmount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(token), user, user, 0, depositAmount); + + // trigger deposit + vm.prank(router); + l1Gateway.outboundTransferCustomRefund{ value: retryableCost }( + address(token), + refundTo, + user, + depositAmount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, depositAmount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + depositAmount, + "Wrong l1 gateway balance" + ); + } + + function test_outboundTransferCustomRefund_revert_InsufficientAllowance() public { + uint256 tooManyTokens = 500 ether; + + vm.prank(router); + vm.expectRevert("ERC20: insufficient allowance"); + l1Gateway.outboundTransferCustomRefund( + address(token), + user, + user, + tooManyTokens, + 0.1 ether, + 0.01 ether, + buildRouterEncodedData("") + ); + } + + function test_getOutboundCalldata() public { + bytes memory outboundCalldata = l1Gateway.getOutboundCalldata({ + _token: address(token), + _from: user, + _to: address(800), + _amount: 355, + _data: abi.encode("doStuff()") + }); + + bytes memory expectedCalldata = abi.encodeWithSelector( + ITokenGateway.finalizeInboundTransfer.selector, + address(token), + user, + address(800), + 355, + abi.encode( + abi.encode(abi.encode("IntArbTestToken"), abi.encode("IARB"), abi.encode(18)), + abi.encode("doStuff()") + ) + ); + + assertEq(outboundCalldata, expectedCalldata, "Invalid outboundCalldata"); + } + + function test_calculateL2TokenAddress(address tokenAddress) public { + address l2TokenAddress = l1Gateway.calculateL2TokenAddress(tokenAddress); + + address expectedL2TokenAddress = Create2.computeAddress( + keccak256(abi.encode(l2Gateway, keccak256(abi.encode(tokenAddress)))), + cloneableProxyHash, + l2BeaconProxyFactory + ); + + assertEq(l2TokenAddress, expectedL2TokenAddress, "Invalid calculateL2TokenAddress"); + } + + //// + // Event declarations + //// + event DepositInitiated( + address l1Token, + address indexed _from, + address indexed _to, + uint256 indexed _sequenceNumber, + uint256 _amount + ); + event TicketData(uint256 maxSubmissionCost); + event RefundAddresses(address excessFeeRefundAddress, address callValueRefundAddress); + event InboxRetryableTicket(address from, address to, uint256 value, uint256 maxGas, bytes data); +} diff --git a/test-foundry/L1GatewayRouter.t.sol b/test-foundry/L1GatewayRouter.t.sol new file mode 100644 index 0000000000..ad53629525 --- /dev/null +++ b/test-foundry/L1GatewayRouter.t.sol @@ -0,0 +1,825 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { GatewayRouterTest } from "./GatewayRouter.t.sol"; +import { L1GatewayRouter } from "contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol"; +import { L2GatewayRouter } from "contracts/tokenbridge/arbitrum/gateway/L2GatewayRouter.sol"; +import { L1ERC20Gateway } from "contracts/tokenbridge/ethereum/gateway/L1ERC20Gateway.sol"; +import { L1CustomGateway } from "contracts/tokenbridge/ethereum/gateway/L1CustomGateway.sol"; +import { InboxMock } from "contracts/tokenbridge/test/InboxMock.sol"; +import { IERC165 } from "contracts/tokenbridge/libraries/IERC165.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20PresetMinterPauser } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; + +contract L1GatewayRouterTest is GatewayRouterTest { + L1GatewayRouter public l1Router; + + address public owner = makeAddr("owner"); + address public user = makeAddr("user"); + address public counterpartGateway = makeAddr("counterpartGateway"); + address public inbox; + + function setUp() public virtual { + inbox = address(new InboxMock()); + defaultGateway = address(new L1ERC20Gateway()); + + router = new L1GatewayRouter(); + l1Router = L1GatewayRouter(address(router)); + l1Router.initialize(owner, defaultGateway, address(0), counterpartGateway, inbox); + + maxSubmissionCost = 50000; + retryableCost = maxSubmissionCost + maxGas * gasPriceBid; + + vm.deal(owner, 100 ether); + vm.deal(user, 100 ether); + } + + /* solhint-disable func-name-mixedcase */ + function test_initialize() public { + L1GatewayRouter router = new L1GatewayRouter(); + + router.initialize(owner, defaultGateway, address(0), counterpartGateway, inbox); + + assertEq(router.router(), address(0), "Invalid router"); + assertEq(router.counterpartGateway(), counterpartGateway, "Invalid counterpartGateway"); + assertEq(router.defaultGateway(), defaultGateway, "Invalid defaultGateway"); + assertEq(router.owner(), owner, "Invalid owner"); + assertEq(router.whitelist(), address(0), "Invalid whitelist"); + assertEq(router.inbox(), inbox, "Invalid inbox"); + } + + function test_initialize_revert_AlreadyInit() public { + L1GatewayRouter router = new L1GatewayRouter(); + router.initialize(owner, defaultGateway, address(0), counterpartGateway, inbox); + vm.expectRevert("ALREADY_INIT"); + router.initialize(owner, defaultGateway, address(0), counterpartGateway, inbox); + } + + function test_initialize_revert_InvalidCounterPart() public { + L1GatewayRouter router = new L1GatewayRouter(); + address invalidCounterpart = address(0); + vm.expectRevert("INVALID_COUNTERPART"); + router.initialize(owner, defaultGateway, address(0), invalidCounterpart, inbox); + } + + function test_postUpgradeInit_revert_NotFromAdmin() public { + vm.expectRevert("NOT_FROM_ADMIN"); + l1Router.postUpgradeInit(); + } + + function test_getGateway_DisabledGateway() public virtual { + address token = makeAddr("some token"); + + address[] memory tokens = new address[](1); + tokens[0] = token; + address[] memory gateways = new address[](1); + gateways[0] = address(1); + + vm.prank(owner); + l1Router.setGateways{ value: retryableCost }( + tokens, + gateways, + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + address gateway = router.getGateway(token); + assertEq(gateway, address(0), "Invalid gateway"); + } + + function test_getGateway_CustomGateway() public virtual { + address token = makeAddr("some token"); + + address[] memory tokens = new address[](1); + tokens[0] = token; + address[] memory gateways = new address[](1); + gateways[0] = address(new L1ERC20Gateway()); + + vm.prank(owner); + l1Router.setGateways{ value: retryableCost }( + tokens, + gateways, + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + address gateway = router.getGateway(token); + assertEq(gateway, gateways[0], "Invalid gateway"); + } + + function test_setDefaultGateway() public virtual { + L1ERC20Gateway newL1DefaultGateway = new L1ERC20Gateway(); + address newDefaultGatewayCounterpart = makeAddr("newDefaultGatewayCounterpart"); + newL1DefaultGateway.initialize( + newDefaultGatewayCounterpart, + address(l1Router), + inbox, + 0x0000000000000000000000000000000000000000000000000000000000000001, + makeAddr("l2BeaconProxyFactory") + ); + + // event checkers + vm.expectEmit(true, true, true, true); + emit DefaultGatewayUpdated(address(newL1DefaultGateway)); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Router), + counterpartGateway, + 0, + maxGas, + abi.encodeWithSelector( + L2GatewayRouter.setDefaultGateway.selector, + newDefaultGatewayCounterpart + ) + ); + + // set it + vm.prank(owner); + uint256 seqNum = l1Router.setDefaultGateway{ value: retryableCost }( + address(newL1DefaultGateway), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + /// checks + assertEq( + l1Router.defaultGateway(), + address(newL1DefaultGateway), + "Invalid newL1DefaultGateway" + ); + + assertEq(seqNum, 0, "Invalid seqNum"); + } + + function test_setDefaultGateway_AddressZero() public virtual { + address newL1DefaultGateway = address(0); + + // event checkers + vm.expectEmit(true, true, true, true); + emit DefaultGatewayUpdated(address(newL1DefaultGateway)); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Router), + counterpartGateway, + 0, + maxGas, + abi.encodeWithSelector(L2GatewayRouter.setDefaultGateway.selector, address(0)) + ); + + // set it + vm.prank(owner); + uint256 seqNum = l1Router.setDefaultGateway{ value: retryableCost }( + newL1DefaultGateway, + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + /// checks + assertEq( + l1Router.defaultGateway(), + address(newL1DefaultGateway), + "Invalid newL1DefaultGateway" + ); + + assertEq(seqNum, 0, "Invalid seqNum"); + } + + function test_setGateway() public virtual { + // create gateway + L1CustomGateway customGateway = new L1CustomGateway(); + address l2Counterpart = makeAddr("l2Counterpart"); + customGateway.initialize(l2Counterpart, address(l1Router), address(inbox), owner); + + // create token + ERC20 customToken = new ERC20("X", "Y"); + vm.deal(address(customToken), 100 ether); + vm.mockCall( + address(customToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + + // register token to gateway + vm.prank(address(customToken)); + customGateway.registerTokenToL2{ value: retryableCost }( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + makeAddr("creditBackAddress") + ); + + // expect events + vm.expectEmit(true, true, true, true); + emit GatewaySet(address(customToken), address(customGateway)); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(address(customToken), address(customToken)); + + vm.expectEmit(true, true, true, true); + address[] memory _tokenArr = new address[](1); + _tokenArr[0] = address(customToken); + address[] memory _gatewayArr = new address[](1); + _gatewayArr[0] = l2Counterpart; + emit InboxRetryableTicket( + address(l1Router), + counterpartGateway, + 0, + maxGas, + abi.encodeWithSelector(L2GatewayRouter.setGateway.selector, _tokenArr, _gatewayArr) + ); + + // set gateway + vm.prank(address(customToken)); + uint256 seqNum = l1Router.setGateway{ value: retryableCost }( + address(customGateway), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + ///// checks + + assertEq( + l1Router.l1TokenToGateway(address(customToken)), + address(customGateway), + "Gateway not set" + ); + + assertEq(seqNum, 1, "Invalid seqNum"); + } + + function test_setGateway_CustomCreditback() public virtual { + // create gateway + L1CustomGateway customGateway = new L1CustomGateway(); + address l2Counterpart = makeAddr("l2Counterpart"); + customGateway.initialize(l2Counterpart, address(l1Router), address(inbox), owner); + + // create token + ERC20 customToken = new ERC20("X", "Y"); + vm.deal(address(customToken), 100 ether); + vm.mockCall( + address(customToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + + // register token to gateway + vm.prank(address(customToken)); + customGateway.registerTokenToL2{ value: retryableCost }( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + + // expect events + vm.expectEmit(true, true, true, true); + emit GatewaySet(address(customToken), address(customGateway)); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, creditBackAddress); + + vm.expectEmit(true, true, true, true); + address[] memory _tokenArr = new address[](1); + _tokenArr[0] = address(customToken); + address[] memory _gatewayArr = new address[](1); + _gatewayArr[0] = l2Counterpart; + emit InboxRetryableTicket( + address(l1Router), + counterpartGateway, + 0, + maxGas, + abi.encodeWithSelector(L2GatewayRouter.setGateway.selector, _tokenArr, _gatewayArr) + ); + + // set gateway + vm.prank(address(customToken)); + uint256 seqNum = l1Router.setGateway{ value: retryableCost }( + address(customGateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + + ///// checks + + assertEq( + l1Router.l1TokenToGateway(address(customToken)), + address(customGateway), + "Gateway not set" + ); + + assertEq(seqNum, 1, "Invalid seqNum"); + } + + function test_setGateway_revert_NotArbEnabled() public virtual { + address nonArbEnabledToken = address(new ERC20("X", "Y")); + vm.deal(nonArbEnabledToken, 100 ether); + vm.mockCall( + nonArbEnabledToken, + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb2)) + ); + + vm.prank(nonArbEnabledToken); + vm.expectRevert("NOT_ARB_ENABLED"); + l1Router.setGateway{ value: 400000 }( + makeAddr("gateway"), + 100000, + 3, + 200, + makeAddr("creditback") + ); + } + + function test_setGateway_revert_NotToContract() public virtual { + address token = address(new ERC20("X", "Y")); + vm.deal(token, 100 ether); + vm.mockCall(token, abi.encodeWithSignature("isArbitrumEnabled()"), abi.encode(uint8(0xb1))); + + address gatewayNotContract = makeAddr("not contract"); + + vm.prank(token); + vm.expectRevert("NOT_TO_CONTRACT"); + l1Router.setGateway{ value: retryableCost }( + gatewayNotContract, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + } + + function test_setGateway_revert_NoUpdateToDifferentAddress() public virtual { + // create gateway + address initialGateway = address(new L1CustomGateway()); + address l2Counterpart = makeAddr("l2Counterpart"); + L1CustomGateway(initialGateway).initialize( + l2Counterpart, + address(l1Router), + address(inbox), + owner + ); + + // create token + address token = address(new ERC20("X", "Y")); + vm.deal(address(token), 100 ether); + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + + // register token to gateway + vm.prank(token); + L1CustomGateway(initialGateway).registerTokenToL2{ value: retryableCost }( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + + // initially set gateway for token + vm.prank(address(token)); + l1Router.setGateway{ value: retryableCost }( + initialGateway, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + assertEq(l1Router.l1TokenToGateway(token), initialGateway, "Initial gateway not set"); + + //// now try setting different gateway + address newGateway = address(new L1CustomGateway()); + + vm.prank(token); + vm.expectRevert("NO_UPDATE_TO_DIFFERENT_ADDR"); + l1Router.setGateway{ value: retryableCost }( + newGateway, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + } + + function test_setGateway_revert_TokenNotHandledByGateway() public virtual { + // create gateway + L1CustomGateway gateway = new L1CustomGateway(); + + // create token + address token = address(new ERC20("X", "Y")); + vm.deal(token, 100 ether); + vm.mockCall(token, abi.encodeWithSignature("isArbitrumEnabled()"), abi.encode(uint8(0xb1))); + + vm.prank(token); + vm.expectRevert("TOKEN_NOT_HANDLED_BY_GATEWAY"); + l1Router.setGateway{ value: retryableCost }( + address(gateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + } + + function test_setGateways() public virtual { + // create tokens and gateways + address[] memory tokens = new address[](2); + tokens[0] = address(new ERC20("1", "1")); + tokens[1] = address(new ERC20("2", "2")); + address[] memory gateways = new address[](2); + gateways[0] = address(new L1CustomGateway()); + gateways[1] = address(new L1CustomGateway()); + + address l2Counterpart = makeAddr("l2Counterpart"); + + /// init all + for (uint256 i = 0; i < 2; i++) { + L1CustomGateway(gateways[i]).initialize( + l2Counterpart, + address(l1Router), + address(inbox), + owner + ); + + vm.mockCall( + tokens[i], + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + + // register tokens to gateways + vm.deal(tokens[i], 100 ether); + vm.prank(tokens[i]); + L1CustomGateway(gateways[i]).registerTokenToL2{ value: retryableCost }( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + makeAddr("creditBackAddress") + ); + } + + // expect events + vm.expectEmit(true, true, true, true); + emit GatewaySet(tokens[0], gateways[0]); + vm.expectEmit(true, true, true, true); + emit GatewaySet(tokens[1], gateways[1]); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + address[] memory _gatewayArr = new address[](2); + _gatewayArr[0] = l2Counterpart; + _gatewayArr[1] = l2Counterpart; + emit InboxRetryableTicket( + address(l1Router), + counterpartGateway, + 0, + maxGas, + abi.encodeWithSelector(L2GatewayRouter.setGateway.selector, tokens, _gatewayArr) + ); + + /// set gateways + vm.prank(owner); + uint256 seqNum = l1Router.setGateways{ value: retryableCost }( + tokens, + gateways, + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + ///// checks + + assertEq(l1Router.l1TokenToGateway(tokens[0]), gateways[0], "Gateway[0] not set"); + assertEq(l1Router.l1TokenToGateway(tokens[1]), gateways[1], "Gateway[1] not set"); + assertEq(seqNum, 2, "Invalid seqNum"); + } + + function test_setGateways_revert_WrongLength() public virtual { + address[] memory tokens = new address[](2); + tokens[0] = address(new ERC20("1", "1")); + tokens[1] = address(new ERC20("2", "2")); + address[] memory gateways = new address[](1); + gateways[0] = address(new L1CustomGateway()); + + /// set gateways + vm.prank(owner); + vm.expectRevert("WRONG_LENGTH"); + l1Router.setGateways{ value: retryableCost }( + tokens, + gateways, + maxGas, + gasPriceBid, + maxSubmissionCost + ); + } + + function test_setGateways_SetZeroAddr() public virtual { + // create gateway + address initialGateway = address(new L1CustomGateway()); + address l2Counterpart = makeAddr("l2Counterpart"); + L1CustomGateway(initialGateway).initialize( + l2Counterpart, + address(l1Router), + address(inbox), + owner + ); + + // create token + address token = address(new ERC20("X", "Y")); + vm.deal(address(token), 100 ether); + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + + // register token to gateway + vm.prank(token); + L1CustomGateway(initialGateway).registerTokenToL2{ value: retryableCost }( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + + // initially set gateway for token + vm.prank(address(token)); + l1Router.setGateway{ value: retryableCost }( + initialGateway, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + assertEq(l1Router.l1TokenToGateway(token), initialGateway, "Initial gateway not set"); + + //// now set to zero addr + address newGateway = address(0); + + // expect events + vm.expectEmit(true, true, true, true); + emit GatewaySet(token, newGateway); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + address[] memory _tokenArr = new address[](1); + _tokenArr[0] = token; + address[] memory _gatewayArr = new address[](1); + _gatewayArr[0] = newGateway; + emit InboxRetryableTicket( + address(l1Router), + counterpartGateway, + 0, + maxGas, + abi.encodeWithSelector(L2GatewayRouter.setGateway.selector, _tokenArr, _gatewayArr) + ); + + /// set gateways + vm.prank(owner); + uint256 seqNum = l1Router.setGateways{ value: retryableCost }( + _tokenArr, + _gatewayArr, + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + ///// checks + + assertEq(l1Router.l1TokenToGateway(token), address(0), "Custom gateway not cleared"); + assertEq(seqNum, 2, "Invalid seqNum"); + } + + function test_setGateways_revert_notOwner() public { + vm.expectRevert("ONLY_OWNER"); + l1Router.setGateways{ value: 1000 }(new address[](1), new address[](1), 100, 8, 10); + } + + function test_setOwner(address newOwner) public { + vm.assume(newOwner != address(0)); + + vm.prank(owner); + l1Router.setOwner(newOwner); + + assertEq(l1Router.owner(), newOwner, "Invalid owner"); + } + + function test_setOwner_revert_InvalidOwner() public { + address invalidOwner = address(0); + + vm.prank(owner); + vm.expectRevert("INVALID_OWNER"); + l1Router.setOwner(invalidOwner); + } + + function test_setOwner_revert_OnlyOwner() public { + address nonOwner = address(250); + + vm.prank(nonOwner); + vm.expectRevert("ONLY_OWNER"); + l1Router.setOwner(address(300)); + } + + function test_supportsInterface(bytes4 iface) public { + bool expected = false; + if ( + iface == type(IERC165).interfaceId || + iface == L1GatewayRouter.outboundTransferCustomRefund.selector + ) { + expected = true; + } + + assertEq(l1Router.supportsInterface(iface), expected, "Interface shouldn't be supported"); + } + + function test_outboundTransfer() public virtual { + // init default gateway + L1ERC20Gateway(defaultGateway).initialize( + makeAddr("defaultGatewayCounterpart"), + address(l1Router), + inbox, + 0x0000000000000000000000000000000000000000000000000000000000000001, + makeAddr("l2BeaconProxyFactory") + ); + + // set default gateway + vm.prank(owner); + l1Router.setDefaultGateway{ value: retryableCost }( + address(defaultGateway), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + // create token + ERC20PresetMinterPauser token = new ERC20PresetMinterPauser("X", "Y"); + token.mint(user, 10000); + vm.prank(user); + token.approve(defaultGateway, 103); + + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(defaultGateway)); + + /// deposit data + address to = address(401); + uint256 amount = 103; + bytes memory userEncodedData = abi.encode(maxSubmissionCost, ""); + + // expect event + vm.expectEmit(true, true, true, true); + emit TransferRouted(address(token), user, to, address(defaultGateway)); + + /// deposit it + vm.prank(user); + l1Router.outboundTransfer{ value: retryableCost }( + address(token), + to, + amount, + maxGas, + gasPriceBid, + userEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(defaultGateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + amount, + "Wrong defaultGateway balance" + ); + } + + function test_outboundTransferCustomRefund() public virtual { + // init default gateway + L1ERC20Gateway(defaultGateway).initialize( + makeAddr("defaultGatewayCounterpart"), + address(l1Router), + inbox, + 0x0000000000000000000000000000000000000000000000000000000000000001, + makeAddr("l2BeaconProxyFactory") + ); + + // set default gateway + vm.prank(owner); + l1Router.setDefaultGateway{ value: retryableCost }( + address(defaultGateway), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + + // create token + ERC20PresetMinterPauser token = new ERC20PresetMinterPauser("X", "Y"); + token.mint(user, 10000); + vm.prank(user); + token.approve(defaultGateway, 103); + + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(defaultGateway)); + + /// deposit data + address refundTo = address(400); + address to = address(401); + uint256 amount = 103; + bytes memory userEncodedData = abi.encode(maxSubmissionCost, ""); + + // expect event + vm.expectEmit(true, true, true, true); + emit TransferRouted(address(token), user, to, address(defaultGateway)); + + /// deposit it + vm.prank(user); + l1Router.outboundTransferCustomRefund{ value: retryableCost }( + address(token), + refundTo, + to, + amount, + maxGas, + gasPriceBid, + userEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(defaultGateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + amount, + "Wrong defaultGateway balance" + ); + } + + //// + // Event declarations + //// + event TransferRouted( + address indexed token, + address indexed _userFrom, + address indexed _userTo, + address gateway + ); + event GatewaySet(address indexed l1Token, address indexed gateway); + event DefaultGatewayUpdated(address newDefaultGateway); + + event DepositInitiated( + address l1Token, + address indexed _from, + address indexed _to, + uint256 indexed _sequenceNumber, + uint256 _amount + ); + event TicketData(uint256 maxSubmissionCost); + event RefundAddresses(address excessFeeRefundAddress, address callValueRefundAddress); + event InboxRetryableTicket(address from, address to, uint256 value, uint256 maxGas, bytes data); +} diff --git a/test-foundry/L1OrbitCustomGateway.t.sol b/test-foundry/L1OrbitCustomGateway.t.sol new file mode 100644 index 0000000000..20093a8db6 --- /dev/null +++ b/test-foundry/L1OrbitCustomGateway.t.sol @@ -0,0 +1,605 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1CustomGatewayTest, IERC20, L2CustomGateway } from "./L1CustomGateway.t.sol"; +import { L1OrbitCustomGateway } from "contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20PresetMinterPauser } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import { TestERC20 } from "contracts/tokenbridge/test/TestERC20.sol"; +import { ERC20InboxMock } from "contracts/tokenbridge/test/InboxMock.sol"; + +contract L1OrbitCustomGatewayTest is L1CustomGatewayTest { + ERC20 public nativeToken; + uint256 public nativeTokenTotalFee; + + function setUp() public virtual override { + inbox = address(new ERC20InboxMock()); + nativeToken = ERC20(address(new ERC20PresetMinterPauser("X", "Y"))); + ERC20PresetMinterPauser(address(nativeToken)).mint(user, 1_000_000 ether); + ERC20PresetMinterPauser(address(nativeToken)).mint(owner, 1_000_000 ether); + ERC20InboxMock(inbox).setMockNativeToken(address(nativeToken)); + + l1Gateway = new L1OrbitCustomGateway(); + L1OrbitCustomGateway(address(l1Gateway)).initialize(l2Gateway, router, inbox, owner); + + token = IERC20(address(new TestERC20())); + + maxSubmissionCost = 0; + nativeTokenTotalFee = maxGas * gasPriceBid; + + // fund user and router + vm.prank(user); + TestERC20(address(token)).mint(); + vm.deal(router, 100 ether); + vm.deal(address(token), 100 ether); + vm.deal(owner, 100 ether); + } + + /* solhint-disable func-name-mixedcase */ + function test_calculateL2TokenAddress(address l1Token, address l2Token) public override { + vm.assume( + l1Token != FOUNDRY_CHEATCODE_ADDRESS && + l2Token != FOUNDRY_CHEATCODE_ADDRESS && + l1Token != address(0) && + l1Token != router + ); + vm.deal(l1Token, 100 ether); + + // approve fees + ERC20PresetMinterPauser(address(nativeToken)).mint(address(l1Token), nativeTokenTotalFee); + vm.prank(address(l1Token)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // register token to gateway + vm.mockCall( + address(l1Token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(l1Token)); + L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + l2Token, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + assertEq(l1Gateway.calculateL2TokenAddress(l1Token), l2Token, "Invalid L2 token address"); + } + + function test_forceRegisterTokenToL2() public override { + address[] memory l1Tokens = new address[](2); + l1Tokens[0] = makeAddr("l1Token1"); + l1Tokens[1] = makeAddr("l1Token2"); + address[] memory l2Tokens = new address[](2); + l2Tokens[0] = makeAddr("l2Token1"); + l2Tokens[1] = makeAddr("l2Token2"); + + // approve fees + vm.prank(owner); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // expect events + vm.expectEmit(true, true, true, true); + emit TokenSet(l1Tokens[0], l2Tokens[0]); + + vm.expectEmit(true, true, true, true); + emit TokenSet(l1Tokens[1], l2Tokens[1]); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + abi.encodeWithSelector(L2CustomGateway.registerTokenFromL1.selector, l1Tokens, l2Tokens) + ); + + // register token to gateway + vm.prank(owner); + uint256 seqNum = L1OrbitCustomGateway(address(l1Gateway)).forceRegisterTokenToL2( + l1Tokens, + l2Tokens, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + + ///// checks + assertEq( + L1OrbitCustomGateway(address(l1Gateway)).l1ToL2Token(l1Tokens[0]), + l2Tokens[0], + "Invalid L2 token" + ); + + assertEq( + L1OrbitCustomGateway(address(l1Gateway)).l1ToL2Token(l1Tokens[1]), + l2Tokens[1], + "Invalid L2 token" + ); + + assertEq(seqNum, 0, "Invalid seqNum"); + } + + function test_forceRegisterTokenToL2_revert_InvalidLength() public override { + vm.prank(owner); + vm.expectRevert("INVALID_LENGTHS"); + L1OrbitCustomGateway(address(l1Gateway)).forceRegisterTokenToL2( + new address[](1), + new address[](2), + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + } + + function test_forceRegisterTokenToL2_revert_NotSupportedInOrbit() public { + // register token to gateway + vm.prank(owner); + vm.expectRevert("NOT_SUPPORTED_IN_ORBIT"); + L1OrbitCustomGateway(address(l1Gateway)).forceRegisterTokenToL2( + new address[](1), + new address[](1), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + } + + function test_outboundTransfer() public virtual override { + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway)); + + uint256 depositAmount = 300; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // register token to gateway + ERC20PresetMinterPauser(address(nativeToken)).mint(address(token), nativeTokenTotalFee); + vm.prank(address(token)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(token)); + uint256 seqNum0 = L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + // approve token + vm.prank(user); + token.approve(address(l1Gateway), depositAmount); + + // approve fees + vm.prank(user); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(user, user); + + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + l1Gateway.getOutboundCalldata(address(token), user, user, depositAmount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(token), user, user, 1, depositAmount); + + // trigger deposit + vm.prank(router); + bytes memory seqNum1 = l1Gateway.outboundTransfer( + address(token), + user, + depositAmount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, depositAmount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + depositAmount, + "Wrong l1 gateway balance" + ); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund() public virtual override { + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway)); + + uint256 depositAmount = 450; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // register token to gateway + ERC20PresetMinterPauser(address(nativeToken)).mint(address(token), nativeTokenTotalFee); + vm.prank(address(token)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(token)); + uint256 seqNum0 = L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + // approve token + vm.prank(user); + token.approve(address(l1Gateway), depositAmount); + + // approve fees + vm.prank(user); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, user); + + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + l1Gateway.getOutboundCalldata(address(token), user, user, depositAmount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(token), user, user, 1, depositAmount); + + // trigger deposit + vm.prank(router); + bytes memory seqNum1 = l1Gateway.outboundTransferCustomRefund( + address(token), + creditBackAddress, + user, + depositAmount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, depositAmount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + depositAmount, + "Wrong l1 gateway balance" + ); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund_revert_InsufficientAllowance() + public + virtual + override + { + uint256 tooManyTokens = 500 ether; + + // register token to gateway + ERC20PresetMinterPauser(address(nativeToken)).mint(address(token), nativeTokenTotalFee); + vm.prank(address(token)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(token)); + L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + vm.prank(router); + vm.expectRevert("ERC20: insufficient allowance"); + l1Gateway.outboundTransferCustomRefund( + address(token), + user, + user, + tooManyTokens, + 0.1 ether, + 0.01 ether, + buildRouterEncodedData("") + ); + } + + function test_registerTokenToL2(address l1Token, address l2Token) public override { + vm.assume( + l1Token != FOUNDRY_CHEATCODE_ADDRESS && + l2Token != FOUNDRY_CHEATCODE_ADDRESS && + l1Token != address(0) + ); + vm.deal(l1Token, 100 ether); + + // approve fees + ERC20PresetMinterPauser(address(nativeToken)).mint(address(l1Token), nativeTokenTotalFee); + vm.prank(address(l1Token)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TokenSet(l1Token, l2Token); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(l1Token, l1Token); + + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(l1Token); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = address(l2Token); + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + abi.encodeWithSelector(L2CustomGateway.registerTokenFromL1.selector, l1Tokens, l2Tokens) + ); + + // register token to gateway + vm.mockCall( + address(l1Token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(l1Token)); + L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + l2Token, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + + assertEq( + L1OrbitCustomGateway(address(l1Gateway)).l1ToL2Token(l1Token), + l2Token, + "Invalid L2 token" + ); + } + + function test_registerTokenToL2_CustomRefund(address l1Token, address l2Token) public override { + vm.assume( + l1Token != FOUNDRY_CHEATCODE_ADDRESS && + l2Token != FOUNDRY_CHEATCODE_ADDRESS && + l1Token != address(0) && + l1Token != router && + l1Token != creditBackAddress + ); + vm.deal(l1Token, 100 ether); + + // approve fees + ERC20PresetMinterPauser(address(nativeToken)).mint(address(l1Token), nativeTokenTotalFee); + vm.prank(address(l1Token)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TokenSet(l1Token, l2Token); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, creditBackAddress); + + address[] memory l1Tokens = new address[](1); + l1Tokens[0] = address(l1Token); + address[] memory l2Tokens = new address[](1); + l2Tokens[0] = address(l2Token); + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + abi.encodeWithSelector(L2CustomGateway.registerTokenFromL1.selector, l1Tokens, l2Tokens) + ); + + // register token to gateway + vm.mockCall( + address(l1Token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(l1Token)); + L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + l2Token, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + assertEq( + L1OrbitCustomGateway(address(l1Gateway)).l1ToL2Token(l1Token), + l2Token, + "Invalid L2 token" + ); + } + + function test_registerTokenToL2_revert_NotArbEnabled() public override { + // wrong answer + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xdd)) + ); + + vm.prank(address(token)); + vm.expectRevert("NOT_ARB_ENABLED"); + L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + address(102), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + } + + function test_registerTokenToL2_revert_NoUpdateToDifferentAddress() public override { + ERC20PresetMinterPauser(address(nativeToken)).mint(address(token), nativeTokenTotalFee); + + // register token to gateway + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + + // set initial address + address initialL2TokenAddress = makeAddr("initial"); + vm.startPrank(address(token)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + initialL2TokenAddress, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + vm.stopPrank(); + assertEq( + L1OrbitCustomGateway(address(l1Gateway)).l1ToL2Token(address(token)), + initialL2TokenAddress + ); + + // try to set different one + address differentL2TokenAddress = makeAddr("different"); + vm.startPrank(address(token)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + vm.expectRevert("NO_UPDATE_TO_DIFFERENT_ADDR"); + L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + differentL2TokenAddress, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + } + + function test_registerTokenToL2_revert_NotSupportedInOrbit() public { + vm.expectRevert("NOT_SUPPORTED_IN_ORBIT"); + L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + address(100), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + } + + function test_registerTokenToL2_revert_CustomRefund_NotSupportedInOrbit() public { + vm.expectRevert("NOT_SUPPORTED_IN_ORBIT"); + L1OrbitCustomGateway(address(l1Gateway)).registerTokenToL2( + address(100), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + } + + /// + // Helper functions + /// + function buildRouterEncodedData(bytes memory callHookData) + internal + view + override + returns (bytes memory) + { + bytes memory userEncodedData = abi.encode( + maxSubmissionCost, + callHookData, + nativeTokenTotalFee + ); + bytes memory routerEncodedData = abi.encode(user, userEncodedData); + + return routerEncodedData; + } + + event ERC20InboxRetryableTicket( + address from, + address to, + uint256 l2CallValue, + uint256 maxGas, + uint256 gasPriceBid, + uint256 tokenTotalFeeAmount, + bytes data + ); +} diff --git a/test-foundry/L1OrbitERC20Gateway.t.sol b/test-foundry/L1OrbitERC20Gateway.t.sol new file mode 100644 index 0000000000..ecd7a78291 --- /dev/null +++ b/test-foundry/L1OrbitERC20Gateway.t.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1ERC20GatewayTest } from "./L1ERC20Gateway.t.sol"; +import "contracts/tokenbridge/ethereum/gateway/L1OrbitERC20Gateway.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20PresetMinterPauser } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import { TestERC20 } from "contracts/tokenbridge/test/TestERC20.sol"; +import { ERC20InboxMock } from "contracts/tokenbridge/test/InboxMock.sol"; + +contract L1OrbitERC20GatewayTest is L1ERC20GatewayTest { + ERC20 public nativeToken; + uint256 public nativeTokenTotalFee; + + function setUp() public override { + inbox = address(new ERC20InboxMock()); + nativeToken = ERC20(address(new ERC20PresetMinterPauser("X", "Y"))); + ERC20PresetMinterPauser(address(nativeToken)).mint(user, 1_000_000 ether); + ERC20InboxMock(inbox).setMockNativeToken(address(nativeToken)); + + l1Gateway = new L1OrbitERC20Gateway(); + L1OrbitERC20Gateway(address(l1Gateway)).initialize( + l2Gateway, + router, + inbox, + cloneableProxyHash, + l2BeaconProxyFactory + ); + + token = IERC20(address(new TestERC20())); + maxSubmissionCost = 0; + nativeTokenTotalFee = maxGas * gasPriceBid; + + // fund user and router + vm.prank(user); + TestERC20(address(token)).mint(); + vm.deal(router, 100 ether); + } + + /* solhint-disable func-name-mixedcase */ + function test_initialize() public override { + L1ERC20Gateway gateway = new L1OrbitERC20Gateway(); + gateway.initialize(l2Gateway, router, inbox, cloneableProxyHash, l2BeaconProxyFactory); + + assertEq(gateway.counterpartGateway(), l2Gateway, "Invalid counterpartGateway"); + assertEq(gateway.router(), router, "Invalid router"); + assertEq(gateway.inbox(), inbox, "Invalid inbox"); + assertEq(gateway.l2BeaconProxyFactory(), l2BeaconProxyFactory, "Invalid beacon"); + assertEq(gateway.whitelist(), address(0), "Invalid whitelist"); + } + + function test_outboundTransfer() public override { + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway)); + + // retryable params + uint256 depositAmount = 300; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // approve token + vm.prank(user); + token.approve(address(l1Gateway), depositAmount); + + // approve fees + vm.prank(user); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // expect events + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(user, user); + + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + l1Gateway.getOutboundCalldata(address(token), user, user, 300, "") + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(token), user, user, 0, depositAmount); + + // trigger deposit + vm.prank(router); + l1Gateway.outboundTransfer( + address(token), + user, + depositAmount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, depositAmount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + depositAmount, + "Wrong l1 gateway balance" + ); + } + + function test_outboundTransfer_revert_NotAllowedToBridgeFeeToken() public { + // trigger deposit + vm.prank(router); + vm.expectRevert("NOT_ALLOWED_TO_BRIDGE_FEE_TOKEN"); + l1Gateway.outboundTransfer(address(nativeToken), user, 100, maxGas, gasPriceBid, ""); + } + + function test_outboundTransferCustomRefund() public override { + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(l1Gateway)); + + // retryable params + uint256 depositAmount = 700; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // approve fees + vm.prank(user); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // approve token + vm.prank(user); + token.approve(address(l1Gateway), depositAmount); + + // expect events + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, user); + + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + l1Gateway.getOutboundCalldata(address(token), user, user, 700, "") + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(token), user, user, 0, depositAmount); + + // trigger deposit + vm.prank(router); + l1Gateway.outboundTransferCustomRefund( + address(token), + creditBackAddress, + user, + depositAmount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, depositAmount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(l1Gateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + depositAmount, + "Wrong l1 gateway balance" + ); + } + + function test_outboundTransferCustomRefund_revert_NotAllowedToBridgeFeeToken() public { + // trigger deposit + vm.prank(router); + vm.expectRevert("NOT_ALLOWED_TO_BRIDGE_FEE_TOKEN"); + l1Gateway.outboundTransferCustomRefund( + address(nativeToken), + creditBackAddress, + user, + 100, + maxGas, + gasPriceBid, + "" + ); + } + + //// + // Helper functions + //// + function buildRouterEncodedData(bytes memory callHookData) + internal + view + override + returns (bytes memory) + { + bytes memory userEncodedData = abi.encode( + maxSubmissionCost, + callHookData, + nativeTokenTotalFee + ); + bytes memory routerEncodedData = abi.encode(user, userEncodedData); + + return routerEncodedData; + } + + event ERC20InboxRetryableTicket( + address from, + address to, + uint256 l2CallValue, + uint256 maxGas, + uint256 gasPriceBid, + uint256 tokenTotalFeeAmount, + bytes data + ); +} diff --git a/test-foundry/L1OrbitGatewayRouter.t.sol b/test-foundry/L1OrbitGatewayRouter.t.sol new file mode 100644 index 0000000000..a263ebb543 --- /dev/null +++ b/test-foundry/L1OrbitGatewayRouter.t.sol @@ -0,0 +1,985 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1GatewayRouterTest } from "./L1GatewayRouter.t.sol"; +import { ERC20InboxMock } from "contracts/tokenbridge/test/InboxMock.sol"; +import { L1OrbitERC20Gateway } from "contracts/tokenbridge/ethereum/gateway/L1OrbitERC20Gateway.sol"; +import { L1OrbitGatewayRouter } from "contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol"; +import { L2GatewayRouter } from "contracts/tokenbridge/arbitrum/gateway/L2GatewayRouter.sol"; +import { L1GatewayRouter } from "contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol"; +import { L1OrbitCustomGateway } from "contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20PresetMinterPauser } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; + +import "forge-std/console.sol"; + +contract L1OrbitGatewayRouterTest is L1GatewayRouterTest { + L1OrbitGatewayRouter public l1OrbitRouter; + ERC20 public nativeToken; + uint256 public nativeTokenTotalFee; + + function setUp() public override { + inbox = address(new ERC20InboxMock()); + nativeToken = ERC20(address(new ERC20PresetMinterPauser("X", "Y"))); + ERC20PresetMinterPauser(address(nativeToken)).mint(user, 1_000_000 ether); + ERC20PresetMinterPauser(address(nativeToken)).mint(owner, 1_000_000 ether); + ERC20InboxMock(inbox).setMockNativeToken(address(nativeToken)); + + defaultGateway = address(new L1OrbitERC20Gateway()); + + router = new L1OrbitGatewayRouter(); + l1Router = L1GatewayRouter(address(router)); + l1OrbitRouter = L1OrbitGatewayRouter(address(router)); + l1OrbitRouter.initialize(owner, defaultGateway, address(0), counterpartGateway, inbox); + + maxSubmissionCost = 0; + nativeTokenTotalFee = gasPriceBid * maxGas; + + vm.deal(owner, 100 ether); + vm.deal(user, 100 ether); + } + + /* solhint-disable func-name-mixedcase */ + function test_getGateway_CustomGateway() public override { + address token = makeAddr("some token"); + + address[] memory tokens = new address[](1); + tokens[0] = token; + address[] memory gateways = new address[](1); + gateways[0] = address(new L1OrbitERC20Gateway()); + + vm.startPrank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + l1OrbitRouter.setGateways( + tokens, + gateways, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + + address gateway = router.getGateway(token); + assertEq(gateway, gateways[0], "Invalid gateway"); + } + + function test_getGateway_DisabledGateway() public override { + address token = makeAddr("some token"); + + address[] memory tokens = new address[](1); + tokens[0] = token; + address[] memory gateways = new address[](1); + gateways[0] = address(1); + + vm.startPrank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + l1OrbitRouter.setGateways( + tokens, + gateways, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + + address gateway = router.getGateway(token); + assertEq(gateway, address(0), "Invalid gateway"); + } + + function test_outboundTransfer() public override { + // init default gateway + L1OrbitERC20Gateway(defaultGateway).initialize( + makeAddr("defaultGatewayCounterpart"), + address(l1Router), + inbox, + 0x0000000000000000000000000000000000000000000000000000000000000001, + makeAddr("l2BeaconProxyFactory") + ); + + // set default gateway + vm.startPrank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + l1OrbitRouter.setDefaultGateway( + address(defaultGateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + vm.stopPrank(); + + // create token + ERC20PresetMinterPauser token = new ERC20PresetMinterPauser("X", "Y"); + token.mint(user, 10000); + + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(defaultGateway)); + uint256 userNativeTokenBalanceBefore = nativeToken.balanceOf(user); + + /// deposit data + uint256 amount = 103; + address to = address(401); + bytes memory userEncodedData = abi.encode(maxSubmissionCost, "", nativeTokenTotalFee); + + /// approve tokens + vm.prank(user); + token.approve(address(defaultGateway), amount); + + // approve fees + vm.prank(user); + nativeToken.approve(address(defaultGateway), nativeTokenTotalFee); + + // expect event + vm.expectEmit(true, true, true, true); + emit TransferRouted(address(token), user, to, address(defaultGateway)); + + /// deposit it + vm.prank(user); + l1Router.outboundTransfer(address(token), to, amount, maxGas, gasPriceBid, userEncodedData); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(defaultGateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + amount, + "Wrong defaultGateway balance" + ); + + uint256 userNativeTokenBalanceAfter = nativeToken.balanceOf(user); + assertEq( + userNativeTokenBalanceBefore - userNativeTokenBalanceAfter, + nativeTokenTotalFee, + "Wrong user native token balance" + ); + } + + function test_outboundTransfer_revert_NotAllowedToBridgeFeeToken() public { + // init default gateway + L1OrbitERC20Gateway(defaultGateway).initialize( + makeAddr("defaultGatewayCounterpart"), + address(l1Router), + inbox, + 0x0000000000000000000000000000000000000000000000000000000000000001, + makeAddr("l2BeaconProxyFactory") + ); + + // set default gateway + vm.startPrank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + l1OrbitRouter.setDefaultGateway( + address(defaultGateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + vm.stopPrank(); + + /// deposit it + vm.prank(user); + vm.expectRevert("NOT_ALLOWED_TO_BRIDGE_FEE_TOKEN"); + l1Router.outboundTransfer(address(nativeToken), user, 100, maxGas, gasPriceBid, ""); + } + + function test_outboundTransferCustomRefund() public override { + // init default gateway + L1OrbitERC20Gateway(defaultGateway).initialize( + makeAddr("defaultGatewayCounterpart"), + address(l1Router), + inbox, + 0x0000000000000000000000000000000000000000000000000000000000000001, + makeAddr("l2BeaconProxyFactory") + ); + + // set default gateway + vm.startPrank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + l1OrbitRouter.setDefaultGateway( + address(defaultGateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + vm.stopPrank(); + + // create token + ERC20PresetMinterPauser token = new ERC20PresetMinterPauser("X", "Y"); + token.mint(user, 10000); + + // snapshot state before + uint256 userBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(defaultGateway)); + uint256 userNativeTokenBalanceBefore = nativeToken.balanceOf(user); + + /// deposit data + address refundTo = address(400); + address to = address(401); + uint256 amount = 103; + bytes memory userEncodedData = abi.encode(maxSubmissionCost, "", nativeTokenTotalFee); + + // approve fees + vm.prank(user); + nativeToken.approve(defaultGateway, nativeTokenTotalFee); + + // approve tokens + vm.prank(user); + token.approve(defaultGateway, amount); + + // expect event + vm.expectEmit(true, true, true, true); + emit TransferRouted(address(token), user, to, address(defaultGateway)); + + /// deposit it + vm.prank(user); + l1Router.outboundTransferCustomRefund( + address(token), + refundTo, + to, + amount, + maxGas, + gasPriceBid, + userEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = token.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + uint256 l1GatewayBalanceAfter = token.balanceOf(address(defaultGateway)); + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + amount, + "Wrong defaultGateway balance" + ); + + uint256 userNativeTokenBalanceAfter = nativeToken.balanceOf(user); + assertEq( + userNativeTokenBalanceBefore - userNativeTokenBalanceAfter, + nativeTokenTotalFee, + "Wrong user native token balance" + ); + } + + function test_outboundTransferCustomRefund_revert_NotAllowedToBridgeFeeToken() public { + // init default gateway + L1OrbitERC20Gateway(defaultGateway).initialize( + makeAddr("defaultGatewayCounterpart"), + address(l1Router), + inbox, + 0x0000000000000000000000000000000000000000000000000000000000000001, + makeAddr("l2BeaconProxyFactory") + ); + + // set default gateway + vm.startPrank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + l1OrbitRouter.setDefaultGateway( + address(defaultGateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + vm.stopPrank(); + + /// deposit it + vm.prank(user); + vm.expectRevert("NOT_ALLOWED_TO_BRIDGE_FEE_TOKEN"); + l1Router.outboundTransferCustomRefund( + address(nativeToken), + user, + user, + 100, + maxGas, + gasPriceBid, + "" + ); + } + + function test_setDefaultGateway() public override { + L1OrbitERC20Gateway newL1DefaultGateway = new L1OrbitERC20Gateway(); + address newDefaultGatewayCounterpart = makeAddr("newDefaultGatewayCounterpart"); + newL1DefaultGateway.initialize( + newDefaultGatewayCounterpart, + address(l1OrbitRouter), + inbox, + 0x0000000000000000000000000000000000000000000000000000000000000001, + makeAddr("l2BeaconProxyFactory") + ); + + // approve fees + vm.prank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + + // event checkers + vm.expectEmit(true, true, true, true); + emit DefaultGatewayUpdated(address(newL1DefaultGateway)); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1OrbitRouter), + counterpartGateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + abi.encodeWithSelector( + L2GatewayRouter.setDefaultGateway.selector, + newDefaultGatewayCounterpart + ) + ); + + // set it + vm.prank(owner); + uint256 seqNum = l1OrbitRouter.setDefaultGateway( + address(newL1DefaultGateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + + /// checks + assertEq( + l1OrbitRouter.defaultGateway(), + address(newL1DefaultGateway), + "Invalid newL1DefaultGateway" + ); + + assertEq(seqNum, 0, "Invalid seqNum"); + } + + function test_setDefaultGateway_AddressZero() public override { + address newL1DefaultGateway = address(0); + + // approve fees + vm.prank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + + // event checkers + vm.expectEmit(true, true, true, true); + emit DefaultGatewayUpdated(address(newL1DefaultGateway)); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1OrbitRouter), + counterpartGateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + abi.encodeWithSelector(L2GatewayRouter.setDefaultGateway.selector, address(0)) + ); + + // set it + vm.prank(owner); + uint256 seqNum = l1OrbitRouter.setDefaultGateway( + newL1DefaultGateway, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + + /// checks + assertEq( + l1OrbitRouter.defaultGateway(), + address(newL1DefaultGateway), + "Invalid newL1DefaultGateway" + ); + + assertEq(seqNum, 0, "Invalid seqNum"); + } + + function test_setDefaultGateway_revert_NotSupportedInOrbit() public { + vm.prank(owner); + vm.expectRevert("NOT_SUPPORTED_IN_ORBIT"); + l1OrbitRouter.setDefaultGateway{ value: retryableCost }( + address(5), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + } + + function test_setGateway() public override { + // create gateway + L1OrbitCustomGateway customGateway = new L1OrbitCustomGateway(); + address l2Counterpart = makeAddr("l2Counterpart"); + customGateway.initialize(l2Counterpart, address(l1Router), address(inbox), owner); + + // create token + ERC20 customToken = new ERC20("X", "Y"); + vm.deal(address(customToken), 100 ether); + + // approve fees + ERC20PresetMinterPauser(address(nativeToken)).mint( + address(customToken), + nativeTokenTotalFee + ); + vm.prank(address(customToken)); + nativeToken.approve(address(customGateway), nativeTokenTotalFee); + + // register token to gateway + vm.mockCall( + address(customToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(customToken)); + customGateway.registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + ERC20PresetMinterPauser(address(nativeToken)).mint( + address(customToken), + nativeTokenTotalFee + ); + + // snapshot state before + uint256 senderNativeTokenBalanceBefore = nativeToken.balanceOf(address(customToken)); + + // approve fees + vm.prank(address(customToken)); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + + // expect events + vm.expectEmit(true, true, true, true); + emit GatewaySet(address(customToken), address(customGateway)); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(address(customToken), address(customToken)); + + vm.expectEmit(true, true, true, true); + address[] memory _tokenArr = new address[](1); + _tokenArr[0] = address(customToken); + address[] memory _gatewayArr = new address[](1); + _gatewayArr[0] = l2Counterpart; + emit ERC20InboxRetryableTicket( + address(l1OrbitRouter), + counterpartGateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + abi.encodeWithSelector(L2GatewayRouter.setGateway.selector, _tokenArr, _gatewayArr) + ); + + // set gateway + vm.prank(address(customToken)); + uint256 seqNum = l1OrbitRouter.setGateway( + address(customGateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + + ///// checks + + assertEq( + l1OrbitRouter.l1TokenToGateway(address(customToken)), + address(customGateway), + "Gateway not set" + ); + + uint256 senderNativeTokenBalanceAfter = nativeToken.balanceOf(address(customToken)); + assertEq( + senderNativeTokenBalanceBefore - senderNativeTokenBalanceAfter, + nativeTokenTotalFee, + "Wrong sender native token balance" + ); + + assertEq(seqNum, 1, "Invalid seqNum"); + } + + function test_setGateway_CustomCreditback() public override { + // create gateway + L1OrbitCustomGateway customGateway = new L1OrbitCustomGateway(); + address l2Counterpart = makeAddr("l2Counterpart"); + customGateway.initialize(l2Counterpart, address(l1Router), address(inbox), owner); + + // create token + ERC20 customToken = new ERC20("X", "Y"); + vm.deal(address(customToken), 100 ether); + + // register token to gateway + ERC20PresetMinterPauser(address(nativeToken)).mint( + address(customToken), + nativeTokenTotalFee + ); + vm.prank(address(customToken)); + nativeToken.approve(address(customGateway), nativeTokenTotalFee); + + vm.mockCall( + address(customToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(address(customToken)); + customGateway.registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + // approve fees + ERC20PresetMinterPauser(address(nativeToken)).mint( + address(customToken), + nativeTokenTotalFee + ); + vm.prank(address(customToken)); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + + // expect events + vm.expectEmit(true, true, true, true); + emit GatewaySet(address(customToken), address(customGateway)); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, creditBackAddress); + + vm.expectEmit(true, true, true, true); + address[] memory _tokenArr = new address[](1); + _tokenArr[0] = address(customToken); + address[] memory _gatewayArr = new address[](1); + _gatewayArr[0] = l2Counterpart; + emit ERC20InboxRetryableTicket( + address(l1OrbitRouter), + counterpartGateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + abi.encodeWithSelector(L2GatewayRouter.setGateway.selector, _tokenArr, _gatewayArr) + ); + + // set gateway + vm.prank(address(customToken)); + uint256 seqNum = l1OrbitRouter.setGateway( + address(customGateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + ///// checks + + assertEq( + l1OrbitRouter.l1TokenToGateway(address(customToken)), + address(customGateway), + "Gateway not set" + ); + + assertEq(seqNum, 1, "Invalid seqNum"); + } + + function test_setGateway_revert_NoUpdateToDifferentAddress() public override { + // create gateway + address initialGateway = address(new L1OrbitCustomGateway()); + address l2Counterpart = makeAddr("l2Counterpart"); + L1OrbitCustomGateway(initialGateway).initialize( + l2Counterpart, + address(l1Router), + address(inbox), + owner + ); + + // create token + address token = address(new ERC20("X", "Y")); + vm.deal(address(token), 100 ether); + + // register token to gateway + ERC20PresetMinterPauser(address(nativeToken)).mint(address(token), nativeTokenTotalFee); + vm.prank(token); + nativeToken.approve(address(initialGateway), nativeTokenTotalFee); + + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(token); + L1OrbitCustomGateway(initialGateway).registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + // initially set gateway for token + ERC20PresetMinterPauser(address(nativeToken)).mint(address(token), nativeTokenTotalFee); + vm.prank(address(token)); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + + vm.prank(address(token)); + l1OrbitRouter.setGateway( + initialGateway, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + assertEq(l1OrbitRouter.l1TokenToGateway(token), initialGateway, "Initial gateway not set"); + + //// now try setting different gateway + address newGateway = address(new L1OrbitCustomGateway()); + + vm.prank(token); + vm.expectRevert("NO_UPDATE_TO_DIFFERENT_ADDR"); + l1OrbitRouter.setGateway( + newGateway, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + } + + function test_setGateway_revert_NotArbEnabled() public override { + address nonArbEnabledToken = address(new ERC20("X", "Y")); + vm.deal(nonArbEnabledToken, 100 ether); + vm.mockCall( + nonArbEnabledToken, + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb2)) + ); + + vm.prank(nonArbEnabledToken); + vm.expectRevert("NOT_ARB_ENABLED"); + l1OrbitRouter.setGateway( + makeAddr("gateway"), + 100000, + 3, + 200, + makeAddr("creditback"), + nativeTokenTotalFee + ); + } + + function test_setGateway_revert_NotToContract() public override { + address token = address(new ERC20("X", "Y")); + vm.deal(token, 100 ether); + vm.mockCall(token, abi.encodeWithSignature("isArbitrumEnabled()"), abi.encode(uint8(0xb1))); + + address gatewayNotContract = makeAddr("not contract"); + + vm.prank(token); + vm.expectRevert("NOT_TO_CONTRACT"); + l1OrbitRouter.setGateway( + gatewayNotContract, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + } + + function test_setGateway_revert_NotSupportedInOrbit() public { + vm.expectRevert("NOT_SUPPORTED_IN_ORBIT"); + l1OrbitRouter.setGateway(address(102), maxGas, gasPriceBid, maxSubmissionCost); + } + + function test_setGateway_revert_CustomCreaditbackNotSupportedInOrbit() public { + vm.expectRevert("NOT_SUPPORTED_IN_ORBIT"); + l1OrbitRouter.setGateway( + address(103), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress + ); + } + + function test_setGateway_revert_TokenNotHandledByGateway() public override { + // create gateway + L1OrbitCustomGateway gateway = new L1OrbitCustomGateway(); + + // create token + address token = address(new ERC20("X", "Y")); + vm.deal(token, 100 ether); + vm.mockCall(token, abi.encodeWithSignature("isArbitrumEnabled()"), abi.encode(uint8(0xb1))); + + vm.prank(token); + vm.expectRevert("TOKEN_NOT_HANDLED_BY_GATEWAY"); + l1OrbitRouter.setGateway( + address(gateway), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + } + + function test_setGateways() public override { + // create tokens and gateways + address[] memory tokens = new address[](2); + tokens[0] = address(new ERC20("1", "1")); + tokens[1] = address(new ERC20("2", "2")); + address[] memory gateways = new address[](2); + gateways[0] = address(new L1OrbitCustomGateway()); + gateways[1] = address(new L1OrbitCustomGateway()); + + address l2Counterpart = makeAddr("l2Counterpart"); + + /// init all + for (uint256 i = 0; i < 2; i++) { + L1OrbitCustomGateway(gateways[i]).initialize( + l2Counterpart, + address(l1Router), + address(inbox), + owner + ); + + vm.mockCall( + tokens[i], + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + + // register tokens to gateways + ERC20PresetMinterPauser(address(nativeToken)).mint(tokens[i], nativeTokenTotalFee); + vm.prank(tokens[i]); + nativeToken.approve(address(gateways[i]), nativeTokenTotalFee); + + vm.deal(tokens[i], 100 ether); + vm.prank(tokens[i]); + L1OrbitCustomGateway(gateways[i]).registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + } + + // approve fees + vm.prank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + + // expect events + vm.expectEmit(true, true, true, true); + emit GatewaySet(tokens[0], gateways[0]); + vm.expectEmit(true, true, true, true); + emit GatewaySet(tokens[1], gateways[1]); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + address[] memory _gatewayArr = new address[](2); + _gatewayArr[0] = l2Counterpart; + _gatewayArr[1] = l2Counterpart; + emit ERC20InboxRetryableTicket( + address(l1OrbitRouter), + counterpartGateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + abi.encodeWithSelector(L2GatewayRouter.setGateway.selector, tokens, _gatewayArr) + ); + + /// set gateways + vm.prank(owner); + uint256 seqNum = l1OrbitRouter.setGateways( + tokens, + gateways, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + + ///// checks + + assertEq(l1Router.l1TokenToGateway(tokens[0]), gateways[0], "Gateway[0] not set"); + assertEq(l1Router.l1TokenToGateway(tokens[1]), gateways[1], "Gateway[1] not set"); + assertEq(seqNum, 2, "Invalid seqNum"); + } + + function test_setGateways_SetZeroAddr() public override { + // create gateway + address initialGateway = address(new L1OrbitCustomGateway()); + address l2Counterpart = makeAddr("l2Counterpart"); + L1OrbitCustomGateway(initialGateway).initialize( + l2Counterpart, + address(l1Router), + address(inbox), + owner + ); + + // create token + address token = address(new ERC20("X", "Y")); + vm.deal(address(token), 100 ether); + + // register token to gateway + ERC20PresetMinterPauser(address(nativeToken)).mint(address(token), nativeTokenTotalFee); + vm.prank(address(token)); + nativeToken.approve(initialGateway, nativeTokenTotalFee); + + vm.mockCall( + address(token), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.prank(token); + L1OrbitCustomGateway(initialGateway).registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + // initially set gateway for token + ERC20PresetMinterPauser(address(nativeToken)).mint(address(token), nativeTokenTotalFee); + vm.prank(address(token)); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + + vm.prank(address(token)); + l1OrbitRouter.setGateway( + initialGateway, + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + assertEq(l1OrbitRouter.l1TokenToGateway(token), initialGateway, "Initial gateway not set"); + + //// now set to zero addr + address newGateway = address(0); + + // approve fees + vm.prank(owner); + nativeToken.approve(address(l1OrbitRouter), nativeTokenTotalFee); + + // expect events + vm.expectEmit(true, true, true, true); + emit GatewaySet(token, newGateway); + + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(owner, owner); + + vm.expectEmit(true, true, true, true); + address[] memory _tokenArr = new address[](1); + _tokenArr[0] = token; + address[] memory _gatewayArr = new address[](1); + _gatewayArr[0] = newGateway; + emit ERC20InboxRetryableTicket( + address(l1OrbitRouter), + counterpartGateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + abi.encodeWithSelector(L2GatewayRouter.setGateway.selector, _tokenArr, _gatewayArr) + ); + + /// set gateways + vm.prank(owner); + uint256 seqNum = l1OrbitRouter.setGateways( + _tokenArr, + _gatewayArr, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + + ///// checks + + assertEq(l1OrbitRouter.l1TokenToGateway(token), address(0), "Custom gateway not cleared"); + assertEq(seqNum, 2, "Invalid seqNum"); + } + + function test_setGateways_revert_WrongLength() public override { + address[] memory tokens = new address[](2); + tokens[0] = address(new ERC20("1", "1")); + tokens[1] = address(new ERC20("2", "2")); + address[] memory gateways = new address[](1); + gateways[0] = address(new L1OrbitCustomGateway()); + + /// set gateways + vm.prank(owner); + vm.expectRevert("WRONG_LENGTH"); + l1OrbitRouter.setGateways( + tokens, + gateways, + maxGas, + gasPriceBid, + maxSubmissionCost, + nativeTokenTotalFee + ); + } + + function test_setGateways_revert_NotSupportedInOrbit() public { + vm.prank(owner); + vm.expectRevert("NOT_SUPPORTED_IN_ORBIT"); + l1OrbitRouter.setGateways( + new address[](2), + new address[](2), + maxGas, + gasPriceBid, + maxSubmissionCost + ); + } + + event ERC20InboxRetryableTicket( + address from, + address to, + uint256 l2CallValue, + uint256 maxGas, + uint256 gasPrice, + uint256 tokenTotalFeeAmount, + bytes data + ); +} diff --git a/test-foundry/L1OrbitIntegration.t.sol b/test-foundry/L1OrbitIntegration.t.sol new file mode 100644 index 0000000000..985856e8c1 --- /dev/null +++ b/test-foundry/L1OrbitIntegration.t.sol @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import { L1GatewayRouterTest } from "./L1GatewayRouter.t.sol"; +import { ERC20InboxMock } from "contracts/tokenbridge/test/InboxMock.sol"; +import { L1OrbitERC20Gateway } from "contracts/tokenbridge/ethereum/gateway/L1OrbitERC20Gateway.sol"; +import { L1OrbitGatewayRouter } from "contracts/tokenbridge/ethereum/gateway/L1OrbitGatewayRouter.sol"; +import { L2GatewayRouter } from "contracts/tokenbridge/arbitrum/gateway/L2GatewayRouter.sol"; +import { L1GatewayRouter } from "contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol"; +import { L1OrbitCustomGateway } from "contracts/tokenbridge/ethereum/gateway/L1OrbitCustomGateway.sol"; +import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20PresetMinterPauser } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import { TestERC20 } from "contracts/tokenbridge/test/TestERC20.sol"; +import { TestOrbitCustomTokenL1 } from "contracts/tokenbridge/test/TestCustomTokenL1.sol"; +import { ERC20Inbox } from "lib/nitro-contracts/src/bridge/ERC20Inbox.sol"; +import { ERC20Bridge } from "lib/nitro-contracts/src/bridge/ERC20Bridge.sol"; +import { ERC20PresetFixedSupply } from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import { IOwnable } from "lib/nitro-contracts/src/bridge/IOwnable.sol"; +import { ISequencerInbox } from "lib/nitro-contracts/src/bridge/ISequencerInbox.sol"; +import "./util/TestUtil.sol"; + +contract IntegrationTest is Test { + address public owner = makeAddr("owner"); + address public user = makeAddr("user"); + address public counterpartGateway = makeAddr("counterpartGateway"); + address public rollup = makeAddr("rollup"); + address public seqInbox = makeAddr("seqInbox"); + address public l2Gateway = makeAddr("l2Gateway"); + bytes32 public cloneableProxyHash = bytes32("123"); + address public l2BeaconProxyFactory = makeAddr("l2BeaconProxyFactory"); + + ERC20PresetFixedSupply public nativeToken; + ERC20Inbox public inbox; + ERC20Bridge public bridge; + L1OrbitERC20Gateway public defaultGateway; + L1OrbitGatewayRouter public router; + + uint256 private maxSubmissionCost = 0; + uint256 private maxGas = 100000; + uint256 private gasPriceBid = 3; + uint256 private nativeTokenTotalFee; + + function setUp() public { + // deploy token, bridge and inbox + nativeToken = new ERC20PresetFixedSupply( + "Appchain Token", + "App", + 1_000_000 ether, + address(this) + ); + inbox = ERC20Inbox(TestUtil.deployProxy(address(new ERC20Inbox()))); + bridge = ERC20Bridge(TestUtil.deployProxy(address(new ERC20Bridge()))); + + // init bridge and inbox + bridge.initialize(IOwnable(rollup), address(nativeToken)); + inbox.initialize(bridge, ISequencerInbox(seqInbox)); + vm.prank(rollup); + bridge.setDelayedInbox(address(inbox), true); + + // create default gateway and router + defaultGateway = L1OrbitERC20Gateway( + TestUtil.deployProxy(address(new L1OrbitERC20Gateway())) + ); + router = L1OrbitGatewayRouter(TestUtil.deployProxy(address(new L1OrbitGatewayRouter()))); + router.initialize( + owner, + address(defaultGateway), + address(0), + counterpartGateway, + address(inbox) + ); + defaultGateway.initialize( + l2Gateway, + address(router), + address(inbox), + cloneableProxyHash, + l2BeaconProxyFactory + ); + + nativeTokenTotalFee = maxGas * gasPriceBid; + } + + /* solhint-disable func-name-mixedcase */ + function test_depositNative(uint256 depositAmount) public { + vm.assume(depositAmount < 1_000_000 ether); + nativeToken.transfer(user, depositAmount); + + // snapshot before + uint256 userNativeTokenBalanceBefore = nativeToken.balanceOf(user); + uint256 bridgeNativeTokenBalanceBefore = nativeToken.balanceOf(address(bridge)); + + vm.prank(user); + nativeToken.approve(address(inbox), depositAmount); + + vm.prank(user); + inbox.depositERC20(depositAmount); + + // snapshot before + uint256 userNativeTokenBalanceAfter = nativeToken.balanceOf(user); + uint256 bridgeNativeTokenBalanceAfter = nativeToken.balanceOf(address(bridge)); + + assertEq( + userNativeTokenBalanceBefore - userNativeTokenBalanceAfter, + depositAmount, + "Invalid user native token balance" + ); + + assertEq( + bridgeNativeTokenBalanceAfter - bridgeNativeTokenBalanceBefore, + depositAmount, + "Invalid bridge token balance" + ); + } + + function test_depositToken_DefaultGateway() public { + uint256 tokenDepositAmount = 250; + + // token to bridge + IERC20 token = IERC20(address(new TestERC20())); + + // fund account + vm.prank(user); + TestERC20(address(token)).mint(); + + // fund user to be able to pay retryable fees + nativeToken.transfer(user, nativeTokenTotalFee); + + // snapshot state before + uint256 userTokenBalanceBefore = token.balanceOf(user); + uint256 l1GatewayBalanceBefore = token.balanceOf(address(defaultGateway)); + uint256 userNativeTokenBalanceBefore = nativeToken.balanceOf(user); + uint256 bridgeNativeTokenBalanceBefore = nativeToken.balanceOf(address(bridge)); + + { + vm.startPrank(user); + + /// approve token + token.approve(address(defaultGateway), tokenDepositAmount); + + // approve fees + nativeToken.approve(address(defaultGateway), nativeTokenTotalFee); + + address refundTo = user; + bytes memory userEncodedData = abi.encode(maxSubmissionCost, "", nativeTokenTotalFee); + router.outboundTransferCustomRefund( + address(token), + refundTo, + user, + tokenDepositAmount, + maxGas, + gasPriceBid, + userEncodedData + ); + + vm.stopPrank(); + } + + /// check token moved user->gateway, and native token user->bridge + { + uint256 userTokenBalanceAfter = token.balanceOf(user); + uint256 l1GatewayBalanceAfter = token.balanceOf(address(defaultGateway)); + uint256 userNativeTokenBalanceAfter = nativeToken.balanceOf(user); + uint256 bridgeNativeTokenBalanceAfter = nativeToken.balanceOf(address(bridge)); + + assertEq( + userTokenBalanceBefore - userTokenBalanceAfter, + tokenDepositAmount, + "Invalid user token balance" + ); + + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + tokenDepositAmount, + "Invalid default gateway token balance" + ); + + assertEq( + userNativeTokenBalanceBefore - userNativeTokenBalanceAfter, + nativeTokenTotalFee, + "Invalid user native token balance" + ); + + assertEq( + bridgeNativeTokenBalanceAfter - bridgeNativeTokenBalanceBefore, + nativeTokenTotalFee, + "Invalid user native token balance" + ); + } + } + + function test_depositToken_CustomGateway() public { + uint256 tokenDepositAmount = 250; + + ///////// + nativeToken.transfer(user, nativeTokenTotalFee * 3); + + // create + init custom gateway + L1OrbitCustomGateway customL1Gateway = new L1OrbitCustomGateway(); + L1OrbitCustomGateway(address(customL1Gateway)).initialize( + makeAddr("l2Gateway"), + address(router), + address(inbox), + owner + ); + + // create token and register it + TestOrbitCustomTokenL1 customToken = new TestOrbitCustomTokenL1( + address(customL1Gateway), + address(router) + ); + + vm.startPrank(user); + nativeToken.approve(address(customToken), nativeTokenTotalFee * 2); + customToken.registerTokenOnL2( + makeAddr("l2CustomTokenAddress"), + maxSubmissionCost, + maxSubmissionCost, + maxGas, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + nativeTokenTotalFee, + makeAddr("creditbackAddr") + ); + assertEq( + router.l1TokenToGateway(address(customToken)), + address(customL1Gateway), + "Invalid custom gateway" + ); + customToken.mint(); + vm.stopPrank(); + + // snapshot state before + uint256 userTokenBalanceBefore = customToken.balanceOf(user); + uint256 l1GatewayBalanceBefore = customToken.balanceOf(address(customL1Gateway)); + uint256 userNativeTokenBalanceBefore = nativeToken.balanceOf(user); + uint256 bridgeNativeTokenBalanceBefore = nativeToken.balanceOf(address(bridge)); + + /// deposit custom token + { + vm.startPrank(user); + + /// approve token + customToken.approve(address(customL1Gateway), tokenDepositAmount); + + // approve fees + nativeToken.transfer(user, nativeTokenTotalFee); + nativeToken.approve(address(customL1Gateway), nativeTokenTotalFee); + + address refundTo = user; + bytes memory userEncodedData = abi.encode(maxSubmissionCost, "", nativeTokenTotalFee); + router.outboundTransferCustomRefund( + address(customToken), + refundTo, + user, + tokenDepositAmount, + maxGas, + gasPriceBid, + userEncodedData + ); + + vm.stopPrank(); + } + + /// check token moved user->gateway, and native token user->bridge + { + uint256 userTokenBalanceAfter = customToken.balanceOf(user); + uint256 l1GatewayBalanceAfter = customToken.balanceOf(address(customL1Gateway)); + uint256 userNativeTokenBalanceAfter = nativeToken.balanceOf(user); + uint256 bridgeNativeTokenBalanceAfter = nativeToken.balanceOf(address(bridge)); + + assertEq( + userTokenBalanceBefore - userTokenBalanceAfter, + tokenDepositAmount, + "Invalid user token balance" + ); + + assertEq( + l1GatewayBalanceAfter - l1GatewayBalanceBefore, + tokenDepositAmount, + "Invalid default gateway token balance" + ); + + assertEq( + userNativeTokenBalanceBefore - userNativeTokenBalanceAfter, + nativeTokenTotalFee, + "Invalid user native token balance" + ); + + assertEq( + bridgeNativeTokenBalanceAfter - bridgeNativeTokenBalanceBefore, + nativeTokenTotalFee, + "Invalid user native token balance" + ); + } + } +} diff --git a/test-foundry/L1OrbitReverseCustomGateway.t.sol b/test-foundry/L1OrbitReverseCustomGateway.t.sol new file mode 100644 index 0000000000..2300b1fb0d --- /dev/null +++ b/test-foundry/L1OrbitReverseCustomGateway.t.sol @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1OrbitCustomGatewayTest, ERC20InboxMock, TestERC20, IERC20, ERC20, ERC20PresetMinterPauser } from "./L1OrbitCustomGateway.t.sol"; +import { L1OrbitReverseCustomGateway } from "contracts/tokenbridge/ethereum/gateway/L1OrbitReverseCustomGateway.sol"; +import { MintableTestCustomTokenL1, ReverseTestCustomTokenL1 } from "contracts/tokenbridge/test/TestCustomTokenL1.sol"; +import { IInbox } from "contracts/tokenbridge/ethereum/L1ArbitrumMessenger.sol"; + +contract L1OrbitReverseCustomGatewayTest is L1OrbitCustomGatewayTest { + function setUp() public override { + inbox = address(new ERC20InboxMock()); + nativeToken = ERC20(address(new ERC20PresetMinterPauser("X", "Y"))); + ERC20PresetMinterPauser(address(nativeToken)).mint(user, 1_000_000 ether); + ERC20PresetMinterPauser(address(nativeToken)).mint(owner, 1_000_000 ether); + ERC20InboxMock(inbox).setMockNativeToken(address(nativeToken)); + + l1Gateway = new L1OrbitReverseCustomGateway(); + L1OrbitReverseCustomGateway(address(l1Gateway)).initialize(l2Gateway, router, inbox, owner); + + token = IERC20(address(new TestERC20())); + + maxSubmissionCost = 0; + nativeTokenTotalFee = maxGas * gasPriceBid; + + // fund user and router + vm.prank(user); + TestERC20(address(token)).mint(); + vm.deal(router, 100 ether); + vm.deal(address(token), 100 ether); + vm.deal(owner, 100 ether); + } + + /* solhint-disable func-name-mixedcase */ + function test_finalizeInboundTransfer() public override { + // fund gateway with bridged tokens + MintableTestCustomTokenL1 bridgedToken = new MintableTestCustomTokenL1( + address(l1Gateway), + router + ); + vm.prank(address(l1Gateway)); + bridgedToken.mint(); + + // snapshot state before + uint256 userBalanceBefore = bridgedToken.balanceOf(user); + + // deposit params + address from = address(3000); + uint256 amount = 25; + uint256 exitNum = 7; + bytes memory callHookData = ""; + bytes memory data = abi.encode(exitNum, callHookData); + + ERC20InboxMock(address(inbox)).setL2ToL1Sender(l2Gateway); + + // trigger deposit + vm.prank(address(IInbox(l1Gateway.inbox()).bridge())); + L1OrbitReverseCustomGateway(address(l1Gateway)).finalizeInboundTransfer( + address(bridgedToken), + from, + user, + amount, + data + ); + + // check tokens are minted + uint256 userBalanceAfter = bridgedToken.balanceOf(user); + assertEq(userBalanceAfter - userBalanceBefore, amount, "Wrong user balance"); + } + + function test_outboundTransfer() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = new ReverseTestCustomTokenL1( + address(l1Gateway), + router + ); + vm.prank(address(user)); + bridgedToken.mint(); + + // snapshot state before + uint256 userBalanceBefore = bridgedToken.balanceOf(user); + + uint256 amount = 300; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // approve fees + ERC20PresetMinterPauser(address(nativeToken)).mint( + address(bridgedToken), + nativeTokenTotalFee + ); + vm.prank(address(bridgedToken)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // register token to gateway + vm.mockCall( + address(bridgedToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.deal(address(bridgedToken), 100 ether); + vm.prank(address(bridgedToken)); + uint256 seqNum0 = L1OrbitReverseCustomGateway(address(l1Gateway)).registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + // approve token + vm.prank(user); + bridgedToken.approve(address(l1Gateway), amount); + + // approve fees + vm.prank(user); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(user, user); + + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + l1Gateway.getOutboundCalldata(address(bridgedToken), user, user, amount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(bridgedToken), user, user, 1, amount); + + // trigger transfer + vm.prank(router); + bytes memory seqNum1 = L1OrbitReverseCustomGateway(address(l1Gateway)).outboundTransfer( + address(bridgedToken), + user, + amount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are burned + uint256 userBalanceAfter = bridgedToken.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = new ReverseTestCustomTokenL1( + address(l1Gateway), + router + ); + vm.prank(address(user)); + bridgedToken.mint(); + + // snapshot state before + uint256 userBalanceBefore = bridgedToken.balanceOf(user); + + uint256 amount = 450; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // approve fees + ERC20PresetMinterPauser(address(nativeToken)).mint( + address(bridgedToken), + nativeTokenTotalFee + ); + vm.prank(address(bridgedToken)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // register token to gateway + vm.mockCall( + address(bridgedToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.deal(address(bridgedToken), 100 ether); + vm.prank(address(bridgedToken)); + uint256 seqNum0 = L1OrbitReverseCustomGateway(address(l1Gateway)).registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + // approve token + vm.prank(user); + bridgedToken.approve(address(l1Gateway), amount); + + // approve fees + vm.prank(user); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, user); + + vm.expectEmit(true, true, true, true); + emit ERC20InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + gasPriceBid, + nativeTokenTotalFee, + l1Gateway.getOutboundCalldata(address(bridgedToken), user, user, amount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(bridgedToken), user, user, 1, amount); + + // trigger deposit + vm.prank(router); + bytes memory seqNum1 = L1OrbitReverseCustomGateway(address(l1Gateway)) + .outboundTransferCustomRefund( + address(bridgedToken), + creditBackAddress, + user, + amount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = bridgedToken.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund_revert_InsufficientAllowance() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = new ReverseTestCustomTokenL1( + address(l1Gateway), + router + ); + vm.prank(address(user)); + bridgedToken.mint(); + + uint256 tooManyTokens = 500 ether; + + // approve fees + ERC20PresetMinterPauser(address(nativeToken)).mint( + address(bridgedToken), + nativeTokenTotalFee + ); + vm.prank(address(bridgedToken)); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + // register token to gateway + vm.mockCall( + address(bridgedToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.deal(address(bridgedToken), 100 ether); + vm.prank(address(bridgedToken)); + L1OrbitReverseCustomGateway(address(l1Gateway)).registerTokenToL2( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + creditBackAddress, + nativeTokenTotalFee + ); + + vm.prank(user); + nativeToken.approve(address(l1Gateway), nativeTokenTotalFee); + + vm.prank(router); + vm.expectRevert("ERC20: burn amount exceeds balance"); + l1Gateway.outboundTransferCustomRefund( + address(bridgedToken), + user, + user, + tooManyTokens, + 0.1 ether, + 0.01 ether, + buildRouterEncodedData("") + ); + } +} diff --git a/test-foundry/L1ReverseCustomGateway.t.sol b/test-foundry/L1ReverseCustomGateway.t.sol new file mode 100644 index 0000000000..30dce5bba1 --- /dev/null +++ b/test-foundry/L1ReverseCustomGateway.t.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import { L1CustomGatewayTest, InboxMock, IERC20, IInbox, TestERC20 } from "./L1CustomGateway.t.sol"; +import { L1ReverseCustomGateway } from "contracts/tokenbridge/ethereum/gateway/L1ReverseCustomGateway.sol"; +import { MintableTestCustomTokenL1, ReverseTestCustomTokenL1 } from "contracts/tokenbridge/test/TestCustomTokenL1.sol"; + +contract L1ReverseCustomGatewayTest is L1CustomGatewayTest { + function setUp() public virtual override { + inbox = address(new InboxMock()); + + l1Gateway = new L1ReverseCustomGateway(); + L1ReverseCustomGateway(address(l1Gateway)).initialize(l2Gateway, router, inbox, owner); + + token = IERC20(address(new TestERC20())); + + maxSubmissionCost = 20; + retryableCost = maxSubmissionCost + gasPriceBid * maxGas; + + // fund user and router + vm.prank(user); + TestERC20(address(token)).mint(); + vm.deal(router, 100 ether); + vm.deal(address(token), 100 ether); + vm.deal(owner, 100 ether); + } + + /* solhint-disable func-name-mixedcase */ + function test_finalizeInboundTransfer() public override { + // fund gateway with bridged tokens + MintableTestCustomTokenL1 bridgedToken = new MintableTestCustomTokenL1( + address(l1Gateway), + router + ); + vm.prank(address(l1Gateway)); + bridgedToken.mint(); + + // snapshot state before + uint256 userBalanceBefore = bridgedToken.balanceOf(user); + + // deposit params + address from = address(3000); + uint256 amount = 25; + uint256 exitNum = 7; + bytes memory callHookData = ""; + bytes memory data = abi.encode(exitNum, callHookData); + + InboxMock(address(inbox)).setL2ToL1Sender(l2Gateway); + + // trigger deposit + vm.prank(address(IInbox(l1Gateway.inbox()).bridge())); + l1Gateway.finalizeInboundTransfer(address(bridgedToken), from, user, amount, data); + + // check tokens are minted + uint256 userBalanceAfter = bridgedToken.balanceOf(user); + assertEq(userBalanceAfter - userBalanceBefore, amount, "Wrong user balance"); + } + + function test_outboundTransfer() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = new ReverseTestCustomTokenL1( + address(l1Gateway), + router + ); + vm.prank(address(user)); + bridgedToken.mint(); + + // snapshot state before + uint256 userBalanceBefore = bridgedToken.balanceOf(user); + + uint256 amount = 300; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // register token to gateway + vm.mockCall( + address(bridgedToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.deal(address(bridgedToken), 100 ether); + vm.prank(address(bridgedToken)); + uint256 seqNum0 = L1ReverseCustomGateway(address(l1Gateway)).registerTokenToL2{ + value: retryableCost + }(makeAddr("tokenL2Address"), maxGas, gasPriceBid, maxSubmissionCost, creditBackAddress); + + // approve token + vm.prank(user); + bridgedToken.approve(address(l1Gateway), amount); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(user, user); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + l1Gateway.getOutboundCalldata(address(bridgedToken), user, user, amount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(bridgedToken), user, user, 1, amount); + + // trigger transfer + vm.prank(router); + bytes memory seqNum1 = l1Gateway.outboundTransfer{ value: retryableCost }( + address(bridgedToken), + user, + amount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are burned + uint256 userBalanceAfter = bridgedToken.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = new ReverseTestCustomTokenL1( + address(l1Gateway), + router + ); + vm.prank(address(user)); + bridgedToken.mint(); + + // snapshot state before + uint256 userBalanceBefore = bridgedToken.balanceOf(user); + + uint256 amount = 450; + bytes memory callHookData = ""; + bytes memory routerEncodedData = buildRouterEncodedData(callHookData); + + // register token to gateway + vm.mockCall( + address(bridgedToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.deal(address(bridgedToken), 100 ether); + vm.prank(address(bridgedToken)); + uint256 seqNum0 = L1ReverseCustomGateway(address(l1Gateway)).registerTokenToL2{ + value: retryableCost + }(makeAddr("tokenL2Address"), maxGas, gasPriceBid, maxSubmissionCost, creditBackAddress); + + // approve token + vm.prank(user); + bridgedToken.approve(address(l1Gateway), amount); + + // event checkers + vm.expectEmit(true, true, true, true); + emit TicketData(maxSubmissionCost); + + vm.expectEmit(true, true, true, true); + emit RefundAddresses(creditBackAddress, user); + + vm.expectEmit(true, true, true, true); + emit InboxRetryableTicket( + address(l1Gateway), + l2Gateway, + 0, + maxGas, + l1Gateway.getOutboundCalldata(address(bridgedToken), user, user, amount, callHookData) + ); + + vm.expectEmit(true, true, true, true); + emit DepositInitiated(address(bridgedToken), user, user, 1, amount); + + // trigger deposit + vm.prank(router); + bytes memory seqNum1 = l1Gateway.outboundTransferCustomRefund{ value: retryableCost }( + address(bridgedToken), + creditBackAddress, + user, + amount, + maxGas, + gasPriceBid, + routerEncodedData + ); + + // check tokens are escrowed + uint256 userBalanceAfter = bridgedToken.balanceOf(user); + assertEq(userBalanceBefore - userBalanceAfter, amount, "Wrong user balance"); + + assertEq(seqNum0, 0, "Invalid seqNum0"); + assertEq(seqNum1, abi.encode(1), "Invalid seqNum1"); + } + + function test_outboundTransferCustomRefund_revert_InsufficientAllowance() public override { + // fund user with tokens + MintableTestCustomTokenL1 bridgedToken = new ReverseTestCustomTokenL1( + address(l1Gateway), + router + ); + vm.prank(address(user)); + bridgedToken.mint(); + + uint256 tooManyTokens = 500 ether; + + // register token to gateway + vm.mockCall( + address(bridgedToken), + abi.encodeWithSignature("isArbitrumEnabled()"), + abi.encode(uint8(0xb1)) + ); + vm.deal(address(bridgedToken), 100 ether); + vm.prank(address(bridgedToken)); + L1ReverseCustomGateway(address(l1Gateway)).registerTokenToL2{ value: retryableCost }( + makeAddr("tokenL2Address"), + maxGas, + gasPriceBid, + maxSubmissionCost, + makeAddr("creditBackAddress") + ); + + vm.prank(router); + vm.expectRevert("ERC20: burn amount exceeds balance"); + l1Gateway.outboundTransferCustomRefund{ value: 1 ether }( + address(bridgedToken), + user, + user, + tooManyTokens, + 0.1 ether, + 0.01 ether, + buildRouterEncodedData("") + ); + } +} diff --git a/test-foundry/util/TestUtil.sol b/test-foundry/util/TestUtil.sol new file mode 100644 index 0000000000..27b3265865 --- /dev/null +++ b/test-foundry/util/TestUtil.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +library TestUtil { + function deployProxy(address logic) public returns (address) { + ProxyAdmin pa = new ProxyAdmin(); + return address(new TransparentUpgradeableProxy(address(logic), address(pa), "")); + } +} diff --git a/yarn.lock b/yarn.lock index f1118e3a26..8185659c93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@aduh95/viz.js@^3.7.0": version "3.7.0" resolved "https://registry.yarnpkg.com/@aduh95/viz.js/-/viz.js-3.7.0.tgz#a20d86c5fc8f6abebdc39b96a4326e10375d77c0" @@ -18,10 +23,10 @@ optionalDependencies: sol2uml "2.2.0" -"@arbitrum/sdk@^3.1.5": - version "3.1.5" - resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-3.1.5.tgz#ef5d99d60fa63a981c14136ee642e4bb00b2b185" - integrity sha512-IQdu3wr0xtC2jPokosjd+DAKcRO3Hl+C6sFdfpik5KJYcGP5YzLKpxknxusE2kHxkR1AMaLmXvwAf52rcflHHA== +"@arbitrum/sdk@^3.1.3": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-3.1.9.tgz#a511bc70cdd0a947445e4e27833c242ccb77ddf5" + integrity sha512-7Rf75cKmQ8nutTV+2JAOXcI6DKG4D7QCJz1JL2nq6hUpRuw4jyKn+irLqJtcIExssylLWt3t/pKV6fYseCYKNg== dependencies: "@ethersproject/address" "^5.0.8" "@ethersproject/bignumber" "^5.1.1" @@ -36,24 +41,25 @@ "@babel/highlight" "^7.10.4" "@babel/code-frame@^7.0.0": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" - integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" + integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== dependencies: - "@babel/highlight" "^7.22.5" + "@babel/highlight" "^7.22.10" + chalk "^2.4.2" "@babel/helper-validator-identifier@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== -"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" - integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== +"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" + integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== dependencies: "@babel/helper-validator-identifier" "^7.22.5" - chalk "^2.0.0" + chalk "^2.4.2" js-tokens "^4.0.0" "@colors/colors@1.5.0": @@ -210,6 +216,11 @@ ethereumjs-util "^7.1.1" miller-rabin "^4.0.0" +"@ethereumjs/rlp@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" + integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== + "@ethereumjs/tx@3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.2.tgz#348d4624bf248aaab6c44fec2ae67265efe3db00" @@ -226,6 +237,15 @@ "@ethereumjs/common" "^2.6.4" ethereumjs-util "^7.1.5" +"@ethereumjs/util@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.1.0.tgz#299df97fb6b034e0577ce9f94c7d9d1004409ed4" + integrity sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA== + dependencies: + "@ethereumjs/rlp" "^4.0.1" + ethereum-cryptography "^2.0.0" + micro-ftch "^0.3.1" + "@ethereumjs/vm@^5.9.0": version "5.9.3" resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.9.3.tgz#6d69202e4c132a4a1e1628ac246e92062e230823" @@ -644,11 +664,28 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" +"@noble/curves@1.1.0", "@noble/curves@~1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== +"@noble/hashes@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + +"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@noble/secp256k1@1.7.1", "@noble/secp256k1@~1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -701,11 +738,24 @@ resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.6.tgz#d11cb063a5f61a77806053e54009c40ddee49a54" integrity sha512-+Wz0hwmJGSI17B+BhU/qFRZ1l6/xMW82QGXE/Gi+WTmwgJrQefuBs1lIf7hzQ1hLk6hpkvb/zwcNkpVKRYTQYg== +"@offchainlabs/upgrade-executor@^1.0.0-beta.2": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@offchainlabs/upgrade-executor/-/upgrade-executor-1.0.0.tgz#226e92d116bce4b08f65d4a213bd1e00be08f089" + integrity sha512-leTFFNKS1z19gExXUhg/GZOBmsm2Tj9Ev7viOmFtc2ktgcczox0J2bx3CLQ+4/SgvCkeUH4ZtKoNosW8rgSJaw== + dependencies: + "@openzeppelin/contracts" "4.7.3" + "@openzeppelin/contracts-upgradeable" "4.7.3" + "@openzeppelin/contracts-upgradeable@4.5.2": version "4.5.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.5.2.tgz#90d9e47bacfd8693bfad0ac8a394645575528d05" integrity sha512-xgWZYaPlrEOQo3cBj97Ufiuv79SPd8Brh4GcFYhPgb6WvAq4ppz8dWKL6h+jLAK01rUqMRp/TS9AdXgAeNvCLA== +"@openzeppelin/contracts-upgradeable@4.7.3": + version "4.7.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.7.3.tgz#f1d606e2827d409053f3e908ba4eb8adb1dd6995" + integrity sha512-+wuegAMaLcZnLCJIvrVUDzA9z/Wp93f0Dla/4jJvIhijRrPabjQbZe6fWiECLaJyfn5ci9fqf9vTw3xpQOad2A== + "@openzeppelin/contracts-upgradeable@4.8.3": version "4.8.3" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.8.3.tgz#6b076a7b751811b90fe3a172a7faeaa603e13a3f" @@ -716,24 +766,29 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.5.0.tgz#3fd75d57de172b3743cdfc1206883f56430409cc" integrity sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA== +"@openzeppelin/contracts@4.7.3": + version "4.7.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.7.3.tgz#939534757a81f8d69cc854c7692805684ff3111e" + integrity sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw== + "@openzeppelin/contracts@4.8.3": version "4.8.3" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.3.tgz#cbef3146bfc570849405f59cba18235da95a252a" integrity sha512-bQHV8R9Me8IaJoJ2vPG4rXcL7seB7YVuskr4f+f5RyOStSZetwzkWtoqDMl5erkBJy0lDRUnIR2WIkPiC0GJlg== "@openzeppelin/upgrades-core@^1.24.1": - version "1.27.1" - resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.27.1.tgz#4dff06915ec665ee45789471071ebc4354e834dd" - integrity sha512-6tLcu6jt0nYdJNr+LRicBgP3jp+//B+dixgB3KsvycSglCHNfmBNDf0ZQ3ZquDdLL0QQmKzIs1EBRVp6lNvPnQ== + version "1.28.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/upgrades-core/-/upgrades-core-1.28.0.tgz#19405f272dc09e766c756d9d149cbd680168aef7" + integrity sha512-8RKlyg98Adv+46GxDaR0awL3R8bVCcQ27DcSEwrgWOp6siHh8sZg4a2l+2dhPl1510S6uBfhHSydMH5VX2BV5g== dependencies: - cbor "^8.0.0" + cbor "^9.0.0" chalk "^4.1.0" - compare-versions "^5.0.0" + compare-versions "^6.0.0" debug "^4.1.1" ethereumjs-util "^7.0.3" minimist "^1.2.7" proper-lockfile "^4.1.1" - solidity-ast "^0.4.15" + solidity-ast "^0.4.26" "@resolver-engine/core@^0.3.3": version "0.3.3" @@ -786,6 +841,15 @@ "@noble/secp256k1" "~1.7.0" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" + integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A== + dependencies: + "@noble/curves" "~1.1.0" + "@noble/hashes" "~1.3.1" + "@scure/base" "~1.1.0" + "@scure/bip39@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" @@ -794,6 +858,14 @@ "@noble/hashes" "~1.2.0" "@scure/base" "~1.1.0" +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -886,17 +958,10 @@ dependencies: antlr4ts "^0.5.0-alpha.4" -"@solidity-parser/parser@^0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.15.0.tgz#1d359be40be84f174dd616ccfadcf43346c6bf63" - integrity sha512-5UFJJTzWi1hgFk6aGCZ5rxG2DJkCJOzJ74qg7UkWSNCDSigW+CJLoYUb5bLiKrtI34Nr9rpFSUNHfkqtlL+N/w== - dependencies: - antlr4ts "^0.5.0-alpha.4" - -"@solidity-parser/parser@^0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.0.tgz#1fb418c816ca1fc3a1e94b08bcfe623ec4e1add4" - integrity sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q== +"@solidity-parser/parser@^0.16.0", "@solidity-parser/parser@^0.16.1": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.1.tgz#f7c8a686974e1536da0105466c4db6727311253c" + integrity sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw== dependencies: antlr4ts "^0.5.0-alpha.4" @@ -927,9 +992,9 @@ integrity sha512-sE7c9IHIGdbK4YayH4BC8i8qMjoAOeg6nUXUDZZp8wlU21/EMpaG+CLx+KqcIPyR+GSWIW3Dm0PXkr2nlggFDA== "@truffle/interface-adapter@^0.5.25": - version "0.5.34" - resolved "https://registry.yarnpkg.com/@truffle/interface-adapter/-/interface-adapter-0.5.34.tgz#a45edc23d6ace0e01ebf237b668119f456729643" - integrity sha512-gPxabfMi2TueE4VxnNuyeudOfvGJQ1ofVC02PFw14cnRQhzH327JikjjQbZ1bT6S7kWl9H6P3hQPFeYFMHdm1g== + version "0.5.35" + resolved "https://registry.yarnpkg.com/@truffle/interface-adapter/-/interface-adapter-0.5.35.tgz#f0eb1c4a2803190ca249143f545029a8b641fe96" + integrity sha512-B5gtJnvsum5j2do393n0UfCT8MklrlAZxuqvEFBeMM9UKnreYct0/D368FVMlZwWo1N50HgGeZ0hlpSJqR/nvg== dependencies: bn.js "^5.1.3" ethers "^4.0.32" @@ -988,9 +1053,9 @@ fs-extra "^9.1.0" "@types/abstract-leveldown@*": - version "7.2.1" - resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-7.2.1.tgz#bb16403c17754b0c4d5772d71d03b924a03d4c80" - integrity sha512-YK8irIC+eMrrmtGx0H4ISn9GgzLd9dojZWJaMbjp1YHLl2VqqNFBNrL5Q3KjGf4VE3sf/4hmq6EhQZ7kZp1NoQ== + version "7.2.2" + resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-7.2.2.tgz#45fed99f966749a6fc351131465026584e99456b" + integrity sha512-EVVYLp4D3zwP5/sS2S5x70t1w0BDrjbv0x0d0znngFKX/YExe+il/JAlHOEpo0cTdtY4Ohnllen32hPganG20w== "@types/bn.js@^4.11.3", "@types/bn.js@^4.11.5": version "4.11.6" @@ -1117,9 +1182,9 @@ form-data "^3.0.0" "@types/node@*": - version "20.3.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe" - integrity sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg== + version "20.5.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.6.tgz#5e9aaa86be03a09decafd61b128d6cec64a5fe40" + integrity sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ== "@types/node@^10.0.3": version "10.17.60" @@ -1132,9 +1197,9 @@ integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== "@types/node@^14.14.28": - version "14.18.51" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.51.tgz#cb90935b89c641201c3d07a595c3e22d1cfaa417" - integrity sha512-P9bsdGFPpVtofEKlhWMVS2qqx1A/rt9QBfihWlklfHHpUpjtYse5AzFz6j4DWrARLYh6gRnw9+5+DJcrq3KvBA== + version "14.18.56" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.56.tgz#09e092d684cd8cfbdb3c5e5802672712242f2600" + integrity sha512-+k+57NVS9opgrEn5l9c0gvD1r6C+PtyhVE4BTnMMRwiEA8ZO8uFcs6Yy2sXIy0eC95ZurBtRSvhZiHXBysbl6w== "@types/node@^8.0.0": version "8.10.66" @@ -1200,11 +1265,11 @@ "@types/node" "*" "@typescript-eslint/eslint-plugin-tslint@^5.30.6": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-5.60.0.tgz#9d3b6cc8f9770e714683e441d580d3c109112d6f" - integrity sha512-4QoOx2ixrFpst/HcvCITjrYOmoDtGm/MYHtCh9zCNDVnaRaJGTs9sOUQmRyNea+wAfDNpRFgu05Nn4HxfAvxbw== + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-5.62.0.tgz#220242dcd23711c400d4f5d5d876d5107cea4be0" + integrity sha512-qsYLld1+xed2lVwHbCxkCWdhRcByLNOjpccxK6HHlem724PbMcL1/dmH7jMQaqIpbfPAGkIypyyk3q5nUgtkhA== dependencies: - "@typescript-eslint/utils" "5.60.0" + "@typescript-eslint/utils" "5.62.0" "@typescript-eslint/eslint-plugin@^4.29.0": version "4.33.0" @@ -1250,23 +1315,23 @@ "@typescript-eslint/types" "4.33.0" "@typescript-eslint/visitor-keys" "4.33.0" -"@typescript-eslint/scope-manager@5.60.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.0.tgz#ae511967b4bd84f1d5e179bb2c82857334941c1c" - integrity sha512-hakuzcxPwXi2ihf9WQu1BbRj1e/Pd8ZZwVTG9kfbxAMZstKz8/9OoexIwnmLzShtsdap5U/CoQGRCWlSuPbYxQ== +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== dependencies: - "@typescript-eslint/types" "5.60.0" - "@typescript-eslint/visitor-keys" "5.60.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" "@typescript-eslint/types@4.33.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== -"@typescript-eslint/types@5.60.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.0.tgz#3179962b28b4790de70e2344465ec97582ce2558" - integrity sha512-ascOuoCpNZBccFVNJRSC6rPq4EmJ2NkuoKnd6LDNyAQmdDnziAtxbCGWCbefG1CNzmDvd05zO36AmB7H8RzKPA== +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== "@typescript-eslint/typescript-estree@4.33.0": version "4.33.0" @@ -1281,30 +1346,30 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.60.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600" - integrity sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ== +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== dependencies: - "@typescript-eslint/types" "5.60.0" - "@typescript-eslint/visitor-keys" "5.60.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.60.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.0.tgz#4667c5aece82f9d4f24a667602f0f300864b554c" - integrity sha512-ba51uMqDtfLQ5+xHtwlO84vkdjrqNzOnqrnwbMHMRY8Tqeme8C2Q8Fc7LajfGR+e3/4LoYiWXUM6BpIIbHJ4hQ== +"@typescript-eslint/utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.60.0" - "@typescript-eslint/types" "5.60.0" - "@typescript-eslint/typescript-estree" "5.60.0" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" eslint-scope "^5.1.1" semver "^7.3.7" @@ -1316,12 +1381,12 @@ "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@5.60.0": - version "5.60.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.0.tgz#b48b29da3f5f31dd1656281727004589d2722a66" - integrity sha512-wm9Uz71SbCyhUKgcaPRauBdTegUyY/ZWl8gLwD/i/ybJqscrrdVSFImpvUz16BLPChIeKBK5Fa9s6KDQjsjyWw== +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== dependencies: - "@typescript-eslint/types" "5.60.0" + "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" "@ungap/promise-all-settled@1.1.2": @@ -1430,9 +1495,9 @@ acorn@^7.4.0: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.4.1: - version "8.9.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" - integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== address@^1.0.1: version "1.2.2" @@ -1654,6 +1719,17 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== +array.prototype.findlast@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.2.tgz#134ef6b7215f131a8884fafe6af46846a032c518" + integrity sha512-p1YDNPNqA+P6cPX9ATsxg7DKir7gOmJ+jh5dEP3LlumMNYVC1F2Jgnyh6oI3n/qD9FeIkqR2jXfd73G68ImYUQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.1.3" + array.prototype.reduce@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac" @@ -1665,6 +1741,18 @@ array.prototype.reduce@^1.0.5: es-array-method-boxes-properly "^1.0.0" is-string "^1.0.7" +arraybuffer.prototype.slice@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb" + integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + asap@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -2757,26 +2845,33 @@ camelcase@^6.0.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30000844: - version "1.0.30001508" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001508.tgz#4461bbc895c692a96da399639cc1e146e7302a33" - integrity sha512-sdQZOJdmt3GJs1UMNpCCCyeuS2IEGLXnHyAo9yIO5JJDjbjoVRij4M1qep6P6gFpptD1PqIYgzM+gwJbOi92mw== + version "1.0.30001523" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001523.tgz#b838f70b1a98c556776b998fafb47d2b64146d4f" + integrity sha512-I5q5cisATTPZ1mc588Z//pj/Ox80ERYDfR71YnvY7raS/NOk8xXlZcB0sF7JdqaV//kOaa6aus7lRfpdnt1eBA== caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -cbor@^8.0.0, cbor@^8.1.0: +cbor@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/cbor/-/cbor-8.1.0.tgz#cfc56437e770b73417a2ecbfc9caf6b771af60d5" integrity sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg== dependencies: nofilter "^3.1.0" +cbor@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/cbor/-/cbor-9.0.1.tgz#b16e393d4948d44758cd54ac6151379d443b37ae" + integrity sha512-/TQOWyamDxvVIv+DY9cOLNuABkoyz8K/F3QE56539pGVYohx0+MEA1f4lChFTX79dBTBS7R1PF6ovH7G+VtBfQ== + dependencies: + nofilter "^3.1.0" + chai@^4.2.0: - version "4.3.7" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" - integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A== + version "4.3.8" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.8.tgz#40c59718ad6928da6629c70496fe990b2bb5b17c" + integrity sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ== dependencies: assertion-error "^1.1.0" check-error "^1.0.2" @@ -2797,7 +2892,7 @@ chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -3077,6 +3172,11 @@ commander@^10.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== +commander@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" + integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== + commander@^2.12.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -3087,10 +3187,10 @@ commander@^9.2.0, commander@^9.4.0: resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== -compare-versions@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-5.0.3.tgz#a9b34fea217472650ef4a2651d905f42c28ebfd7" - integrity sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A== +compare-versions@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" + integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== component-emitter@^1.2.1: version "1.3.0" @@ -3188,9 +3288,9 @@ copy-descriptor@^0.1.0: integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== core-js-pure@^3.0.1: - version "3.31.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.31.0.tgz#052fd9e82fbaaf86457f5db1fadcd06f15966ff2" - integrity sha512-/AnE9Y4OsJZicCzIe97JP5XoPKQJfTuEG43aEVLFJGOJpyqELod+pE6LEl63DfG1Mp8wX97LDaDpy1GmLEUxlg== + version "3.32.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.32.1.tgz#5775b88f9062885f67b6d7edce59984e89d276f3" + integrity sha512-f52QZwkFVDPf7UEQZGHKx6NYxsxmVGJe5DIvbzOdRMJlmT6yv0KDjR8rmy3ngr/t5wU54c7Sp/qIJH0ppbhVpQ== core-js@^2.4.0, core-js@^2.5.0: version "2.6.12" @@ -3282,11 +3382,11 @@ cross-fetch@^2.1.0, cross-fetch@^2.1.1: whatwg-fetch "^2.0.4" cross-fetch@^3.1.4: - version "3.1.6" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" - integrity sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g== + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== dependencies: - node-fetch "^2.6.11" + node-fetch "^2.6.12" cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" @@ -3674,9 +3774,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.3.47: - version "1.4.440" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.440.tgz#d3b1eeb36b717eb479a240c0406ac1fa67901762" - integrity sha512-r6dCgNpRhPwiWlxbHzZQ/d9swfPaEJGi8ekqRBwQYaR3WmA5VkqQfBWSDDjuJU1ntO+W9tHx8OHV/96Q8e0dVw== + version "1.4.502" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.502.tgz#531cda8774813d97d6cfa2fb9d8ee3e2c75851fa" + integrity sha512-xqeGw3Gr6o3uyHy/yKjdnDQHY2RQvXcGC2cfHjccK1IGkH6cX1WQBN8EeC/YpwPhGkBaikDTecJ8+ssxSVRQlw== elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" @@ -3747,11 +3847,12 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: once "^1.4.0" enquirer@^2.3.0, enquirer@^2.3.5, enquirer@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== dependencies: ansi-colors "^4.1.1" + strip-ansi "^6.0.1" entities@^4.2.0, entities@^4.4.0: version "4.5.0" @@ -3778,17 +3879,18 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2: - version "1.21.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff" - integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg== + version "1.22.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" + integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== dependencies: array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.1" available-typed-arrays "^1.0.5" call-bind "^1.0.2" es-set-tostringtag "^2.0.1" es-to-primitive "^1.2.1" function.prototype.name "^1.1.5" - get-intrinsic "^1.2.0" + get-intrinsic "^1.2.1" get-symbol-description "^1.0.0" globalthis "^1.0.3" gopd "^1.0.1" @@ -3808,14 +3910,18 @@ es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2: object-inspect "^1.12.3" object-keys "^1.1.1" object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" + regexp.prototype.flags "^1.5.0" + safe-array-concat "^1.0.0" safe-regex-test "^1.0.0" string.prototype.trim "^1.2.7" string.prototype.trimend "^1.0.6" string.prototype.trimstart "^1.0.6" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" typed-array-length "^1.0.4" unbox-primitive "^1.0.2" - which-typed-array "^1.1.9" + which-typed-array "^1.1.10" es-array-method-boxes-properly@^1.0.0: version "1.0.0" @@ -3831,6 +3937,13 @@ es-set-tostringtag@^2.0.1: has "^1.0.3" has-tostringtag "^1.0.0" +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -3914,9 +4027,9 @@ escodegen@1.8.x: source-map "~0.2.0" eslint-config-prettier@^8.3.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" - integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA== + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" + integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== eslint-plugin-mocha@^9.0.0: version "9.0.0" @@ -3966,9 +4079,9 @@ eslint-visitor-keys@^2.0.0: integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== eslint-visitor-keys@^3.3.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" - integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint@^7.32.0: version "7.32.0" @@ -4268,6 +4381,16 @@ ethereum-cryptography@^1.0.3: "@scure/bip32" "1.1.5" "@scure/bip39" "1.1.1" +ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz#18fa7108622e56481157a5cb7c01c0c6a672eb67" + integrity sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug== + dependencies: + "@noble/curves" "1.1.0" + "@noble/hashes" "1.3.1" + "@scure/bip32" "1.3.1" + "@scure/bip39" "1.2.1" + ethereum-waffle@^3.2.0: version "3.4.4" resolved "https://registry.yarnpkg.com/ethereum-waffle/-/ethereum-waffle-3.4.4.tgz#1378b72040697857b7f5e8f473ca8f97a37b5840" @@ -4726,9 +4849,9 @@ fast-diff@^1.1.2, fast-diff@^1.2.0: integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.0.3, fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -5082,9 +5205,9 @@ fsevents@~2.1.1: integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.1: version "1.1.1" @@ -5172,7 +5295,7 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== @@ -5329,9 +5452,9 @@ global@~4.4.0: process "^0.11.10" globals@^13.6.0, globals@^13.9.0: - version "13.20.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" - integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + version "13.21.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.21.0.tgz#163aae12f34ef502f5153cfbdd3600f36c63c571" + integrity sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg== dependencies: type-fest "^0.20.2" @@ -5444,12 +5567,12 @@ growl@1.10.5: integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== handlebars@^4.0.1: - version "4.7.7" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" - integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== dependencies: minimist "^1.2.5" - neo-async "^2.6.0" + neo-async "^2.6.2" source-map "^0.6.1" wordwrap "^1.0.0" optionalDependencies: @@ -5842,9 +5965,9 @@ immediate@~3.2.3: integrity sha512-RrGCXRm/fRVqMIhqXrGEX9rRADavPiDFSoMb/k64i9XMk8uH4r/Omi5Ctierj6XzNecwDbO4WuFbDD1zmpl3Tg== immutable@^4.0.0-rc.12: - version "4.3.0" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" - integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== + version "4.3.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" + integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" @@ -6010,10 +6133,10 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.11.0: - version "2.12.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" - integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== +is-core-module@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== dependencies: has "^1.0.3" @@ -6205,15 +6328,11 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: has-symbols "^1.0.2" is-typed-array@^1.1.10, is-typed-array@^1.1.3, is-typed-array@^1.1.9: - version "1.1.10" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" - integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" + which-typed-array "^1.1.11" is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" @@ -6498,9 +6617,9 @@ keyv@^3.0.0: json-buffer "3.0.0" keyv@^4.0.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.2.tgz#0e310ce73bf7851ec702f2eaf46ec4e3805cce56" - integrity sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g== + version "4.5.3" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" + integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== dependencies: json-buffer "3.0.1" @@ -7128,6 +7247,11 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micro-ftch@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f" + integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== + micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -7482,7 +7606,7 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -neo-async@^2.6.0: +neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== @@ -7524,10 +7648,10 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^2.6.1, node-fetch@^2.6.11, node-fetch@^2.6.7: - version "2.6.11" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" - integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== +node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" @@ -7749,16 +7873,16 @@ optionator@^0.8.1: word-wrap "~1.2.3" optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" os-homedir@^1.0.0: version "1.0.2" @@ -8550,7 +8674,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3: +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== @@ -8716,11 +8840,11 @@ resolve@1.17.0: path-parse "^1.0.6" resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.8.1, resolve@~1.22.1: - version "1.22.2" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" - integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + version "1.22.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" + integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== dependencies: - is-core-module "^2.11.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -8911,19 +9035,19 @@ semaphore@>=1.0.1, semaphore@^1.0.3, semaphore@^1.1.0: integrity sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA== "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.2.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: - version "7.5.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" - integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== +semver@^7.2.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" @@ -9166,16 +9290,16 @@ sol2uml@2.2.0: klaw "^4.0.1" sol2uml@^2.5.4: - version "2.5.6" - resolved "https://registry.yarnpkg.com/sol2uml/-/sol2uml-2.5.6.tgz#7ab18b40dc7b8c0135ae103ec519dd616b550fc2" - integrity sha512-AN0Iu6x61L36mJWlzL3jsBQQ2VzFxdYdpcACUaoO9buM+7VuQICDBel8fG26EkW87Plqui+mxrgqfpFv/Ye2vQ== + version "2.5.13" + resolved "https://registry.yarnpkg.com/sol2uml/-/sol2uml-2.5.13.tgz#095f34e7ab0e7e62e0680b7eb2dcb32bcf408bc7" + integrity sha512-ACfGQzfciKuCZw7//P2URj1bRHoX/KnJweuPKNcctzalSZGsjNdw8uRzLl1ph00F9xqu4z9wpDGWJ0GSzox2SQ== dependencies: "@aduh95/viz.js" "^3.7.0" - "@solidity-parser/parser" "^0.15.0" + "@solidity-parser/parser" "^0.16.1" axios "^1.3.4" axios-debug-log "^1.0.0" cli-color "^2.0.3" - commander "^10.0.0" + commander "^11.0.0" convert-svg-to-png "^0.6.4" debug "^4.3.4" diff-match-patch "^1.0.5" @@ -9231,9 +9355,9 @@ solhint-plugin-prettier@^0.0.5: prettier-linter-helpers "^1.0.0" solhint@^3.2.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.4.1.tgz#8ea15b21c13d1be0b53fd46d605a24d0b36a0c46" - integrity sha512-pzZn2RlZhws1XwvLPVSsxfHrwsteFf5eySOhpAytzXwKQYbTCJV6z8EevYDiSVKMpWrvbKpEtJ055CuEmzp4Xg== + version "3.6.2" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.6.2.tgz#2b2acbec8fdc37b2c68206a71ba89c7f519943fe" + integrity sha512-85EeLbmkcPwD+3JR7aEMKsVC9YrRSxd4qkXuMzrlf7+z2Eqdfm1wHWq1ffTuo5aDhoZxp2I9yF3QkxZOxOL7aQ== dependencies: "@solidity-parser/parser" "^0.16.0" ajv "^6.12.6" @@ -9248,17 +9372,19 @@ solhint@^3.2.0: js-yaml "^4.1.0" lodash "^4.17.21" pluralize "^8.0.0" - semver "^6.3.0" + semver "^7.5.2" strip-ansi "^6.0.1" table "^6.8.1" text-table "^0.2.0" optionalDependencies: prettier "^2.8.3" -solidity-ast@^0.4.15: - version "0.4.49" - resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.49.tgz#ecba89d10c0067845b7848c3a3e8cc61a4fc5b82" - integrity sha512-Pr5sCAj1SFqzwFZw1HPKSq0PehlQNdM8GwKyAVYh2DOn7/cCK8LUKD1HeHnKtTgBW7hi9h4nnnan7hpAg5RhWQ== +solidity-ast@^0.4.26: + version "0.4.52" + resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.52.tgz#9f1a9abc7e5ba28bbf91146ecd07aec7e70f3c85" + integrity sha512-iOya9BSiB9jhM8Vf40n8lGELGzwrUc57rl5BhfNtJ5cvAaMvRcNlHeAMNvqJJyjoUnczqRbHqdivEqK89du3Cw== + dependencies: + array.prototype.findlast "^1.2.2" solidity-comments-extractor@^0.0.7: version "0.0.7" @@ -10071,6 +10197,36 @@ typechain@^5.1.2: prettier "^2.1.2" ts-essentials "^7.0.1" +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" @@ -10153,9 +10309,9 @@ underscore@1.9.1: integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== undici@^5.14.0, undici@^5.4.0: - version "5.22.1" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.22.1.tgz#877d512effef2ac8be65e695f3586922e1a57d7b" - integrity sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw== + version "5.23.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.23.0.tgz#e7bdb0ed42cebe7b7aca87ced53e6eaafb8f8ca0" + integrity sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg== dependencies: busboy "^1.6.0" @@ -10311,9 +10467,9 @@ v8-compile-cache-lib@^3.0.1: integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + version "2.4.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" + integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== validate-npm-package-license@^3.0.1: version "3.0.4" @@ -10985,7 +11141,7 @@ web3-shh@1.7.4: web3-core-subscriptions "1.7.4" web3-net "1.7.4" -web3-utils@1.10.0, web3-utils@^1.0.0-beta.31, web3-utils@^1.3.0: +web3-utils@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.0.tgz#ca4c1b431a765c14ac7f773e92e0fd9377ccf578" integrity sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg== @@ -11025,6 +11181,20 @@ web3-utils@1.7.4: randombytes "^2.1.0" utf8 "3.0.0" +web3-utils@^1.0.0-beta.31, web3-utils@^1.3.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.1.tgz#97532130d85358628bc0ff14d94b7e9449786983" + integrity sha512-r6iUUw/uMnNcWXjhRv33Nyrhxq3VGOPBXeSzxhOXIci4SvC/LPTpROY0uTrMX7ztKyODYrHp8WhTkEf+ZnHssw== + dependencies: + "@ethereumjs/util" "^8.1.0" + bn.js "^5.2.1" + ethereum-bloom-filters "^1.0.6" + ethereum-cryptography "^2.1.2" + ethjs-unit "0.1.6" + number-to-bn "1.7.0" + randombytes "^2.1.0" + utf8 "3.0.0" + web3@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/web3/-/web3-1.10.0.tgz#2fde0009f59aa756c93e07ea2a7f3ab971091274" @@ -11127,17 +11297,16 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== -which-typed-array@^1.1.2, which-typed-array@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" - integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== +which-typed-array@^1.1.10, which-typed-array@^1.1.11, which-typed-array@^1.1.2: + version "1.1.11" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" + integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== dependencies: available-typed-arrays "^1.0.5" call-bind "^1.0.2" for-each "^0.3.3" gopd "^1.0.1" has-tostringtag "^1.0.0" - is-typed-array "^1.1.10" which@1.3.1, which@^1.1.1, which@^1.2.9, which@^1.3.1: version "1.3.1" @@ -11165,10 +11334,10 @@ window-size@^0.2.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" integrity sha512-UD7d8HFA2+PZsbKyaOCEy8gMh1oDtHgJh1LfgjQ4zVXmYjAT/kvz3PueITKuqDiIXQe7yzpPnxX3lNc+AhQMyw== -word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== wordwrap@^1.0.0: version "1.0.0"