diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 2a675fc22..f26f04748 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 d7feb6211..fe98454a6 100644 --- a/src/lib/assetBridger/erc20Bridger.ts +++ b/src/lib/assetBridger/erc20Bridger.ts @@ -76,7 +76,11 @@ import { OmitTyped, RequiredPick } from '../utils/types' import { RetryableDataTools } from '../dataEntities/retryableData' import { EventArgs } from '../dataEntities/event' import { ParentToChildMessageGasParams } from '../message/ParentToChildMessageCreator' -import { isArbitrumChain } from '../utils/lib' +import { + getNativeTokenDecimals, + isArbitrumChain, + scaleToNativeTokenDecimals, +} from '../utils/lib' import { L2ERC20Gateway__factory } from '../abi/factories/L2ERC20Gateway__factory' import { getErc20ParentAddressFromParentToChildTxRequest } from '../utils/calldata' @@ -610,7 +614,8 @@ export class Erc20Bridger extends AssetBridger< * @returns */ private getDepositRequestOutboundTransferInnerData( - depositParams: OmitTyped + depositParams: OmitTyped, + decimals: number ) { if (!this.nativeTokenIsEth) { return defaultAbiCoder.encode( @@ -621,9 +626,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, + }), ] ) } @@ -676,6 +684,11 @@ export class Erc20Bridger extends AssetBridger< } } + const decimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network: this.childNetwork, + }) + const depositFunc = ( depositParams: OmitTyped ) => { @@ -683,8 +696,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 @@ -1080,6 +1095,11 @@ export class AdminErc20Bridger extends Erc20Bridger { ) } + const nativeTokenDecimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network: this.childNetwork, + }) + type GasParams = { maxSubmissionCost: BigNumber gasLimit: BigNumber @@ -1114,15 +1134,23 @@ export class AdminErc20Bridger extends Erc20Bridger { setTokenGas.gasLimit, setGatewayGas.gasLimit, doubleFeePerGas, - setTokenDeposit, - setGatewayDeposit, + scaleToNativeTokenDecimals({ + amount: setTokenDeposit, + decimals: nativeTokenDecimals, + }), + scaleToNativeTokenDecimals({ + amount: setGatewayDeposit, + decimals: nativeTokenDecimals, + }), parentSenderAddress, ] ) return { data, - value: setTokenDeposit.add(setGatewayDeposit), + value: this.nativeTokenIsEth + ? setTokenDeposit.add(setGatewayDeposit) + : BigNumber.from(0), to: parentToken.address, from, } diff --git a/src/lib/assetBridger/ethBridger.ts b/src/lib/assetBridger/ethBridger.ts index 61dd02bc1..512d50ddf 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 { getArbitrumNetwork } 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.parentProvider, + l2Network: this.childNetwork, + }) + + 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/ParentToChildMessageGasEstimator.ts b/src/lib/message/ParentToChildMessageGasEstimator.ts index e2d052bc4..8c74f3569 100644 --- a/src/lib/message/ParentToChildMessageGasEstimator.ts +++ b/src/lib/message/ParentToChildMessageGasEstimator.ts @@ -11,7 +11,12 @@ import { RetryableDataTools, } from '../dataEntities/retryableData' import { ParentToChildTransactionRequest } from '../dataEntities/transactionRequest' -import { getBaseFee, isDefined } from '../utils/lib' +import { + getBaseFee, + getNativeTokenDecimals, + isDefined, + scaleToNativeTokenDecimals, +} from '../utils/lib' import { OmitTyped } from '../utils/types' import { ParentToChildMessageGasParams, @@ -226,6 +231,12 @@ export class ParentToChildMessageGasEstimator { const { data } = retryableEstimateData const gasLimitDefaults = this.applyGasLimitDefaults(options?.gasLimit) + const l2Network = await getArbitrumNetwork(this.childProvider) + const decimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network, + }) + // estimate the child gas price const maxFeePerGasPromise = this.estimateMaxFeePerGas(options?.maxFeePerGas) @@ -260,10 +271,13 @@ export class ParentToChildMessageGasEstimator { 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 baaab9a0a..ab786d403 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 { ArbSys__factory } from '../abi/factories/ArbSys__factory' import { ARB_SYS_ADDRESS } from '../dataEntities/constants' -import { getNitroGenesisBlock } from '../dataEntities/networks' -import { BigNumber } from 'ethers' +import { ArbitrumNetwork, getNitroGenesisBlock } from '../dataEntities/networks' +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.maxArbitrumBlock] } + +export async function getNativeTokenDecimals({ + l1Provider, + l2Network, +}: { + l1Provider: Provider + l2Network: ArbitrumNetwork +}) { + 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 5964b017d..bda73e2a3 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 { fundParentSigner as fundParentSignerEther, @@ -32,6 +32,7 @@ import { import { describeOnlyWhenCustomGasToken } from './mochaExtensions' import { ChildToParentMessageStatus } from '../../../src' import { ChildToParentMessage } from '../../../src/lib/message/ChildToParentMessage' +import { getNativeTokenDecimals } from '../../../src/lib/utils/lib' dotenv.config() @@ -50,9 +51,18 @@ 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, parentSigner } = - await testSetup() - const amount = ethers.utils.parseEther('1') + const { + ethBridger, + nativeTokenContract, + parentSigner, + parentProvider, + childChain, + } = await testSetup() + const decimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network: childChain, + }) + const amount = ethers.utils.parseUnits('1', decimals) await fundParentSignerEther(parentSigner) await fundParentCustomFeeToken(parentSigner) @@ -164,11 +174,17 @@ describeOnlyWhenCustomGasToken( parentProvider, childSigner, childProvider, + childChain, ethBridger, nativeTokenContract, } = await testSetup() + const decimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network: childChain, + }) + const bridge = ethBridger.childNetwork.ethBridge.bridge - const amount = parseEther('0.2') + const amount = parseUnits('0.2', decimals) await fundParentSignerEther(parentSigner) await fundChildCustomFeeToken(childSigner) diff --git a/tests/integration/custom-fee-token/customFeeTokenTestHelpers.ts b/tests/integration/custom-fee-token/customFeeTokenTestHelpers.ts index 199b12eac..af6444052 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) @@ -48,13 +49,17 @@ export async function fundParentCustomFeeToken( } 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() } @@ -93,9 +98,14 @@ export async function approveParentCustomFeeTokenForErc20Deposit( export async function fundChildCustomFeeToken(childSigner: Signer) { const deployerWallet = new Wallet(config.arbKey, arbProvider()) + const decimals = await getNativeTokenDecimals({ + l1Provider: ethProvider(), + l2Network: localNetworks().l2Network, + }) + const tx = await deployerWallet.sendTransaction({ to: await childSigner.getAddress(), - value: utils.parseEther('1'), + value: utils.parseUnits('1', decimals), }) await tx.wait() } diff --git a/tests/integration/eth.test.ts b/tests/integration/eth.test.ts index bbea2bc69..1e2e46715 100644 --- a/tests/integration/eth.test.ts +++ b/tests/integration/eth.test.ts @@ -39,6 +39,11 @@ import { isArbitrumNetworkWithCustomFeeToken } from './custom-fee-token/customFe import { ERC20__factory } from '../../src/lib/abi/factories/ERC20__factory' import { itOnlyWhenEth } from './custom-fee-token/mochaExtensions' import { ParentTransactionReceipt } from '../../src' +import { + getNativeTokenDecimals, + scaleToNativeTokenDecimals, +} from '../../src/lib/utils/lib' +import { parseUnits } from 'ethers/lib/utils' dotenv.config() @@ -99,7 +104,17 @@ describe('Ether', async () => { ) it('deposits ether', async () => { - const { ethBridger, parentSigner, childSigner } = await testSetup() + const { + ethBridger, + parentSigner, + parentProvider, + childChain, + childSigner, + } = await testSetup() + const decimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network: childChain, + }) await fundParentSigner(parentSigner) const inboxAddress = ethBridger.childNetwork.ethBridge.inbox @@ -107,7 +122,8 @@ describe('Ether', async () => { const initialInboxBalance = await parentSigner.provider!.getBalance( inboxAddress ) - const ethToDeposit = parseEther('0.0002') + const amount = '0.0002' + const ethToDeposit = parseUnits(amount, decimals) const res = await ethBridger.deposit({ amount: ethToDeposit, parentSigner: parentSigner, @@ -127,6 +143,16 @@ describe('Ether', async () => { childSigner.provider! ) + const l1ToL2Messages = await rec.getEthDeposits(childSigner.provider!) + expect(l1ToL2Messages.length).to.eq(1, 'failed to find 1 l1 to l2 message') + const l1ToL2Message = l1ToL2Messages[0] + + const walletAddress = await parentSigner.getAddress() + expect(l1ToL2Message.to).to.eq(walletAddress, 'message inputs value error') + expect(l1ToL2Message.value.toString(), 'message inputs value error').to.eq( + parseEther(amount).toString() + ) + const parentToChildMessages = await rec.getEthDeposits( childSigner.provider! ) @@ -136,30 +162,24 @@ describe('Ether', async () => { ) const parentToChildMessage = parentToChildMessages[0] - const walletAddress = await parentSigner.getAddress() - expect(parentToChildMessage.to).to.eq( - walletAddress, - 'message inputs value error' - ) - expect( - parentToChildMessage.value.toString(), - 'message inputs value error' - ).to.eq(ethToDeposit.toString()) - - prettyLog('childDepositTxHash: ' + waitResult.message.childTxHash) - prettyLog('chain transaction found!') - expect(waitResult.complete).to.eq(true, 'eth deposit not complete') - expect(waitResult.childTxReceipt).to.exist - expect(waitResult.childTxReceipt).to.not.be.null - - const testWalletChildEthBalance = await childSigner.getBalance() - expect(testWalletChildEthBalance.toString(), 'final balance').to.eq( - ethToDeposit.toString() + const testWalletL2EthBalance = await childSigner.getBalance() + expect(testWalletL2EthBalance.toString(), 'final balance').to.eq( + parseEther(amount).toString() ) }) - it('deposits ether to a specific child address', async () => { - const { ethBridger, parentSigner, childSigner } = await testSetup() + it('deposits ether to a specific L2 address', async function () { + const { + ethBridger, + parentSigner, + parentProvider, + childChain, + childSigner, + } = await testSetup() + const decimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network: childChain, + }) await fundParentSigner(parentSigner) const inboxAddress = ethBridger.childNetwork.ethBridge.inbox @@ -168,7 +188,8 @@ describe('Ether', async () => { const initialInboxBalance = await parentSigner.provider!.getBalance( inboxAddress ) - const ethToDeposit = parseEther('0.0002') + const amount = '0.0002' + const ethToDeposit = parseUnits(amount, decimals) const res = await ethBridger.depositTo({ amount: ethToDeposit, parentSigner: parentSigner, @@ -202,7 +223,7 @@ describe('Ether', async () => { expect( parentToChildMessage.messageData.l2CallValue.toString(), 'message inputs value error' - ).to.eq(ethToDeposit.toString()) + ).to.eq(parseEther(amount).toString()) const retryableTicketResult = await parentToChildMessage.waitForStatus() expect(retryableTicketResult.status).to.eq( @@ -233,17 +254,28 @@ describe('Ether', async () => { destWallet.address ) expect(testWalletChildEthBalance.toString(), 'final balance').to.eq( - ethToDeposit.toString() + parseEther(amount).toString() ) }) - it('deposit ether to a specific child address with manual redeem', async () => { - const { ethBridger, parentSigner, childSigner } = await testSetup() + it('deposit ether to a specific L2 address with manual redeem', async function () { + const { + ethBridger, + parentSigner, + parentProvider, + childChain, + childSigner, + } = await testSetup() + const decimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network: childChain, + }) await fundParentSigner(parentSigner) 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, parentSigner: parentSigner, @@ -303,11 +335,17 @@ describe('Ether', async () => { expect( testWalletChildEthBalance.toString(), 'balance after manual redeem' - ).to.eq(ethToDeposit.toString()) + ).to.eq(parseEther(amount).toString()) }) it('withdraw Ether transaction succeeds', async () => { - const { childSigner, parentSigner, ethBridger } = await testSetup() + const { + childSigner, + childChain, + parentSigner, + parentProvider, + ethBridger, + } = await testSetup() await fundChildSigner(childSigner) await fundParentSigner(parentSigner) @@ -404,6 +442,11 @@ describe('Ether', async () => { 'executed status' ).to.eq(ChildToParentMessageStatus.EXECUTED) + const decimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network: childChain, + }) + const finalRandomBalance = isArbitrumNetworkWithCustomFeeToken() ? await ERC20__factory.connect( ethBridger.nativeToken!, @@ -411,7 +454,7 @@ describe('Ether', async () => { ).balanceOf(randomAddress) : await parentSigner.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 c669eeb8d..46b5fa336 100644 --- a/tests/integration/l1l3Bridger.test.ts +++ b/tests/integration/l1l3Bridger.test.ts @@ -35,6 +35,7 @@ import { getArbitrumNetwork, registerCustomArbitrumNetwork, } from '../../src/lib/dataEntities/networks' +import { getNativeTokenDecimals } from '../../src/lib/utils/lib' async function expectPromiseToReject( promise: Promise, @@ -138,7 +139,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! ) @@ -201,6 +202,15 @@ describe('L1 to L3 Bridging', () => { `Signer/provider chain id: ${l1ChainId} doesn't match provided chain id: ${l3ChainId}.` ) } + + if (isArbitrumNetworkWithCustomFeeToken()) { + await fundActualL1CustomFeeToken( + l1Signer, + l3Network.nativeToken!, + l2Network, + l2Signer.provider! + ) + } } // setup for all test cases @@ -371,7 +381,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') } @@ -392,7 +411,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 ArbitrumNetwork @@ -411,7 +439,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'] ) @@ -899,7 +936,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: Erc20L1L3DepositRequestParams = { @@ -913,11 +959,19 @@ describe('L1 to L3 Bridging', () => { await testHappyPathNonFeeOrStandard(depositParams) }) - it('happy path weth', async () => { - assertArbitrumNetworkHasTokenBridge(l2Network) + 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.parentWeth, + l2Network.tokenBridge!.parentWeth, l1Signer ) @@ -934,7 +988,7 @@ describe('L1 to L3 Bridging', () => { ).wait() const depositParams: Erc20L1L3DepositRequestParams = { - erc20L1Address: l2Network.tokenBridge.parentWeth, + erc20L1Address: l2Network.tokenBridge!.parentWeth, destinationAddress: l3Recipient, amount, l2Provider: l2Signer.provider!, @@ -944,7 +998,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 170e4836b..d59b8aa4c 100644 --- a/tests/integration/retryableData.test.ts +++ b/tests/integration/retryableData.test.ts @@ -30,15 +30,26 @@ import { GasOverrides } from '../../src/lib/message/ParentToChildMessageGasEstim const depositAmount = BigNumber.from(100) import { ERC20Inbox__factory } from '../../src/lib/abi/factories/ERC20Inbox__factory' import { isArbitrumNetworkWithCustomFeeToken } 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 { parentProvider, childChain } = await testSetup() + const decimals = await getNativeTokenDecimals({ + l1Provider: parentProvider, + l2Network: childChain, + }) + return { to: Wallet.createRandom().address, excessFeeRefundAddress: Wallet.createRandom().address, @@ -46,10 +57,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 +85,7 @@ describe('RevertData', () => { value, gasLimit, maxFeePerGas, - } = createRevertParams() + } = await createRevertParams() try { if (isArbitrumNetworkWithCustomFeeToken()) { diff --git a/tests/integration/testHelpers.ts b/tests/integration/testHelpers.ts index 2106c009c..d4543b981 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 { isArbitrumNetworkWithCustomFeeToken } 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}`)) @@ -252,6 +253,8 @@ export const depositToken = async ({ retryableOverrides?: GasOverrides destinationAddress?: string }) => { + let feeTokenBalanceBefore: BigNumber | undefined + await ( await erc20Bridger.approveToken({ erc20ParentAddress: parentTokenAddress, @@ -293,6 +296,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!, + parentSigner + ).balanceOf(senderAddress) } const initialBridgeTokenBalance = await parentToken.balanceOf( @@ -332,6 +340,34 @@ export const depositToken = async ({ parentTokenBalanceBefore.sub(depositAmount).toString() ) + if (isArbitrumNetworkWithCustomFeeToken()) { + const nativeTokenContract = ERC20__factory.connect( + erc20Bridger.nativeToken!, + parentSigner + ) + + 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.waitForChildTransactionReceipt(childSigner) const childEthBalanceAfter = await childSigner.provider!.getBalance( diff --git a/tests/unit/nativeToken.test.ts b/tests/unit/nativeToken.test.ts new file mode 100644 index 000000000..239d1f19a --- /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 + }) +})