Skip to content

Commit

Permalink
feat: non-18 decimal custom gas token support (#445)
Browse files Browse the repository at this point in the history
  • Loading branch information
brtkx authored Oct 15, 2024
1 parent 27c5b6d commit 647d341
Show file tree
Hide file tree
Showing 13 changed files with 477 additions and 73 deletions.
38 changes: 30 additions & 8 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
48 changes: 38 additions & 10 deletions src/lib/assetBridger/erc20Bridger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -580,7 +584,8 @@ export class Erc20Bridger extends AssetBridger<
* @returns
*/
private getDepositRequestOutboundTransferInnerData(
depositParams: OmitTyped<L1ToL2MessageGasParams, 'deposit'>
depositParams: OmitTyped<L1ToL2MessageGasParams, 'deposit'>,
decimals: number
) {
if (!this.nativeTokenIsEth) {
return defaultAbiCoder.encode(
Expand All @@ -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,
}),
]
)
}
Expand Down Expand Up @@ -645,15 +653,22 @@ export class Erc20Bridger extends AssetBridger<
}
}

const decimals = await getNativeTokenDecimals({
l1Provider,
l2Network: this.l2Network,
})

const depositFunc = (
depositParams: OmitTyped<L1ToL2MessageGasParams, 'deposit'>
) => {
depositParams.maxSubmissionCost =
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
Expand Down Expand Up @@ -1019,6 +1034,11 @@ export class AdminErc20Bridger extends Erc20Bridger {
)
}

const nativeTokenDecimals = await getNativeTokenDecimals({
l1Provider,
l2Network: this.l2Network,
})

type GasParams = {
maxSubmissionCost: BigNumber
gasLimit: BigNumber
Expand Down Expand Up @@ -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,
}
Expand Down
18 changes: 16 additions & 2 deletions src/lib/assetBridger/ethBridger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -312,10 +316,20 @@ export class EthBridger extends AssetBridger<
public async getDepositToRequest(
params: EthDepositToRequestParams
): Promise<L1ToL2TransactionRequest> {
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',
}
Expand Down
23 changes: 17 additions & 6 deletions src/lib/message/L1ToL2MessageGasEstimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
77 changes: 75 additions & 2 deletions src/lib/utils/lib.ts
Original file line number Diff line number Diff line change
@@ -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<void> =>
new Promise(res => setTimeout(res, ms))
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 647d341

Please sign in to comment.