From 6898263804c7e09e39e0c7db9499019c54383b62 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Fri, 18 Aug 2023 13:29:10 -0400 Subject: [PATCH] feat: add Base bridge adapter (#874) * feat: add Base constants/defaults Signed-off-by: Matt Rice * feat: add Base bridge adapter Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * feat: Add ZkSync function to AtomicWethDepositor (#875) * feat: Add ZkSync function to AtomicWethDepositor * Update AtomicWethDepositor.sol * Update AtomicWethDepositor.sol * Update AtomicWethDepositor.sol * Update AtomicWethDepositor.sol * Update AtomicWethDepositor.sol * Update AtomicWethDepositor.sol * Update src/clients/bridges/OpStackAdapter.ts Co-authored-by: nicholaspai <9457025+nicholaspai@users.noreply.github.com> * new deployment Signed-off-by: Matt Rice * lint Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * updated deployment with Nick's fix Signed-off-by: Matt Rice * lint Signed-off-by: Matt Rice * update address Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * Update contracts/AtomicWethDepositor.sol Co-authored-by: James Morris, MS <96435344+james-a-morris@users.noreply.github.com> * Update AtomicWethDepositor.sol * inherit optimism adapter Signed-off-by: Matt Rice * lint Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * lint Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * restructured Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice * fix Signed-off-by: Matt Rice * WIP Signed-off-by: Matt Rice --------- Signed-off-by: Matt Rice Co-authored-by: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Co-authored-by: James Morris, MS <96435344+james-a-morris@users.noreply.github.com> Co-authored-by: nicholaspai --- contracts/AtomicWethDepositor.sol | 19 +- deploy/001_deploy_atomic_depositor.ts | 18 ++ deployments/mainnet/.chainId | 1 + deployments/mainnet/AtomicWethDepositor.json | 232 +++++++++++++++++ .../5c785432cfb35b0626411450852e3748.json | 34 +++ hardhat.config.ts | 6 + src/clients/bridges/AdapterManager.ts | 4 + src/clients/bridges/BaseAdapter.ts | 1 + src/clients/bridges/OptimismAdapter.ts | 237 ------------------ src/clients/bridges/index.ts | 2 +- .../bridges/op-stack/DefaultErc20Bridge.ts | 58 +++++ .../bridges/op-stack/OpStackAdapter.ts | 182 ++++++++++++++ .../op-stack/OpStackBridgeInterface.ts | 24 ++ src/clients/bridges/op-stack/WethBridge.ts | 71 ++++++ .../bridges/op-stack/base/BaseChainAdapter.ts | 26 ++ src/clients/bridges/op-stack/base/index.ts | 1 + src/clients/bridges/op-stack/index.ts | 4 + .../op-stack/optimism/DaiOptimismBridge.ts | 66 +++++ .../op-stack/optimism/OptimismAdapter.ts | 38 +++ .../op-stack/optimism/SnxOptimismBridge.ts | 59 +++++ .../bridges/op-stack/optimism/index.ts | 2 + src/common/ContractAddresses.ts | 60 ++++- src/utils/AddressUtils.ts | 6 +- src/utils/index.ts | 2 +- test/AdapterManager.SendTokensCrossChain.ts | 41 ++- 25 files changed, 947 insertions(+), 247 deletions(-) create mode 100644 deploy/001_deploy_atomic_depositor.ts create mode 100644 deployments/mainnet/.chainId create mode 100644 deployments/mainnet/AtomicWethDepositor.json create mode 100644 deployments/mainnet/solcInputs/5c785432cfb35b0626411450852e3748.json delete mode 100644 src/clients/bridges/OptimismAdapter.ts create mode 100644 src/clients/bridges/op-stack/DefaultErc20Bridge.ts create mode 100644 src/clients/bridges/op-stack/OpStackAdapter.ts create mode 100644 src/clients/bridges/op-stack/OpStackBridgeInterface.ts create mode 100644 src/clients/bridges/op-stack/WethBridge.ts create mode 100644 src/clients/bridges/op-stack/base/BaseChainAdapter.ts create mode 100644 src/clients/bridges/op-stack/base/index.ts create mode 100644 src/clients/bridges/op-stack/index.ts create mode 100644 src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts create mode 100644 src/clients/bridges/op-stack/optimism/OptimismAdapter.ts create mode 100644 src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts create mode 100644 src/clients/bridges/op-stack/optimism/index.ts diff --git a/contracts/AtomicWethDepositor.sol b/contracts/AtomicWethDepositor.sol index 305a09947..b7b3fe7a2 100644 --- a/contracts/AtomicWethDepositor.sol +++ b/contracts/AtomicWethDepositor.sol @@ -35,23 +35,32 @@ interface ZkSyncL1Bridge { /** * @notice Contract deployed on Ethereum helps relay bots atomically unwrap and bridge WETH over the canonical chain - * bridges for OVM chains, ZkSync and Polygon. Needed as these chains only support bridging of ETH, not WETH. + * bridges for Optimism, Base, Boba, ZkSync, and Polygon. Needed as these chains only support bridging of ETH, not WETH. */ contract AtomicWethDepositor { Weth public immutable weth = Weth(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); OvmL1Bridge public immutable optimismL1Bridge = OvmL1Bridge(0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1); OvmL1Bridge public immutable bobaL1Bridge = OvmL1Bridge(0xdc1664458d2f0B6090bEa60A8793A4E66c2F1c00); + OvmL1Bridge public immutable baseL1Bridge = OvmL1Bridge(0x3154Cf16ccdb4C6d922629664174b904d80F2C35); PolygonL1Bridge public immutable polygonL1Bridge = PolygonL1Bridge(0xA0c68C638235ee32657e8f720a23ceC1bFc77C77); ZkSyncL1Bridge public immutable zkSyncL1Bridge = ZkSyncL1Bridge(0x32400084C286CF3E17e7B677ea9583e60a000324); event ZkSyncEthDepositInitiated(address indexed from, address indexed to, uint256 amount); function bridgeWethToOvm(address to, uint256 amount, uint32 l2Gas, uint256 chainId) public { - require(chainId == 10 || chainId == 288, "Can only bridge to Optimism Or boba"); weth.transferFrom(msg.sender, address(this), amount); weth.withdraw(amount); - (chainId == 10 ? optimismL1Bridge : bobaL1Bridge).depositETHTo{ value: amount }(to, l2Gas, ""); + + if (chainId == 10) { + optimismL1Bridge.depositETHTo{ value: amount }(to, l2Gas, ""); + } else if (chainId == 8453) { + baseL1Bridge.depositETHTo{ value: amount }(to, l2Gas, ""); + } else if (chainId == 288) { + bobaL1Bridge.depositETHTo{ value: amount }(to, l2Gas, ""); + } else { + revert("Invalid OVM chainId"); + } } function bridgeWethToPolygon(address to, uint256 amount) public { @@ -96,4 +105,8 @@ contract AtomicWethDepositor { } fallback() external payable {} + + // Included to remove a compilation warning. + // NOTE: this should not affect behavior. + receive() external payable {} } diff --git a/deploy/001_deploy_atomic_depositor.ts b/deploy/001_deploy_atomic_depositor.ts new file mode 100644 index 000000000..8a90908ac --- /dev/null +++ b/deploy/001_deploy_atomic_depositor.ts @@ -0,0 +1,18 @@ +import { DeployFunction } from "hardhat-deploy/types"; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { + deployments: { deploy }, + getNamedAccounts, + } = hre; + + const { deployer } = await getNamedAccounts(); + + await deploy("AtomicWethDepositor", { + from: deployer, + log: true, + skipIfAlreadyDeployed: true, + }); +}; +module.exports = func; +func.tags = ["AtomicWethDepositor"]; diff --git a/deployments/mainnet/.chainId b/deployments/mainnet/.chainId new file mode 100644 index 000000000..56a6051ca --- /dev/null +++ b/deployments/mainnet/.chainId @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/deployments/mainnet/AtomicWethDepositor.json b/deployments/mainnet/AtomicWethDepositor.json new file mode 100644 index 000000000..d021bcc65 --- /dev/null +++ b/deployments/mainnet/AtomicWethDepositor.json @@ -0,0 +1,232 @@ +{ + "address": "0xaA282C4E86beFda4a1E7C9c06165869026D27852", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ZkSyncEthDepositInitiated", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [], + "name": "baseL1Bridge", + "outputs": [ + { + "internalType": "contract OvmL1Bridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bobaL1Bridge", + "outputs": [ + { + "internalType": "contract OvmL1Bridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "l2Gas", + "type": "uint32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + } + ], + "name": "bridgeWethToOvm", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "bridgeWethToPolygon", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "l2GasLimit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "l2GasPerPubdataByteLimit", + "type": "uint256" + }, + { + "internalType": "address", + "name": "refundRecipient", + "type": "address" + } + ], + "name": "bridgeWethToZkSync", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "optimismL1Bridge", + "outputs": [ + { + "internalType": "contract OvmL1Bridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "polygonL1Bridge", + "outputs": [ + { + "internalType": "contract PolygonL1Bridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "weth", + "outputs": [ + { + "internalType": "contract Weth", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "zkSyncL1Bridge", + "outputs": [ + { + "internalType": "contract ZkSyncL1Bridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "transactionHash": "0x2b534fcf44aa7d96be9db59543ba866916d0e21d921948f1871b96b7a05f3be8", + "receipt": { + "to": null, + "from": "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + "contractAddress": "0xaA282C4E86beFda4a1E7C9c06165869026D27852", + "transactionIndex": 90, + "gasUsed": "846885", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x07a9d8d192f8326da4c945df538cae7c1a50cdf5432f648859b0de500fc42cf3", + "transactionHash": "0x2b534fcf44aa7d96be9db59543ba866916d0e21d921948f1871b96b7a05f3be8", + "logs": [], + "blockNumber": 17895423, + "cumulativeGasUsed": "9784598", + "status": 1, + "byzantium": true + }, + "args": [], + "numDeployments": 1, + "solcInputHash": "5c785432cfb35b0626411450852e3748", + "metadata": "{\"compiler\":{\"version\":\"0.8.18+commit.87f61d96\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"ZkSyncEthDepositInitiated\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[],\"name\":\"baseL1Bridge\",\"outputs\":[{\"internalType\":\"contract OvmL1Bridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bobaL1Bridge\",\"outputs\":[{\"internalType\":\"contract OvmL1Bridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"l2Gas\",\"type\":\"uint32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"}],\"name\":\"bridgeWethToOvm\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"bridgeWethToPolygon\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"l2GasLimit\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"l2GasPerPubdataByteLimit\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"refundRecipient\",\"type\":\"address\"}],\"name\":\"bridgeWethToZkSync\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"optimismL1Bridge\",\"outputs\":[{\"internalType\":\"contract OvmL1Bridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"polygonL1Bridge\",\"outputs\":[{\"internalType\":\"contract PolygonL1Bridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"weth\",\"outputs\":[{\"internalType\":\"contract Weth\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"zkSyncL1Bridge\",\"outputs\":[{\"internalType\":\"contract ZkSyncL1Bridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"notice\":\"Contract deployed on Ethereum helps relay bots atomically unwrap and bridge WETH over the canonical chain bridges for OVM chains, ZkSync and Polygon. Needed as these chains only support bridging of ETH, not WETH.\",\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/AtomicWethDepositor.sol\":\"AtomicWethDepositor\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":1000000},\"remappings\":[],\"viaIR\":true},\"sources\":{\"contracts/AtomicWethDepositor.sol\":{\"content\":\"// SPDX-License-Identifier: GPL-3.0-only\\npragma solidity ^0.8.0;\\n\\ninterface Weth {\\n function withdraw(uint256 _wad) external;\\n\\n function transferFrom(address _from, address _to, uint256 _wad) external;\\n}\\n\\ninterface OvmL1Bridge {\\n function depositETHTo(address _to, uint32 _l2Gas, bytes calldata _data) external payable;\\n}\\n\\ninterface PolygonL1Bridge {\\n function depositEtherFor(address _to) external payable;\\n}\\n\\ninterface ZkSyncL1Bridge {\\n function requestL2Transaction(\\n address _contractL2,\\n uint256 _l2Value,\\n bytes calldata _calldata,\\n uint256 _l2GasLimit,\\n uint256 _l2GasPerPubdataByteLimit,\\n bytes[] calldata _factoryDeps,\\n address _refundRecipient\\n ) external payable;\\n\\n function l2TransactionBaseCost(\\n uint256 _gasPrice,\\n uint256 _l2GasLimit,\\n uint256 _l2GasPerPubdataByteLimit\\n ) external pure returns (uint256);\\n}\\n\\n/**\\n * @notice Contract deployed on Ethereum helps relay bots atomically unwrap and bridge WETH over the canonical chain\\n * bridges for OVM chains, ZkSync and Polygon. Needed as these chains only support bridging of ETH, not WETH.\\n */\\n\\ncontract AtomicWethDepositor {\\n Weth public immutable weth = Weth(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\\n OvmL1Bridge public immutable optimismL1Bridge = OvmL1Bridge(0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1);\\n OvmL1Bridge public immutable bobaL1Bridge = OvmL1Bridge(0xdc1664458d2f0B6090bEa60A8793A4E66c2F1c00);\\n OvmL1Bridge public immutable baseL1Bridge = OvmL1Bridge(0x3154Cf16ccdb4C6d922629664174b904d80F2C35);\\n PolygonL1Bridge public immutable polygonL1Bridge = PolygonL1Bridge(0xA0c68C638235ee32657e8f720a23ceC1bFc77C77);\\n ZkSyncL1Bridge public immutable zkSyncL1Bridge = ZkSyncL1Bridge(0x32400084C286CF3E17e7B677ea9583e60a000324);\\n\\n event ZkSyncEthDepositInitiated(address indexed from, address indexed to, uint256 amount);\\n\\n function bridgeWethToOvm(address to, uint256 amount, uint32 l2Gas, uint256 chainId) public {\\n weth.transferFrom(msg.sender, address(this), amount);\\n weth.withdraw(amount);\\n\\n if (chainId == 10) {\\n optimismL1Bridge.depositETHTo{ value: amount }(to, l2Gas, \\\"\\\");\\n } else if (chainId == 8453) {\\n baseL1Bridge.depositETHTo{ value: amount }(to, l2Gas, \\\"\\\");\\n } else if (chainId == 288) {\\n bobaL1Bridge.depositETHTo{ value: amount }(to, l2Gas, \\\"\\\");\\n } else {\\n revert(\\\"Invalid OVM chainId\\\");\\n }\\n }\\n\\n function bridgeWethToPolygon(address to, uint256 amount) public {\\n weth.transferFrom(msg.sender, address(this), amount);\\n weth.withdraw(amount);\\n polygonL1Bridge.depositEtherFor{ value: amount }(to);\\n }\\n\\n function bridgeWethToZkSync(\\n address to,\\n uint256 amount,\\n uint256 l2GasLimit,\\n uint256 l2GasPerPubdataByteLimit,\\n address refundRecipient\\n ) public {\\n // The ZkSync Mailbox contract checks that the msg.value of the transaction is enough to cover the transaction base\\n // cost. The transaction base cost can be queried from the Mailbox by passing in an L1 \\\"executed\\\" gas price,\\n // which is the priority fee plus base fee. This is the same as calling tx.gasprice on-chain as the Mailbox\\n // contract does here:\\n // https://github.com/matter-labs/era-contracts/blob/3a4506522aaef81485d8abb96f5a6394bd2ba69e/ethereum/contracts/zksync/facets/Mailbox.sol#L287\\n uint256 l2TransactionBaseCost = zkSyncL1Bridge.l2TransactionBaseCost(\\n tx.gasprice,\\n l2GasLimit,\\n l2GasPerPubdataByteLimit\\n );\\n uint256 valueToSubmitXChainMessage = l2TransactionBaseCost + amount;\\n weth.transferFrom(msg.sender, address(this), valueToSubmitXChainMessage);\\n weth.withdraw(valueToSubmitXChainMessage);\\n zkSyncL1Bridge.requestL2Transaction{ value: valueToSubmitXChainMessage }(\\n to,\\n amount,\\n \\\"\\\",\\n l2GasLimit,\\n l2GasPerPubdataByteLimit,\\n new bytes[](0),\\n refundRecipient\\n );\\n\\n // Emit an event that we can easily track in the ZkSyncAdapter because otherwise there is no easy event to\\n // track ETH deposit initiations.\\n emit ZkSyncEthDepositInitiated(msg.sender, to, amount);\\n }\\n\\n fallback() external payable {}\\n\\n receive() external payable {}\\n}\\n\",\"keccak256\":\"0x105a3a347e818da2bac764b3fd622d33a671ea27456565d536b2bc1c8c3bc80c\",\"license\":\"GPL-3.0-only\"}},\"version\":1}", + "bytecode": "0x61014080604052346101235773c02aaa39b223fe8d0a0e5c4f27ead9083c756cc26080527399c9fc46f92e8a1c0dec1b1747d010903e884be160a05273dc1664458d2f0b6090bea60a8793a4e66c2f1c0060c052733154cf16ccdb4c6d922629664174b904d80f2c3560e0526101009073a0c68c638235ee32657e8f720a23cec1bfc77c7782526101207332400084c286cf3e17e7b677ea9583e60a0003248152610e789283610129843960805183818160f001528181610628015281816108590152610962015260a0518381816101cc01526104cf015260c0518381816103490152610daf015260e0518381816102a601526105ad01525182818161053e01526106f90152518181816107ea0152818161092401528181610a600152610b220152f35b600080fdfe6080806040526004908136101561001d575b5050361561001b57005b005b600090813560e01c908163019f8e8114610d6557508063128d5f681461087d5780633fc8cef31461080e5780635970eafa1461079f578063b3d5ccc3146105d1578063b745c3f314610562578063c80dcc38146104f3578063d3cdc8f9146104845763e88650c40361001157346102865760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610286576100c0610dd7565b916024359060443563ffffffff811681036104805773ffffffffffffffffffffffffffffffffffffffff946064357f00000000000000000000000000000000000000000000000000000000000000008716803b1561045d57604080517f23b872dd0000000000000000000000000000000000000000000000000000000081523387820190815230602082015291820188905290889082908190606001038183865af1801561047557908891610461575b5050803b1561045d578680916024604051809481937f2e1a7d4d0000000000000000000000000000000000000000000000000000000083528b8b8401525af180156104525790879161043e575b5050600a8103610298575084957f00000000000000000000000000000000000000000000000000000000000000001690813b1561029457610263948694604051968795869485937f9a2ac6d500000000000000000000000000000000000000000000000000000000855284019060809273ffffffffffffffffffffffffffffffffffffffff63ffffffff9216835216602082015260606040820152600060608201520190565b03925af180156102895761027657505080f35b61027f90610dff565b6102865780f35b80fd5b6040513d84823e3d90fd5b8580fd5b612105810361033d575084957f00000000000000000000000000000000000000000000000000000000000000001690813b1561029457610263948694604051968795869485937f9a2ac6d500000000000000000000000000000000000000000000000000000000855284019060809273ffffffffffffffffffffffffffffffffffffffff63ffffffff9216835216602082015260606040820152600060608201520190565b610120036103e05784957f00000000000000000000000000000000000000000000000000000000000000001690813b1561029457610263948694604051968795869485937f9a2ac6d500000000000000000000000000000000000000000000000000000000855284019060809273ffffffffffffffffffffffffffffffffffffffff63ffffffff9216835216602082015260606040820152600060608201520190565b6064836020604051917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601360248201527f496e76616c6964204f564d20636861696e4964000000000000000000000000006044820152fd5b61044790610dff565b6102945785386101bd565b6040513d89823e3d90fd5b8680fd5b61046a90610dff565b61045d578638610170565b6040513d8a823e3d90fd5b8380fd5b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5080913461079c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261079c5761060b610dd7565b6024359073ffffffffffffffffffffffffffffffffffffffff90817f000000000000000000000000000000000000000000000000000000000000000016803b1561029457604080517f23b872dd0000000000000000000000000000000000000000000000000000000081523387820190815230602082015291820186905290879082908190606001038183865af1908115610452578791610788575b5050803b15610294578580916024604051809481937f2e1a7d4d000000000000000000000000000000000000000000000000000000008352898b8401525af190811561077d578691610765575b5050817f00000000000000000000000000000000000000000000000000000000000000001690813b1561029457859360249260405196879586947f4faa8a2600000000000000000000000000000000000000000000000000000000865216908401525af18015610289576102765750f35b61076e90610dff565b6107795784386106f4565b8480fd5b6040513d88823e3d90fd5b61079190610dff565b6102945785386106a7565b50fd5b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346102865760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610286576108b5610dd7565b91604435606435936084359273ffffffffffffffffffffffffffffffffffffffff9283851680950361029457604051907fb473318e0000000000000000000000000000000000000000000000000000000082523a838301526024958187840152886044840152602083606481897f0000000000000000000000000000000000000000000000000000000000000000165afa928315610475578893610cd2575b50863583018311610ca757857f000000000000000000000000000000000000000000000000000000000000000016803b15610c8557604080517f23b872dd000000000000000000000000000000000000000000000000000000008152338782019081523060208201528a35870192810192909252908a9082908190606001038183865af18015610c9c57610c89575b50803b15610c855788809189604051809481937f2e1a7d4d00000000000000000000000000000000000000000000000000000000835283358a018b8401525af18015610c7a57908991610c66575b505060405191602083019280841067ffffffffffffffff851117610c3957899a8460409b98999a9b52878252897f0000000000000000000000000000000000000000000000000000000000000000163b15610c355760409a989695949a99979951957feb6724190000000000000000000000000000000000000000000000000000000087528888169087015288358987015260e060448701528960e48701526101048601926064870152608486015261010060a48601525180915261012490818501918160051b860101999189905b828210610b97575050505082809281808b8b979560c4899701520391893501887f0000000000000000000000000000000000000000000000000000000000000000165af1801561028957610b83575b50506040519235835216907fa3e601130860a6f97b42655ad74f631ddf0c8e5adaa98402fded9c09bc35a44060203392a380f35b610b8c90610dff565b610480578385610b4f565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedc878d969596030181528b855180518092528c5b828110610c1f575050808d0160209081018d9052601f919091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016909c018c019b93948401936001929092019101610b00565b808f602082818095870101519201015201610bcb565b8780fd5b886041877f4e487b7100000000000000000000000000000000000000000000000000000000600052526000fd5b610c6f90610dff565b610c35578738610a31565b6040513d8b823e3d90fd5b8880fd5b610c9590999199610dff565b97386109e3565b6040513d8c823e3d90fd5b86886011867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b90925060203d602011610d5e575b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f820116820182811067ffffffffffffffff821117610d3357602091839160405281010312610c3557519138610954565b888a6041887f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b503d610ce0565b905034610dd357817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610dd35760209073ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff82168203610dfa57565b600080fd5b67ffffffffffffffff8111610e1357604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fdfea264697066735822122054f15f095a639ca8c1c971d3a9b339c996a6842a1e61f7c9de8575771cdd982364736f6c63430008120033", + "deployedBytecode": "0x6080806040526004908136101561001d575b5050361561001b57005b005b600090813560e01c908163019f8e8114610d6557508063128d5f681461087d5780633fc8cef31461080e5780635970eafa1461079f578063b3d5ccc3146105d1578063b745c3f314610562578063c80dcc38146104f3578063d3cdc8f9146104845763e88650c40361001157346102865760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610286576100c0610dd7565b916024359060443563ffffffff811681036104805773ffffffffffffffffffffffffffffffffffffffff946064357f00000000000000000000000000000000000000000000000000000000000000008716803b1561045d57604080517f23b872dd0000000000000000000000000000000000000000000000000000000081523387820190815230602082015291820188905290889082908190606001038183865af1801561047557908891610461575b5050803b1561045d578680916024604051809481937f2e1a7d4d0000000000000000000000000000000000000000000000000000000083528b8b8401525af180156104525790879161043e575b5050600a8103610298575084957f00000000000000000000000000000000000000000000000000000000000000001690813b1561029457610263948694604051968795869485937f9a2ac6d500000000000000000000000000000000000000000000000000000000855284019060809273ffffffffffffffffffffffffffffffffffffffff63ffffffff9216835216602082015260606040820152600060608201520190565b03925af180156102895761027657505080f35b61027f90610dff565b6102865780f35b80fd5b6040513d84823e3d90fd5b8580fd5b612105810361033d575084957f00000000000000000000000000000000000000000000000000000000000000001690813b1561029457610263948694604051968795869485937f9a2ac6d500000000000000000000000000000000000000000000000000000000855284019060809273ffffffffffffffffffffffffffffffffffffffff63ffffffff9216835216602082015260606040820152600060608201520190565b610120036103e05784957f00000000000000000000000000000000000000000000000000000000000000001690813b1561029457610263948694604051968795869485937f9a2ac6d500000000000000000000000000000000000000000000000000000000855284019060809273ffffffffffffffffffffffffffffffffffffffff63ffffffff9216835216602082015260606040820152600060608201520190565b6064836020604051917f08c379a0000000000000000000000000000000000000000000000000000000008352820152601360248201527f496e76616c6964204f564d20636861696e4964000000000000000000000000006044820152fd5b61044790610dff565b6102945785386101bd565b6040513d89823e3d90fd5b8680fd5b61046a90610dff565b61045d578638610170565b6040513d8a823e3d90fd5b8380fd5b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5080913461079c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261079c5761060b610dd7565b6024359073ffffffffffffffffffffffffffffffffffffffff90817f000000000000000000000000000000000000000000000000000000000000000016803b1561029457604080517f23b872dd0000000000000000000000000000000000000000000000000000000081523387820190815230602082015291820186905290879082908190606001038183865af1908115610452578791610788575b5050803b15610294578580916024604051809481937f2e1a7d4d000000000000000000000000000000000000000000000000000000008352898b8401525af190811561077d578691610765575b5050817f00000000000000000000000000000000000000000000000000000000000000001690813b1561029457859360249260405196879586947f4faa8a2600000000000000000000000000000000000000000000000000000000865216908401525af18015610289576102765750f35b61076e90610dff565b6107795784386106f4565b8480fd5b6040513d88823e3d90fd5b61079190610dff565b6102945785386106a7565b50fd5b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461028657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261028657602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346102865760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610286576108b5610dd7565b91604435606435936084359273ffffffffffffffffffffffffffffffffffffffff9283851680950361029457604051907fb473318e0000000000000000000000000000000000000000000000000000000082523a838301526024958187840152886044840152602083606481897f0000000000000000000000000000000000000000000000000000000000000000165afa928315610475578893610cd2575b50863583018311610ca757857f000000000000000000000000000000000000000000000000000000000000000016803b15610c8557604080517f23b872dd000000000000000000000000000000000000000000000000000000008152338782019081523060208201528a35870192810192909252908a9082908190606001038183865af18015610c9c57610c89575b50803b15610c855788809189604051809481937f2e1a7d4d00000000000000000000000000000000000000000000000000000000835283358a018b8401525af18015610c7a57908991610c66575b505060405191602083019280841067ffffffffffffffff851117610c3957899a8460409b98999a9b52878252897f0000000000000000000000000000000000000000000000000000000000000000163b15610c355760409a989695949a99979951957feb6724190000000000000000000000000000000000000000000000000000000087528888169087015288358987015260e060448701528960e48701526101048601926064870152608486015261010060a48601525180915261012490818501918160051b860101999189905b828210610b97575050505082809281808b8b979560c4899701520391893501887f0000000000000000000000000000000000000000000000000000000000000000165af1801561028957610b83575b50506040519235835216907fa3e601130860a6f97b42655ad74f631ddf0c8e5adaa98402fded9c09bc35a44060203392a380f35b610b8c90610dff565b610480578385610b4f565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedc878d969596030181528b855180518092528c5b828110610c1f575050808d0160209081018d9052601f919091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016909c018c019b93948401936001929092019101610b00565b808f602082818095870101519201015201610bcb565b8780fd5b886041877f4e487b7100000000000000000000000000000000000000000000000000000000600052526000fd5b610c6f90610dff565b610c35578738610a31565b6040513d8b823e3d90fd5b8880fd5b610c9590999199610dff565b97386109e3565b6040513d8c823e3d90fd5b86886011867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b90925060203d602011610d5e575b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f820116820182811067ffffffffffffffff821117610d3357602091839160405281010312610c3557519138610954565b888a6041887f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b503d610ce0565b905034610dd357817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610dd35760209073ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff82168203610dfa57565b600080fd5b67ffffffffffffffff8111610e1357604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fdfea264697066735822122054f15f095a639ca8c1c971d3a9b339c996a6842a1e61f7c9de8575771cdd982364736f6c63430008120033", + "devdoc": { + "kind": "dev", + "methods": {}, + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": {}, + "notice": "Contract deployed on Ethereum helps relay bots atomically unwrap and bridge WETH over the canonical chain bridges for OVM chains, ZkSync and Polygon. Needed as these chains only support bridging of ETH, not WETH.", + "version": 1 + }, + "storageLayout": { + "storage": [], + "types": null + } +} diff --git a/deployments/mainnet/solcInputs/5c785432cfb35b0626411450852e3748.json b/deployments/mainnet/solcInputs/5c785432cfb35b0626411450852e3748.json new file mode 100644 index 000000000..b1895b463 --- /dev/null +++ b/deployments/mainnet/solcInputs/5c785432cfb35b0626411450852e3748.json @@ -0,0 +1,34 @@ +{ + "language": "Solidity", + "sources": { + "contracts/AtomicWethDepositor.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0-only\npragma solidity ^0.8.0;\n\ninterface Weth {\n function withdraw(uint256 _wad) external;\n\n function transferFrom(address _from, address _to, uint256 _wad) external;\n}\n\ninterface OvmL1Bridge {\n function depositETHTo(address _to, uint32 _l2Gas, bytes calldata _data) external payable;\n}\n\ninterface PolygonL1Bridge {\n function depositEtherFor(address _to) external payable;\n}\n\ninterface ZkSyncL1Bridge {\n function requestL2Transaction(\n address _contractL2,\n uint256 _l2Value,\n bytes calldata _calldata,\n uint256 _l2GasLimit,\n uint256 _l2GasPerPubdataByteLimit,\n bytes[] calldata _factoryDeps,\n address _refundRecipient\n ) external payable;\n\n function l2TransactionBaseCost(\n uint256 _gasPrice,\n uint256 _l2GasLimit,\n uint256 _l2GasPerPubdataByteLimit\n ) external pure returns (uint256);\n}\n\n/**\n * @notice Contract deployed on Ethereum helps relay bots atomically unwrap and bridge WETH over the canonical chain\n * bridges for OVM chains, ZkSync and Polygon. Needed as these chains only support bridging of ETH, not WETH.\n */\n\ncontract AtomicWethDepositor {\n Weth public immutable weth = Weth(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\n OvmL1Bridge public immutable optimismL1Bridge = OvmL1Bridge(0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1);\n OvmL1Bridge public immutable bobaL1Bridge = OvmL1Bridge(0xdc1664458d2f0B6090bEa60A8793A4E66c2F1c00);\n OvmL1Bridge public immutable baseL1Bridge = OvmL1Bridge(0x3154Cf16ccdb4C6d922629664174b904d80F2C35);\n PolygonL1Bridge public immutable polygonL1Bridge = PolygonL1Bridge(0xA0c68C638235ee32657e8f720a23ceC1bFc77C77);\n ZkSyncL1Bridge public immutable zkSyncL1Bridge = ZkSyncL1Bridge(0x32400084C286CF3E17e7B677ea9583e60a000324);\n\n event ZkSyncEthDepositInitiated(address indexed from, address indexed to, uint256 amount);\n\n function bridgeWethToOvm(address to, uint256 amount, uint32 l2Gas, uint256 chainId) public {\n weth.transferFrom(msg.sender, address(this), amount);\n weth.withdraw(amount);\n\n if (chainId == 10) {\n optimismL1Bridge.depositETHTo{ value: amount }(to, l2Gas, \"\");\n } else if (chainId == 8453) {\n baseL1Bridge.depositETHTo{ value: amount }(to, l2Gas, \"\");\n } else if (chainId == 288) {\n bobaL1Bridge.depositETHTo{ value: amount }(to, l2Gas, \"\");\n } else {\n revert(\"Invalid OVM chainId\");\n }\n }\n\n function bridgeWethToPolygon(address to, uint256 amount) public {\n weth.transferFrom(msg.sender, address(this), amount);\n weth.withdraw(amount);\n polygonL1Bridge.depositEtherFor{ value: amount }(to);\n }\n\n function bridgeWethToZkSync(\n address to,\n uint256 amount,\n uint256 l2GasLimit,\n uint256 l2GasPerPubdataByteLimit,\n address refundRecipient\n ) public {\n // The ZkSync Mailbox contract checks that the msg.value of the transaction is enough to cover the transaction base\n // cost. The transaction base cost can be queried from the Mailbox by passing in an L1 \"executed\" gas price,\n // which is the priority fee plus base fee. This is the same as calling tx.gasprice on-chain as the Mailbox\n // contract does here:\n // https://github.com/matter-labs/era-contracts/blob/3a4506522aaef81485d8abb96f5a6394bd2ba69e/ethereum/contracts/zksync/facets/Mailbox.sol#L287\n uint256 l2TransactionBaseCost = zkSyncL1Bridge.l2TransactionBaseCost(\n tx.gasprice,\n l2GasLimit,\n l2GasPerPubdataByteLimit\n );\n uint256 valueToSubmitXChainMessage = l2TransactionBaseCost + amount;\n weth.transferFrom(msg.sender, address(this), valueToSubmitXChainMessage);\n weth.withdraw(valueToSubmitXChainMessage);\n zkSyncL1Bridge.requestL2Transaction{ value: valueToSubmitXChainMessage }(\n to,\n amount,\n \"\",\n l2GasLimit,\n l2GasPerPubdataByteLimit,\n new bytes[](0),\n refundRecipient\n );\n\n // Emit an event that we can easily track in the ZkSyncAdapter because otherwise there is no easy event to\n // track ETH deposit initiations.\n emit ZkSyncEthDepositInitiated(msg.sender, to, amount);\n }\n\n fallback() external payable {}\n\n receive() external payable {}\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 1000000 + }, + "viaIR": true, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers", + "metadata", + "devdoc", + "userdoc", + "storageLayout", + "evm.gasEstimates" + ], + "": ["ast"] + } + }, + "metadata": { + "useLiteralContent": true + } + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 6d4d478b6..ae6e6ec90 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -29,6 +29,12 @@ const config: HardhatUserConfig = { }, networks: { hardhat: { accounts: { accountsBalance: "1000000000000000000000000" } }, + mainnet: { + url: getNodeUrl("mainnet", true, 1), + accounts: { mnemonic }, + saveDeployments: true, + chainId: 1, + }, kovan: { url: getNodeUrl("kovan", true, 42), accounts: { mnemonic }, diff --git a/src/clients/bridges/AdapterManager.ts b/src/clients/bridges/AdapterManager.ts index 7b9ce024a..b87b997d1 100644 --- a/src/clients/bridges/AdapterManager.ts +++ b/src/clients/bridges/AdapterManager.ts @@ -3,6 +3,7 @@ import { SpokePoolClient, HubPoolClient } from "../"; import { OptimismAdapter, ArbitrumAdapter, PolygonAdapter, BaseAdapter, ZKSyncAdapter } from "./"; import { OutstandingTransfers } from "../../interfaces"; import { utils } from "@across-protocol/sdk-v2"; +import { BaseChainAdapter } from "./op-stack/base/BaseChainAdapter"; export class AdapterManager { public adapters: { [chainId: number]: BaseAdapter } = {}; @@ -36,6 +37,9 @@ export class AdapterManager { if (this.spokePoolClients[324] !== undefined) { this.adapters[324] = new ZKSyncAdapter(logger, spokePoolClients, monitoredAddresses); } + if (this.spokePoolClients[8453] !== undefined) { + this.adapters[8453] = new BaseChainAdapter(logger, spokePoolClients, monitoredAddresses); + } logger.debug({ at: "AdapterManager#constructor", diff --git a/src/clients/bridges/BaseAdapter.ts b/src/clients/bridges/BaseAdapter.ts index 7cf38fc50..edf12f652 100644 --- a/src/clients/bridges/BaseAdapter.ts +++ b/src/clients/bridges/BaseAdapter.ts @@ -39,6 +39,7 @@ interface Events { const { TOKEN_SYMBOLS_MAP } = sdkConstants; +// TODO: make these generic arguments to BaseAdapter. type SupportedL1Token = string; type SupportedTokenSymbol = string; diff --git a/src/clients/bridges/OptimismAdapter.ts b/src/clients/bridges/OptimismAdapter.ts deleted file mode 100644 index 51482db8a..000000000 --- a/src/clients/bridges/OptimismAdapter.ts +++ /dev/null @@ -1,237 +0,0 @@ -import assert from "assert"; -import { - Contract, - BigNumber, - ZERO_ADDRESS, - paginatedEventQuery, - BigNumberish, - TransactionResponse, - compareAddressesSimple, - ethers, -} from "../../utils"; -import { spreadEventWithBlockNumber, assign, winston } from "../../utils"; -import { SpokePoolClient } from "../../clients"; -import { BaseAdapter } from "./"; -import { SortableEvent } from "../../interfaces"; -import { OutstandingTransfers } from "../../interfaces"; -import { constants } from "@across-protocol/sdk-v2"; -import { CONTRACT_ADDRESSES } from "../../common"; -import { CHAIN_IDs } from "@across-protocol/contracts-v2"; -const { TOKEN_SYMBOLS_MAP } = constants; - -export class OptimismAdapter extends BaseAdapter { - public l2Gas: number; - - private customL1OptimismBridgeAddresses = { - [TOKEN_SYMBOLS_MAP.DAI.addresses[1]]: CONTRACT_ADDRESSES[1].daiOptimismBridge, - [TOKEN_SYMBOLS_MAP.SNX.addresses[1]]: CONTRACT_ADDRESSES[1].snxOptimismBridge, - } as const; - - private customOvmBridgeAddresses = { - [TOKEN_SYMBOLS_MAP.DAI.addresses[1]]: CONTRACT_ADDRESSES[10].daiOptimismBridge, - [TOKEN_SYMBOLS_MAP.SNX.addresses[1]]: CONTRACT_ADDRESSES[10].snxOptimismBridge, - } as const; - - constructor( - logger: winston.Logger, - readonly spokePoolClients: { [chainId: number]: SpokePoolClient }, - monitoredAddresses: string[], - // Optional sender address where the cross chain transfers originate from. This is useful for the use case of - // monitoring transfers from HubPool to SpokePools where the sender is HubPool. - readonly senderAddress?: string - ) { - super(spokePoolClients, 10, monitoredAddresses, logger, [ - "DAI", - "SNX", - "USDC", - "USDT", - "WETH", - "WBTC", - "UMA", - "BAL", - "ACX", - "POOL", - ]); - this.l2Gas = 200000; - } - - async getOutstandingCrossChainTransfers(l1Tokens: string[]): Promise { - const { l1SearchConfig, l2SearchConfig } = this.getUpdatedSearchConfigs(); - this.log("Getting cross-chain txs", { l1Tokens, l1Config: l1SearchConfig, l2Config: l2SearchConfig }); - - const promises = []; - // Fetch bridge events for all monitored addresses. - for (const monitoredAddress of this.monitoredAddresses) { - for (const l1Token of l1Tokens) { - const l1Method = this.isWeth(l1Token) - ? "ETHDepositInitiated" - : this.isSNX(l1Token) - ? "DepositInitiated" - : "ERC20DepositInitiated"; - let l1SearchFilter = [l1Token, undefined, monitoredAddress]; - let l2SearchFilter = [l1Token, undefined, monitoredAddress]; - if (this.isWeth(l1Token)) { - l1SearchFilter = [undefined, monitoredAddress]; - l2SearchFilter = [ZERO_ADDRESS, undefined, monitoredAddress]; - } else if (this.isSNX(l1Token)) { - l1SearchFilter = [monitoredAddress]; - l2SearchFilter = [monitoredAddress]; - } - const l1Bridge = this.getL1Bridge(l1Token); - const l2Bridge = this.getL2Bridge(l1Token); - // Transfers might have come from the monitored address itself or another sender address (if specified). - const senderAddress = this.senderAddress || this.atomicDepositorAddress; - const adapterSearchConfig = this.isSNX(l1Token) ? [senderAddress] : [ZERO_ADDRESS, undefined, senderAddress]; - promises.push( - paginatedEventQuery(l1Bridge, l1Bridge.filters[l1Method](...l1SearchFilter), l1SearchConfig), - paginatedEventQuery(l2Bridge, l2Bridge.filters.DepositFinalized(...l2SearchFilter), l2SearchConfig), - paginatedEventQuery(l2Bridge, l2Bridge.filters.DepositFinalized(...adapterSearchConfig), l2SearchConfig) - ); - } - } - - const results = await Promise.all(promises); - - // 3 events per token. - const numEventsPerMonitoredAddress = 3 * l1Tokens.length; - - // Segregate the events list by monitored address. - const resultsByMonitoredAddress = Object.fromEntries( - this.monitoredAddresses.map((monitoredAddress, index) => { - const start = index * numEventsPerMonitoredAddress; - return [monitoredAddress, results.slice(start, start + numEventsPerMonitoredAddress + 1)]; - }) - ); - - // Process events for each monitored address. - for (const monitoredAddress of this.monitoredAddresses) { - const eventsToProcess = resultsByMonitoredAddress[monitoredAddress]; - // The logic below takes the results from the promises and spreads them into the l1DepositInitiatedEvents, - // l2DepositFinalizedEvents and l2DepositFinalizedEvents_DepositAdapter state from the BaseAdapter. - eventsToProcess.forEach((result, index) => { - const l1Token = l1Tokens[Math.floor(index / 3)]; - const events = result.map((event) => { - const eventSpread = spreadEventWithBlockNumber(event) as SortableEvent & { - _amount: BigNumberish; - _to: string; - }; - return { - amount: eventSpread["_amount"], - to: eventSpread["_to"], - ...eventSpread, - }; - }); - const eventsStorage = [ - this.l1DepositInitiatedEvents, - this.l2DepositFinalizedEvents, - this.l2DepositFinalizedEvents_DepositAdapter, - ][index % 3]; - - assign(eventsStorage, [monitoredAddress, l1Token], events); - }); - } - - this.baseL1SearchConfig.fromBlock = l1SearchConfig.toBlock + 1; - this.baseL1SearchConfig.fromBlock = l2SearchConfig.toBlock + 1; - - return this.computeOutstandingCrossChainTransfers(l1Tokens); - } - - async sendTokenToTargetChain( - address: string, - l1Token: string, - l2Token: string, - amount: BigNumber, - simMode = false - ): Promise { - const { chainId, l2Gas } = this; - - const contract = this.getL1TokenGateway(l1Token); - - let method = this.isSNX(l1Token) ? "depositTo" : "depositERC20"; - let args = this.isSNX(l1Token) ? [address, amount] : [l1Token, l2Token, amount, l2Gas, "0x"]; - - // If this token is WETH(the tokenToEvent maps to the ETH method) then we modify the params to call bridgeWethToOvm - // on the atomic depositor contract. Note that value is still 0 as this method will pull WETH from the caller. - if (this.isWeth(l1Token)) { - method = "bridgeWethToOvm"; - args = [address, amount, l2Gas, chainId]; - } - - // Pad gas when bridging to Optimism: https://community.optimism.io/docs/developers/bedrock/differences - const gasLimitMultiplier = 1.5; - return await this._sendTokenToTargetChain( - l1Token, - l2Token, - amount, - contract, - method, - args, - gasLimitMultiplier, - ethers.constants.Zero, - simMode - ); - } - - async wrapEthIfAboveThreshold(threshold: BigNumber, simMode = false): Promise { - const { chainId } = this; - assert(chainId === 10, `chainId ${chainId} is not supported`); - - const ovmWeth = CONTRACT_ADDRESSES[10].weth; - const ethBalance = await this.getSigner(chainId).getBalance(); - if (ethBalance.gt(threshold)) { - const l2Signer = this.getSigner(chainId); - const contract = new Contract(ovmWeth.address, ovmWeth.abi, l2Signer); - const value = ethBalance.sub(threshold); - this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, value, ethBalance }); - return await this._wrapEthIfAboveThreshold(threshold, contract, value, simMode); - } - return null; - } - - async checkTokenApprovals(address: string, l1Tokens: string[]): Promise { - // We need to approve the Atomic depositor to bridge WETH to optimism via the ETH route. - const associatedL1Bridges = l1Tokens.map((l1Token) => this.getL1TokenGateway(l1Token).address); - await this.checkAndSendTokenApprovals(address, l1Tokens, associatedL1Bridges); - } - - getL1Bridge(l1Token: string): Contract { - if (this.chainId !== 10) { - throw new Error(`chainId ${this.chainId} is not supported`); - } - const l1BridgeData = this.hasCustomL1Bridge(l1Token) - ? this.customL1OptimismBridgeAddresses[l1Token] - : CONTRACT_ADDRESSES[1].ovmStandardBridge; - return new Contract(l1BridgeData.address, l1BridgeData.abi, this.getSigner(1)); - } - - getL1TokenGateway(l1Token: string): Contract { - if (this.isWeth(l1Token)) { - return this.getAtomicDepositor(); - } else { - return this.getL1Bridge(l1Token); - } - } - - getL2Bridge(l1Token: string): Contract { - if (this.chainId !== 10) { - throw new Error(`chainId ${this.chainId} is not supported`); - } - const l2BridgeData = this.hasCustomL2Bridge(l1Token) - ? this.customOvmBridgeAddresses[l1Token] - : CONTRACT_ADDRESSES[10].ovmStandardBridge; - return new Contract(l2BridgeData.address, l2BridgeData.abi, this.getSigner(this.chainId)); - } - - isSNX(l1Token: string): boolean { - return compareAddressesSimple(l1Token, TOKEN_SYMBOLS_MAP.SNX.addresses[CHAIN_IDs.MAINNET]); - } - - private hasCustomL1Bridge(l1Token: string): boolean { - return l1Token in this.customL1OptimismBridgeAddresses; - } - - private hasCustomL2Bridge(l1Token: string): boolean { - return l1Token in this.customOvmBridgeAddresses; - } -} diff --git a/src/clients/bridges/index.ts b/src/clients/bridges/index.ts index 741b8381e..d2bb55e17 100644 --- a/src/clients/bridges/index.ts +++ b/src/clients/bridges/index.ts @@ -1,6 +1,6 @@ export * from "./BaseAdapter"; export * from "./AdapterManager"; -export * from "./OptimismAdapter"; +export * from "./op-stack/optimism/OptimismAdapter"; export * from "./ArbitrumAdapter"; export * from "./PolygonAdapter"; export * from "./CrossChainTransferClient"; diff --git a/src/clients/bridges/op-stack/DefaultErc20Bridge.ts b/src/clients/bridges/op-stack/DefaultErc20Bridge.ts new file mode 100644 index 000000000..fb7d66e5d --- /dev/null +++ b/src/clients/bridges/op-stack/DefaultErc20Bridge.ts @@ -0,0 +1,58 @@ +import { Contract, BigNumber, paginatedEventQuery, Event, Signer, EventSearchConfig, Provider } from "../../../utils"; +import { CONTRACT_ADDRESSES } from "../../../common"; +import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; + +export class DefaultERC20Bridge implements OpStackBridge { + private readonly l1Bridge: Contract; + private readonly l2Bridge: Contract; + + constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`]; + this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); + + const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].ovmStandardBridge; + this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); + } + + get l1Gateway(): string { + return this.l1Bridge.address; + } + + constructL1ToL2Txn( + toAddress: string, + l1Token: string, + l2Token: string, + amount: BigNumber, + l2Gas: number + ): BridgeTransactionDetails { + return { + contract: this.l1Bridge, + method: "depositERC20", + args: [l1Token, l2Token, amount, l2Gas, "0x"], + }; + } + + queryL1BridgeInitiationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise { + return paginatedEventQuery( + this.l1Bridge, + this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), + eventConfig + ); + } + + queryL2BridgeFinalizationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise { + return paginatedEventQuery( + this.l2Bridge, + this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), + eventConfig + ); + } +} diff --git a/src/clients/bridges/op-stack/OpStackAdapter.ts b/src/clients/bridges/op-stack/OpStackAdapter.ts new file mode 100644 index 000000000..7fce08092 --- /dev/null +++ b/src/clients/bridges/op-stack/OpStackAdapter.ts @@ -0,0 +1,182 @@ +import assert from "assert"; +import { + Contract, + BigNumber, + BigNumberish, + TransactionResponse, + Event, + checkAddressChecksum, + ethers, +} from "../../../utils"; +import { spreadEventWithBlockNumber, assign, winston } from "../../../utils"; +import { SpokePoolClient } from "../.."; +import { BaseAdapter } from ".."; +import { SortableEvent } from "../../../interfaces"; +import { OutstandingTransfers } from "../../../interfaces"; +import { CONTRACT_ADDRESSES } from "../../../common"; +import { constants } from "@across-protocol/sdk-v2"; +import { OpStackBridge } from "./OpStackBridgeInterface"; +import { WethBridge } from "./WethBridge"; +import { DefaultERC20Bridge } from "./DefaultErc20Bridge"; + +const { TOKEN_SYMBOLS_MAP } = constants; + +export class OpStackAdapter extends BaseAdapter { + public l2Gas: number; + private readonly defaultBridge: OpStackBridge; + + constructor( + chainId: number, + private customBridges: { [l1Address: string]: OpStackBridge }, + logger: winston.Logger, + supportedTokens: string[], + readonly spokePoolClients: { [chainId: number]: SpokePoolClient }, + monitoredAddresses: string[], + // Optional sender address where the cross chain transfers originate from. This is useful for the use case of + // monitoring transfers from HubPool to SpokePools where the sender is HubPool. + readonly senderAddress?: string + ) { + super(spokePoolClients, chainId, monitoredAddresses, logger, supportedTokens); + this.l2Gas = 200000; + + // Typically, a custom WETH bridge is not provided, so use the standard one. + const wethAddress = TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubChainId]; + if (wethAddress && !this.customBridges[wethAddress]) { + this.customBridges[wethAddress] = new WethBridge( + this.chainId, + this.hubChainId, + this.getSigner(this.hubChainId), + this.getSigner(chainId) + ); + } + + this.defaultBridge = new DefaultERC20Bridge( + this.chainId, + this.hubChainId, + this.getSigner(this.hubChainId), + this.getSigner(chainId) + ); + + // Before using this mapping, we need to verify that every key is a correctly checksummed address. + assert( + Object.keys(this.customBridges).every(checkAddressChecksum), + `Invalid or non-checksummed bridge address in customBridges keys: ${Object.keys(this.customBridges)}` + ); + } + + async getOutstandingCrossChainTransfers(l1Tokens: string[]): Promise { + const { l1SearchConfig, l2SearchConfig } = this.getUpdatedSearchConfigs(); + this.log("Getting cross-chain txs", { l1Tokens, l1Config: l1SearchConfig, l2Config: l2SearchConfig }); + + const processEvent = (event: Event) => { + const eventSpread = spreadEventWithBlockNumber(event) as SortableEvent & { + _amount: BigNumberish; + _to: string; + }; + return { + amount: eventSpread["_amount"], + to: eventSpread["_to"], + ...eventSpread, + }; + }; + + await Promise.all( + this.monitoredAddresses.map((monitoredAddress) => + Promise.all( + l1Tokens.map(async (l1Token) => { + const bridge = this.getBridge(l1Token); + + const [depositInitiatedResults, depositFinalizedResults, depositFinalizedResults_DepositAdapter] = + await Promise.all([ + bridge.queryL1BridgeInitiationEvents(l1Token, monitoredAddress, l1SearchConfig), + bridge.queryL2BridgeFinalizationEvents(l1Token, monitoredAddress, l2SearchConfig), + // Transfers might have come from the monitored address itself or another sender address (if specified). + bridge.queryL2BridgeFinalizationEvents( + l1Token, + this.senderAddress || this.atomicDepositorAddress, + l2SearchConfig + ), + ]); + + assign( + this.l1DepositInitiatedEvents, + [monitoredAddress, l1Token], + depositInitiatedResults.map(processEvent) + ); + assign( + this.l2DepositFinalizedEvents, + [monitoredAddress, l1Token], + depositFinalizedResults.map(processEvent) + ); + assign( + this.l2DepositFinalizedEvents_DepositAdapter, + [monitoredAddress, l1Token], + depositFinalizedResults_DepositAdapter.map(processEvent) + ); + }) + ) + ) + ); + + this.baseL1SearchConfig.fromBlock = l1SearchConfig.toBlock + 1; + this.baseL1SearchConfig.fromBlock = l2SearchConfig.toBlock + 1; + + return this.computeOutstandingCrossChainTransfers(l1Tokens); + } + + async sendTokenToTargetChain( + address: string, + l1Token: string, + l2Token: string, + amount: BigNumber, + simMode = false + ): Promise { + const { l2Gas } = this; + + const bridge = this.getBridge(l1Token); + + const { contract, method, args } = bridge.constructL1ToL2Txn(address, l1Token, l2Token, amount, l2Gas); + + // Pad gas when bridging to Optimism/Base: https://community.optimism.io/docs/developers/bedrock/differences + const gasLimitMultiplier = 1.5; + return await this._sendTokenToTargetChain( + l1Token, + l2Token, + amount, + contract, + method, + args, + gasLimitMultiplier, + ethers.constants.Zero, + simMode + ); + } + + async wrapEthIfAboveThreshold(threshold: BigNumber, simMode = false): Promise { + const { chainId } = this; + assert(chainId === this.chainId, `chainId ${chainId} is not supported`); + + const ovmWeth = CONTRACT_ADDRESSES[this.chainId].weth; + const ethBalance = await this.getSigner(chainId).getBalance(); + if (ethBalance.gt(threshold)) { + const l2Signer = this.getSigner(chainId); + const contract = new Contract(ovmWeth.address, ovmWeth.abi, l2Signer); + const value = ethBalance.sub(threshold); + this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, value, ethBalance }); + return await this._wrapEthIfAboveThreshold(threshold, contract, value, simMode); + } + return null; + } + + async checkTokenApprovals(address: string, l1Tokens: string[]): Promise { + // We need to approve the Atomic depositor to bridge WETH to optimism via the ETH route. + const associatedL1Bridges = l1Tokens.map((l1Token) => this.getBridge(l1Token).l1Gateway); + await this.checkAndSendTokenApprovals(address, l1Tokens, associatedL1Bridges); + } + + getBridge(l1Token: string): OpStackBridge { + // Before doing a lookup, we must verify that the address is correctly checksummed. + assert(checkAddressChecksum(l1Token), `Invalid or non-checksummed token address ${l1Token}`); + return this.customBridges[l1Token] || this.defaultBridge; + } +} diff --git a/src/clients/bridges/op-stack/OpStackBridgeInterface.ts b/src/clients/bridges/op-stack/OpStackBridgeInterface.ts new file mode 100644 index 000000000..9774032dc --- /dev/null +++ b/src/clients/bridges/op-stack/OpStackBridgeInterface.ts @@ -0,0 +1,24 @@ +import { Contract, BigNumber, Event, EventSearchConfig } from "../../../utils"; + +export interface BridgeTransactionDetails { + readonly contract: Contract; + readonly method: string; + readonly args: any[]; +} + +export interface OpStackBridge { + readonly l1Gateway: string; + constructL1ToL2Txn( + toAddress: string, + l1Token: string, + l2Token: string, + amount: BigNumber, + l2Gas: number + ): BridgeTransactionDetails; + queryL1BridgeInitiationEvents(l1Token: string, fromAddress: string, eventConfig: EventSearchConfig): Promise; + queryL2BridgeFinalizationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise; +} diff --git a/src/clients/bridges/op-stack/WethBridge.ts b/src/clients/bridges/op-stack/WethBridge.ts new file mode 100644 index 000000000..0dbc99da2 --- /dev/null +++ b/src/clients/bridges/op-stack/WethBridge.ts @@ -0,0 +1,71 @@ +import { + Contract, + BigNumber, + Event, + EventSearchConfig, + paginatedEventQuery, + Signer, + Provider, + ZERO_ADDRESS, +} from "../../../utils"; +import { CONTRACT_ADDRESSES } from "../../../common"; +import { BridgeTransactionDetails, OpStackBridge } from "./OpStackBridgeInterface"; + +export class WethBridge implements OpStackBridge { + private readonly l1Bridge: Contract; + private readonly l2Bridge: Contract; + private readonly atomicDepositor: Contract; + + constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId][`ovmStandardBridge_${l2chainId}`]; + this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); + + const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].ovmStandardBridge; + this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); + + const { address: atomicDepositorAddress, abi: atomicDepositorAbi } = CONTRACT_ADDRESSES[hubChainId].atomicDepositor; + this.atomicDepositor = new Contract(atomicDepositorAddress, atomicDepositorAbi, l1Signer); + } + + get l1Gateway(): string { + return this.atomicDepositor.address; + } + + constructL1ToL2Txn( + toAddress: string, + l1Token: string, + l2Token: string, + amount: BigNumber, + l2Gas: number + ): BridgeTransactionDetails { + return { + contract: this.atomicDepositor, + method: "bridgeWethToOvm", + args: [toAddress, amount, l2Gas, this.l2chainId], + }; + } + + queryL1BridgeInitiationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise { + return paginatedEventQuery( + this.l1Bridge, + this.l1Bridge.filters.ETHDepositInitiated(undefined, fromAddress), + eventConfig + ); + } + + queryL2BridgeFinalizationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise { + return paginatedEventQuery( + this.l2Bridge, + this.l2Bridge.filters.DepositFinalized(ZERO_ADDRESS, undefined, fromAddress), + eventConfig + ); + } +} diff --git a/src/clients/bridges/op-stack/base/BaseChainAdapter.ts b/src/clients/bridges/op-stack/base/BaseChainAdapter.ts new file mode 100644 index 000000000..0df8ecc36 --- /dev/null +++ b/src/clients/bridges/op-stack/base/BaseChainAdapter.ts @@ -0,0 +1,26 @@ +import { winston } from "../../../../utils"; +import { SpokePoolClient } from "../../.."; +import { OpStackAdapter } from "../OpStackAdapter"; + +// Note: this is called BaseChainAdapter because BaseAdapter is the name of the base class. +export class BaseChainAdapter extends OpStackAdapter { + constructor( + logger: winston.Logger, + readonly spokePoolClients: { [chainId: number]: SpokePoolClient }, + monitoredAddresses: string[], + // Optional sender address where the cross chain transfers originate from. This is useful for the use case of + // monitoring transfers from HubPool to SpokePools where the sender is HubPool. + readonly senderAddress?: string + ) { + super( + 8453, + // Custom Bridges + {}, + logger, + ["BAL", "DAI", "ETH", "WETH", "USDC"], + spokePoolClients, + monitoredAddresses, + senderAddress + ); + } +} diff --git a/src/clients/bridges/op-stack/base/index.ts b/src/clients/bridges/op-stack/base/index.ts new file mode 100644 index 000000000..c42e90657 --- /dev/null +++ b/src/clients/bridges/op-stack/base/index.ts @@ -0,0 +1 @@ +export * from "./BaseChainAdapter"; diff --git a/src/clients/bridges/op-stack/index.ts b/src/clients/bridges/op-stack/index.ts new file mode 100644 index 000000000..dcf278562 --- /dev/null +++ b/src/clients/bridges/op-stack/index.ts @@ -0,0 +1,4 @@ +// Only export the adapters, not the interface or supporting code. +export * from "./OpStackAdapter"; +export * from "./optimism"; +export * from "./base"; diff --git a/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts b/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts new file mode 100644 index 000000000..ead7b6fdd --- /dev/null +++ b/src/clients/bridges/op-stack/optimism/DaiOptimismBridge.ts @@ -0,0 +1,66 @@ +import { + Contract, + BigNumber, + paginatedEventQuery, + Event, + EventSearchConfig, + Signer, + Provider, +} from "../../../../utils"; +import { CONTRACT_ADDRESSES } from "../../../../common"; +import { OpStackBridge, BridgeTransactionDetails } from "../OpStackBridgeInterface"; + +export class DaiOptimismBridge implements OpStackBridge { + private readonly l1Bridge: Contract; + private readonly l2Bridge: Contract; + + constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].daiOptimismBridge; + this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); + + const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].daiOptimismBridge; + this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); + } + + get l1Gateway(): string { + return this.l1Bridge.address; + } + + constructL1ToL2Txn( + toAddress: string, + l1Token: string, + l2Token: string, + amount: BigNumber, + l2Gas: number + ): BridgeTransactionDetails { + return { + contract: this.l1Bridge, + method: "depositERC20", + args: [l1Token, l2Token, amount, l2Gas, "0x"], + }; + } + + queryL1BridgeInitiationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise { + return paginatedEventQuery( + this.l1Bridge, + this.l1Bridge.filters.ERC20DepositInitiated(l1Token, undefined, fromAddress), + eventConfig + ); + } + + queryL2BridgeFinalizationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise { + return paginatedEventQuery( + this.l2Bridge, + this.l2Bridge.filters.DepositFinalized(l1Token, undefined, fromAddress), + eventConfig + ); + } +} diff --git a/src/clients/bridges/op-stack/optimism/OptimismAdapter.ts b/src/clients/bridges/op-stack/optimism/OptimismAdapter.ts new file mode 100644 index 000000000..72ecd4bab --- /dev/null +++ b/src/clients/bridges/op-stack/optimism/OptimismAdapter.ts @@ -0,0 +1,38 @@ +import { winston } from "../../../../utils"; +import { SpokePoolClient } from "../../.."; +import { BaseAdapter } from "../.."; +import { constants } from "@across-protocol/sdk-v2"; +import { OpStackAdapter } from "../OpStackAdapter"; +const { TOKEN_SYMBOLS_MAP } = constants; +import { DaiOptimismBridge } from "./DaiOptimismBridge"; +import { SnxOptimismBridge } from "./SnxOptimismBridge"; + +export class OptimismAdapter extends OpStackAdapter { + constructor( + logger: winston.Logger, + readonly spokePoolClients: { [chainId: number]: SpokePoolClient }, + monitoredAddresses: string[], + // Optional sender address where the cross chain transfers originate from. This is useful for the use case of + // monitoring transfers from HubPool to SpokePools where the sender is HubPool. + readonly senderAddress?: string + ) { + const hubChainId = BaseAdapter.HUB_CHAIN_ID; + const l2ChainId = 10; + const hubChainSigner = spokePoolClients[hubChainId].spokePool.signer; + const l2Signer = spokePoolClients[l2ChainId].spokePool.signer; + const daiBridge = new DaiOptimismBridge(l2ChainId, hubChainId, hubChainSigner, l2Signer); + const snxBridge = new SnxOptimismBridge(l2ChainId, hubChainId, hubChainSigner, l2Signer); + super( + 10, + { + [TOKEN_SYMBOLS_MAP.DAI.addresses[hubChainId]]: daiBridge, + [TOKEN_SYMBOLS_MAP.SNX.addresses[hubChainId]]: snxBridge, + }, + logger, + ["DAI", "SNX", "USDC", "USDT", "WETH", "WBTC", "UMA", "BAL", "ACX", "POOL"], + spokePoolClients, + monitoredAddresses, + senderAddress + ); + } +} diff --git a/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts b/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts new file mode 100644 index 000000000..11f7b28f8 --- /dev/null +++ b/src/clients/bridges/op-stack/optimism/SnxOptimismBridge.ts @@ -0,0 +1,59 @@ +import { + Contract, + BigNumber, + paginatedEventQuery, + Event, + EventSearchConfig, + Signer, + Provider, +} from "../../../../utils"; +import { CONTRACT_ADDRESSES } from "../../../../common"; +import { OpStackBridge, BridgeTransactionDetails } from "../OpStackBridgeInterface"; + +export class SnxOptimismBridge implements OpStackBridge { + private readonly l1Bridge: Contract; + private readonly l2Bridge: Contract; + + constructor(private l2chainId: number, hubChainId: number, l1Signer: Signer, l2SignerOrProvider: Signer | Provider) { + const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].snxOptimismBridge; + this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer); + + const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].snxOptimismBridge; + this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider); + } + + get l1Gateway(): string { + return this.l1Bridge.address; + } + + constructL1ToL2Txn( + toAddress: string, + l1Token: string, + l2Token: string, + amount: BigNumber, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + l2Gas: number + ): BridgeTransactionDetails { + return { + contract: this.l1Bridge, + method: "depositTo", + args: [toAddress, amount], + }; + } + + queryL1BridgeInitiationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise { + return paginatedEventQuery(this.l1Bridge, this.l1Bridge.filters.DepositInitiated(fromAddress), eventConfig); + } + + queryL2BridgeFinalizationEvents( + l1Token: string, + fromAddress: string, + eventConfig: EventSearchConfig + ): Promise { + return paginatedEventQuery(this.l2Bridge, this.l2Bridge.filters.DepositFinalized(fromAddress), eventConfig); + } +} diff --git a/src/clients/bridges/op-stack/optimism/index.ts b/src/clients/bridges/op-stack/optimism/index.ts new file mode 100644 index 000000000..de2fff3d7 --- /dev/null +++ b/src/clients/bridges/op-stack/optimism/index.ts @@ -0,0 +1,2 @@ +// Note: the custom bridges _do not_ need to be exported. +export * from "./OptimismAdapter"; diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index 07620a09e..79998d770 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -125,7 +125,7 @@ export const CONTRACT_ADDRESSES: { }, ], }, - // Optimism and Polygon cant deposit WETH directly so we use an atomic depositor contract that unwraps WETH and + // OVM, ZkSync and Polygon cant deposit WETH directly so we use an atomic depositor contract that unwraps WETH and // bridges ETH other the canonical bridge. atomicDepositor: { address: "0xaA282C4E86beFda4a1E7C9c06165869026D27852", @@ -179,7 +179,9 @@ export const CONTRACT_ADDRESSES: { { stateMutability: "payable", type: "receive" }, ], }, - ovmStandardBridge: { + // Since there are multiple ovmStandardBridges on mainnet for different OP Stack chains, we append the chain id of the Op + // Stack chain to the name to differentiate. This one is for Optimism. + ovmStandardBridge_10: { address: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", abi: [ { @@ -231,6 +233,60 @@ export const CONTRACT_ADDRESSES: { }, ], }, + // Since there are multiple ovmStandardBridges on mainnet for different OP Stack chains, we append the chain id of the Op + // Stack chain to the name to differentiate. This one is for Base. + ovmStandardBridge_8453: { + address: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + abi: [ + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "_l1Token", type: "address" }, + { indexed: true, internalType: "address", name: "_l2Token", type: "address" }, + { indexed: true, internalType: "address", name: "_from", type: "address" }, + { indexed: false, internalType: "address", name: "_to", type: "address" }, + { indexed: false, internalType: "uint256", name: "_amount", type: "uint256" }, + { indexed: false, internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "ERC20DepositInitiated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "_from", type: "address" }, + { indexed: true, internalType: "address", name: "_to", type: "address" }, + { indexed: false, internalType: "uint256", name: "_amount", type: "uint256" }, + { indexed: false, internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "ETHDepositInitiated", + type: "event", + }, + { + inputs: [ + { internalType: "uint32", name: "_l2Gas", type: "uint32" }, + { internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "depositETH", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_l1Token", type: "address" }, + { internalType: "address", name: "_l2Token", type: "address" }, + { internalType: "uint256", name: "_amount", type: "uint256" }, + { internalType: "uint32", name: "_l2Gas", type: "uint32" }, + { internalType: "bytes", name: "_data", type: "bytes" }, + ], + name: "depositERC20", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + ], + }, polygonRootChainManager: { address: "0xA0c68C638235ee32657e8f720a23ceC1bFc77C77", abi: [ diff --git a/src/utils/AddressUtils.ts b/src/utils/AddressUtils.ts index 357d5756a..126885879 100644 --- a/src/utils/AddressUtils.ts +++ b/src/utils/AddressUtils.ts @@ -1,5 +1,5 @@ import { TOKEN_SYMBOLS_MAP } from "@across-protocol/contracts-v2"; -import { BigNumber } from "."; +import { BigNumber, ethers } from "."; export function compareAddresses(addressA: string, addressB: string): 1 | -1 | 0 { // Convert address strings to BigNumbers and then sort numerical value of the BigNumber, which sorts the addresses @@ -57,3 +57,7 @@ export function getTokenAddress(tokenAddress: string, chainId: number, targetCha } return targetAddress; } + +export function checkAddressChecksum(tokenAddress: string): boolean { + return ethers.utils.getAddress(tokenAddress) === tokenAddress; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index ffa45d36b..f738553f2 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,7 +7,7 @@ export { Logger } from "@uma/financial-templates-lib"; export { BigNumber, Signer, Contract, ContractFactory, Transaction, BigNumberish } from "ethers"; export { utils, EventFilter, BaseContract, Event, Wallet } from "ethers"; export { ethers, providers } from "ethers"; -export type { Block, TransactionResponse, TransactionReceipt } from "@ethersproject/abstract-provider"; +export type { Block, TransactionResponse, TransactionReceipt, Provider } from "@ethersproject/abstract-provider"; export { config } from "dotenv"; // Utils specifically for this bot. diff --git a/test/AdapterManager.SendTokensCrossChain.ts b/test/AdapterManager.SendTokensCrossChain.ts index ff71cb004..6dfa7f249 100644 --- a/test/AdapterManager.SendTokensCrossChain.ts +++ b/test/AdapterManager.SendTokensCrossChain.ts @@ -32,7 +32,10 @@ let l1ArbitrumBridge: FakeContract; let l1MailboxContract: FakeContract; let l1ZkSyncBridge: FakeContract; -const enabledChainIds = [1, 10, 137, 288, 42161, 324]; +// Base contracts +let l1BaseBridge: FakeContract; + +const enabledChainIds = [1, 10, 137, 288, 42161, 324, 8453]; const mainnetTokens = { usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", @@ -236,6 +239,37 @@ describe("AdapterManager: Send tokens cross-chain", async function () { ); expect(l1AtomicDepositor.bridgeWethToZkSync).to.have.been.calledWithValue(0); }); + it("Correctly sends tokens to chain: Base", async function () { + const chainId = 8453; // Base ChainId + // ERC20 tokens: + await adapterManager.sendTokenCrossChain(relayer.address, chainId, mainnetTokens["usdc"], amountToSend); + expect(l1BaseBridge.depositERC20).to.have.been.calledWith( + mainnetTokens["usdc"], // l1 token + getL2TokenAddresses(mainnetTokens["usdc"])[chainId], // l2 token + amountToSend, // amount + (adapterManager.adapters[chainId] as any).l2Gas, // l2Gas + "0x" // data + ); + + // DAI should not be a custom token on base. + await adapterManager.sendTokenCrossChain(relayer.address, chainId, mainnetTokens["dai"], amountToSend); + expect(l1BaseBridge.depositERC20).to.have.been.calledWith( + mainnetTokens["dai"], // l1 token + getL2TokenAddresses(mainnetTokens["dai"])[chainId], // l2 token + amountToSend, // amount + (adapterManager.adapters[chainId] as any).l2Gas, // l2Gas + "0x" // data + ); + + // Weth is not directly sendable over the canonical bridge. Rather, we should see a call against the atomic depositor. + await adapterManager.sendTokenCrossChain(relayer.address, chainId, mainnetTokens["weth"], amountToSend); + expect(l1AtomicDepositor.bridgeWethToOvm).to.have.been.calledWith( + relayer.address, // to + amountToSend, // amount + (adapterManager.adapters[chainId] as any).l2Gas, // l2Gas + chainId // chainId + ); + }); }); async function seedMocks() { @@ -262,7 +296,7 @@ async function constructChainSpecificFakes() { l1AtomicDepositor = await makeFake("atomicDepositor", CONTRACT_ADDRESSES[1].atomicDepositor.address!); // Optimism contracts - l1OptimismBridge = await makeFake("ovmStandardBridge", CONTRACT_ADDRESSES[1].ovmStandardBridge.address!); + l1OptimismBridge = await makeFake("ovmStandardBridge_10", CONTRACT_ADDRESSES[1].ovmStandardBridge_10.address!); l1OptimismDaiBridge = await makeFake("daiOptimismBridge", CONTRACT_ADDRESSES[1].daiOptimismBridge.address!); l1OptimismSnxBridge = await makeFake("snxOptimismBridge", CONTRACT_ADDRESSES[1].snxOptimismBridge.address!); @@ -281,6 +315,9 @@ async function constructChainSpecificFakes() { // zkSync contracts l1ZkSyncBridge = await makeFake("zkSyncDefaultErc20Bridge", CONTRACT_ADDRESSES[1].zkSyncDefaultErc20Bridge.address!); l1MailboxContract = await makeFake("zkSyncMailbox", CONTRACT_ADDRESSES[1].zkSyncMailbox.address!); + + // Base contracts + l1BaseBridge = await makeFake("ovmStandardBridge_8453", CONTRACT_ADDRESSES[1].ovmStandardBridge_8453.address!); } async function makeFake(contractName: string, address: string) {