From 99f1f05beb914fdcc7a40506b6c72fb7e5f35642 Mon Sep 17 00:00:00 2001 From: Sean Darcy Date: Wed, 20 Nov 2024 11:33:17 +1100 Subject: [PATCH] Deploy Token and Bridge to L2 script --- hardhat.config.js | 5 + package.json | 1 + pnpm-lock.yaml | 25 +++++ scripts/deploy-and-bridge.js | 201 +++++++++++++++++++++++++++++++++++ scripts/deploy.js | 33 ------ 5 files changed, 232 insertions(+), 33 deletions(-) create mode 100644 scripts/deploy-and-bridge.js delete mode 100644 scripts/deploy.js diff --git a/hardhat.config.js b/hardhat.config.js index c20690d4..a1f48fee 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -16,6 +16,11 @@ module.exports = { chainId: 42161, accounts: arb_account, }, + sepolia: { + url: "https://ethereum-sepolia-rpc.publicnode.com", + chainId: 11155111, + accounts: arb_sepolia_account, + }, arbitrumSepolia: { url: "https://sepolia-rollup.arbitrum.io/rpc", chainId: 421614, diff --git a/package.json b/package.json index 3e0598df..711a3231 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "typescript": "^5.3.2" }, "dependencies": { + "@arbitrum/sdk": "^4.0.2", "@openzeppelin/contracts": "^5.0.2", "@openzeppelin/contracts-upgradeable": "^5.0.2", "@openzeppelin/hardhat-upgrades": "^3.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f58d340e..45fa8408 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@arbitrum/sdk': + specifier: ^4.0.2 + version: 4.0.2 '@openzeppelin/contracts': specifier: ^5.0.2 version: 5.0.2 @@ -93,6 +96,10 @@ packages: '@adraffy/ens-normalize@1.10.1': resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + '@arbitrum/sdk@4.0.2': + resolution: {integrity: sha512-KkuXNwbG5c/hCT66EG2tFMHXxIDCvt9dxAIeykZYnW7KyEH5GNlRwaPzwo6MU0shHNc0qg6pZzy2XakJWuSw2Q==} + engines: {node: '>=v11', npm: please-use-yarn, yarn: '>= 1.0.0'} + '@aws-crypto/sha256-js@1.2.2': resolution: {integrity: sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==} @@ -816,6 +823,9 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} + async-mutex@0.4.1: + resolution: {integrity: sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==} + async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} @@ -2433,6 +2443,17 @@ snapshots: '@adraffy/ens-normalize@1.10.1': {} + '@arbitrum/sdk@4.0.2': + dependencies: + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + async-mutex: 0.4.1 + ethers: 5.7.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@aws-crypto/sha256-js@1.2.2': dependencies: '@aws-crypto/util': 1.2.2 @@ -3362,6 +3383,10 @@ snapshots: astral-regex@2.0.0: {} + async-mutex@0.4.1: + dependencies: + tslib: 2.7.0 + async-retry@1.3.3: dependencies: retry: 0.13.1 diff --git a/scripts/deploy-and-bridge.js b/scripts/deploy-and-bridge.js new file mode 100644 index 00000000..9ce04c6e --- /dev/null +++ b/scripts/deploy-and-bridge.js @@ -0,0 +1,201 @@ +// This deploys our main token and bridges it over to the L2, intended to replicate mainnet TGE as much as possible +// https://docs.arbitrum.io/build-decentralized-apps/token-bridging/bridge-tokens-programmatically/how-to-bridge-tokens-standard + +const hre = require("hardhat"); +const chalk = require("chalk"); +const { getArbitrumNetwork } = require("@arbitrum/sdk"); + +const ethers = hre.ethers; + +async function main() { + const SENT_UNIT = 1_000_000_000n; + const SUPPLY = 240_000_000n * SENT_UNIT; + //const L1_CHAIN_ID = 1 // mainnet (Sepolia 11155111) + //const L2_CHAIN_ID = 42161 // ARB (ARB Sepolia 421614) + const L1_CHAIN_ID = 11155111; // Sepolia + const L2_CHAIN_ID = 421614; // ARB Sepolia + + const args = { + SENT_UNIT, + SUPPLY, + L1_CHAIN_ID, + L2_CHAIN_ID, + }; + + const { sentERC20 } = await deploySENT(args); + await bridgeSENT(sentERC20, args); +} + +async function deploySENT(args = {}, verify = true) { + [owner] = await ethers.getSigners(); + const ownerAddress = await owner.getAddress(); + + const networkName = hre.network.name; + console.log("Deploying SENT contract to:", chalk.yellow(networkName)); + + if (verify) { + let apiKey; + if (typeof hre.config.etherscan?.apiKey === "object") { + apiKey = hre.config.etherscan.apiKey[networkName]; + } else { + apiKey = hre.config.etherscan?.apiKey; + } + if (!apiKey || apiKey == "") { + console.error( + chalk.red("Error: API key for contract verification is missing."), + ); + console.error( + "Please set it in your Hardhat configuration under 'etherscan.apiKey'.", + ); + process.exit(1); + } + } + const SENT_UNIT = args.SENT_UNIT || 1_000_000_000n; + const SUPPLY = args.SUPPLY || 240_000_000n * SENT_UNIT; + + const SentERC20 = await ethers.getContractFactory("SENT", owner); + let sentERC20; + + try { + sentERC20 = await SentERC20.deploy(SUPPLY, ownerAddress); + } catch (error) { + console.error("Failed to deploy SENT contract:", error); + process.exit(1); + } + + console.log( + " ", + chalk.cyan(`SENT Contract`), + "deployed to:", + chalk.greenBright(await sentERC20.getAddress()), + "on network:", + chalk.yellow(networkName), + ); + console.log( + " ", + "Initial Supply will be received by:", + chalk.green(ownerAddress), + ); + await sentERC20.waitForDeployment(); + + if (verify) { + console.log(chalk.yellow("\n--- Verifying SENT ---\n")); + console.log("Waiting 6 confirmations to ensure etherscan has processed tx"); + await sentERC20.deploymentTransaction().wait(6); + console.log("Finished Waiting"); + try { + await hre.run("verify:verify", { + address: await sentERC20.getAddress(), + constructorArguments: [SUPPLY, ownerAddress], + contract: "contracts/SENT.sol:SENT", + force: true, + }); + } catch (error) { + console.error(chalk.red("Verification failed:"), error); + } + console.log(chalk.green("Contract verification complete.")); + } + + return { sentERC20 }; +} + +async function bridgeSENT(l1ERC20Contract, args = {}) { + [owner] = await ethers.getSigners(); + const L1_CHAIN_ID = args.L1_CHAIN_ID || 1; // mainnet + const L2_CHAIN_ID = args.L2_CHAIN_ID || 42161; // Arbitrum One + + const ownerAddress = await owner.getAddress(); + const l1TokenAddress = await l1ERC20Contract.getAddress(); + + const l2Network = await getArbitrumNetwork(L2_CHAIN_ID); + let l1Name, l2Name + for (const networkName of Object.keys(hre.config.networks)) { + const networkConfig = hre.config.networks[networkName]; + if (networkConfig.chainId === L1_CHAIN_ID) { + let l1Url = networkConfig.url + l1Name = networkName + } + if (networkConfig.chainId === L2_CHAIN_ID) { + l2Url = networkConfig.url + l2Name = networkName + } + } + const l2Provider = new ethers.JsonRpcProvider(l2Url); + + console.log( + `\nBridging tokens from L1: ${l1Name} (${L1_CHAIN_ID}) to L2 ${l2Name} (${L2_CHAIN_ID})...`, + ); + + // Instantiate contracts + const l1GatewayRouter = new ethers.Contract( + l2Network.tokenBridge.parentGatewayRouter, + [ + "function getGateway(address token) view returns (address)", + "function outboundTransferCustomRefund( address _token, address _refundTo, address _to, uint256 _amount, uint256 _maxGas, uint256 _gasPriceBid, bytes calldata _data) external payable returns (bytes memory)", + "function calculateL2TokenAddress(address l1ERC20) view returns (address)", + ], + owner, + ); + + const l1ERC20Gateway = await l1GatewayRouter.getGateway(l1TokenAddress); + const l2TokenAddress = + await l1GatewayRouter.calculateL2TokenAddress(l1TokenAddress); + console.log(" ", "Calculated L2TokenAddress: ", chalk.green(l2TokenAddress)); + + // Approve the token for the gateway + const tokensToSend = "1000" + const depositAmount = ethers.parseUnits(tokensToSend, 9); + console.log(` Approving ${tokensToSend} SENT for L1 Gateway...`); + const approveTx = await l1ERC20Contract.approve(l1ERC20Gateway, depositAmount); + await approveTx.wait(); + + // Calculate retryable ticket parameters + const l1MaxGas = BigInt(300000); + const l2MaxGas = BigInt(1000000); + const feeData = await owner.provider.getFeeData(); + const l1GasPriceBid = feeData.gasPrice * BigInt(2); + if (!l1GasPriceBid) { + console.log("no gas price data"); + l1GasPriceBid = ethers.parseUnits('10', 'gwei'); + } + const l2GasPriceBid = BigInt("1000000000"); + const maxSubmissionCost = BigInt("500000000000000"); + const callHookData = "0x"; + const l2amount = ethers.parseEther("0.002"); + const totalL2GasCost = l2MaxGas * l2GasPriceBid; + const totalL2Value = maxSubmissionCost + totalL2GasCost + l2amount; + + const extraData = ethers.AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [maxSubmissionCost, callHookData], + ); + + // Execute outbound transfer + console.log(` Initiating outbound transfer...`); + const outboundTx = await l1GatewayRouter.outboundTransferCustomRefund( + l1TokenAddress, + ownerAddress, + ownerAddress, + depositAmount, + l2MaxGas, + l2GasPriceBid, + extraData, + { + gasLimit: l1MaxGas, + gasPrice: l1GasPriceBid, + value: totalL2Value, + }, + ); + + const receipt = await outboundTx.wait(); + console.log(` Outbound transfer submitted: ${receipt.hash}`); + + // Generate link to Arbitrum Retryable Dashboard + const retryableDashboardLink = `https://retryable-dashboard.arbitrum.io/tx/${receipt.hash}`; + console.log(` Track the retryable ticket here: ${retryableDashboardLink}`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/deploy.js b/scripts/deploy.js deleted file mode 100644 index 39c08d67..00000000 --- a/scripts/deploy.js +++ /dev/null @@ -1,33 +0,0 @@ -// We require the Hardhat Runtime Environment explicitly here. This is optional -// but useful for running the script in a standalone fashion through `node