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".
Summary
The chain-abstraction swap path helpers appear to reinterpret raw token amounts differently from the rest of the package:
getSwapPathForUniV3()convertsamountwith unary+before passing it toCurrencyAmount.fromRawAmount(), which loses precision for normal wei-sized values.getSwapPathForUniV2()callsparseUnits(amount, decimals)even though callers/tests passamountas a raw integer string, effectively scaling it by10 ** decimalsa 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:
Relevant code
packages/agents/chain-abstraction/src/helpers/swaputil.tsdefinesamountas a string in the callback args:The Uniswap V3 path converts that string to a JS number:
For typical 18-decimal raw amounts,
+amountis aboveNumber.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:
If
amountis already raw units,parseUnits("1000000000000000000", 18)turns a 1-token raw input into1e36base units.The Pancake path handles the same field as a raw amount string:
The tests also pass
amountas a raw integer string and comment it as 1 token:But the V2 test mirrors the double-scaling behavior instead of asserting the raw amount:
The origin xcall path passes
amountInthrough as a raw string to route generation: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:
for Uniswap V3, and for Uniswap V2 avoid
parseUnitswhen the API receives raw base units: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_INTEGERand a non-round wei value such as"1000000000000000001".