Skip to content

Commit 12dcd89

Browse files
Josh Hardyclaude
andcommitted
fix: wrap oracle ABI encoding in tuple to match alloy's abi_decode
alloy's `(T1, T2, T3, T4).abi_encode()` produces the encoding of a *single wrapping tuple* (first word is an offset to the tuple content), while viem's `encodeAbiParameters([T1, T2, T3, T4], [...])` produces the encoding of *four separate parameters* (first word is the offset to the first dynamic parameter). These are different ABI layouts. The oracle server uses alloy's `abi_decode()` which expects the wrapped form. Fix: encode all four fields as components of a single tuple parameter instead of four top-level parameters. Also: - Skip bounty task bytecode compilation in intra-orderbook mode when gasCoveragePercentage="0" (the task is never included in the withdraw call anyway, and computing it requires a dispair address which may not be available for V6 orderbooks whose registry doesn't expose I_INTERPRETER/I_STORE directly). - Make resolveVersionContracts resilient to V6 deployers that don't expose I_INTERPRETER/I_STORE (falls back to using the deployer address itself, with a warning). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 22d758e commit 12dcd89

3 files changed

Lines changed: 115 additions & 76 deletions

File tree

src/core/modes/intra/simulation.ts

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -125,43 +125,59 @@ export class IntraOrderbookTradeSimulator extends TradeSimulatorBase {
125125
)!;
126126

