Skip to content

Keep chain-abstraction swap path amounts in raw integer units #6394

Description

@chenshj73

Summary

The chain-abstraction swap path helpers appear to reinterpret raw token amounts differently from the rest of the package:

  • getSwapPathForUniV3() converts amount with unary + before passing it to CurrencyAmount.fromRawAmount(), which loses precision for normal wei-sized values.
  • getSwapPathForUniV2() calls parseUnits(amount, decimals) even though callers/tests pass amount as a raw integer string, effectively scaling it by 10 ** decimals a second time.
  • getPathForPanCake() uses the safer/raw behavior: CurrencyAmount.fromRawAmount(tokenIn, amount).

This can make the generated destination swap path/quote use a different amount than the user intent / xcall amount. I reviewed this at commit:

7758e62037bba281b8844c37831bde0b838edd36

Relevant code

packages/agents/chain-abstraction/src/helpers/swaputil.ts defines amount as a string in the callback args:

14  type SwapPathCallBackArgs = {
15    fromTokenContractAddress: string;
16    toTokenContractAddress: string;
17    signerAddress: string;
18    chainId: number;
19    rpc: string;
20    amount: string;
21    fromTokenDecimal?: number;
22    toTokenDecimal?: number;
23  };

The Uniswap V3 path converts that string to a JS number:

30  export const getSwapPathForUniV3 = async (_args: SwapPathCallBackArgs) => {
31    try {
32      const { fromTokenContractAddress, toTokenContractAddress, chainId, rpc, fromTokenDecimal, toTokenDecimal, amount } =
33        _args;
34      const provider = new ethers.providers.JsonRpcProvider(rpc);
35      const tokenIn = new Token(chainId, fromTokenContractAddress, fromTokenDecimal ?? 18);
36      const tokenOut = new Token(chainId, toTokenContractAddress, toTokenDecimal ?? 18);
37      const amountIn = CurrencyAmount.fromRawAmount(tokenIn, +amount);
38
39      const router = new AlphaRouter({
40        chainId,
41        provider,
42      });
43
44      const routes = await router.route(amountIn, tokenOut, TradeType.EXACT_INPUT);

For typical 18-decimal raw amounts, +amount is above Number.MAX_SAFE_INTEGER; e.g. "1000000000000000000" and "1000000000000000001" both collapse to an imprecise JavaScript number. The route can therefore be quoted for a different input amount than the xcall amount.

The Uniswap V2 path has the opposite problem: it treats the same raw amount string as a human-readable decimal amount and scales it again:

66  export const getSwapPathForUniV2 = async (_args: SwapPathCallBackArgs) => {
67    try {
68      const { fromTokenContractAddress, toTokenContractAddress, chainId, rpc, fromTokenDecimal, toTokenDecimal, amount } =
69        _args;
70
71      const provider = new ethers.providers.JsonRpcProvider(rpc);
72
73      const tokenIn = new _Token(chainId, fromTokenContractAddress, fromTokenDecimal ?? 18);
74      const tokenOut = new _Token(chainId, toTokenContractAddress, toTokenDecimal ?? 18);
75      const amountIn = new TokenAmount(tokenIn, ethers.utils.parseUnits(amount, fromTokenDecimal).toString());
76
77      const pair: Pair = await Fetcher.fetchPairData(tokenIn, tokenOut, provider);
78      const route = new Route([pair], tokenIn);
79      const trade = new Trade(route, amountIn, TradeType.EXACT_INPUT);

If amount is already raw units, parseUnits("1000000000000000000", 18) turns a 1-token raw input into 1e36 base units.

The Pancake path handles the same field as a raw amount string:

91  export const getPathForPanCake = async (_args: SwapPathCallBackArgs) => {
92    try {
93      const { fromTokenContractAddress, toTokenContractAddress, chainId, fromTokenDecimal, toTokenDecimal, amount } =
94        _args;
95      const tokenIn = new PancakeToken(chainId, fromTokenContractAddress as `0x${string}`, fromTokenDecimal ?? 18, "");
96
97      const tokenOut = new PancakeToken(chainId, toTokenContractAddress as `0x${string}`, toTokenDecimal ?? 18, "");
98      const pair = await PancakeFetcher.fetchPairData(tokenIn, tokenOut);
99
100      const amountIn = PancakeCurrencyAmount.fromRawAmount(tokenIn, amount);
101      const route = new PancakeRoute([pair], tokenIn, tokenOut);
102      const trade = new PancakeTrade(route, amountIn, TradeType.EXACT_INPUT);

The tests also pass amount as a raw integer string and comment it as 1 token:

43    const mockArgs = {
44      fromTokenContractAddress: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
45      toTokenContractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
46      chainId: 1,
47      rpc: "http://localhost:8545",
48      fromTokenDecimal: 18,
49      toTokenDecimal: 18,
50      amount: "1000000000000000000", // 1 token for example
51      signerAddress: mkAddress("1"),
52    };

But the V2 test mirrors the double-scaling behavior instead of asserting the raw amount:

141      const tokenIn = new _Token(mockArgs.chainId, mockArgs.fromTokenContractAddress, 18);
142      const tokenOut = new _Token(mockArgs.chainId, mockArgs.toTokenContractAddress, 18);
143      const amountIn = new TokenAmount(
144        tokenIn,
145        ethers.utils.parseUnits(mockArgs.amount, mockArgs.fromTokenDecimal).toString(),
146      );
147      const amountOut = new TokenAmount(
148        tokenOut,
149        ethers.utils.parseUnits(mockArgs.amount, mockArgs.toTokenDecimal).toString(),
150      );

The origin xcall path passes amountIn through as a raw string to route generation:

75      const isSameAsset = utils.getAddress(toAsset) === utils.getAddress(fromAsset);
76      const originRoute = !isSameAsset
77        ? _route ??
78          (await calculateRouteForSwapAndXCall(originDomain, fromAsset, toAsset, amountIn, swapAndXCallAddress, config))
79        : null;
...
167  export const calculateRouteForSwapAndXCall = async (
168    domainId: string,
169    fromAsset: string,
170    toAsset: string,
171    amountIn: string,
172    fromAddress: string,
...
199    const chainId = domainToChainId(+domainId);
200    const originOriginSwapDataCallbackFn = OriginSwapDataFns[swapperConfig.type];
201    const swapData = await originOriginSwapDataCallbackFn({ chainId, fromAsset, toAsset, amountIn, fromAddress, config });
202
203    return { swapper: swapperConfig.address, swapData };
204  };

Why this matters

This is a chain-abstraction lifecycle issue: the user intent / xcall uses one amountIn, while the route helper can quote or choose a path for a different amount.

For V3, precision loss can silently round large raw amounts. For V2, the amount can be inflated by 10 ** decimals. Either case can produce invalid quotes, bad paths, or destination swap data that does not match the amount actually bridged/executed.

Suggested fix

Use raw integer strings or BigNumber/JSBI-compatible values consistently:

const amountIn = CurrencyAmount.fromRawAmount(tokenIn, amount);

for Uniswap V3, and for Uniswap V2 avoid parseUnits when the API receives raw base units:

const amountIn = new TokenAmount(tokenIn, amount);

If the intended API is decimal human units instead, then the type/tests and Pancake implementation should be changed to make that explicit and consistent across all path callbacks.

Regression tests should include values above Number.MAX_SAFE_INTEGER and a non-round wei value such as "1000000000000000001".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions