Skip to content

Commit cc42b9a

Browse files
authored
[simplex]: fix phantom commitment from multi-placeOrder calldata mis-pairing (#870)
1 parent 23a9463 commit cc42b9a

11 files changed

Lines changed: 687 additions & 87 deletions

File tree

sdk/packages/sdk/src/chain.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,11 @@ export interface IEvmChain extends IChain {
231231
getHostNonce(): Promise<bigint>
232232
quoteNative(request: IPostRequest | IGetRequest, fee: bigint): Promise<bigint>
233233
getFeeTokenWithDecimals(): Promise<{ address: HexString; decimals: number }>
234-
getPlaceOrderCalldata(txHash: string, intentGatewayAddress: string): Promise<HexString>
234+
getPlaceOrderCalldata(
235+
txHash: string,
236+
intentGatewayAddress: string,
237+
occurrenceIndex?: number,
238+
): Promise<HexString>
235239
broadcastTransaction(signedTransaction: HexString): Promise<TransactionReceipt>
236240
getTransactionReceipt(hash: HexString): Promise<TransactionReceipt>
237241
}

sdk/packages/sdk/src/chains/evm.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
keccak256,
1111
pad,
1212
toBytes,
13+
toFunctionSelector,
1314
toHex,
1415
maxUint256,
1516
} from "viem"
@@ -39,6 +40,7 @@ import type { GetProofParameters, Hex, TransactionReceipt } from "viem"
3940
import EvmHost from "@/abis/evmHost"
4041
import evmHost from "@/abis/evmHost"
4142
import HandlerV2 from "@/abis/handlerV2"
43+
import { ABI as IntentGatewayV2ABI } from "@/abis/IntentGatewayV2"
4244
import type { IChain, IIsmpMessage } from "@/chain"
4345
import { ChainConfigService } from "@/configs/ChainConfigService"
4446
import type {
@@ -53,7 +55,7 @@ import type {
5355
import {
5456
ADDRESS_ZERO,
5557
EvmStateProof,
56-
getContractCallInput,
58+
getContractCallInputs,
5759
MmrProof,
5860
SubstrateStateProof,
5961
generateRootWithProof,
@@ -392,13 +394,36 @@ export class EvmChain implements IChain {
392394

393395
/**
394396
* Retrieves the placeOrder calldata from a transaction using debug_traceTransaction.
397+
* Filters to placeOrder calls by selector so unrelated calls to the gateway in
398+
* the same transaction (e.g. quote, fillOrder) do not skew indexing.
399+
* When the transaction contains multiple placeOrder calls, `occurrenceIndex`
400+
* selects which call's calldata to return (0-indexed in execution order).
395401
*/
396-
async getPlaceOrderCalldata(txHash: string, intentGatewayAddress: string): Promise<HexString> {
397-
const callInput = await getContractCallInput(this.publicClient, txHash as HexString, intentGatewayAddress)
398-
if (!callInput) {
399-
throw new Error(`Failed to extract calldata from trace for tx ${txHash}`)
402+
async getPlaceOrderCalldata(
403+
txHash: string,
404+
intentGatewayAddress: string,
405+
occurrenceIndex: number = 0,
406+
): Promise<HexString> {
407+
const callInputs = await getContractCallInputs(
408+
this.publicClient,
409+
txHash as HexString,
410+
intentGatewayAddress,
411+
)
412+
const placeOrderSelector = toFunctionSelector(
413+
IntentGatewayV2ABI.find((item: any) => item.type === "function" && item.name === "placeOrder") as any,
414+
)
415+
const placeOrderInputs = callInputs.filter(
416+
(input) => input.slice(0, 10).toLowerCase() === placeOrderSelector.toLowerCase(),
417+
)
418+
if (placeOrderInputs.length === 0) {
419+
throw new Error(`Failed to extract placeOrder calldata from trace for tx ${txHash}`)
420+
}
421+
if (occurrenceIndex >= placeOrderInputs.length) {
422+
throw new Error(
423+
`placeOrder occurrence ${occurrenceIndex} out of range for tx ${txHash} (found ${placeOrderInputs.length})`,
424+
)
400425
}
401-
return callInput
426+
return placeOrderInputs[occurrenceIndex]
402427
}
403428

404429
/**

sdk/packages/sdk/src/chains/pharos.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,8 +407,12 @@ export class PharosChain implements IChain {
407407
return this.evm.getFeeTokenWithDecimals()
408408
}
409409

410-
getPlaceOrderCalldata(txHash: string, intentGatewayAddress: string): Promise<HexString> {
411-
return this.evm.getPlaceOrderCalldata(txHash, intentGatewayAddress)
410+
getPlaceOrderCalldata(
411+
txHash: string,
412+
intentGatewayAddress: string,
413+
occurrenceIndex?: number,
414+
): Promise<HexString> {
415+
return this.evm.getPlaceOrderCalldata(txHash, intentGatewayAddress, occurrenceIndex)
412416
}
413417

414418
estimateGas(request: IPostRequest): Promise<{ gas: bigint; postRequestCalldata: HexString }> {

sdk/packages/sdk/src/chains/polkadotHub.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,12 @@ export class PolkadotHubChain implements IChain {
321321
return this.evm.getFeeTokenWithDecimals()
322322
}
323323

324-
getPlaceOrderCalldata(txHash: string, intentGatewayAddress: string): Promise<HexString> {
325-
return this.evm.getPlaceOrderCalldata(txHash, intentGatewayAddress)
324+
getPlaceOrderCalldata(
325+
txHash: string,
326+
intentGatewayAddress: string,
327+
occurrenceIndex?: number,
328+
): Promise<HexString> {
329+
return this.evm.getPlaceOrderCalldata(txHash, intentGatewayAddress, occurrenceIndex)
326330
}
327331

328332
estimateGas(request: IPostRequest): Promise<{ gas: bigint; postRequestCalldata: HexString }> {

sdk/packages/sdk/src/chains/tron.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ export class TronChain implements IChain {
111111
* Only works for direct calls to IntentGateway (not nested/multicall).
112112
* Tron does not support debug_traceTransaction.
113113
*/
114-
async getPlaceOrderCalldata(txHash: string, _intentGatewayAddress: string): Promise<HexString> {
114+
async getPlaceOrderCalldata(
115+
txHash: string,
116+
_intentGatewayAddress: string,
117+
_occurrenceIndex?: number,
118+
): Promise<HexString> {
115119
const tx = await retryPromise(() => this.tronWeb.trx.getTransaction(txHash), {
116120
maxRetries: 3,
117121
backoffMs: 250,

sdk/packages/sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export {
3232
parseStateMachineId,
3333
retryPromise,
3434
getContractCallInput,
35+
getContractCallInputs,
3536
calculateBalanceMappingLocation,
3637
calculateAllowanceMappingLocation,
3738
MOCK_ADDRESS,
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { describe, it, expect } from "vitest"
2+
import { encodeFunctionData, type PublicClient, type Hex } from "viem"
3+
import { getContractCallInputs, getContractCallInput, type HexString } from "@/utils"
4+
import { ABI as IntentGatewayV2ABI } from "@/abis/IntentGatewayV2"
5+
import { EvmChain } from "@/chains/evm"
6+
7+
const GATEWAY = "0x000000000000000000000000000000000000ca11"
8+
const OTHER = "0x0000000000000000000000000000000000000bee"
9+
const TX_HASH = ("0x" + "ab".repeat(32)) as HexString
10+
11+
const GRAFFITI = ("0x" + "00".repeat(32)) as Hex
12+
const USER = ("0x" + "11".repeat(32)) as Hex
13+
const TOKEN_IN = ("0x" + "aa".repeat(32)) as Hex
14+
const TOKEN_OUT = ("0x" + "bb".repeat(32)) as Hex
15+
const BENEFICIARY = ("0x" + "cc".repeat(32)) as Hex
16+
17+
function makeOrderStruct(nonce: bigint) {
18+
return {
19+
user: USER,
20+
source: "0x45564d2d31" as Hex, // "EVM-1"
21+
destination: "0x45564d2d3130" as Hex, // "EVM-10"
22+
deadline: 1700000000n,
23+
nonce,
24+
fees: 0n,
25+
session: "0x0000000000000000000000000000000000000000" as Hex,
26+
predispatch: { assets: [], call: "0x" as Hex },
27+
inputs: [{ token: TOKEN_IN, amount: 100n }],
28+
output: {
29+
beneficiary: BENEFICIARY,
30+
assets: [{ token: TOKEN_OUT, amount: 100n }],
31+
call: "0x" as Hex,
32+
},
33+
}
34+
}
35+
36+
function placeOrderCalldata(nonce: bigint): HexString {
37+
return encodeFunctionData({
38+
abi: IntentGatewayV2ABI,
39+
functionName: "placeOrder",
40+
args: [makeOrderStruct(nonce) as any, GRAFFITI],
41+
}) as HexString
42+
}
43+
44+
/**
45+
* Builds a stub PublicClient.extend / debug_traceTransaction pipeline that
46+
* surfaces a fixed CallTracerCall fixture to the code under test.
47+
*/
48+
function stubClient(trace: any): PublicClient {
49+
const request = async ({ method }: { method: string }) => {
50+
if (method === "debug_traceTransaction") return trace
51+
throw new Error(`unexpected method: ${method}`)
52+
}
53+
const client: any = {
54+
request,
55+
extend(decorator: (c: any) => any) {
56+
return { ...client, ...decorator(client) }
57+
},
58+
}
59+
return client as PublicClient
60+
}
61+
62+
describe("getContractCallInputs", () => {
63+
it("returns every call to the target in execution order", async () => {
64+
const trace = {
65+
from: "0xabc",
66+
to: GATEWAY,
67+
input: "0xtop",
68+
calls: [
69+
{ from: GATEWAY, to: OTHER, input: "0x01", calls: [] },
70+
{ from: "0xabc", to: GATEWAY, input: "0x02", calls: [] },
71+
{
72+
from: "0xabc",
73+
to: OTHER,
74+
input: "0x03",
75+
calls: [{ from: OTHER, to: GATEWAY, input: "0x04", calls: [] }],
76+
},
77+
],
78+
}
79+
80+
const inputs = await getContractCallInputs(stubClient(trace), TX_HASH, GATEWAY)
81+
82+
expect(inputs).toEqual(["0xtop", "0x02", "0x04"])
83+
})
84+
85+
it("returns an empty list when the target is not called", async () => {
86+
const trace = {
87+
from: "0xabc",
88+
to: OTHER,
89+
input: "0x00",
90+
calls: [{ from: "0xabc", to: OTHER, input: "0x01", calls: [] }],
91+
}
92+
93+
const inputs = await getContractCallInputs(stubClient(trace), TX_HASH, GATEWAY)
94+
95+
expect(inputs).toEqual([])
96+
})
97+
98+
it("getContractCallInput (legacy wrapper) returns the first match", async () => {
99+
const trace = {
100+
from: "0xabc",
101+
to: OTHER,
102+
input: "0x00",
103+
calls: [
104+
{ from: "0xabc", to: GATEWAY, input: "0xfirst", calls: [] },
105+
{ from: "0xabc", to: GATEWAY, input: "0xsecond", calls: [] },
106+
],
107+
}
108+
109+
const result = await getContractCallInput(stubClient(trace), TX_HASH, GATEWAY)
110+
111+
expect(result).toBe("0xfirst")
112+
})
113+
})
114+
115+
describe("EvmChain.getPlaceOrderCalldata", () => {
116+
function newChain(trace: any): EvmChain {
117+
const chain = Object.create(EvmChain.prototype) as EvmChain
118+
;(chain as any).publicClient = stubClient(trace)
119+
return chain
120+
}
121+
122+
it("returns the K-th placeOrder calldata in execution order", async () => {
123+
const order0 = placeOrderCalldata(1n)
124+
const order1 = placeOrderCalldata(2n)
125+
126+
const trace = {
127+
from: "0xabc",
128+
to: "0xabc",
129+
input: "0x",
130+
calls: [
131+
{ from: "0xabc", to: GATEWAY, input: order0, calls: [] },
132+
{ from: "0xabc", to: OTHER, input: "0xnoise", calls: [] },
133+
{ from: "0xabc", to: GATEWAY, input: order1, calls: [] },
134+
],
135+
}
136+
137+
const chain = newChain(trace)
138+
const first = await chain.getPlaceOrderCalldata(TX_HASH, GATEWAY, 0)
139+
const second = await chain.getPlaceOrderCalldata(TX_HASH, GATEWAY, 1)
140+
141+
expect(first).toBe(order0)
142+
expect(second).toBe(order1)
143+
})
144+
145+
it("defaults occurrenceIndex to 0", async () => {
146+
const order0 = placeOrderCalldata(7n)
147+
const trace = {
148+
from: "0xabc",
149+
to: GATEWAY,
150+
input: order0,
151+
calls: [],
152+
}
153+
154+
const chain = newChain(trace)
155+
const result = await chain.getPlaceOrderCalldata(TX_HASH, GATEWAY)
156+
expect(result).toBe(order0)
157+
})
158+
159+
it("throws when occurrenceIndex is out of range", async () => {
160+
const order0 = placeOrderCalldata(1n)
161+
const trace = {
162+
from: "0xabc",
163+
to: GATEWAY,
164+
input: order0,
165+
calls: [],
166+
}
167+
168+
const chain = newChain(trace)
169+
await expect(chain.getPlaceOrderCalldata(TX_HASH, GATEWAY, 1)).rejects.toThrow(/out of range/)
170+
})
171+
172+
it("throws when the gateway is not called at all", async () => {
173+
const trace = {
174+
from: "0xabc",
175+
to: "0xabc",
176+
input: "0x",
177+
calls: [{ from: "0xabc", to: OTHER, input: "0xnoise", calls: [] }],
178+
}
179+
180+
const chain = newChain(trace)
181+
await expect(chain.getPlaceOrderCalldata(TX_HASH, GATEWAY, 0)).rejects.toThrow(/Failed to extract placeOrder/)
182+
})
183+
})

0 commit comments

Comments
 (0)