diff --git a/customNetwork.json b/customNetwork.json index be59f7c..ae17823 100644 --- a/customNetwork.json +++ b/customNetwork.json @@ -6,7 +6,7 @@ "bridge": "0x5eCF728ffC5C5E802091875f96281B5aeECf6C49", "inbox": "0x9f8c1c641336A371031499e3c362e40d58d0f254", "outbox": "0x50143333b44Ea46255BEb67255C9Afd35551072F", - "rollup": "0x7d98BA231d29D5C202981542C0291718A7358c63", + "rollup": "0xe5Ab92C74CD297F0a1F2914cE37204FC5Bc4e82D", "sequencerInbox": "0x18d19C5d3E685f5be5b9C86E097f0E439285D216" }, "explorerUrl": "", @@ -44,7 +44,7 @@ "bridge": "0xA584795e24628D9c067A6480b033C9E96281fcA3", "inbox": "0xDcA690902d3154886Ec259308258D10EA5450996", "outbox": "0xda243bD61B011024FC923164db75Dde198AC6175", - "rollup": "0xfe808cD61B3fe45c67c47B17DB49B96Fb2BFDfae", + "rollup": "0x47b238E195b638b8972Cb3649e5d6775c279245d", "sequencerInbox": "0x16c54EE2015CD824415c2077F4103f444E00A8cb" }, "explorerUrl": "", diff --git a/packages/contract-deposit/contracts/CustomGasTokenDeposit.sol b/packages/contract-deposit/contracts/CustomGasTokenDeposit.sol new file mode 100644 index 0000000..0522c25 --- /dev/null +++ b/packages/contract-deposit/contracts/CustomGasTokenDeposit.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@arbitrum/nitro-contracts/src/bridge/IERC20Inbox.sol"; +import "@arbitrum/nitro-contracts/src/bridge/IERC20Bridge.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract CustomGasTokenDeposit { + using SafeERC20 for IERC20; + + IERC20Inbox public inbox; + + event CustomGasTokenDeposited(uint256 indexed ticketId); + event RetryableTicketCreated(uint256 indexed ticketId); + + constructor(address _inbox) { + inbox = IERC20Inbox(_inbox); + } + + function depositToChildChain(uint256 amount) public returns (uint256) { + // Transfer the native token to this contract + // and allow Inbox to transfer those tokens + address bridge = address(inbox.bridge()); + address nativeToken = IERC20Bridge(bridge).nativeToken(); + + IERC20(nativeToken).safeTransferFrom(msg.sender, address(this), amount); + IERC20(nativeToken).approve(address(inbox), amount); + + uint256 ticketID = inbox.depositERC20(amount); + + emit CustomGasTokenDeposited(ticketID); + return ticketID; + } + + function moveFundsFromChildChainAliasToAnotherAddress( + address to, + uint256 l2callvalue, + uint256 maxSubmissionCost, + uint256 maxGas, + uint256 gasPriceBid, + uint256 tokenAmount + ) public returns (uint256) { + // Transfer the native token to this contract + // and allow Inbox to transfer those tokens + address bridge = address(inbox.bridge()); + address nativeToken = IERC20Bridge(bridge).nativeToken(); + + IERC20(nativeToken).safeTransferFrom(msg.sender, address(this), tokenAmount); + IERC20(nativeToken).approve(address(inbox), tokenAmount); + + /** + * We are using unsafeCreateRetryableTicket because the safe one will check if + * the parent chain's msg.value can be used to pay for the child chain's callvalue, while in this case + * we'll use child chain's balance to pay for the callvalue rather than parent chain's msg.value + */ + uint256 ticketID = inbox.unsafeCreateRetryableTicket( + to, + l2callvalue, + maxSubmissionCost, + msg.sender, + msg.sender, + maxGas, + gasPriceBid, + tokenAmount, + "" + ); + + emit RetryableTicketCreated(ticketID); + return ticketID; + } +} diff --git a/packages/contract-deposit/package.json b/packages/contract-deposit/package.json index 8fa974f..23fe0df 100644 --- a/packages/contract-deposit/package.json +++ b/packages/contract-deposit/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@arbitrum/sdk": "^4.0.1", - "@arbitrum/nitro-contracts": "2.1.0" + "@arbitrum/nitro-contracts": "2.1.0", + "@openzeppelin/contracts": "^4.8.3" } } diff --git a/packages/contract-deposit/scripts/exec.js b/packages/contract-deposit/scripts/exec.js index ecbccde..c29a12c 100644 --- a/packages/contract-deposit/scripts/exec.js +++ b/packages/contract-deposit/scripts/exec.js @@ -18,6 +18,7 @@ const { ParentEthDepositTransactionReceipt, } = require('@arbitrum/sdk'); const { getBaseFee } = require('@arbitrum/sdk/dist/lib/utils/lib'); +const { ERC20__factory } = require('@arbitrum/sdk/dist/lib/abi/factories/ERC20__factory'); require('dotenv').config(); requireEnvVariables(['PRIVATE_KEY', 'CHAIN_RPC', 'PARENT_CHAIN_RPC', 'TransferTo']); @@ -50,15 +51,24 @@ const main = async () => { const ethBridger = new EthBridger(childChainNetwork); const inboxAddress = ethBridger.childNetwork.ethBridge.inbox; + /** + * We find out whether the child chain we are using is a custom gas token chain + * We need to perform an additional approve call to transfer + * the native tokens to pay for the gas of the retryable tickets. + */ + const isCustomGasTokenChain = + childChainNetwork.nativeToken && childChainNetwork.nativeToken !== ethers.constants.AddressZero; + /** * We deploy EthDeposit contract to the parent chain first and send eth to * the child chain via this contract. * Funds will deposit to the contract's alias address first. */ - const DepositContract = await ( - await hre.ethers.getContractFactory('EthDeposit') - ).connect(parentChainWallet); - console.log('Deploying EthDeposit contract...'); + const depositContractName = isCustomGasTokenChain ? 'CustomGasTokenDeposit' : 'EthDeposit'; + const DepositContract = (await hre.ethers.getContractFactory(depositContractName)).connect( + parentChainWallet, + ); + console.log(`Deploying ${depositContractName} contract...`); const depositContract = await DepositContract.deploy(inboxAddress); await depositContract.deployed(); console.log(`deployed to ${depositContract.address}`); @@ -71,18 +81,41 @@ const main = async () => { console.log(`Sending deposit transaction...`); - const ethDepositTx = await depositContract.depositToChildChain({ - value: ethers.utils.parseEther('0.01'), - }); - const ethDepositRec = await ethDepositTx.wait(); + let depositTx; + let nativeTokenDecimals = 18; // We default to 18 decimals for ETH and most of ERC-20 tokens + if (isCustomGasTokenChain) { + // Approve the gas token to be sent to the contract + console.log('Giving allowance to the contract to transfer the chain native token'); + const nativeToken = new ethers.Contract( + childChainNetwork.nativeToken, + ERC20__factory.abi, + parentChainWallet, + ); + nativeTokenDecimals = await nativeToken.decimals(); + const approvalTransaction = await nativeToken.approve( + depositContract.address, + ethers.utils.parseUnits('1', nativeTokenDecimals), + ); + const approvalTransactionReceipt = await approvalTransaction.wait(); + console.log(`Approval transaction receipt is: ${approvalTransactionReceipt.transactionHash}`); + + depositTx = await depositContract.depositToChildChain( + ethers.utils.parseUnits('0.01', nativeTokenDecimals), + ); + } else { + depositTx = await depositContract.depositToChildChain({ + value: ethers.utils.parseEther('0.01'), // Here we know we are using ETH, so we can use parseEther + }); + } + const depositReceipt = await depositTx.wait(); - console.log(`Deposit txn confirmed on the parent chain! 🙌 ${ethDepositRec.transactionHash}`); + console.log(`Deposit txn confirmed on the parent chain! 🙌 ${depositReceipt.transactionHash}`); console.log( 'Waiting for the execution of the deposit in the child chain. This may take up to 10-15 minutes ⏰', ); - const parentChainDepositTxReceipt = new ParentEthDepositTransactionReceipt(ethDepositRec); + const parentChainDepositTxReceipt = new ParentEthDepositTransactionReceipt(depositReceipt); const childChainDepositResult = await parentChainDepositTxReceipt.waitForChildTransactionReceipt( childChainProvider, ); @@ -103,7 +136,7 @@ const main = async () => { ); } else { throw new Error( - `Deposit to the child chain failed, EthDepositStatus is ${ + `Deposit to the child chain failed, DepositStatus is ${ EthDepositStatus[childChainDepositResult.message.status] }`, ); @@ -147,7 +180,7 @@ const main = async () => { { from: contractAliasAddress, to: transferTo, - l2CallValue: ethers.utils.parseEther('0.01'), // because we deposited 0.01 ether, so we also transfer 0.01 ether out here. + l2CallValue: ethers.utils.parseUnits('0.01', nativeTokenDecimals), // because we deposited 0.01 ether, so we also transfer 0.01 ether out here. excessFeeRefundAddress: depositContract.address, callValueRefundAddress: depositContract.address, data: [], @@ -175,7 +208,9 @@ const main = async () => { * we need to subtract it here so the transaction in the parent chain doesn't pay l2callvalue * and instead uses the alias balance on the child chain directly. */ - const depositAmount = parentToChildMessageGasParams.deposit.sub(ethers.utils.parseEther('0.01')); + const depositAmount = parentToChildMessageGasParams.deposit.sub( + ethers.utils.parseUnits('0.01', nativeTokenDecimals), + ); console.log( `Transfer funds txn needs ${ethers.utils.formatEther( @@ -186,16 +221,31 @@ const main = async () => { /** * Call the contract's method to transfer the funds from the alias to the address you set */ - const setTransferTx = await depositContract.moveFundsFromChildChainAliasToAnotherAddress( - transferTo, - ethers.utils.parseEther('0.01'), // because we deposited 0.01 ether, so we also transfer 0.01 ether out here. - parentToChildMessageGasParams.maxSubmissionCost, - parentToChildMessageGasParams.gasLimit, - gasPriceBid, - { - value: depositAmount, - }, - ); + let setTransferTx; + if (isCustomGasTokenChain) { + // We don't need to give allowance to the contract now since we already gave plenty in the + // previous step + + setTransferTx = await depositContract.moveFundsFromChildChainAliasToAnotherAddress( + transferTo, + ethers.utils.parseUnits('0.01', nativeTokenDecimals), // because we deposited 0.01 ether, so we also transfer 0.01 ether out here. + parentToChildMessageGasParams.maxSubmissionCost, + parentToChildMessageGasParams.gasLimit, + gasPriceBid, + depositAmount, + ); + } else { + setTransferTx = await depositContract.moveFundsFromChildChainAliasToAnotherAddress( + transferTo, + ethers.utils.parseEther('0.01'), // because we deposited 0.01 ether, so we also transfer 0.01 ether out here. + parentToChildMessageGasParams.maxSubmissionCost, + parentToChildMessageGasParams.gasLimit, + gasPriceBid, + { + value: depositAmount, + }, + ); + } const setTransferRec = await setTransferTx.wait(); console.log( diff --git a/packages/custom-gateway-bridging/contracts/ChildChainCustomGateway.sol b/packages/custom-gateway-bridging/contracts/ChildChainCustomGateway.sol index 9747253..be28d82 100644 --- a/packages/custom-gateway-bridging/contracts/ChildChainCustomGateway.sol +++ b/packages/custom-gateway-bridging/contracts/ChildChainCustomGateway.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "./interfaces/ICustomGateway.sol"; import "./CrosschainMessenger.sol"; -import "./interfaces/IArbToken.sol"; +import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/arbitrum/IArbToken.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; /** diff --git a/packages/custom-gateway-bridging/contracts/ChildChainToken.sol b/packages/custom-gateway-bridging/contracts/ChildChainToken.sol index 0e8bcc1..2849139 100644 --- a/packages/custom-gateway-bridging/contracts/ChildChainToken.sol +++ b/packages/custom-gateway-bridging/contracts/ChildChainToken.sol @@ -1,29 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "./interfaces/IArbToken.sol"; +import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/arbitrum/IArbToken.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; /** * @title Example implementation of a custom ERC20 token to be deployed on L2 */ contract ChildChainToken is ERC20, IArbToken { - address public l2GatewayAddress; - address public override l1Address; + address public gateway; // The child chain custom gateway contract + address public override l1Address; // The address of the token on the parent chain modifier onlyL2Gateway() { - require(msg.sender == l2GatewayAddress, "NOT_GATEWAY"); + require(msg.sender == gateway, "NOT_GATEWAY"); _; } /** * @dev See {ERC20-constructor} - * @param l2GatewayAddress_ address of the L2 custom gateway - * @param l1TokenAddress_ address of the custom token deployed on L1 + * @param _gateway address of the L2 custom gateway + * @param _l1Address address of the custom token deployed on L1 */ - constructor(address l2GatewayAddress_, address l1TokenAddress_) ERC20("L2CustomToken", "LCT") { - l2GatewayAddress = l2GatewayAddress_; - l1Address = l1TokenAddress_; + constructor(address _gateway, address _l1Address) ERC20("L2CustomToken", "LCT") { + gateway = _gateway; + l1Address = _l1Address; } /** diff --git a/packages/custom-gateway-bridging/contracts/CrosschainMessenger.sol b/packages/custom-gateway-bridging/contracts/CrosschainMessenger.sol index f83f90a..a4bde7b 100644 --- a/packages/custom-gateway-bridging/contracts/CrosschainMessenger.sol +++ b/packages/custom-gateway-bridging/contracts/CrosschainMessenger.sol @@ -1,46 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +import "@arbitrum/nitro-contracts/src/bridge/IBridge.sol"; +import "@arbitrum/nitro-contracts/src/bridge/IInbox.sol"; +import "@arbitrum/nitro-contracts/src/bridge/IERC20Inbox.sol"; +import "@arbitrum/nitro-contracts/src/bridge/IOutbox.sol"; import "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; import "@arbitrum/nitro-contracts/src/libraries/AddressAliasHelper.sol"; -/** - * @title Interface needed to call function activeOutbox of the Bridge - */ -interface IBridge { - function activeOutbox() external view returns (address); -} - -/** - * @title Interface needed to call functions createRetryableTicket and bridge of the Inbox - */ -interface IInbox { - function createRetryableTicket( - address to, - uint256 arbTxCallValue, - uint256 maxSubmissionCost, - address submissionRefundAddress, - address valueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - - function bridge() external view returns (IBridge); -} - -/** - * @title Interface needed to call function l2ToL1Sender of the Outbox - */ -interface IOutbox { - function l2ToL1Sender() external view returns (address); -} - /** * @title Minimum expected implementation of a crosschain messenger contract to be deployed on L1 */ abstract contract L1CrosschainMessenger { - IInbox public immutable inbox; + address public immutable inbox; /** * Emitted when calling sendTxToL2CustomRefund @@ -52,12 +24,12 @@ abstract contract L1CrosschainMessenger { event TxToL2(address indexed from, address indexed to, uint256 indexed seqNum, bytes data); constructor(address inbox_) { - inbox = IInbox(inbox_); + inbox = inbox_; } modifier onlyCounterpartGateway(address l2Counterpart) { // A message coming from the counterpart gateway was executed by the bridge - IBridge bridge = inbox.bridge(); + IBridge bridge = IInbox(inbox).bridge(); require(msg.sender == address(bridge), "NOT_FROM_BRIDGE"); // And the outbox reports that the L2 address of the sender is the counterpart gateway @@ -78,7 +50,51 @@ abstract contract L1CrosschainMessenger { * @param maxGas max gas deducted from user's L2 balance to cover L2 execution * @param gasPriceBid gas price for L2 execution * @param data encoded data for the retryable - * @return seqnum id for the retryable ticket + * @return seqNum id for the retryable ticket + */ + function _sendTxToL2CustomRefund( + address to, + address refundTo, + address user, + uint256 l1CallValue, + uint256 l2CallValue, + uint256 maxSubmissionCost, + uint256 maxGas, + uint256 gasPriceBid, + bytes memory data + ) internal virtual returns (uint256 seqNum) { + seqNum = IInbox(inbox).createRetryableTicket{ value: l1CallValue }( + to, + l2CallValue, + maxSubmissionCost, + refundTo, + user, + maxGas, + gasPriceBid, + data + ); + + emit TxToL2(user, to, seqNum, data); + } +} + +/** + * @title Minimum expected implementation of a crosschain messenger contract to be deployed on L1 + * when using a custom gas orbit chain + */ +abstract contract L1CrosschainMessengerCustomGas is L1CrosschainMessenger { + /** + * Creates the retryable ticket to send over to L2 through the Inbox + * @param to account to be credited with the tokens in the destination layer + * @param refundTo account, or its L2 alias if it have code in L1, to be credited with excess gas refund in L2 + * @param user account with rights to cancel the retryable and receive call value refund + * @param l1CallValue callvalue sent in the L1 submission transaction + * @param l2CallValue callvalue for the 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 gas price for L2 execution + * @param data encoded data for the retryable + * @return seqNum id for the retryable ticket */ function _sendTxToL2CustomRefund( address to, @@ -90,8 +106,8 @@ abstract contract L1CrosschainMessenger { uint256 maxGas, uint256 gasPriceBid, bytes memory data - ) internal returns (uint256) { - uint256 seqNum = inbox.createRetryableTicket{ value: l1CallValue }( + ) internal override returns (uint256 seqNum) { + seqNum = IERC20Inbox(inbox).createRetryableTicket( to, l2CallValue, maxSubmissionCost, @@ -99,11 +115,11 @@ abstract contract L1CrosschainMessenger { user, maxGas, gasPriceBid, + l1CallValue, data ); emit TxToL2(user, to, seqNum, data); - return seqNum; } } diff --git a/packages/custom-gateway-bridging/contracts/ParentChainCustomGatewayCustomGas.sol b/packages/custom-gateway-bridging/contracts/ParentChainCustomGatewayCustomGas.sol new file mode 100644 index 0000000..e078ce0 --- /dev/null +++ b/packages/custom-gateway-bridging/contracts/ParentChainCustomGatewayCustomGas.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./interfaces/ICustomGateway.sol"; +import "./CrosschainMessenger.sol"; +import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/ICustomToken.sol"; +import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/IERC20Bridge.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title Example implementation of a custom gateway to be deployed on L1 + * @dev Inheritance of Ownable is optional. In this case we use it to call the function setTokenBridgeInformation + * and simplify the test + */ +contract ParentChainCustomGateway is IL1CustomGateway, L1CrosschainMessengerCustomGas, Ownable { + // Token bridge state variables + address public l1CustomToken; + address public l2CustomToken; + address public l2Gateway; + address public router; + + // Custom functionality + bool public allowsDeposits; + + /** + * Contract constructor, sets the L1 router to be used in the contract's functions and calls L1CrosschainMessenger's constructor + * @param router_ L1GatewayRouter address + * @param inbox_ Inbox address + */ + constructor(address router_, address inbox_) L1CrosschainMessenger(inbox_) { + router = router_; + allowsDeposits = false; + } + + /** + * Sets the information needed to use the gateway. To simplify the process of testing, this function can be called once + * by the owner of the contract to set these addresses. + * @param l1CustomToken_ address of the custom token on L1 + * @param l2CustomToken_ address of the custom token on L2 + * @param l2Gateway_ address of the counterpart gateway (on L2) + */ + function setTokenBridgeInformation( + address l1CustomToken_, + address l2CustomToken_, + address l2Gateway_ + ) public onlyOwner { + require(l1CustomToken == address(0), "Token bridge information already set"); + l1CustomToken = l1CustomToken_; + l2CustomToken = l2CustomToken_; + l2Gateway = l2Gateway_; + + // Allows deposits after the information has been set + allowsDeposits = true; + } + + /// @dev See {ICustomGateway-outboundTransfer} + function outboundTransfer( + address l1Token, + address to, + uint256 amount, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) public payable override returns (bytes memory) { + return outboundTransferCustomRefund(l1Token, to, to, amount, maxGas, gasPriceBid, data); + } + + /// @dev See {IL1CustomGateway-outboundTransferCustomRefund} + function outboundTransferCustomRefund( + address l1Token, + address refundTo, + address to, + uint256 amount, + uint256 maxGas, + uint256 gasPriceBid, + bytes calldata data + ) public payable override returns (bytes memory res) { + // Only execute if deposits are allowed + require(allowsDeposits, "Deposits are currently disabled"); + + // Only allow calls from the router + require(msg.sender == router, "Call not received from router"); + + // Only allow the custom token to be bridged through this gateway + require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); + + // Not allow value for custom gas token chains + // (We should be able to override this function as non-payable in the future, but this is now not allowed) + require(msg.value == 0, "Value not allowed"); + + address from; + uint256 seqNum; + { + uint256 maxSubmissionCost; + uint256 tokenTotalFeeAmount; + { + bytes memory extraData; + (from, maxSubmissionCost, extraData, tokenTotalFeeAmount) = _parseOutboundData( + data + ); + + // The inboundEscrowAndCall functionality has been disabled, so no data is allowed + require(extraData.length == 0, "EXTRA_DATA_DISABLED"); + + // Escrowing the tokens in the gateway + IERC20(l1Token).transferFrom(from, address(this), amount); + + // We override the res field to save on the stack + res = getOutboundCalldata(l1Token, from, to, amount, extraData); + } + + // transfer fees from user to here, and approve router to use it + approveCustomGasTokenForInbox(from, tokenTotalFeeAmount); + + // Trigger the crosschain message + seqNum = _sendTxToL2CustomRefund( + l2Gateway, + refundTo, + from, + tokenTotalFeeAmount, + 0, + maxSubmissionCost, + maxGas, + gasPriceBid, + res + ); + + // reset allowance back to 0 in case not all approved native tokens are spent + approveCustomGasTokenForInbox(from, 0); + } + + emit DepositInitiated(l1Token, from, to, seqNum, amount); + res = abi.encode(seqNum); + } + + /// @dev See {ICustomGateway-finalizeInboundTransfer} + function finalizeInboundTransfer( + address l1Token, + address from, + address to, + uint256 amount, + bytes calldata data + ) public payable override onlyCounterpartGateway(l2Gateway) { + // Only allow the custom token to be bridged through this gateway + require(l1Token == l1CustomToken, "Token is not allowed through this gateway"); + + // Decoding exitNum + (uint256 exitNum, ) = abi.decode(data, (uint256, bytes)); + + // Releasing the tokens in the gateway + IERC20(l1Token).transfer(to, amount); + + emit WithdrawalFinalized(l1Token, from, to, exitNum, amount); + } + + /// @dev See {ICustomGateway-getOutboundCalldata} + function getOutboundCalldata( + address l1Token, + address from, + address to, + uint256 amount, + bytes memory data + ) public pure override returns (bytes memory outboundCalldata) { + bytes memory emptyBytes = ""; + + outboundCalldata = abi.encodeWithSelector( + ICustomGateway.finalizeInboundTransfer.selector, + l1Token, + from, + to, + amount, + abi.encode(emptyBytes, data) + ); + } + + /// @dev See {ICustomGateway-calculateL2TokenAddress} + function calculateL2TokenAddress(address l1Token) public view override returns (address) { + if (l1Token == l1CustomToken) { + return l2CustomToken; + } + + return address(0); + } + + /// @dev See {ICustomGateway-counterpartGateway} + function counterpartGateway() public view override returns (address) { + return l2Gateway; + } + + /** + * Parse data received in outboundTransfer + * @param data encoded data received + * @return from account that initiated the deposit, + * maxSubmissionCost max gas deducted from user's L2 balance to cover base submission fee, + * extraData decoded data + */ + function _parseOutboundData( + bytes calldata data + ) + internal + pure + returns ( + address from, + uint256 maxSubmissionCost, + bytes memory extraData, + uint256 tokenTotalFeeAmount + ) + { + // Router encoded + (from, extraData) = abi.decode(data, (address, bytes)); + + // User encoded + (maxSubmissionCost, extraData, tokenTotalFeeAmount) = abi.decode( + extraData, + (uint256, bytes, uint256) + ); + } + + /** + * Approves custom gas token to be used by the Inbox + * @param user User that initiated the deposit + * @param value Amount of gas token to pay for the retryable ticket + */ + function approveCustomGasTokenForInbox(address user, uint256 value) internal { + address bridge = address(IInbox(inbox).bridge()); + address nativeToken = IERC20Bridge(bridge).nativeToken(); + if (value > 0) { + IERC20(nativeToken).transferFrom(user, address(this), value); + } + IERC20(nativeToken).approve(inbox, value); + } + + // -------------------- + // Custom methods + // -------------------- + /** + * Disables the ability to deposit funds + */ + function disableDeposits() external onlyOwner { + allowsDeposits = false; + } + + /** + * Enables the ability to deposit funds + */ + function enableDeposits() external onlyOwner { + require(l1CustomToken != address(0), "Token bridge information has not been set yet"); + allowsDeposits = true; + } +} diff --git a/packages/custom-gateway-bridging/contracts/ParentChainToken.sol b/packages/custom-gateway-bridging/contracts/ParentChainToken.sol index 101856a..36611d3 100644 --- a/packages/custom-gateway-bridging/contracts/ParentChainToken.sol +++ b/packages/custom-gateway-bridging/contracts/ParentChainToken.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; -import "./interfaces/ICustomToken.sol"; +import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/ICustomToken.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; @@ -9,7 +9,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; * @title Interface needed to call function registerTokenToL2 of the L1CustomGateway * (We don't need this interface for this example, but we're keeping it for completion) */ -interface IL1CustomGenericGateway { +interface IL1CustomGateway { function registerTokenToL2( address l2Address, uint256 maxGas, @@ -35,25 +35,25 @@ interface IL1GatewayRouter { /** * @title Example implementation of a custom ERC20 token to be deployed on L1 */ -contract ParentChainToken is Ownable, ICustomToken, ERC20 { - address public l1GatewayAddress; - address public routerAddress; +contract ParentChainToken is Ownable, ERC20, ICustomToken { + address public gateway; // The parent chain custom gateway contract + address public router; // The parent chain router contract bool private shouldRegisterGateway; /** * @dev See {ERC20-constructor} and {Ownable-constructor} * An initial supply amount is passed, which is preminted to the deployer. - * @param l1GatewayAddress_ address of the L1 custom gateway - * @param routerAddress_ address of the L1GatewayRouter + * @param _gateway address of the L1 custom gateway + * @param _router address of the L1GatewayRouter * @param initialSupply initial supply amount to be minted to the deployer */ constructor( - address l1GatewayAddress_, - address routerAddress_, + address _gateway, + address _router, uint256 initialSupply ) ERC20("L1CustomToken", "LCT") { - l1GatewayAddress = l1GatewayAddress_; - routerAddress = routerAddress_; + gateway = _gateway; + router = _router; _mint(msg.sender, initialSupply * 10 ** decimals()); } @@ -83,8 +83,8 @@ contract ParentChainToken is Ownable, ICustomToken, ERC20 { bool prev = shouldRegisterGateway; shouldRegisterGateway = true; - IL1GatewayRouter(routerAddress).setGateway{ value: valueForRouter }( - l1GatewayAddress, + IL1GatewayRouter(router).setGateway{ value: valueForRouter }( + gateway, maxGasForRouter, gasPriceBid, maxSubmissionCostForRouter, diff --git a/packages/custom-gateway-bridging/contracts/ParentChainTokenCustomGas.sol b/packages/custom-gateway-bridging/contracts/ParentChainTokenCustomGas.sol new file mode 100644 index 0000000..e7efc4d --- /dev/null +++ b/packages/custom-gateway-bridging/contracts/ParentChainTokenCustomGas.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@arbitrum/nitro-contracts/src/bridge/IInbox.sol"; +import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/ethereum/ICustomToken.sol"; +import "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/IERC20Bridge.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title Interface needed to call function registerTokenToL2 of the L1CustomGateway + * (We don't need this interface for this example, but we're keeping it for completion) + */ +interface IL1OrbitCustomGateway { + function registerTokenToL2( + address l2Address, + uint256 maxGas, + uint256 gasPriceBid, + uint256 maxSubmissionCost, + address creditBackAddress, + uint256 feeAmount + ) external 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 ParentChainToken is Ownable, ERC20, ICustomToken { + using SafeERC20 for ERC20; + + address public gateway; + address public router; + bool internal shouldRegisterGateway; + + constructor( + address _gateway, + address _router, + uint256 initialSupply + ) ERC20("L1CustomToken", "LCT") { + gateway = _gateway; + router = _router; + _mint(msg.sender, initialSupply * 10 ** decimals()); + } + + /// @dev we only set shouldRegisterGateway to true when in `registerTokenOnL2` + function isArbitrumEnabled() external view override returns (uint8) { + require(shouldRegisterGateway, "NOT_EXPECTED_CALL"); + return uint8(0xb1); + } + + function registerTokenOnL2( + address /* l2CustomTokenAddress */, + uint256 /* maxSubmissionCostForCustomGateway */, + uint256 maxSubmissionCostForRouter, + uint256 /* maxGasForCustomGateway */, + uint256 maxGasForRouter, + uint256 gasPriceBid, + uint256 /* valueForGateway */, + uint256 valueForRouter, + address creditBackAddress + ) public payable override onlyOwner { + // 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(); + + ERC20(nativeToken).safeTransferFrom(msg.sender, address(this), valueForRouter); + ERC20(nativeToken).approve(router, valueForRouter); + } + + 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(); + + ERC20(nativeToken).approve(router, 0); + } + + shouldRegisterGateway = prev; + } + + /// @dev See {ERC20-transferFrom} + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public override(ICustomToken, ERC20) returns (bool) { + return super.transferFrom(sender, recipient, amount); + } + + /// @dev See {ERC20-balanceOf} + function balanceOf( + address account + ) public view override(ICustomToken, ERC20) returns (uint256) { + return super.balanceOf(account); + } +} diff --git a/packages/custom-gateway-bridging/contracts/interfaces/IArbToken.sol b/packages/custom-gateway-bridging/contracts/interfaces/IArbToken.sol deleted file mode 100644 index f47bba0..0000000 --- a/packages/custom-gateway-bridging/contracts/interfaces/IArbToken.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -/** - * @title Minimum expected interface for L2 token that interacts with the L2 token bridge (this is the interface necessary - * for a custom token that interacts with the bridge). - */ -interface IArbToken { - /** - * Should increase token supply by amount, and should only be callable by the L1 gateway. - * @param account Account to be credited with the tokens in the L2 - * @param amount Token amount - */ - function bridgeMint(address account, uint256 amount) external; - - /** - * Should decrease token supply by amount. - * @param account Account whose tokens will be burned in the L2, to be released on L1 - * @param amount Token amount - */ - function bridgeBurn(address account, uint256 amount) external; - - /** - * @return address of layer 1 token - */ - function l1Address() external view returns (address); -} diff --git a/packages/custom-gateway-bridging/contracts/interfaces/ICustomToken.sol b/packages/custom-gateway-bridging/contracts/interfaces/ICustomToken.sol deleted file mode 100644 index 457b5ad..0000000 --- a/packages/custom-gateway-bridging/contracts/interfaces/ICustomToken.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -interface ArbitrumEnabledToken { - /// Should return `0xb1` if token is enabled for arbitrum gateways - function isArbitrumEnabled() external view returns (uint8); -} - -/** - * @title Minimum expected interface for an L1 custom token - */ -interface ICustomToken is ArbitrumEnabledToken { - /** - * Should make an external call to L2GatewayRouter.setGateway and probably L1CustomGateway.registerTokenToL2 - * @param l2CustomTokenAddress address of the custom token on L2 - * @param maxSubmissionCostForCustomBridge max gas deducted from user's L2 balance to cover submission fee for registerTokenToL2 - * @param maxSubmissionCostForRouter max gas deducted from user's L2 balance to cover submission fee for setGateway - * @param maxGasForCustomBridge max gas deducted from user's L2 balance to cover L2 execution of registerTokenToL2 - * @param maxGasForRouter max gas deducted from user's L2 balance to cover L2 execution of setGateway - * @param gasPriceBid gas price for L2 execution - * @param valueForGateway callvalue sent on call to registerTokenToL2 - * @param valueForRouter callvalue sent on call to setGateway - * @param creditBackAddress address for crediting back overpayment of maxSubmissionCosts - */ - function registerTokenOnL2( - address l2CustomTokenAddress, - uint256 maxSubmissionCostForCustomBridge, - uint256 maxSubmissionCostForRouter, - uint256 maxGasForCustomBridge, - uint256 maxGasForRouter, - uint256 gasPriceBid, - uint256 valueForGateway, - uint256 valueForRouter, - address creditBackAddress - ) external payable; - - /// @dev See {IERC20-transferFrom} - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); - - /// @dev See {IERC20-balanceOf} - function balanceOf(address account) external view returns (uint256); -} diff --git a/packages/custom-gateway-bridging/package.json b/packages/custom-gateway-bridging/package.json index 1130024..4be9761 100644 --- a/packages/custom-gateway-bridging/package.json +++ b/packages/custom-gateway-bridging/package.json @@ -7,6 +7,7 @@ }, "dependencies": { "@arbitrum/sdk": "^4.0.1", + "@arbitrum/token-bridge-contracts": "^1.2.3", "@openzeppelin/contracts": "^4.8.3" } } diff --git a/packages/custom-gateway-bridging/scripts/exec.js b/packages/custom-gateway-bridging/scripts/exec.js index ebf7e2b..4d9a2fd 100644 --- a/packages/custom-gateway-bridging/scripts/exec.js +++ b/packages/custom-gateway-bridging/scripts/exec.js @@ -1,5 +1,5 @@ const { ethers } = require('hardhat'); -const { providers, Wallet, BigNumber } = require('ethers'); +const { providers, Wallet, BigNumber, Contract, constants } = require('ethers'); const { getArbitrumNetwork, ParentToChildMessageStatus } = require('@arbitrum/sdk'); const { arbLog, @@ -11,6 +11,7 @@ const { AdminErc20Bridger, Erc20Bridger, } = require('@arbitrum/sdk/dist/lib/assetBridger/erc20Bridger'); +const { ERC20__factory } = require('@arbitrum/sdk/dist/lib/abi/factories/ERC20__factory'); const { expect } = require('chai'); require('dotenv').config(); requireEnvVariables(['PRIVATE_KEY', 'CHAIN_RPC', 'PARENT_CHAIN_RPC']); @@ -54,13 +55,26 @@ const main = async () => { const childChainGatewayRouter = childChainNetwork.tokenBridge.childGatewayRouter; const inbox = childChainNetwork.ethBridge.inbox; + /** + * We first find out whether the child chain we are using is a custom gas token chain + * We'll use a different parent chain token contract in that case (the register method has + * a slightly different behavior), and a different parent chain custom gateway + * and we'll perform an additional approve call to transfer the native tokens to pay for + * the gas of the retryable tickets + */ + const isCustomGasTokenChain = + adminTokenBridger.nativeToken && adminTokenBridger.nativeToken !== constants.AddressZero; + arbLogTitle('Deployment of custom gateways and tokens'); /** * Deploy our custom gateway to the parent chain */ + const parentChainCustomGatewayContractName = isCustomGasTokenChain + ? 'contracts/ParentChainCustomGatewayCustomGas.sol:ParentChainCustomGateway' + : 'contracts/ParentChainCustomGateway.sol:ParentChainCustomGateway'; const ParentChainCustomGateway = await ethers.getContractFactory( - 'ParentChainCustomGateway', + parentChainCustomGatewayContractName, parentChainWallet, ); console.log('Deploying custom gateway to the parent chain'); @@ -91,11 +105,15 @@ const main = async () => { /** * Deploy our custom token smart contract to the parent chain - * We give the custom token contract the address of parentChainCustomGateway - * and parentChainGatewayRouter as well as the initial supply (premint) + * We give the custom token contract the address of parentChainCustomGateway and parentChainGatewayRouter + * as well as the initial supply (premine). If the child chain we are using is a custom gas token chain, + * we will deploy a different ParentChainToken contract, since the register method has a slightly different behavior */ + const parentChainTokenContractName = isCustomGasTokenChain + ? 'contracts/ParentChainTokenCustomGas.sol:ParentChainToken' + : 'contracts/ParentChainToken.sol:ParentChainToken'; const ParentChainCustomToken = await ethers.getContractFactory( - 'ParentChainToken', + parentChainTokenContractName, parentChainWallet, ); console.log('Deploying custom token to the parent chain'); @@ -156,10 +174,31 @@ const main = async () => { `Token bridge information set on ChildChainCustomGateway! Transaction receipt in the child chain is: ${setTokenBridgeInfoOnChildChainTransactionReceipt.transactionHash}`, ); + /** + * For chains that use a custom gas token, we'll have to approve the transfer of native tokens + * to pay for the execution of the retryable tickets on the child chain + */ + if (isCustomGasTokenChain) { + console.log( + 'Giving allowance to the deployed token to transfer the chain native token (to register the gateway in the router)', + ); + const nativeToken = new Contract( + childChainNetwork.nativeToken, + ERC20__factory.abi, + parentChainWallet, + ); + const approvalTransaction = await nativeToken.approve( + parentChainCustomToken.address, + ethers.utils.parseEther('1'), + ); + const approvalTransactionReceipt = await approvalTransaction.wait(); + console.log(`Approval transaction receipt is: ${approvalTransactionReceipt.transactionHash}`); + } + /** * Register the custom gateway as the gateway of our custom token */ - console.log('Registering custom token on the child chain:'); + console.log('Registering the custom gateway as the gateway of the custom token:'); const registerTokenTransaction = await adminTokenBridger.registerCustomToken( parentChainCustomToken.address, childChainCustomToken.address, @@ -169,7 +208,7 @@ const main = async () => { const registerTokenTransactionReceipt = await registerTokenTransaction.wait(); console.log( - `Registering token txn confirmed on the parent chain! 🙌 Receipt is: ${registerTokenTransactionReceipt.transactionHash}.`, + `Registering gateway txn confirmed on the parent chain! 🙌 Receipt is: ${registerTokenTransactionReceipt.transactionHash}.`, ); console.log( `Waiting for the retryable to be executed on the child chain (takes 10-15 minutes); current time: ${new Date().toTimeString()})`, @@ -223,7 +262,7 @@ const main = async () => { const tokenDepositAmount = tokenAmountToDeposit.mul(BigNumber.from(10).pow(tokenDecimals)); /** - * Approving the parentChainCustomGateway to transfer the tokens being deposited + * Allowing the parentChainCustomGateway to transfer the tokens being deposited */ console.log('Approving ParentChainCustomGateway:'); const approveTransaction = await erc20Bridger.approveToken({ @@ -233,7 +272,7 @@ const main = async () => { const approveTransactionReceipt = await approveTransaction.wait(); console.log( - `You successfully allowed the Arbitrum Bridge to spend ParentChainToken. Tx hash: ${approveTransactionReceipt.transactionHash}`, + `You successfully allowed the custom gateway to spend the custom token. Tx hash: ${approveTransactionReceipt.transactionHash}`, ); /** @@ -241,7 +280,24 @@ const main = async () => { * This will escrow funds in the custom gateway contract on the parent chain, * and send a message to mint tokens on the child chain */ - console.log('Transferring ParentChainToken to the child chain:'); + console.log('Transferring the custom token to the child chain:'); + + /** + * For chains that use a custom gas token, we'll have to approve the transfer of native tokens + * to pay for the execution of the retryable tickets on the child chain + */ + if (isCustomGasTokenChain) { + console.log('Allowing the custom gateway to transfer the chain native token to pay the fees'); + const approvalTransaction = await erc20Bridger.approveGasToken({ + erc20ParentAddress: parentChainCustomToken.address, + parentSigner: parentChainWallet, + }); + const approvalTransactionReceipt = await approvalTransaction.wait(); + console.log( + `Native token approval transaction receipt is: ${approvalTransactionReceipt.transactionHash}`, + ); + } + const depositTransaction = await erc20Bridger.deposit({ amount: tokenDepositAmount, erc20ParentAddress: parentChainCustomToken.address, @@ -327,7 +383,7 @@ const main = async () => { * Withdraw ChildChainToken to the parent chain using erc20Bridger. * This will burn tokens on the child chain and release funds in the custom gateway contract on the parent chain */ - console.log('Withdrawing ChildChainToken to the parent chain:'); + console.log('Withdrawing custom token to the parent chain:'); const withdrawTransaction = await erc20Bridger.withdraw({ amount: tokenWithdrawAmount, destinationAddress: parentChainWallet.address, diff --git a/packages/gas-estimation/scripts/exec.ts b/packages/gas-estimation/scripts/exec.ts index e528374..b55b055 100644 --- a/packages/gas-estimation/scripts/exec.ts +++ b/packages/gas-estimation/scripts/exec.ts @@ -1,7 +1,11 @@ import { utils, providers } from 'ethers'; import { NodeInterface__factory } from '@arbitrum/sdk/dist/lib/abi/factories/NodeInterface__factory'; import { NODE_INTERFACE_ADDRESS } from '@arbitrum/sdk/dist/lib/dataEntities/constants'; -const { requireEnvVariables, addCustomNetworkFromFile } = require('arb-shared-dependencies'); +const { + requireEnvVariables, + addCustomNetworkFromFile, + arbLog, +} = require('arb-shared-dependencies'); // Importing configuration // require('dotenv').config(); @@ -24,6 +28,8 @@ const destinationAddress = '0x1234563d5de0d7198451f87bcbf15aefd00d434d'; const txData = '0x'; const gasEstimator = async () => { + await arbLog('Gas estimation'); + // *************************** // * Gas formula explanation * // *************************** @@ -93,7 +99,9 @@ const gasEstimator = async () => { // ------------------------------------------------------------------------------- // NOTE: This one might be a bit confusing, but parentChainGasEstimated (B in the formula) is calculated based on child-chain's gas fees const parentChainCost = parentChainGasEstimated.mul(childChainEstimatedPrice); - const parentChainSize = parentChainCost.div(parentChainEstimatedPrice); + const parentChainSize = parentChainEstimatedPrice.eq(0) + ? 0 + : parentChainCost.div(parentChainEstimatedPrice); // Getting the result of the formula // --------------------------------- diff --git a/packages/greeter/contracts/ethereum/GreeterParent.sol b/packages/greeter/contracts/ethereum/GreeterParent.sol index 89f2eaa..50c8006 100644 --- a/packages/greeter/contracts/ethereum/GreeterParent.sol +++ b/packages/greeter/contracts/ethereum/GreeterParent.sol @@ -3,17 +3,23 @@ pragma solidity ^0.8.0; import "@arbitrum/nitro-contracts/src/bridge/Inbox.sol"; import "@arbitrum/nitro-contracts/src/bridge/Outbox.sol"; +import "@arbitrum/nitro-contracts/src/bridge/IERC20Inbox.sol"; +import "@arbitrum/nitro-contracts/src/bridge/IERC20Bridge.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../Greeter.sol"; contract GreeterParent is Greeter { + using SafeERC20 for IERC20; + address public childTarget; - IInbox public inbox; + address public inbox; event RetryableTicketCreated(uint256 indexed ticketId); constructor(string memory _greeting, address _childTarget, address _inbox) Greeter(_greeting) { childTarget = _childTarget; - inbox = IInbox(_inbox); + inbox = _inbox; } function updateChildTarget(address _childTarget) public { @@ -27,16 +33,50 @@ contract GreeterParent is Greeter { uint256 gasPriceBid ) public payable returns (uint256) { bytes memory data = abi.encodeWithSelector(Greeter.setGreeting.selector, _greeting); - uint256 ticketID = inbox.createRetryableTicket{ value: msg.value }( - childTarget, - 0, - maxSubmissionCost, - msg.sender, - msg.sender, - maxGas, - gasPriceBid, - data - ); + + // Find out if this chain uses a custom gas token + // NOTE: in a real dApp, you don't need to perform this check since you'll + // know what kind of chain you'll be deploying this contract to. We added this + // check here to simplify showcasing how the Greeter contract works in any chain. + address bridge = address(IInbox(inbox).bridge()); + address nativeToken; + try IERC20Bridge(bridge).nativeToken() returns (address nativeTokenAddress) { + nativeToken = nativeTokenAddress; + } catch {} + + uint256 ticketID; + if (nativeToken == address(0)) { + // Chain uses ETH as the gas token + ticketID = IInbox(inbox).createRetryableTicket{ value: msg.value }( + childTarget, + 0, + maxSubmissionCost, + msg.sender, + msg.sender, + maxGas, + gasPriceBid, + data + ); + } else { + // Chain uses a custom gas token + // l2callvalue + maxSubmissionCost + execution fee + uint256 tokenAmount = 0 + maxSubmissionCost + (maxGas * gasPriceBid); + + IERC20(nativeToken).safeTransferFrom(msg.sender, address(this), tokenAmount); + IERC20(nativeToken).approve(inbox, tokenAmount); + + ticketID = IERC20Inbox(inbox).createRetryableTicket( + childTarget, + 0, + maxSubmissionCost, + msg.sender, + msg.sender, + maxGas, + gasPriceBid, + tokenAmount, + data + ); + } emit RetryableTicketCreated(ticketID); return ticketID; @@ -44,7 +84,7 @@ contract GreeterParent is Greeter { /// @notice only childTarget can update greeting function setGreeting(string memory _greeting) public override { - IBridge bridge = inbox.bridge(); + IBridge bridge = IInbox(inbox).bridge(); // this prevents reentrancies on Child-to-Parent transactions require(msg.sender == address(bridge), "NOT_BRIDGE"); IOutbox outbox = IOutbox(bridge.activeOutbox()); diff --git a/packages/greeter/scripts/exec.js b/packages/greeter/scripts/exec.js index 0132987..cf01df5 100644 --- a/packages/greeter/scripts/exec.js +++ b/packages/greeter/scripts/exec.js @@ -15,6 +15,7 @@ const { getArbitrumNetwork, } = require('@arbitrum/sdk'); const { getBaseFee } = require('@arbitrum/sdk/dist/lib/utils/lib'); +const { ERC20__factory } = require('@arbitrum/sdk/dist/lib/abi/factories/ERC20__factory'); require('dotenv').config(); requireEnvVariables(['PRIVATE_KEY', 'CHAIN_RPC', 'PARENT_CHAIN_RPC']); @@ -158,13 +159,38 @@ const main = async () => { console.log( `Sending greeting to the child chain with ${parentToChildMessageGasParams.deposit.toString()} callValue for child chain's fees:`, ); + + /** + * We find out whether the child chain we are using is a custom gas token chain + * We need to perform an additional approve call to transfer + * the native tokens to pay for the gas of the retryable tickets. + */ + const isCustomGasTokenChain = + childChainNetwork.nativeToken && childChainNetwork.nativeToken !== ethers.constants.AddressZero; + + if (isCustomGasTokenChain) { + // Approve the gas token to be sent to the contract + console.log('Giving allowance to the greeter to transfer the chain native token'); + const nativeToken = new ethers.Contract( + childChainNetwork.nativeToken, + ERC20__factory.abi, + parentChainWallet, + ); + const approvalTransaction = await nativeToken.approve( + greeterParent.address, + ethers.utils.parseEther('1'), + ); + const approvalTransactionReceipt = await approvalTransaction.wait(); + console.log(`Approval transaction receipt is: ${approvalTransactionReceipt.transactionHash}`); + } + const setGreetingTransaction = await greeterParent.setGreetingInChild( newGreeting, // string memory _greeting, parentToChildMessageGasParams.maxSubmissionCost, parentToChildMessageGasParams.gasLimit, gasPriceBid, { - value: parentToChildMessageGasParams.deposit, + value: isCustomGasTokenChain ? 0 : parentToChildMessageGasParams.deposit, }, ); const setGreetingTransactionReceipt = await setGreetingTransaction.wait(); diff --git a/packages/redeem-pending-retryable/contracts/parent-chain/GreeterParent.sol b/packages/redeem-pending-retryable/contracts/parent-chain/GreeterParent.sol index 89f2eaa..181de31 100644 --- a/packages/redeem-pending-retryable/contracts/parent-chain/GreeterParent.sol +++ b/packages/redeem-pending-retryable/contracts/parent-chain/GreeterParent.sol @@ -3,17 +3,23 @@ pragma solidity ^0.8.0; import "@arbitrum/nitro-contracts/src/bridge/Inbox.sol"; import "@arbitrum/nitro-contracts/src/bridge/Outbox.sol"; +import "@arbitrum/nitro-contracts/src/bridge/IERC20Inbox.sol"; +import "@arbitrum/nitro-contracts/src/bridge/IERC20Bridge.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../Greeter.sol"; contract GreeterParent is Greeter { + using SafeERC20 for IERC20; + address public childTarget; - IInbox public inbox; + address public inbox; event RetryableTicketCreated(uint256 indexed ticketId); constructor(string memory _greeting, address _childTarget, address _inbox) Greeter(_greeting) { childTarget = _childTarget; - inbox = IInbox(_inbox); + inbox = _inbox; } function updateChildTarget(address _childTarget) public { @@ -27,16 +33,47 @@ contract GreeterParent is Greeter { uint256 gasPriceBid ) public payable returns (uint256) { bytes memory data = abi.encodeWithSelector(Greeter.setGreeting.selector, _greeting); - uint256 ticketID = inbox.createRetryableTicket{ value: msg.value }( - childTarget, - 0, - maxSubmissionCost, - msg.sender, - msg.sender, - maxGas, - gasPriceBid, - data - ); + + // Find out if this chain uses a custom gas token + address bridge = address(IInbox(inbox).bridge()); + address nativeToken; + try IERC20Bridge(bridge).nativeToken() returns (address nativeTokenAddress) { + nativeToken = nativeTokenAddress; + } catch {} + + uint256 ticketID; + if (nativeToken == address(0)) { + // Chain uses ETH as the gas token + ticketID = IInbox(inbox).createRetryableTicket{ value: msg.value }( + childTarget, + 0, + maxSubmissionCost, + msg.sender, + msg.sender, + maxGas, + gasPriceBid, + data + ); + } else { + // Chain uses a custom gas token + // l2callvalue + maxSubmissionCost + execution fee + uint256 tokenAmount = 0 + maxSubmissionCost + (maxGas * gasPriceBid); + + IERC20(nativeToken).safeTransferFrom(msg.sender, address(this), tokenAmount); + IERC20(nativeToken).approve(inbox, tokenAmount); + + ticketID = IERC20Inbox(inbox).createRetryableTicket( + childTarget, + 0, + maxSubmissionCost, + msg.sender, + msg.sender, + maxGas, + gasPriceBid, + tokenAmount, + data + ); + } emit RetryableTicketCreated(ticketID); return ticketID; @@ -44,7 +81,7 @@ contract GreeterParent is Greeter { /// @notice only childTarget can update greeting function setGreeting(string memory _greeting) public override { - IBridge bridge = inbox.bridge(); + IBridge bridge = IInbox(inbox).bridge(); // this prevents reentrancies on Child-to-Parent transactions require(msg.sender == address(bridge), "NOT_BRIDGE"); IOutbox outbox = IOutbox(bridge.activeOutbox()); diff --git a/packages/redeem-pending-retryable/scripts/exec-createFailedRetryable.js b/packages/redeem-pending-retryable/scripts/exec-createFailedRetryable.js index 447378c..df866d3 100644 --- a/packages/redeem-pending-retryable/scripts/exec-createFailedRetryable.js +++ b/packages/redeem-pending-retryable/scripts/exec-createFailedRetryable.js @@ -15,6 +15,7 @@ const { getArbitrumNetwork, } = require('@arbitrum/sdk'); const { getBaseFee } = require('@arbitrum/sdk/dist/lib/utils/lib'); +const { ERC20__factory } = require('@arbitrum/sdk/dist/lib/abi/factories/ERC20__factory'); require('dotenv').config(); requireEnvVariables(['PRIVATE_KEY', 'CHAIN_RPC', 'PARENT_CHAIN_RPC']); @@ -164,13 +165,38 @@ const main = async () => { `Sending greeting to the child chain with ${parentToChildMessageGasParams.deposit.toString()} callValue for child chain's fees:`, ); const maxGasLimit = 10; + + /** + * We now find out whether the child chain we are using is a custom gas token chain + * We need to perform an additional approve call to transfer + * the native tokens to pay for the gas of the retryable tickets. + */ + const isCustomGasTokenChain = + childChainNetwork.nativeToken && childChainNetwork.nativeToken !== ethers.constants.AddressZero; + + if (isCustomGasTokenChain) { + // Approve the gas token to be sent to the contract + console.log('Giving allowance to the greeter to transfer the chain native token'); + const nativeToken = new ethers.Contract( + childChainNetwork.nativeToken, + ERC20__factory.abi, + parentChainWallet, + ); + const approvalTransaction = await nativeToken.approve( + greeterParent.address, + ethers.utils.parseEther('1'), + ); + const approvalTransactionReceipt = await approvalTransaction.wait(); + console.log(`Approval transaction receipt is: ${approvalTransactionReceipt.transactionHash}`); + } + const setGreetingTransaction = await greeterParent.setGreetingInChild( newGreeting, // string memory _greeting, parentToChildMessageGasParams.maxSubmissionCost, maxGasLimit, gasPriceBid, { - value: parentToChildMessageGasParams.deposit, + value: isCustomGasTokenChain ? 0 : parentToChildMessageGasParams.deposit, }, ); const setGreetingTransactionReceipt = await setGreetingTransaction.wait();