diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index d6e08261..31727184 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -59,8 +59,17 @@ jobs: run: yarn test:unit test-integration: - name: Test (Integration) + name: Test (Integration) - ${{ matrix.config.name }} runs-on: ubuntu-latest + strategy: + matrix: + config: + - name: Custom gas token with 18 decimals + args: --tokenbridge --l3node --l3-token-bridge --l3-fee-token + decimals: 18 + - name: Custom gas token with 6 decimals + args: --tokenbridge --l3node --l3-token-bridge --l3-fee-token --l3-fee-token-decimals 6 + decimals: 6 steps: - name: Checkout uses: actions/checkout@v4 @@ -72,7 +81,7 @@ jobs: uses: OffchainLabs/actions/run-nitro-test-node@feat-simplify with: nitro-testnode-ref: release - args: --tokenbridge --l3node --l3-token-bridge --l3-fee-token + args: ${{ matrix.config.args }} - name: Copy .env run: cp ./.env.example ./.env @@ -81,4 +90,4 @@ jobs: run: yarn build - name: Test - run: yarn test:integration + run: DECIMALS=${{matrix.config.decimals}} yarn test:integration diff --git a/src/chains.ts b/src/chains.ts index 04dc167c..89e307c6 100644 --- a/src/chains.ts +++ b/src/chains.ts @@ -125,13 +125,16 @@ export function registerCustomParentChain( customParentChains[chain.id] = chain; } -export const chains = [ +export const mainnets = [ // mainnet L1 mainnet, // mainnet L2 arbitrumOne, arbitrumNova, base, +]; + +export const testnets = [ // testnet L1 sepolia, holesky, @@ -142,7 +145,9 @@ export const chains = [ nitroTestnodeL1, nitroTestnodeL2, nitroTestnodeL3, -] as const; +]; + +export const chains = [...mainnets, ...testnets] as const; export { // mainnet L1 diff --git a/src/createRollup.integration.test.ts b/src/createRollup.integration.test.ts index 278c9a5f..98114faf 100644 --- a/src/createRollup.integration.test.ts +++ b/src/createRollup.integration.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { Address, createPublicClient, http, parseGwei, zeroAddress } from 'viem'; +import { createPublicClient, http, parseGwei, zeroAddress } from 'viem'; import { nitroTestnodeL2 } from './chains'; import { diff --git a/src/createRollupEnoughCustomFeeTokenAllowance.ts b/src/createRollupEnoughCustomFeeTokenAllowance.ts index e7cbfa23..735fd3ad 100644 --- a/src/createRollupEnoughCustomFeeTokenAllowance.ts +++ b/src/createRollupEnoughCustomFeeTokenAllowance.ts @@ -1,11 +1,12 @@ import { Address, PublicClient, Transport, Chain } from 'viem'; -import { fetchAllowance } from './utils/erc20'; +import { fetchAllowance, fetchDecimals } from './utils/erc20'; import { getRollupCreatorAddress } from './utils/getRollupCreatorAddress'; import { Prettify } from './types/utils'; import { WithRollupCreatorAddressOverride } from './types/createRollupTypes'; import { createRollupGetRetryablesFeesWithDefaults } from './createRollupGetRetryablesFees'; +import { scaleToNativeTokenDecimals } from './utils/decimals'; export type CreateRollupEnoughCustomFeeTokenAllowanceParams = Prettify< @@ -37,5 +38,10 @@ export async function createRollupEnoughCustomFeeTokenAllowance= fees; + const decimals = await fetchDecimals({ + address: nativeToken, + publicClient, + }); + + return allowance >= scaleToNativeTokenDecimals({ amount: fees, decimals }); } diff --git a/src/createRollupGetRetryablesFees.ts b/src/createRollupGetRetryablesFees.ts index 4b73e562..0ebf2148 100644 --- a/src/createRollupGetRetryablesFees.ts +++ b/src/createRollupGetRetryablesFees.ts @@ -7,6 +7,7 @@ import { EstimateGasParameters, encodeFunctionData, decodeFunctionResult, + parseEther, } from 'viem'; import { rollupCreatorABI } from './contracts/RollupCreator'; @@ -104,9 +105,8 @@ export async function createRollupGetRetryablesFees { it('successfully fetches retryable fees for a custom gas token chain', async () => { const fees = await createRollupGetRetryablesFees(sepoliaClient, { account: '0x38f918D0E9F1b721EDaA41302E399fa1B79333a9', - nativeToken: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + nativeToken: '0x0625afb445c3b6b7b929342a04a22599fd5dbb59', maxFeePerGasForRetryables: parseGwei('0.1'), }); expect(fees).toBeTypeOf('bigint'); - expect(fees).toEqual(124708400000000000n); + expect(fees).toEqual(124800000000000000n); }); diff --git a/src/createRollupPrepareCustomFeeTokenApprovalTransactionRequest.ts b/src/createRollupPrepareCustomFeeTokenApprovalTransactionRequest.ts index 989698e7..e3695e9f 100644 --- a/src/createRollupPrepareCustomFeeTokenApprovalTransactionRequest.ts +++ b/src/createRollupPrepareCustomFeeTokenApprovalTransactionRequest.ts @@ -1,13 +1,13 @@ import { Address, PublicClient, Transport, Chain } from 'viem'; -import { approvePrepareTransactionRequest } from './utils/erc20'; +import { approvePrepareTransactionRequest, fetchDecimals } from './utils/erc20'; import { validateParentChain } from './types/ParentChain'; import { getRollupCreatorAddress } from './utils/getRollupCreatorAddress'; import { Prettify } from './types/utils'; import { WithRollupCreatorAddressOverride } from './types/createRollupTypes'; import { createRollupGetRetryablesFeesWithDefaults } from './createRollupGetRetryablesFees'; -import { applyPercentIncrease } from './utils/gasOverrides'; +import { scaleToNativeTokenDecimals } from './utils/decimals'; export type CreateRollupPrepareCustomFeeTokenApprovalTransactionRequestParams< TChain extends Chain | undefined, @@ -39,11 +39,16 @@ export async function createRollupPrepareCustomFeeTokenApprovalTransactionReques maxFeePerGasForRetryables, }); + const decimals = await fetchDecimals({ + address: nativeToken, + publicClient, + }); + const request = await approvePrepareTransactionRequest({ address: nativeToken, owner: account, spender: rollupCreatorAddressOverride ?? getRollupCreatorAddress(publicClient), - amount: amount ?? fees, + amount: amount ?? scaleToNativeTokenDecimals({ amount: fees, decimals }), publicClient, }); diff --git a/src/createRollupPrepareTransactionRequest.ts b/src/createRollupPrepareTransactionRequest.ts index e86846d1..0fbf4d7b 100644 --- a/src/createRollupPrepareTransactionRequest.ts +++ b/src/createRollupPrepareTransactionRequest.ts @@ -68,10 +68,9 @@ export async function createRollupPrepareTransactionRequest 36) { throw new Error( - `"params.nativeToken" can only be configured with a token that uses 18 decimals.`, + `"params.nativeToken" can only be configured with a token that uses 36 decimals or less.`, ); } } diff --git a/src/createRollupPrepareTransactionRequest.unit.test.ts b/src/createRollupPrepareTransactionRequest.unit.test.ts index 8db6405e..81871239 100644 --- a/src/createRollupPrepareTransactionRequest.unit.test.ts +++ b/src/createRollupPrepareTransactionRequest.unit.test.ts @@ -262,38 +262,6 @@ it(`fails to prepare transaction request if ArbOS version is incompatible with C ); }); -it(`fails to prepare transaction request if "params.nativeToken" doesn't use 18 decimals`, async () => { - // generate a random chain id - const chainId = generateChainId(); - - // create the chain config - const chainConfig = prepareChainConfig({ - chainId, - arbitrum: { InitialChainOwner: deployer.address, DataAvailabilityCommittee: true }, - }); - - // prepare the transaction for deploying the core contracts - await expect( - createRollupPrepareTransactionRequest({ - params: { - config: createRollupPrepareDeploymentParamsConfig(publicClient, { - chainId: BigInt(chainId), - owner: deployer.address, - chainConfig, - }), - batchPosters: [deployer.address], - validators: [deployer.address], - // USDC on Arbitrum Sepolia has 6 decimals - nativeToken: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d', - }, - account: deployer.address, - publicClient, - }), - ).rejects.toThrowError( - `"params.nativeToken" can only be configured with a token that uses 18 decimals.`, - ); -}); - it(`fails to prepare transaction request if "params.maxDataSize" is not provided for a custom parent chain`, async () => { // generate a random chain id const chainId = generateChainId(); @@ -458,3 +426,37 @@ it(`successfully prepares a transaction request with a custom parent chain`, asy expect(txRequest.chainId).toEqual(chainId); expect(txRequest.gas).toEqual(1_000n); }); + +it(`successfully prepare transaction request if "params.nativeToken" uses supported number of decimals`, async () => { + // generate a random chain id + const chainId = generateChainId(); + + // create the chain config + const chainConfig = prepareChainConfig({ + chainId, + arbitrum: { InitialChainOwner: deployer.address, DataAvailabilityCommittee: true }, + }); + + const txRequest = await createRollupPrepareTransactionRequest({ + params: { + config: createRollupPrepareDeploymentParamsConfig(publicClient, { + chainId: BigInt(chainId), + owner: deployer.address, + chainConfig, + }), + batchPosters: [deployer.address], + validators: [deployer.address], + nativeToken: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d', // USDC + }, + value: createRollupDefaultRetryablesFees, + account: deployer.address, + publicClient, + gasOverrides: { gasLimit: { base: 1_000n } }, + }); + + expect(txRequest.account).toEqual(deployer.address); + expect(txRequest.from).toEqual(deployer.address); + expect(txRequest.to).toEqual(rollupCreatorAddress[arbitrumSepolia.id]); + expect(txRequest.chainId).toEqual(arbitrumSepolia.id); + expect(txRequest.gas).toEqual(1_000n); +}); diff --git a/src/createTokenBridge.integration.test.ts b/src/createTokenBridge.integration.test.ts index 83d870dc..66dc038a 100644 --- a/src/createTokenBridge.integration.test.ts +++ b/src/createTokenBridge.integration.test.ts @@ -20,6 +20,7 @@ import { createTokenBridgePrepareSetWethGatewayTransactionRequest } from './crea import { createTokenBridgePrepareSetWethGatewayTransactionReceipt } from './createTokenBridgePrepareSetWethGatewayTransactionReceipt'; import { createTokenBridge } from './createTokenBridge'; import { TokenBridgeContracts } from './types/TokenBridgeContracts'; +import { scaleToNativeTokenDecimals } from './utils/decimals'; const testnodeAccounts = getNitroTestnodePrivateKeyAccounts(); const l2RollupOwner = testnodeAccounts.l2RollupOwner; @@ -104,6 +105,8 @@ async function checkWethGateways( expect(tokenBridgeContracts.orbitChainContracts.wethGateway).not.toEqual(zeroAddress); } +const nativeTokenDecimals = process.env.DECIMALS ? Number(process.env.DECIMALS) : 18; + describe('createTokenBridge utils function', () => { it(`successfully deploys token bridge contracts through token bridge creator`, async () => { const testnodeInformation = getInformationFromTestnode(); @@ -218,7 +221,10 @@ describe('createTokenBridge utils function', () => { data: encodeFunctionData({ abi: erc20ABI, functionName: 'transfer', - args: [l3RollupOwner.address, parseEther('500')], + args: [ + l3RollupOwner.address, + scaleToNativeTokenDecimals({ amount: 500n, decimals: nativeTokenDecimals }), + ], }), value: BigInt(0), account: l3TokenBridgeDeployer, @@ -384,7 +390,10 @@ describe('createTokenBridge', () => { data: encodeFunctionData({ abi: erc20ABI, functionName: 'transfer', - args: [l3RollupOwner.address, parseEther('500')], + args: [ + l3RollupOwner.address, + scaleToNativeTokenDecimals({ amount: 500n, decimals: nativeTokenDecimals }), + ], }), value: BigInt(0), account: l3TokenBridgeDeployer, diff --git a/src/createTokenBridgeEnoughCustomFeeTokenAllowance.ts b/src/createTokenBridgeEnoughCustomFeeTokenAllowance.ts index 1d4fbb87..29b6b816 100644 --- a/src/createTokenBridgeEnoughCustomFeeTokenAllowance.ts +++ b/src/createTokenBridgeEnoughCustomFeeTokenAllowance.ts @@ -1,11 +1,12 @@ import { Address, PublicClient, Transport, Chain } from 'viem'; -import { fetchAllowance } from './utils/erc20'; +import { fetchAllowance, fetchDecimals } from './utils/erc20'; import { createTokenBridgeDefaultRetryablesFees } from './constants'; import { Prettify } from './types/utils'; import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; import { getTokenBridgeCreatorAddress } from './utils/getTokenBridgeCreatorAddress'; +import { scaleToNativeTokenDecimals } from './utils/decimals'; export type CreateTokenBridgeEnoughCustomFeeTokenAllowanceParams = Prettify< @@ -31,5 +32,16 @@ export async function createTokenBridgeEnoughCustomFeeTokenAllowance< publicClient, }); - return allowance >= createTokenBridgeDefaultRetryablesFees; + const decimals = await fetchDecimals({ + address: nativeToken, + publicClient, + }); + + return ( + allowance >= + scaleToNativeTokenDecimals({ + amount: createTokenBridgeDefaultRetryablesFees, + decimals, + }) + ); } diff --git a/src/createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest.ts b/src/createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest.ts index 11614683..a439bbe9 100644 --- a/src/createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest.ts +++ b/src/createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest.ts @@ -1,12 +1,13 @@ import { Address, PublicClient, Transport, Chain, maxInt256 } from 'viem'; -import { approvePrepareTransactionRequest } from './utils/erc20'; +import { approvePrepareTransactionRequest, fetchDecimals } from './utils/erc20'; import { Prettify } from './types/utils'; import { validateParentChain } from './types/ParentChain'; import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; import { getTokenBridgeCreatorAddress } from './utils/getTokenBridgeCreatorAddress'; import { createTokenBridgeDefaultRetryablesFees } from './constants'; +import { scaleToNativeTokenDecimals } from './utils/decimals'; export type CreateTokenBridgePrepareCustomFeeTokenApprovalTransactionRequestParams< TChain extends Chain | undefined, @@ -30,11 +31,21 @@ export async function createTokenBridgePrepareCustomFeeTokenApprovalTransactionR }: CreateTokenBridgePrepareCustomFeeTokenApprovalTransactionRequestParams) { const { chainId } = validateParentChain(publicClient); + const decimals = await fetchDecimals({ + address: nativeToken, + publicClient, + }); + const request = await approvePrepareTransactionRequest({ address: nativeToken, owner, spender: tokenBridgeCreatorAddressOverride ?? getTokenBridgeCreatorAddress(publicClient), - amount: amount ?? createTokenBridgeDefaultRetryablesFees, + amount: + amount ?? + scaleToNativeTokenDecimals({ + amount: createTokenBridgeDefaultRetryablesFees, + decimals, + }), publicClient, }); diff --git a/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts b/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts index d4558ec6..1ec887e3 100644 --- a/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts +++ b/src/decorators/arbOwnerPublicActionsUpgradeExecutor.integration.test.ts @@ -1,5 +1,5 @@ import { it, expect } from 'vitest'; -import { Address, createPublicClient, http, parseGwei, Client } from 'viem'; +import { Address, createPublicClient, http } from 'viem'; import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; import { nitroTestnodeL3 } from '../chains'; import { arbOwnerPublicActions } from './arbOwnerPublicActions'; diff --git a/src/package.json b/src/package.json index 3c64c48a..6a277446 100644 --- a/src/package.json +++ b/src/package.json @@ -51,7 +51,7 @@ "viem": "^1.20.0" }, "dependencies": { - "@arbitrum/sdk": "^4.0.0", + "@arbitrum/sdk": "^4.0.2-beta.0", "@arbitrum/token-bridge-contracts": "^1.2.2", "@offchainlabs/fund-distribution-contracts": "^1.0.1", "@safe-global/protocol-kit": "^4.0.2", diff --git a/src/utils/decimals.ts b/src/utils/decimals.ts new file mode 100644 index 00000000..3d8fc5b9 --- /dev/null +++ b/src/utils/decimals.ts @@ -0,0 +1,26 @@ +import { BigNumber } from 'ethers'; +import utils from '@arbitrum/sdk/dist/lib/utils/lib'; + +export function scaleToNativeTokenDecimals({ + amount, + decimals, +}: { + amount: bigint; + decimals: number; +}) { + const amountBigNumber = BigNumber.from(amount); + const result = utils.scaleToNativeTokenDecimals({ amount: amountBigNumber, decimals }); + return BigInt(result.toString()); +} + +export function nativeTokenDecimalsTo18Decimals({ + amount, + decimals, +}: { + amount: bigint; + decimals: number; +}) { + const amountBigNumber = BigNumber.from(amount); + const result = utils.nativeTokenDecimalsTo18Decimals({ amount: amountBigNumber, decimals }); + return BigInt(result.toString()); +} diff --git a/src/utils/registerNewNetwork.ts b/src/utils/registerNewNetwork.ts index 0aaf09fd..bdd926c6 100644 --- a/src/utils/registerNewNetwork.ts +++ b/src/utils/registerNewNetwork.ts @@ -4,6 +4,11 @@ import { getArbitrumNetworkInformationFromRollup, registerCustomArbitrumNetwork, } from '@arbitrum/sdk'; +import { testnets } from '../chains'; + +const isTestnet = (parentChainId: number) => { + return testnets.some((testnet) => testnet.id === parentChainId); +}; export const registerNewNetwork = async ( parentProvider: JsonRpcProvider, @@ -21,6 +26,7 @@ export const registerNewNetwork = async ( confirmPeriodBlocks, ethBridge, isCustom: true, + isTestnet: isTestnet(parentChainId), }; return registerCustomArbitrumNetwork(arbitrumNetwork); diff --git a/yarn.lock b/yarn.lock index 20c775ca..77b52ca1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,10 +22,10 @@ "@openzeppelin/contracts-upgradeable" "4.5.2" patch-package "^6.4.7" -"@arbitrum/sdk@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-4.0.0.tgz#8eb2deed1a438250acb4084a4bb8fc7eae7659b6" - integrity sha512-wNZFRs4yYxd6Pyc6xEjksp3C59PikmmVPIw49ceMoLY5D0e6xJt2nuA7jjsemDgRe7q6EvLFvHAY2At6XUWrkg== +"@arbitrum/sdk@^4.0.2-beta.0": + version "4.0.2-beta.0" + resolved "https://registry.yarnpkg.com/@arbitrum/sdk/-/sdk-4.0.2-beta.0.tgz#864e38797d9f25e0b0d6a5f0147d60bd5b5db3b6" + integrity sha512-Q22DqEYzlkspvx6h9LUgTFgpUr9PNtPiyp4hTaODOdNl+cbpVAz6G9x04iMAnNia9sP0KffhDD5WxS4CzFrFFw== dependencies: "@ethersproject/address" "^5.0.8" "@ethersproject/bignumber" "^5.1.1"