@rabby-wallet/rabby-bridge 0.0.13-alpha.1 → 0.0.14-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -21
- package/dist/calldata/chains.js +3 -2
- package/dist/calldata/destination.js +18 -2
- package/dist/calldata/destinationSwap.js +6 -3
- package/dist/calldata/destinationUniswap.d.ts +7 -0
- package/dist/calldata/destinationUniswap.js +57 -0
- package/dist/calldata/expect.d.ts +3 -1
- package/dist/calldata/expect.js +9 -10
- package/dist/calldata/lifi/across/acrossV4Erc20Min.js +1 -0
- package/dist/calldata/lifi/across/acrossV4NativeMin.js +1 -0
- package/dist/calldata/lifi/params.js +2 -0
- package/dist/calldata/protocols.js +2 -1
- package/dist/calldata/relay/calls.d.ts +1 -1
- package/dist/calldata/relay/calls.js +5 -1
- package/dist/calldata/relay/index.js +5 -2
- package/dist/calldata/relay/multicall.js +1 -1
- package/dist/calldata/relay/nestedDeposit.d.ts +8 -0
- package/dist/calldata/relay/nestedDeposit.js +107 -0
- package/dist/calldata/relay/permit2TransferAndMulticall.js +1 -1
- package/dist/calldata/relay/permit3009TransferAndMulticall.js +1 -1
- package/dist/calldata/relay/permit3009TransferAndMulticallSigned.js +1 -1
- package/dist/calldata/relay/permitTransferAndMulticall.js +1 -1
- package/dist/calldata/relay/permitTransferAndMulticallLegacy.js +1 -1
- package/dist/calldata/relay/transferAndMulticall.js +1 -1
- package/dist/calldata/socket/addresses.d.ts +9 -0
- package/dist/calldata/socket/addresses.js +9 -0
- package/dist/calldata/socket/protocols/cctp.js +4 -3
- package/dist/calldata/socket/protocols/eco.d.ts +1 -1
- package/dist/calldata/socket/protocols/eco.js +32 -3
- package/dist/calldata/socket/protocols/index.js +4 -3
- package/dist/calldata/socket/protocols/native.js +1 -0
- package/dist/calldata/types.d.ts +4 -0
- package/dist/chains.d.ts +2 -1
- package/dist/chains.js +2 -0
- package/dist/data.d.ts +3 -0
- package/dist/data.js +3 -0
- package/dist/errors/constants.d.ts +15 -0
- package/dist/errors/constants.js +15 -0
- package/dist/errors/index.d.ts +3 -4
- package/dist/errors/index.js +1 -1
- package/dist/errors/sanitize.js +1 -1
- package/dist/providers/relay.d.ts +3 -0
- package/dist/providers/relay.js +52 -0
- package/dist/providers/socket.js +2 -1
- package/dist/quote.d.ts +10 -5
- package/dist/quote.js +61 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,10 +20,10 @@ See [reachable route discovery](tests/fixtures/route-discovery-20261104/README.m
|
|
|
20
20
|
|
|
21
21
|
## Validation rollout
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`getQuoteListWithTx`
|
|
26
|
-
|
|
23
|
+
Router/Spender allowlists and required calldata checks are always enforced.
|
|
24
|
+
Reported calldata failures block: `validateBridgeTx` and `buildTx` throw
|
|
25
|
+
`InvalidBridgeCalldataError`, and `getQuoteListWithTx` filters the failed quote.
|
|
26
|
+
No provider API or RPC requests are made.
|
|
27
27
|
|
|
28
28
|
```ts
|
|
29
29
|
const quotes = await getQuoteListWithTx(aggregatorId, {
|
|
@@ -33,7 +33,7 @@ const quotes = await getQuoteListWithTx(aggregatorId, {
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
- Quotes and transactions come from the existing backend flow, including Relay
|
|
36
|
-
and Socket
|
|
36
|
+
and Socket. The SDK no longer accepts `providerClient` or exposes
|
|
37
37
|
a direct-provider client/fetch adapter.
|
|
38
38
|
- Router/Spender mismatches, unsupported source chains/aggregators,
|
|
39
39
|
missing required slippage, missing backend results and backend request failures
|
|
@@ -42,7 +42,7 @@ const quotes = await getQuoteListWithTx(aggregatorId, {
|
|
|
42
42
|
checked locally. Missing source fields and mismatches are reported.
|
|
43
43
|
- Socket quote IDs (including CCTP), RFQ outer/vault IDs and Relay multicall
|
|
44
44
|
request IDs are compared locally when a selected quote provides a matching
|
|
45
|
-
identifier. Mismatches
|
|
45
|
+
identifier. Mismatches block without changing the three field flags;
|
|
46
46
|
missing/unrecognized keys are skipped. See the [binding limits](src/calldata/README.md).
|
|
47
47
|
- Known destination tokens, minimum amounts and chains are compared. Unknown
|
|
48
48
|
destination paths remain unverified and are reported with available method,
|
|
@@ -50,7 +50,7 @@ const quotes = await getQuoteListWithTx(aggregatorId, {
|
|
|
50
50
|
- Locally derivable final receivers are compared. External order data is skipped
|
|
51
51
|
without a provider request; a skipped receiver is never counted as verified.
|
|
52
52
|
- Parse failures, known mismatches and unexpected missing fields are reported with
|
|
53
|
-
`
|
|
53
|
+
`pass: false, blocking: true`.
|
|
54
54
|
- Relay and Socket RFQ/CCTP external destination fields are skipped rather than
|
|
55
55
|
fetched. LayerSwap uses its local receiver; no background order lookup runs.
|
|
56
56
|
Skipping an external receiver does not claim order-hash or provider verification.
|
|
@@ -61,16 +61,15 @@ const quotes = await getQuoteListWithTx(aggregatorId, {
|
|
|
61
61
|
Actual mismatches retain their existing error codes. Repeated failures are not
|
|
62
62
|
deduplicated, sampled or rate-limited by the SDK.
|
|
63
63
|
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
calldata enforcement and never skips a missing Router allowlist.
|
|
64
|
+
`validateBridgeTx` accepts `{ onError }` after the optional expectation as its sixth
|
|
65
|
+
argument. A missing Router allowlist or an approval address without a configured
|
|
66
|
+
spender is rejected.
|
|
68
67
|
|
|
69
68
|
`validateBridgeTx` now returns `BridgeValidationResult` instead of `true`:
|
|
70
69
|
|
|
71
70
|
```ts
|
|
72
71
|
const result = validateBridgeTx(aggregatorId, chainId, bridgeId, tx, expectation);
|
|
73
|
-
// result.pass
|
|
72
|
+
// Throws on required-check failures; otherwise result.pass is true.
|
|
74
73
|
// result.fromPass / result.toPass / result.receiverPass
|
|
75
74
|
// result.unverifiedFields / result.errors
|
|
76
75
|
```
|
|
@@ -79,12 +78,13 @@ The three field flags are true only when all required local comparisons for that
|
|
|
79
78
|
group succeeded. False means a failure or an incomplete check; `errors` and
|
|
80
79
|
`unverifiedFields` distinguish them. Known skipped fields can leave `errors` empty
|
|
81
80
|
while a field flag is false. Without an expectation, field flags are false.
|
|
82
|
-
|
|
81
|
+
Validation rejects reportable failures, not every false field flag: known skipped
|
|
82
|
+
fields remain allowed. Router rejection still throws
|
|
83
83
|
`InvalidBridgeContractError`; its sanitized detail carries `pass: false`,
|
|
84
84
|
`routerPass: false` and `blocking: true`.
|
|
85
85
|
|
|
86
|
-
Raw decode/calldata-check results retain `ok: false` on failure.
|
|
87
|
-
details include all five pass flags, path diagnostics
|
|
86
|
+
Raw decode/calldata-check results retain `ok: false` on failure. Failure
|
|
87
|
+
details include all five pass flags, path diagnostics and `blocking`.
|
|
88
88
|
These are failure-event/path counts, not distinct user counts or a denominator for
|
|
89
89
|
all successful transactions. See error reporting below for failures that still throw.
|
|
90
90
|
|
|
@@ -103,7 +103,9 @@ legacy `reason` fields remain available.
|
|
|
103
103
|
- Thrown SDK errors extend `BridgeError`. Use `error.toJSON()` for a report.
|
|
104
104
|
- `onError` receives failures filtered from `getQuote` / `getQuoteListWithTx`,
|
|
105
105
|
plus local validation failures and unexpected missing fields from transaction checks.
|
|
106
|
-
|
|
106
|
+
Calldata failures are reported once before throwing; do not report those
|
|
107
|
+
errors again in a caller's catch when an `onError` reporter is supplied.
|
|
108
|
+
Other directly thrown errors are handled by the caller.
|
|
107
109
|
Reporting callback failures do not affect validation or quote filtering.
|
|
108
110
|
|
|
109
111
|
```ts
|
|
@@ -138,17 +140,19 @@ reports merely because a known local limitation prevents verification.
|
|
|
138
140
|
|
|
139
141
|
Use `toBridgeErrorReport(resultOrError)` to convert a calldata check,
|
|
140
142
|
transaction validation result, detailed decode result or exception into a report.
|
|
141
|
-
|
|
142
|
-
`errors` are nonempty. Results without reportable failures return `undefined`,
|
|
143
|
+
Results without reportable failures return `undefined`,
|
|
143
144
|
including known skipped destination fields.
|
|
144
145
|
The `onError` callback and `BridgeError.toJSON()` use the same
|
|
145
146
|
runtime allowlist. Do not upload the entire check, exception, parsed transaction,
|
|
146
147
|
request parameters or quote response.
|
|
147
148
|
|
|
148
149
|
```ts
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
try {
|
|
151
|
+
validateBridgeTx(aggregatorId, chainId, bridgeId, tx, expectation);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
reportToBusiness(toBridgeErrorReport(error));
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
152
156
|
```
|
|
153
157
|
|
|
154
158
|
Reports can retain recognized aggregator/bridge/method labels, source/destination
|
package/dist/calldata/chains.js
CHANGED
|
@@ -23,11 +23,12 @@ export const evmChainId = (chain) => {
|
|
|
23
23
|
return CHAIN_IDS[chainEnum ? CHAIN_ENUM_TO_SERVER_ID[chainEnum] : chain.toLowerCase()];
|
|
24
24
|
};
|
|
25
25
|
export const sourceChainMismatch = (expected, actual) => {
|
|
26
|
-
|
|
26
|
+
const expectedId = evmChainId(expected);
|
|
27
|
+
if (actual === undefined || expectedId === undefined)
|
|
27
28
|
return false;
|
|
28
29
|
try {
|
|
29
30
|
return (typeof actual === 'number' && !Number.isSafeInteger(actual)) ||
|
|
30
|
-
|
|
31
|
+
BigInt(actual) !== expectedId;
|
|
31
32
|
}
|
|
32
33
|
catch {
|
|
33
34
|
return true;
|
|
@@ -4,6 +4,7 @@ import { isZeroWord, readSelector } from './hex.js';
|
|
|
4
4
|
import { NATIVE_TOKEN, WRAPPED_NATIVE_TOKENS, subtractFees } from './protocols.js';
|
|
5
5
|
import { calldataDiagnostics } from './diagnostics.js';
|
|
6
6
|
import { ALLOWANCE_HOLDER, HYPEREVM_LIFI_ROUTER, readLifiSwap, readSettlerSwap } from './destinationSwap.js';
|
|
7
|
+
import { AVALANCHE_UNIVERSAL_ROUTER, readUniversalRouterSwap } from './destinationUniswap.js';
|
|
7
8
|
export const destinationCall = {
|
|
8
9
|
name: 'call', kind: 'tuple', components: [
|
|
9
10
|
{ name: 'target', kind: 'address' }, { name: 'data', kind: 'bytes' }, { name: 'value', kind: 'uint' },
|
|
@@ -47,6 +48,7 @@ const ACROSS_MESSAGE_HANDLERS = {
|
|
|
47
48
|
143: '0xec41f75c686e376ab2a4f18bde263ab5822c4511',
|
|
48
49
|
999: '0x5e7840e06faccb6d1c3b5f5e0d1d3d07f2829bba',
|
|
49
50
|
4663: '0xa8ad2e87e2043711d8bec77e8bc3e2683c0ab6bd',
|
|
51
|
+
9745: '0x5e7840e06faccb6d1c3b5f5e0d1d3d07f2829bba',
|
|
50
52
|
43114: '0x9610954acdca5ff7905f051a040ce33fe613c60e',
|
|
51
53
|
};
|
|
52
54
|
const readAcrossMessage = (parsed, message) => {
|
|
@@ -88,8 +90,8 @@ const readAcrossMessage = (parsed, message) => {
|
|
|
88
90
|
const lifiRouter = parsed.destinationChainId === 999n ? HYPEREVM_LIFI_ROUTER : undefined;
|
|
89
91
|
const possibleRefundTokens = new Set();
|
|
90
92
|
const refundReceivers = [];
|
|
91
|
-
for (
|
|
92
|
-
const call = asRecord(
|
|
93
|
+
for (let index = 0; index < calls.length; index++) {
|
|
94
|
+
const call = asRecord(calls[index]);
|
|
93
95
|
const target = asString(call.target);
|
|
94
96
|
const data = asString(call.callData);
|
|
95
97
|
const value = asBigint(call.value);
|
|
@@ -151,6 +153,20 @@ const readAcrossMessage = (parsed, message) => {
|
|
|
151
153
|
const drain = decodeCall([{ name: 'token', kind: 'address' }, { name: 'receiver', kind: 'address' }], data);
|
|
152
154
|
const token = asString(drain.token);
|
|
153
155
|
const amount = balances.get(token) ?? 0n;
|
|
156
|
+
if (parsed.destinationChainId === 43114n && asString(drain.receiver) === AVALANCHE_UNIVERSAL_ROUTER) {
|
|
157
|
+
if (index + 1 >= calls.length)
|
|
158
|
+
return parsed;
|
|
159
|
+
const next = asRecord(calls[index + 1]);
|
|
160
|
+
if (asString(next.target) !== AVALANCHE_UNIVERSAL_ROUTER || asBigint(next.value) !== 0n)
|
|
161
|
+
return parsed;
|
|
162
|
+
const swap = readUniversalRouterSwap(asString(next.callData), handler);
|
|
163
|
+
if (!swap || swap.inputToken !== token || swap.inputAmount !== amount)
|
|
164
|
+
return parsed;
|
|
165
|
+
balances.set(token, 0n);
|
|
166
|
+
balances.set(swap.outputToken, (balances.get(swap.outputToken) ?? 0n) + swap.minOutputAmount);
|
|
167
|
+
index++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
154
170
|
if (possibleRefundTokens.delete(token))
|
|
155
171
|
refundReceivers.push(asString(drain.receiver));
|
|
156
172
|
if (amount > 0n)
|
|
@@ -5,7 +5,9 @@ import { methods as lifiSwaps } from './lifi/swap/index.js';
|
|
|
5
5
|
export const ALLOWANCE_HOLDER = '0x0000000000001ff3684f28c67538d4d072c22734';
|
|
6
6
|
// Reviewed deployments, not dynamic registry lookups. Evidence and renewal limits:
|
|
7
7
|
// tests/fixtures/route-discovery-20261104/evidence/base-settler-verification.json
|
|
8
|
+
// tests/fixtures/reported-20261111-evidence.json (Ethereum)
|
|
8
9
|
const SETTLERS = {
|
|
10
|
+
1: '0xaddc26a3854feabfd20d6bf658254088d83a425a',
|
|
9
11
|
8453: '0x7d6839415693801e94cdc4a7d732c9a3fdbbd1d7',
|
|
10
12
|
999: '0xf58d4122251f2ace689f257eaf2688883e30fc6e',
|
|
11
13
|
};
|
|
@@ -52,7 +54,8 @@ export const readSettlerSwap = (data, chain, recipient, balances) => {
|
|
|
52
54
|
// These reviewed actions cannot mutate/zeroize slippage. CHECK_SLIPPAGE, SELECT
|
|
53
55
|
// and unreviewed actions must not bypass the final enforced transfer to Handler.
|
|
54
56
|
if (!actions.length || readSelector(actions[0]) !== '0xc1fb425e' ||
|
|
55
|
-
actions.slice(1).some((action) => !['0x38c9c147', '0x8d68a156', '0x5f80abae'].includes(readSelector(action) ?? '')
|
|
57
|
+
actions.slice(1).some((action) => !['0x38c9c147', '0x8d68a156', '0x5f80abae'].includes(readSelector(action) ?? '') &&
|
|
58
|
+
!(chain === 1n && readSelector(action) === '0xf61460f9')))
|
|
56
59
|
return undefined;
|
|
57
60
|
const funding = decodeCall([
|
|
58
61
|
{ name: 'recipient', kind: 'address' }, { name: 'token', kind: 'address' },
|
|
@@ -64,8 +67,8 @@ export const readSettlerSwap = (data, chain, recipient, balances) => {
|
|
|
64
67
|
return undefined;
|
|
65
68
|
let inputAmount = asBigint(args.amount);
|
|
66
69
|
const maxUint256 = (1n << 256n) - 1n;
|
|
67
|
-
// shortcut: only HyperEVM
|
|
68
|
-
if (chain === 999n && inputAmount > maxUint256 - 1000000n) {
|
|
70
|
+
// shortcut: only reviewed Ethereum/HyperEVM 100%-balance encoding; review other ratios before support.
|
|
71
|
+
if ((chain === 1n || chain === 999n) && inputAmount > maxUint256 - 1000000n) {
|
|
69
72
|
if (inputAmount !== maxUint256)
|
|
70
73
|
return undefined;
|
|
71
74
|
inputAmount = balances?.get(asString(args.token)) ?? 0n;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const AVALANCHE_UNIVERSAL_ROUTER = "0x661e93cca42afacb172121ef892830ca3b70f08d";
|
|
2
|
+
export declare const readUniversalRouterSwap: (data: string, recipient: string) => {
|
|
3
|
+
inputToken: string;
|
|
4
|
+
inputAmount: bigint;
|
|
5
|
+
outputToken: string;
|
|
6
|
+
minOutputAmount: bigint;
|
|
7
|
+
} | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { asArray, asBigint, asRecord, asString, decodeCall, decodeParameters } from './abi.js';
|
|
2
|
+
import { readSelector } from './hex.js';
|
|
3
|
+
import { isNative, NATIVE_TOKEN } from './protocols.js';
|
|
4
|
+
// Reviewed UniversalRouter v2.1.2; see reported-20261111-evidence.json.
|
|
5
|
+
export const AVALANCHE_UNIVERSAL_ROUTER = '0x661e93cca42afacb172121ef892830ca3b70f08d';
|
|
6
|
+
export const readUniversalRouterSwap = (data, recipient) => {
|
|
7
|
+
if (readSelector(data) !== '0x24856bc3')
|
|
8
|
+
return undefined;
|
|
9
|
+
const execution = decodeCall([
|
|
10
|
+
{ name: 'commands', kind: 'bytes' },
|
|
11
|
+
{ name: 'inputs', kind: 'array', item: { name: 'input', kind: 'bytes' } },
|
|
12
|
+
], data);
|
|
13
|
+
const inputs = asArray(execution.inputs);
|
|
14
|
+
// No optional/reverting commands or unrelated transfers may bypass the payout.
|
|
15
|
+
if (asString(execution.commands) !== '0x10' || inputs.length !== 1)
|
|
16
|
+
return undefined;
|
|
17
|
+
const plan = decodeParameters([
|
|
18
|
+
{ name: 'actions', kind: 'bytes' },
|
|
19
|
+
{ name: 'params', kind: 'array', item: { name: 'param', kind: 'bytes' } },
|
|
20
|
+
], asString(inputs[0]));
|
|
21
|
+
const params = asArray(plan.params).map(asString);
|
|
22
|
+
if (asString(plan.actions) !== '0x070b0e' || params.length !== 3)
|
|
23
|
+
return undefined;
|
|
24
|
+
const swap = asRecord(decodeParameters([{ name: 'swap', kind: 'tuple', components: [
|
|
25
|
+
{ name: 'currencyIn', kind: 'address' },
|
|
26
|
+
{ name: 'path', kind: 'array', item: { name: 'hop', kind: 'tuple', components: [
|
|
27
|
+
{ name: 'intermediateCurrency', kind: 'address' }, { name: 'fee', kind: 'uint' },
|
|
28
|
+
{ name: 'tickSpacing', kind: 'uint' }, { name: 'hooks', kind: 'address' }, { name: 'hookData', kind: 'bytes' },
|
|
29
|
+
] } },
|
|
30
|
+
{ name: 'minHopPriceX36', kind: 'array', item: { name: 'price', kind: 'uint' } },
|
|
31
|
+
{ name: 'amountIn', kind: 'uint' }, { name: 'amountOutMinimum', kind: 'uint' },
|
|
32
|
+
] }], params[0]).swap);
|
|
33
|
+
const path = asArray(swap.path).map(asRecord);
|
|
34
|
+
const prices = asArray(swap.minHopPriceX36);
|
|
35
|
+
// shortcut: one ERC20 hop without hooks; review additional V4 plans before support.
|
|
36
|
+
if (path.length !== 1 || prices.length > 1)
|
|
37
|
+
return undefined;
|
|
38
|
+
const hop = path[0];
|
|
39
|
+
const inputToken = asString(swap.currencyIn), outputToken = asString(hop.intermediateCurrency);
|
|
40
|
+
const inputAmount = asBigint(swap.amountIn), minOutputAmount = asBigint(swap.amountOutMinimum);
|
|
41
|
+
if (isNative(inputToken) || isNative(outputToken) || inputToken === outputToken ||
|
|
42
|
+
asString(hop.hooks) !== NATIVE_TOKEN || asString(hop.hookData) !== '0x' ||
|
|
43
|
+
asBigint(hop.fee) > 1000000n || asBigint(hop.tickSpacing) <= 0n || asBigint(hop.tickSpacing) > 32767n ||
|
|
44
|
+
inputAmount <= 0n || inputAmount >= (1n << 128n) || minOutputAmount <= 0n || minOutputAmount >= (1n << 128n))
|
|
45
|
+
return undefined;
|
|
46
|
+
const settle = decodeParameters([
|
|
47
|
+
{ name: 'currency', kind: 'address' }, { name: 'amount', kind: 'uint' }, { name: 'payerIsUser', kind: 'bool' },
|
|
48
|
+
], params[1]);
|
|
49
|
+
const take = decodeParameters([
|
|
50
|
+
{ name: 'currency', kind: 'address' }, { name: 'recipient', kind: 'address' }, { name: 'amount', kind: 'uint' },
|
|
51
|
+
], params[2]);
|
|
52
|
+
// OPEN_DELTA (0) settles the swap's debt from Router and takes all output back to Handler.
|
|
53
|
+
if (asString(settle.currency) !== inputToken || asBigint(settle.amount) !== 0n || settle.payerIsUser !== false ||
|
|
54
|
+
asString(take.currency) !== outputToken || asString(take.recipient) !== recipient || asBigint(take.amount) !== 0n)
|
|
55
|
+
return undefined;
|
|
56
|
+
return { inputToken, inputAmount, outputToken, minOutputAmount };
|
|
57
|
+
};
|
|
@@ -10,6 +10,8 @@ export type BridgeCallExpectation = {
|
|
|
10
10
|
toTokenRawAmount?: string;
|
|
11
11
|
/** Selected backend quote key; only protocol identifiers with known semantics are compared. */
|
|
12
12
|
quoteKey?: string | Record<string, unknown>;
|
|
13
|
+
/** Relay `protocol.v2.orderData` captured with the transaction being signed. */
|
|
14
|
+
orderData?: unknown;
|
|
13
15
|
/** 小数滑点。0.01 表示 1%。 */
|
|
14
16
|
slippage?: string | number;
|
|
15
17
|
};
|
|
@@ -21,7 +23,7 @@ export type CompareMask = {
|
|
|
21
23
|
receiver: boolean;
|
|
22
24
|
};
|
|
23
25
|
export declare const knownCompare: (parsed: ParsedBridgeCall) => CompareMask;
|
|
24
|
-
/**
|
|
26
|
+
/** Source payment is capped at 100.1% of local intent; smaller payments are allowed. */
|
|
25
27
|
export declare const sourceAmountMatches: (actual: bigint | undefined, expected: bigint) => boolean;
|
|
26
28
|
export declare const expectationFailures: (parsed: ParsedBridgeCall, expectation: BridgeCallExpectation, chainId: string, mask: CompareMask) => BridgeErrorDefinition[];
|
|
27
29
|
export declare const finishCheck: (aggregatorId: string, parsed: ParsedBridgeCall, structural: BridgeErrorDefinition | undefined, expectation: BridgeCallExpectation | undefined, chainId: string, mask: CompareMask) => BridgeCheck;
|
package/dist/calldata/expect.js
CHANGED
|
@@ -55,9 +55,9 @@ const selectedQuoteIdentifier = (quoteKey) => {
|
|
|
55
55
|
const match = /^(0x[0-9a-f]{64})(?::[^:]+)?$/i.exec(key.quote_id);
|
|
56
56
|
return match?.[1].toLowerCase();
|
|
57
57
|
};
|
|
58
|
-
/**
|
|
59
|
-
export const sourceAmountMatches = (actual, expected) => actual !== undefined &&
|
|
60
|
-
/** Allow
|
|
58
|
+
/** Source payment is capped at 100.1% of local intent; smaller payments are allowed. */
|
|
59
|
+
export const sourceAmountMatches = (actual, expected) => actual !== undefined && actual >= 0n && expected >= 0n && actual * 1000n <= expected * 1001n;
|
|
60
|
+
/** Allow up to 5% below the slippage-adjusted minimum; higher guarantees always pass. */
|
|
61
61
|
const minReceiveMatches = (minReceive, quoted, slippage) => {
|
|
62
62
|
if (minReceive <= 0n || quoted <= 0n || slippage < 0 || slippage >= 1) {
|
|
63
63
|
return false;
|
|
@@ -67,11 +67,7 @@ const minReceiveMatches = (minReceive, quoted, slippage) => {
|
|
|
67
67
|
if (minimum <= 0n) {
|
|
68
68
|
return false;
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
const difference = minReceive > minimum ? minReceive - minimum : minimum - minReceive;
|
|
74
|
-
return difference * 20n <= minimum;
|
|
70
|
+
return minReceive * 20n >= minimum * 19n;
|
|
75
71
|
};
|
|
76
72
|
export const expectationFailures = (parsed, expectation, chainId, mask) => {
|
|
77
73
|
const reasons = [];
|
|
@@ -116,9 +112,12 @@ export const expectationFailures = (parsed, expectation, chainId, mask) => {
|
|
|
116
112
|
reasons.push(BRIDGE_ERRORS.MIN_RECEIVE_MISMATCH);
|
|
117
113
|
}
|
|
118
114
|
}
|
|
119
|
-
if (mask.receiver && !sameToken(parsed.receiver, expectation.userAddress, chainId)) {
|
|
115
|
+
if (mask.receiver && !parsed.fixedReceiver && !sameToken(parsed.receiver, expectation.userAddress, chainId)) {
|
|
120
116
|
reasons.push(BRIDGE_ERRORS.RECEIVER_MISMATCH);
|
|
121
117
|
}
|
|
118
|
+
if (parsed.refundRecipients?.some((recipient) => recipient.toLowerCase() !== expectation.userAddress.toLowerCase())) {
|
|
119
|
+
reasons.push(BRIDGE_ERRORS.REFUND_RECIPIENT_MISMATCH);
|
|
120
|
+
}
|
|
122
121
|
return reasons;
|
|
123
122
|
};
|
|
124
123
|
export const finishCheck = (aggregatorId, parsed, structural, expectation, chainId, mask) => {
|
|
@@ -148,7 +147,7 @@ export const finishCheck = (aggregatorId, parsed, structural, expectation, chain
|
|
|
148
147
|
minOutputAmount: mask.toAmount && expectation?.toTokenRawAmount !== undefined,
|
|
149
148
|
receiver: mask.receiver,
|
|
150
149
|
}).filter(([, checked]) => !expectation || !checked).map(([field]) => field);
|
|
151
|
-
if (expectation?.toChainId &&
|
|
150
|
+
if (expectation?.toChainId && parsed.destinationChainId === undefined) {
|
|
152
151
|
unverifiedFields.push('destinationChainId');
|
|
153
152
|
}
|
|
154
153
|
if (errors.length) {
|
|
@@ -19,6 +19,7 @@ export const startBridgeTokensViaAcrossV4ERC20Min = {
|
|
|
19
19
|
sourceAmount: asBigint(args.inputAmount),
|
|
20
20
|
destinationChainId: asBigint(parameters.destinationChainId),
|
|
21
21
|
receiver: accountFromBytes32(asString(parameters.receiver)),
|
|
22
|
+
refundRecipients: [accountFromBytes32(asString(parameters.depositor))],
|
|
22
23
|
outputToken: accountFromBytes32(asString(parameters.receivingAssetId)),
|
|
23
24
|
minOutputAmount: asBigint(parameters.outputAmount),
|
|
24
25
|
};
|
|
@@ -15,6 +15,7 @@ export const startBridgeTokensViaAcrossV4NativeMin = {
|
|
|
15
15
|
sourceAmount: value,
|
|
16
16
|
destinationChainId: asBigint(parameters.destinationChainId),
|
|
17
17
|
receiver: accountFromBytes32(asString(parameters.receiver)),
|
|
18
|
+
refundRecipients: [accountFromBytes32(asString(parameters.depositor))],
|
|
18
19
|
outputToken: accountFromBytes32(asString(parameters.receivingAssetId)),
|
|
19
20
|
minOutputAmount: asBigint(parameters.outputAmount),
|
|
20
21
|
};
|
|
@@ -164,6 +164,7 @@ export const applyBridgeData = (method, raw) => {
|
|
|
164
164
|
};
|
|
165
165
|
export const applyAcrossData = (parsed, raw) => {
|
|
166
166
|
const data = asRecord(raw);
|
|
167
|
+
parsed.refundRecipients = [accountFromBytes32(asString(data.refundAddress))];
|
|
167
168
|
parsed.receiver = accountFromBytes32(asString(data.receiverAddress));
|
|
168
169
|
parsed.outputToken = accountFromBytes32(asString(data.receivingAssetId));
|
|
169
170
|
parsed.minOutputAmount = asBigint(data.outputAmount);
|
|
@@ -258,6 +259,7 @@ export const applyPolymerData = (parsed, raw) => {
|
|
|
258
259
|
};
|
|
259
260
|
export const applyLifiIntentData = (parsed, raw) => {
|
|
260
261
|
const data = asRecord(raw);
|
|
262
|
+
parsed.refundRecipients = [asString(data.depositAndRefundAddress)];
|
|
261
263
|
parsed.receiver = accountFromBytes32(asString(data.recipient));
|
|
262
264
|
parsed.outputToken = accountFromBytes32(asString(data.outputToken));
|
|
263
265
|
parsed.minOutputAmount = (parsed.bridgeAmount ?? 0n) * asBigint(data.outputAmountMultiplier) / 10n ** 18n;
|
|
@@ -16,7 +16,7 @@ export const isNative = (token) => token === NATIVE_TOKEN || token.toLowerCase()
|
|
|
16
16
|
// Circle CCTP domains and Wormhole IDs are different namespaces from EVM chain IDs.
|
|
17
17
|
export const CCTP_CHAINS = {
|
|
18
18
|
0: 1n, 1: 43114n, 2: 10n, 3: 42161n, 6: 8453n, 7: 137n,
|
|
19
|
-
10: 130n, 11: 59144n, 13: 146n, 15: 143n, 19: 999n,
|
|
19
|
+
10: 130n, 11: 59144n, 13: 146n, 15: 143n, 19: 999n, 21: 57073n,
|
|
20
20
|
};
|
|
21
21
|
export const WORMHOLE_CHAINS = {
|
|
22
22
|
2: 1n, 4: 56n, 5: 137n, 6: 43114n, 23: 42161n, 24: 10n,
|
|
@@ -48,6 +48,7 @@ export const CCTP_USDC = {
|
|
|
48
48
|
146: '0x29219dd400f2bf60e5a23d13be72b486d4038894',
|
|
49
49
|
999: '0xb88339cb7199b77e23db6e890353e22632ba630f',
|
|
50
50
|
59144: '0x176211869ca2b568f2a7d4ee941e073a821ee1ff',
|
|
51
|
+
57073: '0x2d270e6886d130d724215a266106e6832161eaed',
|
|
51
52
|
};
|
|
52
53
|
export const subtractFees = (amount, ...fees) => {
|
|
53
54
|
const total = fees.reduce((sum, fee) => sum + fee, 0n);
|
|
@@ -3,7 +3,7 @@ import type { ParsedBridgeCall } from '../types.js';
|
|
|
3
3
|
export declare const callTuple: AbiParam;
|
|
4
4
|
export declare const callsParam: AbiParam;
|
|
5
5
|
export declare const requestIdFromMetadata: (metadata: string) => string | undefined;
|
|
6
|
-
export declare const relayCall: (method: string, metadata: string, value: bigint, source?: {
|
|
6
|
+
export declare const relayCall: (method: string, metadata: string, value: bigint, calls: AbiValue, source?: {
|
|
7
7
|
token?: string;
|
|
8
8
|
amount?: bigint;
|
|
9
9
|
}) => ParsedBridgeCall;
|
|
@@ -2,6 +2,7 @@ import { BRIDGE_ERRORS, BridgeError } from '../../errors/index.js';
|
|
|
2
2
|
import { asBigint, asRecord, asString } from '../abi.js';
|
|
3
3
|
import { isZeroWord } from '../hex.js';
|
|
4
4
|
import { NATIVE_TOKEN } from '../protocols.js';
|
|
5
|
+
import { nestedRelayOrder } from './nestedDeposit.js';
|
|
5
6
|
export const callTuple = {
|
|
6
7
|
name: 'call',
|
|
7
8
|
kind: 'tuple',
|
|
@@ -25,13 +26,16 @@ export const requestIdFromMetadata = (metadata) => {
|
|
|
25
26
|
const requestId = `0x${body.slice(0, 64)}`;
|
|
26
27
|
return isZeroWord(requestId) ? undefined : requestId;
|
|
27
28
|
};
|
|
28
|
-
export const relayCall = (method, metadata, value, source) => ({
|
|
29
|
+
export const relayCall = (method, metadata, value, calls, source) => ({
|
|
29
30
|
aggregatorId: 'relay',
|
|
30
31
|
method,
|
|
31
32
|
kind: 'deposit',
|
|
32
33
|
requestId: requestIdFromMetadata(metadata),
|
|
33
34
|
sourceToken: source?.token ?? (value > 0n ? NATIVE_TOKEN : undefined),
|
|
34
35
|
sourceAmount: source?.amount && source.amount > 0n ? source.amount : value,
|
|
36
|
+
...nestedRelayOrder(calls, method === 'transferAndMulticall' && value === 0n && source?.token && source.amount !== undefined
|
|
37
|
+
? { token: source.token, amount: source.amount }
|
|
38
|
+
: method === 'multicall' && value > 0n ? { token: NATIVE_TOKEN, amount: value } : undefined),
|
|
35
39
|
});
|
|
36
40
|
const amountOf = (record) => {
|
|
37
41
|
const amount = record.amount ?? record.value;
|
|
@@ -10,6 +10,7 @@ import { permit3009TransferAndMulticallSigned } from './permit3009TransferAndMul
|
|
|
10
10
|
import { permitTransferAndMulticall } from './permitTransferAndMulticall.js';
|
|
11
11
|
import { permitTransferAndMulticallLegacy } from './permitTransferAndMulticallLegacy.js';
|
|
12
12
|
import { transferAndMulticall } from './transferAndMulticall.js';
|
|
13
|
+
import { getBridgeRouter } from '../../list.js';
|
|
13
14
|
import { relayFailure } from './validate.js';
|
|
14
15
|
import { BRIDGE_ERRORS } from '../../errors/index.js';
|
|
15
16
|
const METHODS = [
|
|
@@ -37,6 +38,8 @@ export const validateRelay = (data, value, expectation, chainId = '') => {
|
|
|
37
38
|
if (!hit.ok) {
|
|
38
39
|
return { ...hit, aggregatorId: 'relay' };
|
|
39
40
|
}
|
|
40
|
-
return finishCheck('relay', hit.parsed,
|
|
41
|
-
? BRIDGE_ERRORS.
|
|
41
|
+
return finishCheck('relay', hit.parsed, hit.parsed.innerTarget && chainId && hit.parsed.innerTarget !== getBridgeRouter('relay', chainId)
|
|
42
|
+
? BRIDGE_ERRORS.RELAY_CANONICAL_DEPOSITORY_MISMATCH
|
|
43
|
+
: expectation && hit.parsed.depositor !== undefined && hit.parsed.depositor !== expectation.userAddress.toLowerCase()
|
|
44
|
+
? BRIDGE_ERRORS.DEPOSITOR_MISMATCH : relayFailure(hit.parsed), expectation, chainId, relayCompareMask(hit.parsed));
|
|
42
45
|
};
|
|
@@ -10,6 +10,6 @@ export const multicall = {
|
|
|
10
10
|
{ name: 'nftRecipient', kind: 'address' },
|
|
11
11
|
{ name: 'metadata', kind: 'bytes' },
|
|
12
12
|
], data);
|
|
13
|
-
return relayCall('multicall', asString(args.metadata), value);
|
|
13
|
+
return relayCall('multicall', asString(args.metadata), value, args.calls);
|
|
14
14
|
},
|
|
15
15
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type AbiValue } from '../abi.js';
|
|
2
|
+
import type { ParsedBridgeCall } from '../types.js';
|
|
3
|
+
export declare const relayFundingRouter: (method: string, target: string) => string | undefined;
|
|
4
|
+
/** Read one final deposit from actual calls, never from metadata or signatures. */
|
|
5
|
+
export declare const nestedRelayOrder: (rawCalls: AbiValue, funding?: {
|
|
6
|
+
token: string;
|
|
7
|
+
amount: bigint;
|
|
8
|
+
}) => Pick<ParsedBridgeCall, "orderId" | "depositor" | "innerTarget" | "bridgeToken" | "bridgeAmount" | "bridgeReceiver"> | undefined;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { depositErc20 } from './depositErc20.js';
|
|
2
|
+
import { depositErc20Allowance } from './depositErc20Allowance.js';
|
|
3
|
+
import { depositNative } from './depositNative.js';
|
|
4
|
+
import { asArray, asBigint, asRecord, asString, decodeCall } from '../abi.js';
|
|
5
|
+
import { BRIDGE_ROUTER_WHITELIST } from '../../list.js';
|
|
6
|
+
import { isZeroWord, readSelector } from '../hex.js';
|
|
7
|
+
import { NATIVE_TOKEN } from '../protocols.js';
|
|
8
|
+
const DEPOSITORIES = new Set(Object.values(BRIDGE_ROUTER_WHITELIST.relay));
|
|
9
|
+
// shortcut: reviewed v3.2 cleanup wrappers only; review new deployments before adding them.
|
|
10
|
+
const PROXY_ROUTERS = {
|
|
11
|
+
'0x5567954cd026cebfe486062cb75848e0f1c12e58': '0x0729e2a4716d9f7dd17858d5d504426d228697f7',
|
|
12
|
+
'0xe4dfbd5e6784db1058124e2c87554c0591f0fb7b': '0x6328ea02d28131e58b28e55c6258258a30d90e05',
|
|
13
|
+
};
|
|
14
|
+
const ROUTERS = new Set(Object.values(PROXY_ROUTERS));
|
|
15
|
+
export const relayFundingRouter = (method, target) => method === 'transferAndMulticall' ? PROXY_ROUTERS[target]
|
|
16
|
+
: method === 'multicall' && ROUTERS.has(target) ? target : undefined;
|
|
17
|
+
const CLEANUP_NATIVE = '0x5d1fe6a2';
|
|
18
|
+
const CLEANUP_ERC20 = '0x73b7bb2f';
|
|
19
|
+
/** Read one final deposit from actual calls, never from metadata or signatures. */
|
|
20
|
+
export const nestedRelayOrder = (rawCalls, funding) => {
|
|
21
|
+
try {
|
|
22
|
+
const calls = asArray(rawCalls).map(asRecord);
|
|
23
|
+
if (!calls.length)
|
|
24
|
+
return undefined;
|
|
25
|
+
// Multiple deposit/cleanup calls cannot be bound to a single backend order.
|
|
26
|
+
if (calls.slice(0, -1).some((call) => DEPOSITORIES.has(asString(call.target)) ||
|
|
27
|
+
(ROUTERS.has(asString(call.target)) && [CLEANUP_NATIVE, CLEANUP_ERC20].includes(readSelector(asString(call.callData)) ?? ''))))
|
|
28
|
+
return undefined;
|
|
29
|
+
const last = calls[calls.length - 1];
|
|
30
|
+
if (last.allowFailure !== false)
|
|
31
|
+
return undefined;
|
|
32
|
+
let target = asString(last.target), data = asString(last.callData);
|
|
33
|
+
const wrapper = ROUTERS.has(target) ? target : undefined;
|
|
34
|
+
let value = asBigint(last.value);
|
|
35
|
+
let token;
|
|
36
|
+
let nativeCleanup = false;
|
|
37
|
+
let cleanupAmount;
|
|
38
|
+
if (ROUTERS.has(target)) {
|
|
39
|
+
if (value !== 0n)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (readSelector(data) === CLEANUP_NATIVE) {
|
|
42
|
+
const args = decodeCall([{ name: 'amount', kind: 'uint' }, { name: 'to', kind: 'address' }, { name: 'data', kind: 'bytes' }], data);
|
|
43
|
+
target = asString(args.to);
|
|
44
|
+
data = asString(args.data);
|
|
45
|
+
value = asBigint(args.amount);
|
|
46
|
+
cleanupAmount = value;
|
|
47
|
+
nativeCleanup = true;
|
|
48
|
+
}
|
|
49
|
+
else if (readSelector(data) === CLEANUP_ERC20) {
|
|
50
|
+
const args = decodeCall([
|
|
51
|
+
{ name: 'tokens', kind: 'array', item: { name: 'token', kind: 'address' } },
|
|
52
|
+
{ name: 'tos', kind: 'array', item: { name: 'to', kind: 'address' } },
|
|
53
|
+
{ name: 'datas', kind: 'array', item: { name: 'data', kind: 'bytes' } },
|
|
54
|
+
{ name: 'amounts', kind: 'array', item: { name: 'amount', kind: 'uint' } },
|
|
55
|
+
], data);
|
|
56
|
+
if (Object.values(args).some((items) => asArray(items).length !== 1))
|
|
57
|
+
return undefined;
|
|
58
|
+
target = asString(asArray(args.tos)[0]);
|
|
59
|
+
data = asString(asArray(args.datas)[0]);
|
|
60
|
+
token = asString(asArray(args.tokens)[0]);
|
|
61
|
+
cleanupAmount = asBigint(asArray(args.amounts)[0]);
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (!DEPOSITORIES.has(target))
|
|
67
|
+
return undefined;
|
|
68
|
+
const method = [depositNative, depositErc20, depositErc20Allowance].find((item) => item.selector === readSelector(data));
|
|
69
|
+
if (!method || (nativeCleanup && method !== depositNative) || (token && method === depositNative))
|
|
70
|
+
return undefined;
|
|
71
|
+
const parsed = method.decode(data, value);
|
|
72
|
+
if (isZeroWord(parsed.orderId) || (token && token !== parsed.sourceToken))
|
|
73
|
+
return undefined;
|
|
74
|
+
const result = { orderId: parsed.orderId, depositor: parsed.depositor, innerTarget: target };
|
|
75
|
+
if (!funding || funding.amount <= 0n || funding.token !== parsed.sourceToken)
|
|
76
|
+
return result;
|
|
77
|
+
// shortcut: only one approval before deposit; add funding analysis before supporting swaps/other calls.
|
|
78
|
+
const prefix = calls.slice(0, -1);
|
|
79
|
+
if (prefix.length > 1)
|
|
80
|
+
return result;
|
|
81
|
+
let approved;
|
|
82
|
+
if (prefix.length) {
|
|
83
|
+
const call = prefix[0];
|
|
84
|
+
if (funding.token === NATIVE_TOKEN || call.allowFailure !== false ||
|
|
85
|
+
asString(call.target) !== funding.token || asBigint(call.value) !== 0n ||
|
|
86
|
+
readSelector(asString(call.callData)) !== '0x095ea7b3')
|
|
87
|
+
return result;
|
|
88
|
+
const args = decodeCall([{ name: 'spender', kind: 'address' }, { name: 'amount', kind: 'uint' }], asString(call.callData));
|
|
89
|
+
if (asString(args.spender) !== target)
|
|
90
|
+
return result;
|
|
91
|
+
approved = asBigint(args.amount);
|
|
92
|
+
}
|
|
93
|
+
// Zero cleanup means all remaining balance; here the full input is still on Router.
|
|
94
|
+
const amount = cleanupAmount === 0n ? funding.amount : cleanupAmount;
|
|
95
|
+
const depositAmount = method === depositErc20Allowance ? amount ?? approved
|
|
96
|
+
: nativeCleanup ? amount : parsed.sourceAmount;
|
|
97
|
+
if (depositAmount !== funding.amount ||
|
|
98
|
+
(cleanupAmount !== undefined && amount !== funding.amount) ||
|
|
99
|
+
(method !== depositNative && value !== 0n))
|
|
100
|
+
return result;
|
|
101
|
+
return { ...result, bridgeToken: funding.token, bridgeAmount: funding.amount, bridgeReceiver: wrapper };
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Unknown or malformed calls leave the order unresolved for the signing check.
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -35,7 +35,7 @@ export const permit2TransferAndMulticall = {
|
|
|
35
35
|
], data);
|
|
36
36
|
const source = firstPositive(asArray(asRecord(args.permit).permitted));
|
|
37
37
|
return {
|
|
38
|
-
...relayCall('permit2TransferAndMulticall', asString(args.metadata), value, source),
|
|
38
|
+
...relayCall('permit2TransferAndMulticall', asString(args.metadata), value, args.calls, source),
|
|
39
39
|
depositor: asString(args.user),
|
|
40
40
|
};
|
|
41
41
|
},
|
|
@@ -32,7 +32,7 @@ const decodePermits = (name, selector, tail) => ({
|
|
|
32
32
|
], data);
|
|
33
33
|
const source = firstPositive(asArray(args.permits));
|
|
34
34
|
const tokens = asArray(args.tokens).map((token) => asString(token));
|
|
35
|
-
return relayCall(name, asString(args.metadata), value, {
|
|
35
|
+
return relayCall(name, asString(args.metadata), value, args.calls, {
|
|
36
36
|
token: tokens[0] ?? source.token,
|
|
37
37
|
amount: source.amount,
|
|
38
38
|
});
|
|
@@ -32,7 +32,7 @@ export const permit3009TransferAndMulticallSigned = {
|
|
|
32
32
|
], data);
|
|
33
33
|
const source = firstPositive(asArray(args.permits));
|
|
34
34
|
const tokens = asArray(args.tokens).map((token) => asString(token));
|
|
35
|
-
return relayCall('permit3009TransferAndMulticallSigned', asString(args.metadata), value, {
|
|
35
|
+
return relayCall('permit3009TransferAndMulticallSigned', asString(args.metadata), value, args.calls, {
|
|
36
36
|
token: tokens[0] ?? source.token,
|
|
37
37
|
amount: source.amount,
|
|
38
38
|
});
|
|
@@ -29,6 +29,6 @@ export const permitTransferAndMulticall = {
|
|
|
29
29
|
{ name: 'metadata', kind: 'bytes' },
|
|
30
30
|
], data);
|
|
31
31
|
const source = firstPositive(asArray(args.permits));
|
|
32
|
-
return relayCall('permitTransferAndMulticall', asString(args.metadata), value, source);
|
|
32
|
+
return relayCall('permitTransferAndMulticall', asString(args.metadata), value, args.calls, source);
|
|
33
33
|
},
|
|
34
34
|
};
|
|
@@ -30,6 +30,6 @@ export const permitTransferAndMulticallLegacy = {
|
|
|
30
30
|
{ name: 'metadata', kind: 'bytes' },
|
|
31
31
|
], data);
|
|
32
32
|
const source = firstPositive(asArray(args.permits));
|
|
33
|
-
return relayCall('permitTransferAndMulticallLegacy', asString(args.metadata), value, source);
|
|
33
|
+
return relayCall('permitTransferAndMulticallLegacy', asString(args.metadata), value, args.calls, source);
|
|
34
34
|
},
|
|
35
35
|
};
|