Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions packages/contract-deposit/contracts/CustomGasTokenDeposit.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
3 changes: 2 additions & 1 deletion packages/contract-deposit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
86 changes: 65 additions & 21 deletions packages/contract-deposit/scripts/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);

Expand Down Expand Up @@ -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}`);
Expand All @@ -71,18 +81,37 @@ 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;
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,
);
const approvalTransaction = await nativeToken.approve(
depositContract.address,
ethers.utils.parseEther('1'),
);
const approvalTransactionReceipt = await approvalTransaction.wait();
console.log(`Approval transaction receipt is: ${approvalTransactionReceipt.transactionHash}`);

depositTx = await depositContract.depositToChildChain(ethers.utils.parseEther('0.01'));
} else {
depositTx = await depositContract.depositToChildChain({
value: ethers.utils.parseEther('0.01'),
});
}
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,
);
Expand All @@ -103,7 +132,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]
}`,
);
Expand Down Expand Up @@ -186,16 +215,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.parseEther('0.01'), // 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down
18 changes: 9 additions & 9 deletions packages/custom-gateway-bridging/contracts/ChildChainToken.sol
Original file line number Diff line number Diff line change
@@ -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;
}

/**
Expand Down
Loading