Skip to content

Commit

Permalink
feat: add support for non-18 decimal custom gas token (#216)
Browse files Browse the repository at this point in the history
Co-authored-by: spsjvc <[email protected]>
  • Loading branch information
chrstph-dvx and spsjvc authored Oct 24, 2024
1 parent 0522b1c commit bb1e4ce
Show file tree
Hide file tree
Showing 17 changed files with 161 additions and 65 deletions.
15 changes: 12 additions & 3 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -81,4 +90,4 @@ jobs:
run: yarn build

- name: Test
run: yarn test:integration
run: DECIMALS=${{matrix.config.decimals}} yarn test:integration
9 changes: 7 additions & 2 deletions src/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -142,7 +145,9 @@ export const chains = [
nitroTestnodeL1,
nitroTestnodeL2,
nitroTestnodeL3,
] as const;
];

export const chains = [...mainnets, ...testnets] as const;

export {
// mainnet L1
Expand Down
2 changes: 1 addition & 1 deletion src/createRollup.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
10 changes: 8 additions & 2 deletions src/createRollupEnoughCustomFeeTokenAllowance.ts
Original file line number Diff line number Diff line change
@@ -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<TChain extends Chain | undefined> =
Prettify<
Expand Down Expand Up @@ -37,5 +38,10 @@ export async function createRollupEnoughCustomFeeTokenAllowance<TChain extends C
maxFeePerGasForRetryables,
});

return allowance >= fees;
const decimals = await fetchDecimals({
address: nativeToken,
publicClient,
});

return allowance >= scaleToNativeTokenDecimals({ amount: fees, decimals });
}
16 changes: 11 additions & 5 deletions src/createRollupGetRetryablesFees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
EstimateGasParameters,
encodeFunctionData,
decodeFunctionResult,
parseEther,
} from 'viem';

import { rollupCreatorABI } from './contracts/RollupCreator';
Expand Down Expand Up @@ -104,9 +105,8 @@ export async function createRollupGetRetryablesFees<TChain extends Chain | undef

const baseFeeWithBuffer = applyPercentIncrease({
base: await publicClient.getGasPrice(),
// for custom gas token chains, retryable fees don't scale with parent base fee, so there's no need for any buffer
// for eth chains, add 30% buffer in case of a spike
percentIncrease: isCustomGasToken ? undefined : 30n,
// add 30% buffer in case of a spike
percentIncrease: 30n,
});

const callParams: CallParameters = {
Expand Down Expand Up @@ -140,8 +140,14 @@ export async function createRollupGetRetryablesFees<TChain extends Chain | undef
});

return isCustomGasToken
? // for custom gas token chains, retryable fees don't scale with parent base fee, so there's no need for any buffer
decodedResult
? // for custom gas token chains, retryable fees don't scale with parent base fee and are constant at 124708400000000000
//
// we add some buffer (around 100k gwei) due to potential rounding issues for non-18 decimals, because:
// - in the sdk, we get the total cost, then scale and round up
// - in the contract, we scale and round up each component, then add them together, which can lead to a very tiny discrepancy
//
// https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/RollupCreator.sol#L287-L302
parseEther('0.1248')
: // for eth chains, add 3% buffer
applyPercentIncrease({ base: decodedResult, percentIncrease: 3n });
}
Expand Down
4 changes: 2 additions & 2 deletions src/createRollupGetRetryablesFees.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ it('successfully fetches retryable fees for an eth-based chain', async () => {
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);
});
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
});

Expand Down
5 changes: 2 additions & 3 deletions src/createRollupPrepareTransactionRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,9 @@ export async function createRollupPrepareTransactionRequest<TChain extends Chain
);
}

// custom fee token is only allowed to have 18 decimals
if ((await fetchDecimals({ address: params.nativeToken, publicClient })) !== 18) {
if ((await fetchDecimals({ address: params.nativeToken, publicClient })) > 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.`,
);
}
}
Expand Down
66 changes: 34 additions & 32 deletions src/createRollupPrepareTransactionRequest.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
13 changes: 11 additions & 2 deletions src/createTokenBridge.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 14 additions & 2 deletions src/createTokenBridgeEnoughCustomFeeTokenAllowance.ts
Original file line number Diff line number Diff line change
@@ -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<TChain extends Chain | undefined> =
Prettify<
Expand All @@ -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,
})
);
}
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -30,11 +31,21 @@ export async function createTokenBridgePrepareCustomFeeTokenApprovalTransactionR
}: CreateTokenBridgePrepareCustomFeeTokenApprovalTransactionRequestParams<TChain>) {
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,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit bb1e4ce

Please sign in to comment.