diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index b8b1acd692..84fe5018f8 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -124,25 +124,47 @@ jobs: run: CI=true yarn test:unit test-integration: - name: Test (Integration) on Node.js v${{ matrix.node-version }}${{ matrix.orbit-test == '1' && ' with L3' || '' }}${{ matrix.custom-fee == '1' && ' with custom gas token' || '' }} + name: Test (Integration) on Node.js v${{ matrix.node-version }}${{ matrix.orbit-test == '1' && ' with L3' || '' }}${{ matrix.decimals == '16' && ' with custom gas token (16 decimals)' || matrix.decimals == '20' && ' with custom gas token (20 decimals)' || matrix.decimals == '18' && ' with custom gas token (18 decimals)' || '' }} runs-on: ubuntu-latest strategy: fail-fast: false # runs all tests to completion even if one fails matrix: - node-version: [18, 20] - orbit-test: ['0', '1'] - custom-fee: ['0'] include: + - orbit-test: '0' + node-version: 18 + - orbit-test: '0' + node-version: 20 + + - orbit-test: '1' + node-version: 18 + - orbit-test: '1' + node-version: 20 + + - orbit-test: '1' + decimals: 16 + node-version: 18 + - orbit-test: '1' + decimals: 16 + node-version: 20 + + - orbit-test: '1' + decimals: 18 + node-version: 18 + - orbit-test: '1' + decimals: 18 + node-version: 20 + - orbit-test: '1' - custom-fee: '1' + decimals: 20 node-version: 18 - orbit-test: '1' - custom-fee: '1' + decimals: 20 node-version: 20 needs: install env: ORBIT_TEST: ${{ matrix.orbit-test }} + DECIMALS: ${{ matrix.decimals || '18' }} steps: - name: Checkout uses: actions/checkout@v3 @@ -158,9 +180,9 @@ jobs: - name: Set up the local node uses: OffchainLabs/actions/run-nitro-test-node@main with: - nitro-testnode-ref: ed3cda65c4723b58a2f8be0fbc0c41f4ff2609cd + nitro-testnode-ref: adapt-bridge-amount l3-node: ${{ matrix.orbit-test == '1' }} - args: ${{ matrix.custom-fee == '1' && '--l3-fee-token' || '' }} + args: ${{ matrix.decimals == 16 && '--l3-fee-token --l3-fee-token-decimals 16' || matrix.decimals == 20 && '--l3-fee-token --l3-fee-token-decimals 20' || matrix.decimals == 18 && '--l3-fee-token' || '' }} - name: Copy .env run: cp ./.env-sample ./.env diff --git a/src/lib/assetBridger/erc20Bridger.ts b/src/lib/assetBridger/erc20Bridger.ts index 957e13cf03..ec30990365 100644 --- a/src/lib/assetBridger/erc20Bridger.ts +++ b/src/lib/assetBridger/erc20Bridger.ts @@ -71,7 +71,11 @@ import { OmitTyped, RequiredPick } from '../utils/types' import { RetryableDataTools } from '../dataEntities/retryableData' import { EventArgs } from '../dataEntities/event' import { L1ToL2MessageGasParams } from '../message/L1ToL2MessageCreator' -import { isArbitrumChain } from '../utils/lib' +import { + getNativeTokenDecimals, + isArbitrumChain, + scaleToNativeTokenDecimals, +} from '../utils/lib' import { L2ERC20Gateway__factory } from '../abi/factories/L2ERC20Gateway__factory' export interface TokenApproveParams { @@ -580,7 +584,8 @@ export class Erc20Bridger extends AssetBridger< * @returns */ private getDepositRequestOutboundTransferInnerData( - depositParams: OmitTyped + depositParams: OmitTyped, + decimals: number ) { if (!this.nativeTokenIsEth) { return defaultAbiCoder.encode( @@ -591,9 +596,12 @@ export class Erc20Bridger extends AssetBridger< // callHookData '0x', // nativeTokenTotalFee - depositParams.gasLimit - .mul(depositParams.maxFeePerGas) - .add(depositParams.maxSubmissionCost), // will be zero + scaleToNativeTokenDecimals({ + amount: depositParams.gasLimit + .mul(depositParams.maxFeePerGas) + .add(depositParams.maxSubmissionCost), // will be zero + decimals, + }), ] ) } @@ -645,6 +653,11 @@ export class Erc20Bridger extends AssetBridger< } } + const decimals = await getNativeTokenDecimals({ + l1Provider, + l2Network: this.l2Network, + }) + const depositFunc = ( depositParams: OmitTyped ) => { @@ -652,8 +665,10 @@ export class Erc20Bridger extends AssetBridger< params.maxSubmissionCost || depositParams.maxSubmissionCost const iGatewayRouter = L1GatewayRouter__factory.createInterface() - const innerData = - this.getDepositRequestOutboundTransferInnerData(depositParams) + const innerData = this.getDepositRequestOutboundTransferInnerData( + depositParams, + decimals + ) const functionData = defaultedParams.excessFeeRefundAddress !== defaultedParams.from @@ -1019,6 +1034,11 @@ export class AdminErc20Bridger extends Erc20Bridger { ) } + const nativeTokenDecimals = await getNativeTokenDecimals({ + l1Provider, + l2Network: this.l2Network, + }) + type GasParams = { maxSubmissionCost: BigNumber gasLimit: BigNumber @@ -1051,14 +1071,22 @@ export class AdminErc20Bridger extends Erc20Bridger { setTokenGas.gasLimit, setGatewayGas.gasLimit, doubleFeePerGas, - setTokenDeposit, - setGatewayDeposit, + scaleToNativeTokenDecimals({ + amount: setTokenDeposit, + decimals: nativeTokenDecimals, + }), + scaleToNativeTokenDecimals({ + amount: setGatewayDeposit, + decimals: nativeTokenDecimals, + }), l1SenderAddress, ]) return { data, - value: setTokenDeposit.add(setGatewayDeposit), + value: this.nativeTokenIsEth + ? setTokenDeposit.add(setGatewayDeposit) + : BigNumber.from(0), to: l1Token.address, from, } diff --git a/src/lib/assetBridger/ethBridger.ts b/src/lib/assetBridger/ethBridger.ts index 7c949dc34e..ad44c8cdd1 100644 --- a/src/lib/assetBridger/ethBridger.ts +++ b/src/lib/assetBridger/ethBridger.ts @@ -48,7 +48,11 @@ import { SignerProviderUtils } from '../dataEntities/signerOrProvider' import { MissingProviderArbSdkError } from '../dataEntities/errors' import { getL2Network } from '../dataEntities/networks' import { ERC20__factory } from '../abi/factories/ERC20__factory' -import { isArbitrumChain } from '../utils/lib' +import { + getNativeTokenDecimals, + isArbitrumChain, + nativeTokenDecimalsTo18Decimals, +} from '../utils/lib' export type ApproveGasTokenParams = { /** @@ -312,10 +316,20 @@ export class EthBridger extends AssetBridger< public async getDepositToRequest( params: EthDepositToRequestParams ): Promise { + const decimals = await getNativeTokenDecimals({ + l1Provider: params.l1Provider, + l2Network: this.l2Network, + }) + + const amountToBeMintedOnChildChain = nativeTokenDecimalsTo18Decimals({ + amount: params.amount, + decimals, + }) + const requestParams = { ...params, to: params.destinationAddress, - l2CallValue: params.amount, + l2CallValue: amountToBeMintedOnChildChain, callValueRefundAddress: params.destinationAddress, data: '0x', } diff --git a/src/lib/message/L1ToL2MessageGasEstimator.ts b/src/lib/message/L1ToL2MessageGasEstimator.ts index 1b45628b66..8dae8adeab 100644 --- a/src/lib/message/L1ToL2MessageGasEstimator.ts +++ b/src/lib/message/L1ToL2MessageGasEstimator.ts @@ -11,7 +11,12 @@ import { RetryableDataTools, } from '../dataEntities/retryableData' import { L1ToL2TransactionRequest } from '../dataEntities/transactionRequest' -import { getBaseFee, isDefined } from '../utils/lib' +import { + getBaseFee, + getNativeTokenDecimals, + isDefined, + scaleToNativeTokenDecimals, +} from '../utils/lib' import { OmitTyped } from '../utils/types' import { L1ToL2MessageGasParams, @@ -220,7 +225,10 @@ export class L1ToL2MessageGasEstimator { const { data } = retryableEstimateData const gasLimitDefaults = this.applyGasLimitDefaults(options?.gasLimit) - // estimate the l2 gas price + const l2Network = await getL2Network(this.l2Provider) + const decimals = await getNativeTokenDecimals({ l1Provider, l2Network }) + + // estimate the l1 gas price const maxFeePerGasPromise = this.estimateMaxFeePerGas(options?.maxFeePerGas) // estimate the submission fee @@ -254,10 +262,13 @@ export class L1ToL2MessageGasEstimator { const deposit = options?.deposit?.base || - gasLimit - .mul(maxFeePerGas) - .add(maxSubmissionFee) - .add(retryableEstimateData.l2CallValue) + scaleToNativeTokenDecimals({ + amount: gasLimit + .mul(maxFeePerGas) + .add(maxSubmissionFee) + .add(retryableEstimateData.l2CallValue), + decimals, + }) return { gasLimit, diff --git a/src/lib/utils/lib.ts b/src/lib/utils/lib.ts index b53c6f6f29..b4511adc73 100644 --- a/src/lib/utils/lib.ts +++ b/src/lib/utils/lib.ts @@ -1,11 +1,12 @@ +import { BigNumber, constants } from 'ethers' import { Provider } from '@ethersproject/abstract-provider' import { TransactionReceipt, JsonRpcProvider } from '@ethersproject/providers' import { ArbSdkError } from '../dataEntities/errors' import { ArbitrumProvider } from './arbProvider' -import { l2Networks } from '../dataEntities/networks' +import { L2Network, l2Networks } from '../dataEntities/networks' import { ArbSys__factory } from '../abi/factories/ArbSys__factory' import { ARB_SYS_ADDRESS } from '../dataEntities/constants' -import { BigNumber } from 'ethers' +import { ERC20__factory } from '../abi/factories/ERC20__factory' export const wait = (ms: number): Promise => new Promise(res => setTimeout(res, ms)) @@ -195,3 +196,75 @@ export const getBlockRangesForL1Block = async ( return [result[0], props.maxL2Block] } + +export async function getNativeTokenDecimals({ + l1Provider, + l2Network, +}: { + l1Provider: Provider + l2Network: L2Network +}) { + const nativeTokenAddress = l2Network.nativeToken + + if (!nativeTokenAddress || nativeTokenAddress === constants.AddressZero) { + return 18 + } + + const nativeTokenContract = ERC20__factory.connect( + nativeTokenAddress, + l1Provider + ) + + try { + return await nativeTokenContract.decimals() + } catch { + return 0 + } +} + +export function scaleToNativeTokenDecimals({ + amount, + decimals, +}: { + amount: BigNumber + decimals: number +}) { + // do nothing for 18 decimals + if (decimals === 18) { + return amount + } + + if (decimals < 18) { + const scaledAmount = amount.div( + BigNumber.from(10).pow(BigNumber.from(18 - decimals)) + ) + // round up if necessary + if ( + scaledAmount + .mul(BigNumber.from(10).pow(BigNumber.from(18 - decimals))) + .lt(amount) + ) { + return scaledAmount.add(BigNumber.from(1)) + } + return scaledAmount + } + + // decimals > 18 + return amount.mul(BigNumber.from(10).pow(BigNumber.from(decimals - 18))) +} + +export function nativeTokenDecimalsTo18Decimals({ + amount, + decimals, +}: { + amount: BigNumber + decimals: number +}) { + if (decimals < 18) { + return amount.mul(BigNumber.from(10).pow(18 - decimals)) + } else if (decimals > 18) { + return amount.div(BigNumber.from(10).pow(decimals - 18)) + } + + return amount +} diff --git a/tests/integration/custom-fee-token/customFeeTokenEthBridger.test.ts b/tests/integration/custom-fee-token/customFeeTokenEthBridger.test.ts index 0e5593bee8..6cc6d28565 100644 --- a/tests/integration/custom-fee-token/customFeeTokenEthBridger.test.ts +++ b/tests/integration/custom-fee-token/customFeeTokenEthBridger.test.ts @@ -20,7 +20,7 @@ import { expect } from 'chai' import { ethers, constants, Wallet } from 'ethers' import dotenv from 'dotenv' -import { parseEther } from '@ethersproject/units' +import { parseEther, parseUnits } from '@ethersproject/units' import { fundL1 as fundL1Ether, @@ -30,6 +30,7 @@ import { } from '../testHelpers' import { L2ToL1Message, L2ToL1MessageStatus } from '../../../src' import { describeOnlyWhenCustomGasToken } from './mochaExtensions' +import { getNativeTokenDecimals } from '../../../src/lib/utils/lib' dotenv.config() @@ -48,8 +49,15 @@ describeOnlyWhenCustomGasToken( }) it('approves the custom fee token to be spent by the Inbox on the parent chain (arbitrary amount, using params)', async function () { - const { ethBridger, nativeTokenContract, l1Signer } = await testSetup() - const amount = ethers.utils.parseEther('1') + const { + ethBridger, + nativeTokenContract, + l1Signer, + l1Provider, + l2Network, + } = await testSetup() + const decimals = await getNativeTokenDecimals({ l1Provider, l2Network }) + const amount = ethers.utils.parseUnits('1', decimals) await fundL1Ether(l1Signer) await fundL1CustomFeeToken(l1Signer) @@ -158,11 +166,17 @@ describeOnlyWhenCustomGasToken( l1Provider, l2Signer, l2Provider, + l2Network, ethBridger, nativeTokenContract, } = await testSetup() + const decimals = await getNativeTokenDecimals({ + l1Provider, + l2Network, + }) + const bridge = ethBridger.l2Network.ethBridge.bridge - const amount = parseEther('0.2') + const amount = parseUnits('0.2', decimals) await fundL1Ether(l1Signer) await fundL2CustomFeeToken(l2Signer) diff --git a/tests/integration/custom-fee-token/customFeeTokenTestHelpers.ts b/tests/integration/custom-fee-token/customFeeTokenTestHelpers.ts index 2a9f127e08..d4a4ca3da6 100644 --- a/tests/integration/custom-fee-token/customFeeTokenTestHelpers.ts +++ b/tests/integration/custom-fee-token/customFeeTokenTestHelpers.ts @@ -8,6 +8,7 @@ import { } from '../../../scripts/testSetup' import { Erc20Bridger, EthBridger } from '../../../src' import { ERC20__factory } from '../../../src/lib/abi/factories/ERC20__factory' +import { getNativeTokenDecimals } from '../../../src/lib/utils/lib' // `config` isn't initialized yet, so we have to wrap these in functions const ethProvider = () => new StaticJsonRpcProvider(config.ethUrl) @@ -43,13 +44,17 @@ export async function fundL1CustomFeeToken(l1SignerOrAddress: Signer | string) { } const deployerWallet = new Wallet( - utils.sha256(utils.toUtf8Bytes('user_token_bridge_deployer')), + utils.sha256(utils.toUtf8Bytes('user_fee_token_deployer')), ethProvider() ) const tokenContract = ERC20__factory.connect(nativeToken, deployerWallet) + const decimals = await tokenContract.decimals() - const tx = await tokenContract.transfer(address, utils.parseEther('10')) + const tx = await tokenContract.transfer( + address, + utils.parseUnits('10', decimals) + ) await tx.wait() } @@ -85,9 +90,14 @@ export async function approveL1CustomFeeTokenForErc20Deposit( export async function fundL2CustomFeeToken(l2Signer: Signer) { const deployerWallet = new Wallet(config.arbKey, arbProvider()) + const decimals = await getNativeTokenDecimals({ + l1Provider: ethProvider(), + l2Network: localNetworks().l2Network, + }) + const tx = await deployerWallet.sendTransaction({ to: await l2Signer.getAddress(), - value: utils.parseEther('1'), + value: utils.parseUnits('1', decimals), }) await tx.wait() } diff --git a/tests/integration/customerc20.test.ts b/tests/integration/customerc20.test.ts index bf99d6c864..ade6e032d1 100644 --- a/tests/integration/customerc20.test.ts +++ b/tests/integration/customerc20.test.ts @@ -42,10 +42,7 @@ import { L1ToL2MessageStatus, L2Network } from '../../src' import { AdminErc20Bridger } from '../../src/lib/assetBridger/erc20Bridger' import { testSetup } from '../../scripts/testSetup' import { ERC20__factory } from '../../src/lib/abi/factories/ERC20__factory' -import { - fundL1CustomFeeToken, - isL2NetworkWithCustomFeeToken, -} from './custom-fee-token/customFeeTokenTestHelpers' +import { isL2NetworkWithCustomFeeToken } from './custom-fee-token/customFeeTokenTestHelpers' const depositAmount = BigNumber.from(100) const withdrawalAmount = BigNumber.from(10) @@ -71,10 +68,6 @@ describe('Custom ERC20', () => { } await fundL1(testState.l1Signer) await fundL2(testState.l2Signer) - - if (isL2NetworkWithCustomFeeToken()) { - await fundL1CustomFeeToken(testState.l1Signer) - } }) it('register custom token', async () => { diff --git a/tests/integration/eth.test.ts b/tests/integration/eth.test.ts index 0d16bbab13..169811299c 100644 --- a/tests/integration/eth.test.ts +++ b/tests/integration/eth.test.ts @@ -39,6 +39,11 @@ import { isL2NetworkWithCustomFeeToken } from './custom-fee-token/customFeeToken import { ERC20__factory } from '../../src/lib/abi/factories/ERC20__factory' import { itOnlyWhenEth } from './custom-fee-token/mochaExtensions' import { L1TransactionReceipt } from '../../src' +import { + getNativeTokenDecimals, + scaleToNativeTokenDecimals, +} from '../../src/lib/utils/lib' +import { parseUnits } from 'ethers/lib/utils' dotenv.config() @@ -99,7 +104,9 @@ describe('Ether', async () => { ) it('deposits ether', async () => { - const { ethBridger, l1Signer, l2Signer } = await testSetup() + const { ethBridger, l1Signer, l1Provider, l2Network, l2Signer } = + await testSetup() + const decimals = await getNativeTokenDecimals({ l1Provider, l2Network }) await fundL1(l1Signer) const inboxAddress = ethBridger.l2Network.ethBridge.inbox @@ -107,7 +114,8 @@ describe('Ether', async () => { const initialInboxBalance = await l1Signer.provider!.getBalance( inboxAddress ) - const ethToDeposit = parseEther('0.0002') + const amount = '0.0002' + const ethToDeposit = parseUnits(amount, decimals) const res = await ethBridger.deposit({ amount: ethToDeposit, l1Signer: l1Signer, @@ -130,7 +138,7 @@ describe('Ether', async () => { const walletAddress = await l1Signer.getAddress() expect(l1ToL2Message.to).to.eq(walletAddress, 'message inputs value error') expect(l1ToL2Message.value.toString(), 'message inputs value error').to.eq( - ethToDeposit.toString() + parseEther(amount).toString() ) prettyLog('l2TxHash: ' + waitResult.message.l2DepositTxHash) @@ -141,12 +149,14 @@ describe('Ether', async () => { const testWalletL2EthBalance = await l2Signer.getBalance() expect(testWalletL2EthBalance.toString(), 'final balance').to.eq( - ethToDeposit.toString() + parseEther(amount).toString() ) }) - it('deposits ether to a specific L2 address', async () => { - const { ethBridger, l1Signer, l2Signer } = await testSetup() + it('deposits ether to a specific L2 address', async function () { + const { ethBridger, l1Signer, l1Provider, l2Network, l2Signer } = + await testSetup() + const decimals = await getNativeTokenDecimals({ l1Provider, l2Network }) await fundL1(l1Signer) const inboxAddress = ethBridger.l2Network.ethBridge.inbox @@ -155,7 +165,8 @@ describe('Ether', async () => { const initialInboxBalance = await l1Signer.provider!.getBalance( inboxAddress ) - const ethToDeposit = parseEther('0.0002') + const amount = '0.0002' + const ethToDeposit = parseUnits(amount, decimals) const res = await ethBridger.depositTo({ amount: ethToDeposit, l1Signer: l1Signer, @@ -182,7 +193,7 @@ describe('Ether', async () => { expect( l1ToL2Message.messageData.l2CallValue.toString(), 'message inputs value error' - ).to.eq(ethToDeposit.toString()) + ).to.eq(parseEther(amount).toString()) const retryableTicketResult = await l1ToL2Message.waitForStatus() expect(retryableTicketResult.status).to.eq( @@ -209,17 +220,20 @@ describe('Ether', async () => { destWallet.address ) expect(testWalletL2EthBalance.toString(), 'final balance').to.eq( - ethToDeposit.toString() + parseEther(amount).toString() ) }) - it('deposit ether to a specific L2 address with manual redeem', async () => { - const { ethBridger, l1Signer, l2Signer } = await testSetup() + it('deposit ether to a specific L2 address with manual redeem', async function () { + const { ethBridger, l1Signer, l1Provider, l2Network, l2Signer } = + await testSetup() + const decimals = await getNativeTokenDecimals({ l1Provider, l2Network }) await fundL1(l1Signer) const destWallet = Wallet.createRandom() - const ethToDeposit = parseEther('0.0002') + const amount = '0.0002' + const ethToDeposit = parseUnits(amount, decimals) const res = await ethBridger.depositTo({ amount: ethToDeposit, l1Signer: l1Signer, @@ -269,11 +283,12 @@ describe('Ether', async () => { expect( testWalletL2EthBalance.toString(), 'balance after manual redeem' - ).to.eq(ethToDeposit.toString()) + ).to.eq(parseEther(amount).toString()) }) it('withdraw Ether transaction succeeds', async () => { - const { l2Signer, l1Signer, ethBridger } = await testSetup() + const { l2Signer, l1Signer, l1Provider, l2Network, ethBridger } = + await testSetup() await fundL2(l2Signer) await fundL1(l1Signer) @@ -368,6 +383,8 @@ describe('Ether', async () => { 'executed status' ).to.eq(L2ToL1MessageStatus.EXECUTED) + const decimals = await getNativeTokenDecimals({ l1Provider, l2Network }) + const finalRandomBalance = isL2NetworkWithCustomFeeToken() ? await ERC20__factory.connect( ethBridger.nativeToken!, @@ -375,7 +392,7 @@ describe('Ether', async () => { ).balanceOf(randomAddress) : await l1Signer.provider!.getBalance(randomAddress) expect(finalRandomBalance.toString(), 'L1 final balance').to.eq( - ethToWithdraw.toString() + scaleToNativeTokenDecimals({ amount: ethToWithdraw, decimals }).toString() ) }) }) diff --git a/tests/integration/l1l3Bridger.test.ts b/tests/integration/l1l3Bridger.test.ts index 8ffe355296..84cd8dee26 100644 --- a/tests/integration/l1l3Bridger.test.ts +++ b/tests/integration/l1l3Bridger.test.ts @@ -26,6 +26,7 @@ import { itOnlyWhenCustomGasToken, itOnlyWhenEth, } from './custom-fee-token/mochaExtensions' +import { getNativeTokenDecimals } from '../../src/lib/utils/lib' async function expectPromiseToReject( promise: Promise, @@ -129,7 +130,7 @@ async function fundActualL1CustomFeeToken( ) const deployerWallet = new Wallet( - utils.sha256(utils.toUtf8Bytes('user_token_bridge_deployer')), + utils.sha256(utils.toUtf8Bytes('user_fee_token_deployer')), l1Signer.provider! ) @@ -192,6 +193,15 @@ describe('L1 to L3 Bridging', () => { `Signer/provider chain id: ${l1ChainId} doesn't match provided chain id: ${l3ChainId}.` ) } + + if (isL2NetworkWithCustomFeeToken()) { + await fundActualL1CustomFeeToken( + l1Signer, + l3Network.nativeToken!, + l2Network, + l2Signer.provider! + ) + } } // setup for all test cases @@ -356,7 +366,16 @@ describe('L1 to L3 Bridging', () => { itOnlyWhenCustomGasToken( 'should properly get l2 and l1 fee token addresses', - async () => { + async function () { + const decimals = await getNativeTokenDecimals({ + l1Provider: l1Signer.provider!, + l2Network: l3Network, + }) + + if (decimals !== 18) { + this.skip() + } + if (l1l3Bridger.l2GasTokenAddress === undefined) { throw new Error('L2 fee token address is undefined') } @@ -377,7 +396,16 @@ describe('L1 to L3 Bridging', () => { itOnlyWhenCustomGasToken( 'should throw getting l1 gas token address when it is unavailable', - async () => { + async function () { + const decimals = await getNativeTokenDecimals({ + l1Provider: l1Signer.provider!, + l2Network: l3Network, + }) + + if (decimals !== 18) { + this.skip() + } + const networkCopy = JSON.parse(JSON.stringify(l3Network)) as L2Network networkCopy.nativeToken = ethers.utils.hexlify( ethers.utils.randomBytes(20) @@ -394,7 +422,16 @@ describe('L1 to L3 Bridging', () => { itOnlyWhenCustomGasToken( 'should throw when the fee token does not use 18 decimals on L1 or L2', - async () => { + async function () { + const decimals = await getNativeTokenDecimals({ + l1Provider: l1Signer.provider!, + l2Network: l3Network, + }) + + if (decimals !== 18) { + this.skip() + } + const hackedL1Provider = new ethers.providers.JsonRpcProvider( process.env['ETH_URL'] ) @@ -874,7 +911,16 @@ describe('L1 to L3 Bridging', () => { assert(l3Balance.eq(amount)) } - it('happy path non fee token or standard', async () => { + it('happy path non fee token or standard', async function () { + const decimals = await getNativeTokenDecimals({ + l1Provider: l1Signer.provider!, + l2Network: l3Network, + }) + + if (decimals !== 18) { + this.skip() + } + const l3Recipient = ethers.utils.hexlify(ethers.utils.randomBytes(20)) const depositParams: Erc20DepositRequestParams = { @@ -888,7 +934,16 @@ describe('L1 to L3 Bridging', () => { await testHappyPathNonFeeOrStandard(depositParams) }) - it('happy path weth', async () => { + it('happy path weth', async function () { + const decimals = await getNativeTokenDecimals({ + l1Provider: l1Signer.provider!, + l2Network: l3Network, + }) + + if (decimals !== 18) { + this.skip() + } + const l3Recipient = ethers.utils.hexlify(ethers.utils.randomBytes(20)) const weth = AeWETH__factory.connect( l2Network.tokenBridge.l1Weth, @@ -918,7 +973,16 @@ describe('L1 to L3 Bridging', () => { await testHappyPathNonFeeOrStandard(depositParams) }) - itOnlyWhenCustomGasToken('happy path OnlyCustomFee', async () => { + itOnlyWhenCustomGasToken('happy path OnlyCustomFee', async function () { + const decimals = await getNativeTokenDecimals({ + l1Provider: l1Signer.provider!, + l2Network: l3Network, + }) + + if (decimals !== 18) { + this.skip() + } + const l3Recipient = ethers.utils.hexlify(ethers.utils.randomBytes(20)) const l1FeeToken = (await l1l3Bridger.getGasTokenOnL1( l1Signer.provider!, diff --git a/tests/integration/retryableData.test.ts b/tests/integration/retryableData.test.ts index a8d6e95999..ed35d4cf9b 100644 --- a/tests/integration/retryableData.test.ts +++ b/tests/integration/retryableData.test.ts @@ -30,15 +30,23 @@ import { GasOverrides } from '../../src/lib/message/L1ToL2MessageGasEstimator' const depositAmount = BigNumber.from(100) import { ERC20Inbox__factory } from '../../src/lib/abi/factories/ERC20Inbox__factory' import { isL2NetworkWithCustomFeeToken } from './custom-fee-token/customFeeTokenTestHelpers' +import { + getNativeTokenDecimals, + scaleToNativeTokenDecimals, +} from '../../src/lib/utils/lib' describe('RevertData', () => { beforeEach('skipIfMainnet', async function () { await skipIfMainnet(this) }) - const createRevertParams = () => { + const createRevertParams = async () => { const l2CallValue = BigNumber.from(137) const maxSubmissionCost = BigNumber.from(1618) + + const { l1Provider, l2Network } = await testSetup() + const decimals = await getNativeTokenDecimals({ l1Provider, l2Network }) + return { to: Wallet.createRandom().address, excessFeeRefundAddress: Wallet.createRandom().address, @@ -46,10 +54,13 @@ describe('RevertData', () => { l2CallValue, data: hexlify(randomBytes(32)), maxSubmissionCost: maxSubmissionCost, - value: l2CallValue - .add(maxSubmissionCost) - .add(RetryableDataTools.ErrorTriggeringParams.gasLimit) - .add(RetryableDataTools.ErrorTriggeringParams.maxFeePerGas), + value: scaleToNativeTokenDecimals({ + amount: l2CallValue + .add(maxSubmissionCost) + .add(RetryableDataTools.ErrorTriggeringParams.gasLimit) + .add(RetryableDataTools.ErrorTriggeringParams.maxFeePerGas), + decimals, + }), gasLimit: RetryableDataTools.ErrorTriggeringParams.gasLimit, maxFeePerGas: RetryableDataTools.ErrorTriggeringParams.maxFeePerGas, } @@ -71,7 +82,7 @@ describe('RevertData', () => { value, gasLimit, maxFeePerGas, - } = createRevertParams() + } = await createRevertParams() try { if (isL2NetworkWithCustomFeeToken()) { diff --git a/tests/integration/testHelpers.ts b/tests/integration/testHelpers.ts index 3d7cc0ba1f..9c54b2a7bb 100644 --- a/tests/integration/testHelpers.ts +++ b/tests/integration/testHelpers.ts @@ -21,7 +21,7 @@ import chalk from 'chalk' import { BigNumber } from '@ethersproject/bignumber' import { JsonRpcProvider } from '@ethersproject/providers' -import { parseEther } from '@ethersproject/units' +import { parseEther } from 'ethers/lib/utils' import { config, getSigner, testSetup } from '../../scripts/testSetup' @@ -37,8 +37,9 @@ import { ArbSdkError } from '../../src/lib/dataEntities/errors' import { ERC20 } from '../../src/lib/abi/ERC20' import { isL2NetworkWithCustomFeeToken } from './custom-fee-token/customFeeTokenTestHelpers' import { ERC20__factory } from '../../src/lib/abi/factories/ERC20__factory' +import { scaleToNativeTokenDecimals } from '../../src/lib/utils/lib' -export const preFundAmount = parseEther('0.1') +const preFundAmount = parseEther('0.1') export const prettyLog = (text: string): void => { console.log(chalk.blue(` *** ${text}`)) @@ -248,6 +249,8 @@ export const depositToken = async ({ retryableOverrides?: GasOverrides destinationAddress?: string }) => { + let feeTokenBalanceBefore: BigNumber | undefined + await ( await erc20Bridger.approveToken({ erc20L1Address: l1TokenAddress, @@ -288,6 +291,11 @@ export const depositToken = async ({ feeTokenAllowance.eq(Erc20Bridger.MAX_APPROVAL), 'set fee token allowance failed' ).to.be.true + + feeTokenBalanceBefore = await ERC20__factory.connect( + erc20Bridger.nativeToken!, + l1Signer + ).balanceOf(senderAddress) } const initialBridgeTokenBalance = await l1Token.balanceOf( @@ -327,6 +335,34 @@ export const depositToken = async ({ tokenBalL1Before.sub(depositAmount).toString() ) + if (isL2NetworkWithCustomFeeToken()) { + const nativeTokenContract = ERC20__factory.connect( + erc20Bridger.nativeToken!, + l1Signer + ) + + const feeTokenBalanceAfter = await nativeTokenContract.balanceOf( + senderAddress + ) + + // makes sure gas spent was rescaled correctly for non-18 decimal fee tokens + const feeTokenDecimals = await nativeTokenContract.decimals() + + const MAX_BASE_ESTIMATED_GAS_FEE = BigNumber.from(1_000_000_000_000_000) + + const maxScaledEstimatedGasFee = scaleToNativeTokenDecimals({ + amount: MAX_BASE_ESTIMATED_GAS_FEE, + decimals: feeTokenDecimals, + }) + + expect( + feeTokenBalanceBefore! + .sub(feeTokenBalanceAfter) + .lte(maxScaledEstimatedGasFee), + 'Too much custom fee token used as gas' + ).to.be.true + } + const waitRes = await depositRec.waitForL2(l2Signer) const ethBalL2After = await l2Signer.provider!.getBalance( diff --git a/tests/unit/nativeToken.test.ts b/tests/unit/nativeToken.test.ts new file mode 100644 index 0000000000..239d1f19a0 --- /dev/null +++ b/tests/unit/nativeToken.test.ts @@ -0,0 +1,111 @@ +'use strict' + +import { expect } from 'chai' + +import { BigNumber } from 'ethers' +import { parseEther } from 'ethers/lib/utils' +import { + nativeTokenDecimalsTo18Decimals, + scaleToNativeTokenDecimals, +} from '../../src/lib/utils/lib' + +const AMOUNT_TO_SCALE = parseEther('1.23456789') + +describe('Native token', () => { + function decimalsToError(decimals: number) { + return `incorrect scaling result for ${decimals} decimals` + } + + it('scales to native token decimals', () => { + expect( + scaleToNativeTokenDecimals({ amount: AMOUNT_TO_SCALE, decimals: 18 }).eq( + BigNumber.from('1234567890000000000') + ), + decimalsToError(18) + ).to.be.true + + // Rounds up the last digit - in this case no decimals so rounds up 1 to 2 + expect( + scaleToNativeTokenDecimals({ amount: AMOUNT_TO_SCALE, decimals: 0 }).eq( + BigNumber.from('2') + ), + decimalsToError(0) + ).to.be.true + + // Rounds up the last digit + expect( + scaleToNativeTokenDecimals({ amount: AMOUNT_TO_SCALE, decimals: 1 }).eq( + BigNumber.from('13') + ), + decimalsToError(1) + ).to.be.true + + // Rounds up the last digit + expect( + scaleToNativeTokenDecimals({ + amount: AMOUNT_TO_SCALE, + decimals: 6, + }).eq(BigNumber.from('1234568')), + decimalsToError(6) + ).to.be.true + + // Rounds up the last digit + expect( + scaleToNativeTokenDecimals({ amount: AMOUNT_TO_SCALE, decimals: 7 }).eq( + BigNumber.from('12345679') + ), + decimalsToError(7) + ).to.be.true + + // Does not round up the last digit because all original decimals are included + expect( + scaleToNativeTokenDecimals({ amount: AMOUNT_TO_SCALE, decimals: 8 }).eq( + BigNumber.from('123456789') + ), + decimalsToError(8) + ).to.be.true + + // Does not round up the last digit because all original decimals are included + expect( + scaleToNativeTokenDecimals({ amount: AMOUNT_TO_SCALE, decimals: 9 }).eq( + BigNumber.from('1234567890') + ), + decimalsToError(9) + ).to.be.true + + // Does not round up the last digit because all original decimals are included + expect( + scaleToNativeTokenDecimals({ + amount: AMOUNT_TO_SCALE, + decimals: 24, + }).eq(BigNumber.from('1234567890000000000000000')), + decimalsToError(24) + ).to.be.true + }) + + it('scales native token decimals to 18 decimals', () => { + expect( + nativeTokenDecimalsTo18Decimals({ + amount: AMOUNT_TO_SCALE, + decimals: 16, + }).eq(BigNumber.from('123456789000000000000')), + decimalsToError(16) + ).to.be.true + + expect( + nativeTokenDecimalsTo18Decimals({ + amount: AMOUNT_TO_SCALE, + decimals: 18, + }).eq(BigNumber.from('1234567890000000000')), + decimalsToError(18) + ).to.be.true + + expect( + nativeTokenDecimalsTo18Decimals({ + amount: AMOUNT_TO_SCALE, + decimals: 20, + }).eq(BigNumber.from('12345678900000000')), + decimalsToError(20) + ).to.be.true + }) +})