Skip to content

Commit d02feb9

Browse files
carlos-cowmgrabina
andauthored
feat: simulation before sending order (#2774)
Co-authored-by: Martin Grabina <[email protected]>
1 parent f0eab11 commit d02feb9

File tree

4 files changed

+277
-0
lines changed

4 files changed

+277
-0
lines changed

.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ NEXT_PUBLIC_API_BASEURL=https://aave-api-v2.aave.com
1111
NEXT_PUBLIC_SUBGRAPH_API_KEY=
1212
FAMILY_API_KEY=
1313
FAMILY_API_URL=
14+
NEXT_PUBLIC_COW_SIMULATION_ONLY=true

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ NEXT_PUBLIC_SUBGRAPH_API_KEY=
1919
NEXT_PUBLIC_IS_CYPRESS_ENABLED=false
2020
NEXT_PUBLIC_AMPLITUDE_API_KEY=6b28cb736c53d59f0951a50f59597aae
2121
NEXT_PUBLIC_PRIVATE_RPC_ENABLED=false
22+
NEXT_PUBLIC_COW_SIMULATION_ONLY=true
2223

2324

2425

src/components/transactions/Swap/actions/CollateralSwap/CollateralSwapActionsViaCoWAdapters.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
getCowTradingSdkByChainIdAndAppCode,
2222
} from '../../helpers/cow';
2323
import { calculateInstanceAddress } from '../../helpers/cow/adapters.helpers';
24+
import { simulateCollateralSwapPreHook } from '../../helpers/cow/simulation.helpers';
2425
import { useSwapGasEstimation } from '../../hooks/useSwapGasEstimation';
2526
import {
2627
areActionsBlocked,
@@ -32,6 +33,14 @@ import {
3233
} from '../../types';
3334
import { useSwapTokenApproval } from '../approval/useSwapTokenApproval';
3435

36+
const cowSimulationOnlyFlag = process.env.NEXT_PUBLIC_COW_SIMULATION_ONLY;
37+
const COW_SIMULATION_ONLY =
38+
cowSimulationOnlyFlag === 'true'
39+
? true
40+
: cowSimulationOnlyFlag === 'false'
41+
? false
42+
: process.env.NODE_ENV !== 'production';
43+
3544
/**
3645
* Collateral swap via CoW Protocol Flashloan Adapters.
3746
*
@@ -230,6 +239,43 @@ export const CollateralSwapActionsViaCowAdapters = ({
230239
orderPostParams.swapSettings.appData
231240
);
232241

242+
const hooks = orderPostParams.swapSettings.appData?.metadata?.hooks;
243+
console.log('[CoW][CollateralSwap] Hooks before order submission', {
244+
preHooks: hooks?.pre,
245+
postHooks: hooks?.post,
246+
});
247+
if (COW_SIMULATION_ONLY) {
248+
console.info('[CoW][CollateralSwap] Simulation-only mode is active');
249+
const simulationOk = await simulateCollateralSwapPreHook({
250+
chainId: state.chainId,
251+
from: user as `0x${string}`,
252+
preHook: hooks?.pre?.[0],
253+
flashloan: orderPostParams.swapSettings.appData?.metadata?.flashloan,
254+
postHook: hooks?.post?.[0],
255+
settlementContext: {
256+
receiver: orderPostParams.instanceAddress,
257+
buyToken: state.buyAmountToken?.underlyingAddress,
258+
buyAmount: state.buyAmountBigInt,
259+
},
260+
});
261+
console.info('[CoW][CollateralSwap] Simulation result', simulationOk);
262+
if (!simulationOk) {
263+
console.info(
264+
'[CoW][CollateralSwap] Simulation failed; skipping CoW order submission in simulation-only mode'
265+
);
266+
setMainTxState({
267+
txHash: undefined,
268+
loading: false,
269+
success: false,
270+
});
271+
setState({
272+
actionsLoading: false,
273+
});
274+
return;
275+
}
276+
console.info('[CoW][CollateralSwap] Simulation passed; proceeding to post order');
277+
}
278+
233279
// Safe-check in case any param changed between approval and order posting
234280
const instanceAddress = orderPostParams.instanceAddress;
235281
if (instanceAddress !== approvedAddress) {
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import {
2+
encodeAbiParameters,
3+
erc20Abi,
4+
encodeFunctionData,
5+
keccak256,
6+
toHex,
7+
} from 'viem';
8+
import { getPublicClient } from 'wagmi/actions';
9+
10+
import { wagmiConfig } from 'src/ui-config/wagmiConfig';
11+
12+
const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
13+
const MULTICALL3_AGGREGATE3_ABI = [
14+
{
15+
inputs: [
16+
{
17+
components: [
18+
{ internalType: 'address', name: 'target', type: 'address' },
19+
{ internalType: 'bool', name: 'allowFailure', type: 'bool' },
20+
{ internalType: 'bytes', name: 'callData', type: 'bytes' },
21+
],
22+
internalType: 'struct IMulticall3.Call3[]',
23+
name: 'calls',
24+
type: 'tuple[]',
25+
},
26+
],
27+
name: 'aggregate3',
28+
outputs: [
29+
{
30+
components: [
31+
{ internalType: 'bool', name: 'success', type: 'bool' },
32+
{ internalType: 'bytes', name: 'returnData', type: 'bytes' },
33+
],
34+
internalType: 'struct IMulticall3.Result[]',
35+
name: 'returnData',
36+
type: 'tuple[]',
37+
},
38+
],
39+
stateMutability: 'payable',
40+
type: 'function',
41+
},
42+
] as const;
43+
44+
type HookDefinition = {
45+
target?: string;
46+
callData?: string;
47+
gasLimit?: string | number;
48+
};
49+
50+
type FlashloanMetadata = {
51+
amount?: string;
52+
token?: string;
53+
protocolAdapter?: string;
54+
receiver?: string;
55+
};
56+
57+
type OrderSettlementContext = {
58+
receiver?: string;
59+
buyToken?: string;
60+
buyAmount?: bigint;
61+
};
62+
63+
const DEFAULT_BALANCE_SLOT_CANDIDATES = [0n, 1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 9n, 10n, 11n, 12n];
64+
65+
const getBalanceSlotKeys = (owner: string, slotCandidates = DEFAULT_BALANCE_SLOT_CANDIDATES) => {
66+
return slotCandidates.map((slot) => {
67+
const encoded = encodeAbiParameters(
68+
[{ type: 'address' }, { type: 'uint256' }],
69+
[owner as `0x${string}`, slot]
70+
);
71+
return keccak256(encoded);
72+
});
73+
};
74+
75+
const buildBalanceOverride = async ({
76+
chainId,
77+
token,
78+
recipient,
79+
amount,
80+
}: {
81+
chainId: number;
82+
token?: string;
83+
recipient?: string;
84+
amount?: bigint;
85+
}) => {
86+
const normalizedToken = token as `0x${string}` | undefined;
87+
const normalizedRecipient = recipient as `0x${string}` | undefined;
88+
if (!normalizedToken || !normalizedRecipient || amount === undefined) return undefined;
89+
90+
const publicClient = getPublicClient(wagmiConfig, { chainId });
91+
if (!publicClient) return undefined;
92+
93+
let currentBalance = 0n;
94+
try {
95+
currentBalance = await publicClient.readContract({
96+
address: normalizedToken,
97+
functionName: 'balanceOf',
98+
args: [normalizedRecipient],
99+
abi: erc20Abi,
100+
});
101+
} catch (error) {
102+
console.warn('[CoW][CollateralSwap] Could not read current balance for state override', error);
103+
}
104+
105+
const updatedBalance = currentBalance + amount;
106+
const balanceSlots = getBalanceSlotKeys(normalizedRecipient);
107+
108+
const stateDiff: Record<string, string> = {};
109+
balanceSlots.forEach((slot) => {
110+
stateDiff[slot] = toHex(updatedBalance, { size: 32 });
111+
});
112+
113+
return { [normalizedToken]: { stateDiff } };
114+
};
115+
116+
const mergeOverrides = (...overrides: (Record<string, { stateDiff: Record<string, string> }> | undefined)[]) => {
117+
const merged: Record<string, { stateDiff: Record<string, string> }> = {};
118+
overrides.forEach((override) => {
119+
if (!override) return;
120+
Object.entries(override).forEach(([addr, overrideVal]) => {
121+
if (!merged[addr]) {
122+
merged[addr] = { stateDiff: {} };
123+
}
124+
merged[addr].stateDiff = { ...merged[addr].stateDiff, ...overrideVal.stateDiff };
125+
});
126+
});
127+
return Object.keys(merged).length ? merged : undefined;
128+
};
129+
130+
export const simulateCollateralSwapPreHook = async ({
131+
chainId,
132+
from,
133+
preHook,
134+
flashloan,
135+
postHook,
136+
settlementContext,
137+
}: {
138+
chainId: number;
139+
from?: `0x${string}`;
140+
preHook?: HookDefinition;
141+
flashloan?: FlashloanMetadata;
142+
postHook?: HookDefinition;
143+
settlementContext?: OrderSettlementContext;
144+
}) => {
145+
const caller = from;
146+
147+
if (!caller || !preHook?.target || !preHook?.callData) {
148+
console.log('[CoW][CollateralSwap] Skipping preHook simulation, missing data');
149+
return false;
150+
}
151+
152+
const publicClient = getPublicClient(wagmiConfig, { chainId });
153+
if (!publicClient) {
154+
console.warn('[CoW][CollateralSwap] No public client available for simulation');
155+
return;
156+
}
157+
158+
const flashloanOverride = await buildBalanceOverride({
159+
chainId,
160+
token: flashloan?.token,
161+
recipient: flashloan?.protocolAdapter ?? flashloan?.receiver,
162+
amount: flashloan?.amount ? BigInt(flashloan.amount) : undefined,
163+
});
164+
165+
const settlementOverride = await buildBalanceOverride({
166+
chainId,
167+
token: settlementContext?.buyToken,
168+
recipient: settlementContext?.receiver,
169+
amount: settlementContext?.buyAmount,
170+
});
171+
172+
const encodedPreHook = {
173+
from: caller,
174+
to: preHook.target as `0x${string}`,
175+
input: preHook.callData as `0x${string}`,
176+
gas: preHook.gasLimit ? toHex(BigInt(preHook.gasLimit)) : undefined,
177+
};
178+
179+
const encodedPostHook =
180+
postHook?.target && postHook?.callData
181+
? {
182+
from: caller,
183+
to: postHook.target as `0x${string}`,
184+
input: postHook.callData as `0x${string}`,
185+
gas: postHook?.gasLimit !== undefined ? toHex(BigInt(postHook.gasLimit)) : undefined,
186+
}
187+
: undefined;
188+
189+
const callsSequence = [{ ...encodedPreHook, label: 'preHook' }, ...(encodedPostHook ? [{ ...encodedPostHook, label: 'postHook' }] : [])];
190+
191+
const aggregateData = encodeFunctionData({
192+
abi: MULTICALL3_AGGREGATE3_ABI,
193+
functionName: 'aggregate3',
194+
args: [
195+
callsSequence.map((c) => ({
196+
target: c.to as `0x${string}`,
197+
allowFailure: true,
198+
callData: (c as any).input ?? c.data,
199+
})),
200+
],
201+
});
202+
203+
const stateOverrides = mergeOverrides(flashloanOverride, settlementOverride);
204+
205+
const blockStateCall = {
206+
calls: [{ from, to: MULTICALL3_ADDRESS as `0x${string}`, input: aggregateData, label: 'multicall' }],
207+
transactions: [{ from, to: MULTICALL3_ADDRESS as `0x${string}`, data: aggregateData }],
208+
...(stateOverrides ? { stateOverrides } : {}),
209+
};
210+
211+
const simulationPayload = {
212+
parentBlock: 'latest',
213+
blockStateCalls: [blockStateCall],
214+
};
215+
216+
console.log('[CoW][CollateralSwap] PreHook simulation payload', simulationPayload);
217+
218+
try {
219+
const result = await publicClient.request({
220+
method: 'eth_simulateV1',
221+
params: [simulationPayload] as unknown[],
222+
});
223+
console.log('[CoW][CollateralSwap] PreHook simulation result', result);
224+
return true;
225+
} catch (error) {
226+
console.error('[CoW][CollateralSwap] PreHook simulation failed', error);
227+
return false;
228+
}
229+
};

0 commit comments

Comments
 (0)