127127
// build clear function call data and withdraw tasks
128-
const taskBytecodeResult = await getEnsureBountyTaskBytecode(
129-
{
130-
type: EnsureBountyTaskType.Internal,
131-
botAddress: this.tradeArgs.signer.account.address,
132-
inputToken: this.tradeArgs.orderDetails.buyToken,
133-
outputToken: this.tradeArgs.orderDetails.sellToken,
134-
orgInputBalance: this.tradeArgs.inputBalance,
135-
orgOutputBalance: this.tradeArgs.outputBalance,
136-
inputToEthPrice: parseUnits(this.tradeArgs.inputToEthPrice, 18),
137-
outputToEthPrice: parseUnits(this.tradeArgs.outputToEthPrice, 18),
138-
minimumExpected: params.minimumExpected,
139-
sender: this.tradeArgs.signer.account.address,
140-
},
141-
this.tradeArgs.solver.state.client,
142-
addresses.dispair,
143-
);
144-
if (taskBytecodeResult.isErr()) {
145-
const errMsg = await errorSnapshot("", taskBytecodeResult.error);
146-
this.spanAttributes["isNodeError"] =
147-
taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError;
148-
this.spanAttributes["error"] = errMsg;
149-
const result = {
150-
type: TradeType.IntraOrderbook,
151-
spanAttributes: this.spanAttributes,
152-
reason: SimulationHaltReason.FailedToGetTaskBytecode,
128+
// When gasCoveragePercentage is "0", the bounty task isn't
129+
// included in the withdraw call, so skip the expensive on-chain
130+
// compilation that requires dispair addresses.
131+
const noBounty = this.tradeArgs.solver.appOptions.gasCoveragePercentage === "0";
132+
let task: TaskType;
133+
if (noBounty) {
134+
task = {
135+
evaluable: {
136+
interpreter: "0x0000000000000000000000000000000000000000",
137+
store: "0x0000000000000000000000000000000000000000",
138+
bytecode: "0x",
139+
},
140+
signedContext: [],
141+
};
142+
} else {
143+
const taskBytecodeResult = await getEnsureBountyTaskBytecode(
144+
{
145+
type: EnsureBountyTaskType.Internal,
146+
botAddress: this.tradeArgs.signer.account.address,
147+
inputToken: this.tradeArgs.orderDetails.buyToken,
148+
outputToken: this.tradeArgs.orderDetails.sellToken,
149+
orgInputBalance: this.tradeArgs.inputBalance,
150+
orgOutputBalance: this.tradeArgs.outputBalance,
151+
inputToEthPrice: parseUnits(this.tradeArgs.inputToEthPrice, 18),
152+
outputToEthPrice: parseUnits(this.tradeArgs.outputToEthPrice, 18),
153+
minimumExpected: params.minimumExpected,
154+
sender: this.tradeArgs.signer.account.address,
155+
},
156+
this.tradeArgs.solver.state.client,
157+
addresses.dispair,
158+
);
159+
if (taskBytecodeResult.isErr()) {
160+
const errMsg = await errorSnapshot("", taskBytecodeResult.error);
161+
this.spanAttributes["isNodeError"] =
162+
taskBytecodeResult.error.type === EnsureBountyTaskErrorType.ParseError;
163+
this.spanAttributes["error"] = errMsg;
164+
const result = {
165+
type: TradeType.IntraOrderbook,
166+
spanAttributes: this.spanAttributes,
167+
reason: SimulationHaltReason.FailedToGetTaskBytecode,
168+
};
169+
this.spanAttributes["duration"] = performance.now() - this.startTime;
170+
return Result.err(result);
171+
}
172+
task = {
173+
evaluable: {
174+
interpreter: addresses.dispair.interpreter as `0x${string}`,
175+
store: addresses.dispair.store as `0x${string}`,
176+
bytecode: taskBytecodeResult.value,
177+
},
178+
signedContext: [],
153179
};
154-
this.spanAttributes["duration"] = performance.now() - this.startTime;
155-
return Result.err(result);
156180
}
157-
const task = {
158-
evaluable: {
159-
interpreter: addresses.dispair.interpreter as `0x${string}`,
160-
store: addresses.dispair.store as `0x${string}`,
161-
bytecode: taskBytecodeResult.value,
162-
},
163-
signedContext: [],
164-
};
165181

166182
params.rawtx.data = this.getCalldata(task);
167183
return Result.ok(void 0);

src/oracle/index.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -103,47 +103,57 @@ export function recordOracleFailure(healthMap: OracleHealthMap, url: string) {
103103

104104
/**
105105
* ABI parameter definition for a single oracle request body.
106-
* Encodes as: abi.encode(OrderV4, uint256, uint256, address)
107106
*
108-
* Uses the same struct shape as ABI.Orderbook.V5.OrderV4 / IOV2 / EvaluableV4.
107+
* The oracle server (rain.orderbook and st0x-oracle-server) decodes
108+
* with alloy's `abi_decode()` which expects the body to be
109+
* `abi.encode((OrderV4, uint256, uint256, address))` — a SINGLE
110+
* wrapping tuple. viem's `encodeAbiParameters` with 4 top-level
111+
* params produces `abi.encode(OrderV4, uint256, uint256, address)`
112+
* (separate params, different byte layout). We fix the mismatch by
113+
* wrapping all fields in a single tuple parameter.
109114
*/
110115
const oracleSingleAbiParams = [
111116
{
112-
name: "order",
113-
type: "tuple",
117+
type: "tuple" as const,
114118
components: [
115-
{ name: "owner", type: "address" },
116119
{
117-
name: "evaluable",
118-
type: "tuple",
120+
name: "order",
121+
type: "tuple" as const,
119122
components: [
120-
{ name: "interpreter", type: "address" },
121-
{ name: "store", type: "address" },
122-
{ name: "bytecode", type: "bytes" },
123+
{ name: "owner", type: "address" as const },
124+
{
125+
name: "evaluable",
126+
type: "tuple" as const,
127+
components: [
128+
{ name: "interpreter", type: "address" as const },
129+
{ name: "store", type: "address" as const },
130+
{ name: "bytecode", type: "bytes" as const },
131+
],
132+
},
133+
{
134+
name: "validInputs",
135+
type: "tuple[]" as const,
136+
components: [
137+
{ name: "token", type: "address" as const },
138+
{ name: "vaultId", type: "bytes32" as const },
139+
],
140+
},
141+
{
142+
name: "validOutputs",
143+
type: "tuple[]" as const,
144+
components: [
145+
{ name: "token", type: "address" as const },
146+
{ name: "vaultId", type: "bytes32" as const },
147+
],
148+
},
149+
{ name: "nonce", type: "bytes32" as const },
123150
],
124151
},
125-
{
126-
name: "validInputs",
127-
type: "tuple[]",
128-
components: [
129-
{ name: "token", type: "address" },
130-
{ name: "vaultId", type: "bytes32" },
131-
],
132-
},
133-
{
134-
name: "validOutputs",
135-
type: "tuple[]",
136-
components: [
137-
{ name: "token", type: "address" },
138-
{ name: "vaultId", type: "bytes32" },
139-
],
140-
},
141-
{ name: "nonce", type: "bytes32" },
152+
{ name: "inputIOIndex", type: "uint256" as const },
153+
{ name: "outputIOIndex", type: "uint256" as const },
154+
{ name: "counterparty", type: "address" as const },
142155
],
143156
},
144-
{ name: "inputIOIndex", type: "uint256" },
145-
{ name: "outputIOIndex", type: "uint256" },
146-
{ name: "counterparty", type: "address" },
147157
] as const;
148158

149159
// ---------------------------------------------------------------------------
@@ -171,10 +181,12 @@ export async function fetchSignedContext(
171181
// Strip the internal `type` discriminant before ABI encoding
172182
const { type: _type, ...orderStruct } = request.order;
173183
const encoded = encodeAbiParameters(oracleSingleAbiParams, [
174-
orderStruct,
175-
BigInt(request.inputIOIndex),
176-
BigInt(request.outputIOIndex),
177-
request.counterparty,
184+
{
185+
order: orderStruct,
186+
inputIOIndex: BigInt(request.inputIOIndex),
187+
outputIOIndex: BigInt(request.outputIOIndex),
188+
counterparty: request.counterparty,
189+
},
178190
]);
179191
const body = hexToBytes(encoded);
180192

src/state/contracts.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,26 +78,37 @@ export async function resolveVersionContracts(
7878
return undefined;
7979
}
8080

81-
const interpreter = await client
81+
let interpreter = await client
8282
.readContract({
8383
address: addresses.dispair,
8484
functionName: version === "v6" ? "I_INTERPRETER" : "iInterpreter",
8585
abi: version === "v6" ? ABI.Deployer.Primary.DeployerV6 : ABI.Deployer.Primary.Deployer,
8686
})
8787
.catch(() => undefined);
88-
if (!interpreter) {
89-
return undefined;
90-
}
9188

92-
const store = await client
89+
let store = await client
9390
.readContract({
9491
address: addresses.dispair,
9592
functionName: version === "v6" ? "I_STORE" : "iStore",
9693
abi: version === "v6" ? ABI.Deployer.Primary.DeployerV6 : ABI.Deployer.Primary.Deployer,
9794
})
9895
.catch(() => undefined);
99-
if (!store) {
100-
return undefined;
96+
97+
// In Rain V6, the "deployer" address from the rainlang registry may
98+
// actually be the parser, which doesn't expose I_INTERPRETER/I_STORE.
99+
// Fall back to using the dispair address itself for all three fields —
100+
// the actual interpreter/store will be taken from the order's evaluable
101+
// struct at execution time. This allows intra-orderbook clearing
102+
// (which doesn't need the task deployer) to work without a "real"
103+
// deployer address.
104+
if (!interpreter || !store) {
105+
console.warn(
106+
`Could not read interpreter/store from dispair ${addresses.dispair} — ` +
107+
`using fallback. Task bytecode generation will fail; set gasCoveragePercentage="0" ` +
108+
`to skip bounty tasks.`,
109+
);
110+
interpreter = addresses.dispair;
111+
store = addresses.dispair;
101112
}
102113

103114
const result: any = {

0 commit comments

Comments
 (0)