@rabby-wallet/rabby-bridge 0.0.13 → 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 +163 -1
- package/dist/calldata/abi.d.ts +29 -0
- package/dist/calldata/abi.js +160 -0
- package/dist/calldata/across/deposit.d.ts +2 -0
- package/dist/calldata/across/deposit.js +36 -0
- package/dist/calldata/across/depositNative.d.ts +2 -0
- package/dist/calldata/across/depositNative.js +45 -0
- package/dist/calldata/across/depositNow.d.ts +2 -0
- package/dist/calldata/across/depositNow.js +35 -0
- package/dist/calldata/across/depositV3.d.ts +2 -0
- package/dist/calldata/across/depositV3.js +35 -0
- package/dist/calldata/across/depositV3Now.d.ts +2 -0
- package/dist/calldata/across/depositV3Now.js +34 -0
- package/dist/calldata/across/index.d.ts +4 -0
- package/dist/calldata/across/index.js +31 -0
- package/dist/calldata/across/swapAndBridge.d.ts +2 -0
- package/dist/calldata/across/swapAndBridge.js +70 -0
- package/dist/calldata/across/validate.d.ts +3 -0
- package/dist/calldata/across/validate.js +34 -0
- package/dist/calldata/chains.d.ts +2 -0
- package/dist/calldata/chains.js +36 -0
- package/dist/calldata/destination.d.ts +6 -0
- package/dist/calldata/destination.js +227 -0
- package/dist/calldata/destinationSwap.d.ts +14 -0
- package/dist/calldata/destinationSwap.js +83 -0
- package/dist/calldata/destinationUniswap.d.ts +7 -0
- package/dist/calldata/destinationUniswap.js +57 -0
- package/dist/calldata/diagnostics.d.ts +4 -0
- package/dist/calldata/diagnostics.js +18 -0
- package/dist/calldata/expect.d.ts +29 -0
- package/dist/calldata/expect.js +157 -0
- package/dist/calldata/hex.d.ts +6 -0
- package/dist/calldata/hex.js +46 -0
- package/dist/calldata/index.d.ts +20 -0
- package/dist/calldata/index.js +88 -0
- package/dist/calldata/lifi/across/acrossV4Erc20Min.d.ts +2 -0
- package/dist/calldata/lifi/across/acrossV4Erc20Min.js +27 -0
- package/dist/calldata/lifi/across/acrossV4NativeMin.d.ts +2 -0
- package/dist/calldata/lifi/across/acrossV4NativeMin.js +23 -0
- package/dist/calldata/lifi/across/index.d.ts +6 -0
- package/dist/calldata/lifi/across/index.js +19 -0
- package/dist/calldata/lifi/allbridge/index.d.ts +3 -0
- package/dist/calldata/lifi/allbridge/index.js +26 -0
- package/dist/calldata/lifi/celer/index.d.ts +4 -0
- package/dist/calldata/lifi/celer/index.js +10 -0
- package/dist/calldata/lifi/centrifuge/index.d.ts +3 -0
- package/dist/calldata/lifi/centrifuge/index.js +22 -0
- package/dist/calldata/lifi/destination.d.ts +5 -0
- package/dist/calldata/lifi/destination.js +40 -0
- package/dist/calldata/lifi/frax/index.d.ts +4 -0
- package/dist/calldata/lifi/frax/index.js +29 -0
- package/dist/calldata/lifi/garden/index.d.ts +3 -0
- package/dist/calldata/lifi/garden/index.js +22 -0
- package/dist/calldata/lifi/gaszip/index.d.ts +6 -0
- package/dist/calldata/lifi/gaszip/index.js +15 -0
- package/dist/calldata/lifi/index.d.ts +4 -0
- package/dist/calldata/lifi/index.js +62 -0
- package/dist/calldata/lifi/intents/index.d.ts +4 -0
- package/dist/calldata/lifi/intents/index.js +10 -0
- package/dist/calldata/lifi/layerswap/index.d.ts +4 -0
- package/dist/calldata/lifi/layerswap/index.js +10 -0
- package/dist/calldata/lifi/m0/index.d.ts +3 -0
- package/dist/calldata/lifi/m0/index.js +33 -0
- package/dist/calldata/lifi/mayan/index.d.ts +4 -0
- package/dist/calldata/lifi/mayan/index.js +10 -0
- package/dist/calldata/lifi/method.d.ts +6 -0
- package/dist/calldata/lifi/method.js +16 -0
- package/dist/calldata/lifi/native/index.d.ts +4 -0
- package/dist/calldata/lifi/native/index.js +80 -0
- package/dist/calldata/lifi/params.d.ts +28 -0
- package/dist/calldata/lifi/params.js +349 -0
- package/dist/calldata/lifi/paxos/index.d.ts +4 -0
- package/dist/calldata/lifi/paxos/index.js +10 -0
- package/dist/calldata/lifi/polymer/index.d.ts +4 -0
- package/dist/calldata/lifi/polymer/index.js +10 -0
- package/dist/calldata/lifi/relay/index.d.ts +6 -0
- package/dist/calldata/lifi/relay/index.js +15 -0
- package/dist/calldata/lifi/shape.d.ts +4 -0
- package/dist/calldata/lifi/shape.js +31 -0
- package/dist/calldata/lifi/squid/index.d.ts +6 -0
- package/dist/calldata/lifi/squid/index.js +15 -0
- package/dist/calldata/lifi/stargate/index.d.ts +5 -0
- package/dist/calldata/lifi/stargate/index.js +15 -0
- package/dist/calldata/lifi/swap/index.d.ts +5 -0
- package/dist/calldata/lifi/swap/index.js +18 -0
- package/dist/calldata/lifi/swap/swapMethod.d.ts +2 -0
- package/dist/calldata/lifi/swap/swapMethod.js +40 -0
- package/dist/calldata/lifi/swapData.d.ts +5 -0
- package/dist/calldata/lifi/swapData.js +33 -0
- package/dist/calldata/match.d.ts +3 -0
- package/dist/calldata/match.js +21 -0
- package/dist/calldata/mayan/destination.d.ts +2 -0
- package/dist/calldata/mayan/destination.js +13 -0
- package/dist/calldata/mayan/fastMctpBridge.d.ts +3 -0
- package/dist/calldata/mayan/fastMctpBridge.js +48 -0
- package/dist/calldata/mayan/fastMctpCreateOrder.d.ts +3 -0
- package/dist/calldata/mayan/fastMctpCreateOrder.js +39 -0
- package/dist/calldata/mayan/forward.d.ts +8 -0
- package/dist/calldata/mayan/forward.js +35 -0
- package/dist/calldata/mayan/forwardERC20.d.ts +2 -0
- package/dist/calldata/mayan/forwardERC20.js +19 -0
- package/dist/calldata/mayan/forwardEth.d.ts +2 -0
- package/dist/calldata/mayan/forwardEth.js +16 -0
- package/dist/calldata/mayan/index.d.ts +4 -0
- package/dist/calldata/mayan/index.js +21 -0
- package/dist/calldata/mayan/mctp.d.ts +5 -0
- package/dist/calldata/mayan/mctp.js +41 -0
- package/dist/calldata/mayan/readProtocol.d.ts +2 -0
- package/dist/calldata/mayan/readProtocol.js +29 -0
- package/dist/calldata/mayan/swapAndForwardERC20.d.ts +2 -0
- package/dist/calldata/mayan/swapAndForwardERC20.js +24 -0
- package/dist/calldata/mayan/swapAndForwardEth.d.ts +2 -0
- package/dist/calldata/mayan/swapAndForwardEth.js +23 -0
- package/dist/calldata/mayan/swiftCreateOrder.d.ts +3 -0
- package/dist/calldata/mayan/swiftCreateOrder.js +49 -0
- package/dist/calldata/mayan/validate.d.ts +3 -0
- package/dist/calldata/mayan/validate.js +20 -0
- package/dist/calldata/protocols.d.ts +9 -0
- package/dist/calldata/protocols.js +58 -0
- package/dist/calldata/relay/calls.d.ts +13 -0
- package/dist/calldata/relay/calls.js +58 -0
- package/dist/calldata/relay/depositErc20.d.ts +2 -0
- package/dist/calldata/relay/depositErc20.js +22 -0
- package/dist/calldata/relay/depositErc20Allowance.d.ts +2 -0
- package/dist/calldata/relay/depositErc20Allowance.js +20 -0
- package/dist/calldata/relay/depositNative.d.ts +2 -0
- package/dist/calldata/relay/depositNative.js +20 -0
- package/dist/calldata/relay/index.d.ts +4 -0
- package/dist/calldata/relay/index.js +45 -0
- package/dist/calldata/relay/multicall.d.ts +2 -0
- package/dist/calldata/relay/multicall.js +15 -0
- package/dist/calldata/relay/nestedDeposit.d.ts +8 -0
- package/dist/calldata/relay/nestedDeposit.js +107 -0
- package/dist/calldata/relay/permit2TransferAndMulticall.d.ts +2 -0
- package/dist/calldata/relay/permit2TransferAndMulticall.js +42 -0
- package/dist/calldata/relay/permit3009TransferAndMulticall.d.ts +2 -0
- package/dist/calldata/relay/permit3009TransferAndMulticall.js +41 -0
- package/dist/calldata/relay/permit3009TransferAndMulticallSigned.d.ts +2 -0
- package/dist/calldata/relay/permit3009TransferAndMulticallSigned.js +40 -0
- package/dist/calldata/relay/permitTransferAndMulticall.d.ts +2 -0
- package/dist/calldata/relay/permitTransferAndMulticall.js +34 -0
- package/dist/calldata/relay/permitTransferAndMulticallLegacy.d.ts +2 -0
- package/dist/calldata/relay/permitTransferAndMulticallLegacy.js +35 -0
- package/dist/calldata/relay/transferAndMulticall.d.ts +2 -0
- package/dist/calldata/relay/transferAndMulticall.js +26 -0
- package/dist/calldata/relay/validate.d.ts +3 -0
- package/dist/calldata/relay/validate.js +18 -0
- package/dist/calldata/resolve.d.ts +2 -0
- package/dist/calldata/resolve.js +28 -0
- package/dist/calldata/socket/addresses.d.ts +9 -0
- package/dist/calldata/socket/addresses.js +9 -0
- package/dist/calldata/socket/bridge.d.ts +2 -0
- package/dist/calldata/socket/bridge.js +28 -0
- package/dist/calldata/socket/exec.d.ts +2 -0
- package/dist/calldata/socket/exec.js +71 -0
- package/dist/calldata/socket/index.d.ts +4 -0
- package/dist/calldata/socket/index.js +13 -0
- package/dist/calldata/socket/params.d.ts +5 -0
- package/dist/calldata/socket/params.js +14 -0
- package/dist/calldata/socket/protocols/ccip.d.ts +2 -0
- package/dist/calldata/socket/protocols/ccip.js +32 -0
- package/dist/calldata/socket/protocols/cctp.d.ts +2 -0
- package/dist/calldata/socket/protocols/cctp.js +25 -0
- package/dist/calldata/socket/protocols/eco.d.ts +2 -0
- package/dist/calldata/socket/protocols/eco.js +49 -0
- package/dist/calldata/socket/protocols/index.d.ts +3 -0
- package/dist/calldata/socket/protocols/index.js +87 -0
- package/dist/calldata/socket/protocols/native.d.ts +2 -0
- package/dist/calldata/socket/protocols/native.js +71 -0
- package/dist/calldata/socket/protocols/oft.d.ts +2 -0
- package/dist/calldata/socket/protocols/oft.js +33 -0
- package/dist/calldata/socket/protocols/swapsXYZ.d.ts +2 -0
- package/dist/calldata/socket/protocols/swapsXYZ.js +62 -0
- package/dist/calldata/socket/swapAndBridge.d.ts +2 -0
- package/dist/calldata/socket/swapAndBridge.js +41 -0
- package/dist/calldata/socket/validate.d.ts +3 -0
- package/dist/calldata/socket/validate.js +24 -0
- package/dist/calldata/types.d.ts +86 -0
- package/dist/calldata/types.js +20 -0
- package/dist/errors/constants.d.ts +906 -0
- package/dist/errors/constants.js +913 -0
- package/dist/errors/index.d.ts +65 -0
- package/dist/errors/index.js +53 -0
- package/dist/errors/sanitize.d.ts +3 -0
- package/dist/errors/sanitize.js +74 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -0
- package/dist/providers/data.d.ts +23 -0
- package/dist/providers/data.js +116 -0
- package/dist/providers/errors.d.ts +7 -0
- package/dist/providers/errors.js +10 -0
- package/dist/providers/layerswap.d.ts +9 -0
- package/dist/providers/layerswap.js +95 -0
- package/dist/providers/relay.d.ts +62 -0
- package/dist/providers/relay.js +237 -0
- package/dist/providers/socket.d.ts +5 -0
- package/dist/providers/socket.js +58 -0
- package/dist/quote.d.ts +30 -3
- package/dist/quote.js +236 -66
- package/package.json +13 -3
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { BRIDGE_ERRORS, BridgeError } from '../../errors/index.js';
|
|
2
|
+
import { applyAcrossMessage } from '../destination.js';
|
|
3
|
+
import { asBigint, asRecord, asString, decodeCall } from '../abi.js';
|
|
4
|
+
import { accountFromBytes32 } from '../hex.js';
|
|
5
|
+
import { NATIVE_TOKEN } from '../protocols.js';
|
|
6
|
+
const swapAndDepositData = {
|
|
7
|
+
name: 'swapAndDepositData',
|
|
8
|
+
kind: 'tuple',
|
|
9
|
+
components: [
|
|
10
|
+
{
|
|
11
|
+
name: 'submissionFees',
|
|
12
|
+
kind: 'tuple',
|
|
13
|
+
components: [
|
|
14
|
+
{ name: 'amount', kind: 'uint' },
|
|
15
|
+
{ name: 'recipient', kind: 'address' },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'depositData',
|
|
20
|
+
kind: 'tuple',
|
|
21
|
+
components: [
|
|
22
|
+
{ name: 'inputToken', kind: 'address' },
|
|
23
|
+
{ name: 'outputToken', kind: 'bytes', size: 32 },
|
|
24
|
+
{ name: 'outputAmount', kind: 'uint' },
|
|
25
|
+
{ name: 'depositor', kind: 'address' },
|
|
26
|
+
{ name: 'recipient', kind: 'bytes', size: 32 },
|
|
27
|
+
{ name: 'destinationChainId', kind: 'uint' },
|
|
28
|
+
{ name: 'exclusiveRelayer', kind: 'bytes', size: 32 },
|
|
29
|
+
{ name: 'quoteTimestamp', kind: 'uint' },
|
|
30
|
+
{ name: 'fillDeadline', kind: 'uint' },
|
|
31
|
+
{ name: 'exclusivityParameter', kind: 'uint' },
|
|
32
|
+
{ name: 'message', kind: 'bytes' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{ name: 'swapToken', kind: 'address' },
|
|
36
|
+
{ name: 'exchange', kind: 'address' },
|
|
37
|
+
{ name: 'transferType', kind: 'uint' },
|
|
38
|
+
{ name: 'swapTokenAmount', kind: 'uint' },
|
|
39
|
+
{ name: 'minExpectedInputTokenAmount', kind: 'uint' },
|
|
40
|
+
{ name: 'routerCalldata', kind: 'bytes' },
|
|
41
|
+
{ name: 'enableProportionalAdjustment', kind: 'bool' },
|
|
42
|
+
{ name: 'spokePool', kind: 'address' },
|
|
43
|
+
{ name: 'nonce', kind: 'uint' },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
export const swapAndBridge = {
|
|
47
|
+
name: 'swapAndBridge',
|
|
48
|
+
selector: '0x110560ad',
|
|
49
|
+
decode(data, value) {
|
|
50
|
+
const args = asRecord(decodeCall([swapAndDepositData], data).swapAndDepositData);
|
|
51
|
+
const depositData = asRecord(args.depositData);
|
|
52
|
+
const sourceAmount = asBigint(args.swapTokenAmount);
|
|
53
|
+
if (value > 0n && value !== sourceAmount)
|
|
54
|
+
throw new BridgeError(BRIDGE_ERRORS.ACROSS_SWAP_VALUE_MISMATCH);
|
|
55
|
+
return applyAcrossMessage({
|
|
56
|
+
aggregatorId: 'across',
|
|
57
|
+
method: 'swapAndBridge',
|
|
58
|
+
kind: 'bridge',
|
|
59
|
+
depositor: asString(depositData.depositor),
|
|
60
|
+
sourceToken: value > 0n ? NATIVE_TOKEN : asString(args.swapToken),
|
|
61
|
+
sourceAmount,
|
|
62
|
+
destinationChainId: asBigint(depositData.destinationChainId),
|
|
63
|
+
receiver: accountFromBytes32(asString(depositData.recipient)),
|
|
64
|
+
outputToken: accountFromBytes32(asString(depositData.outputToken)),
|
|
65
|
+
minOutputAmount: asBigint(depositData.outputAmount),
|
|
66
|
+
spokePool: asString(args.spokePool),
|
|
67
|
+
innerTarget: asString(args.exchange),
|
|
68
|
+
}, asString(depositData.message));
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BRIDGE_ERRORS } from '../../errors/index.js';
|
|
2
|
+
import { getBridgeRouter } from '../../list.js';
|
|
3
|
+
import { evmChainId } from '../chains.js';
|
|
4
|
+
import { isZeroWord } from '../hex.js';
|
|
5
|
+
// These chains use a Periphery as the outer router, separate from their canonical SpokePool.
|
|
6
|
+
const SPOKE_POOLS = {
|
|
7
|
+
56: '0x4e8e101924ede233c13e2d8622dc8aed2872d505',
|
|
8
|
+
324: '0xe0b015e54d54fc84a6cb9b666099c46ade9335ff',
|
|
9
|
+
57073: '0xef684c38f94f48775959ecf2012d7e864ffb9dd4',
|
|
10
|
+
};
|
|
11
|
+
export const acrossFailure = (parsed, chainId, expectedDepositor) => {
|
|
12
|
+
if (expectedDepositor && parsed.depositor?.toLowerCase() !== expectedDepositor.toLowerCase()) {
|
|
13
|
+
return BRIDGE_ERRORS.DEPOSITOR_MISMATCH;
|
|
14
|
+
}
|
|
15
|
+
if (parsed.destinationChainId === undefined || parsed.destinationChainId <= 0n) {
|
|
16
|
+
return BRIDGE_ERRORS.MISSING_DESTINATION_CHAIN;
|
|
17
|
+
}
|
|
18
|
+
if (isZeroWord(parsed.receiver ?? parsed.bridgeReceiver)) {
|
|
19
|
+
return BRIDGE_ERRORS.MISSING_RECEIVER;
|
|
20
|
+
}
|
|
21
|
+
if ((parsed.sourceAmount ?? 0n) <= 0n) {
|
|
22
|
+
return BRIDGE_ERRORS.MISSING_SOURCE_AMOUNT;
|
|
23
|
+
}
|
|
24
|
+
if (!parsed.spokePool) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const spoke = parsed.spokePool.toLowerCase();
|
|
28
|
+
const expectedSpoke = SPOKE_POOLS[evmChainId(chainId)?.toString() ?? ''] ??
|
|
29
|
+
getBridgeRouter('across', chainId)?.toLowerCase();
|
|
30
|
+
if (expectedSpoke !== spoke) {
|
|
31
|
+
return BRIDGE_ERRORS.SPOKE_POOL_IS_NOT_IN_THE_ALLOWLIST;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CHAIN_ENUM_TO_SERVER_ID, normalizeChainEnum } from '../chains.js';
|
|
2
|
+
const CHAIN_IDS = {
|
|
3
|
+
eth: 1n, op: 10n, flr: 14n, cro: 25n, rsk: 30n, xdc: 50n, bsc: 56n,
|
|
4
|
+
xdai: 100n, fuse: 122n, uni: 130n, matic: 137n, monad: 143n, sonic: 146n,
|
|
5
|
+
manta: 169n, xlayer: 196n, opbnb: 204n, lens: 232n, frax: 252n, era: 324n,
|
|
6
|
+
stable: 988n, hyper: 999n, metis: 1088n, lisk: 1135n, sei: 1329n, story: 1514n,
|
|
7
|
+
gravity: 1625n, soneium: 1868n, swell: 1923n, ron: 2020n, abs: 2741n,
|
|
8
|
+
morph: 2818n, tempo: 4217n, megaeth: 4326n, hood: 4663n, world: 480n,
|
|
9
|
+
mnt: 5000n, arc: 5042n, cyber: 7560n, klay: 8217n, base: 8453n, plasma: 9745n,
|
|
10
|
+
ape: 33139n, mode: 34443n, arb: 42161n, celo: 42220n, hemi: 43111n, avax: 43114n,
|
|
11
|
+
zircuit: 48900n, sophon: 50104n, ink: 57073n, linea: 59144n, bob: 60808n,
|
|
12
|
+
bera: 80094n, blast: 81457n, plume: 98866n, taiko: 167000n, scrl: 534352n,
|
|
13
|
+
katana: 747474n, zora: 7777777n,
|
|
14
|
+
ethereum: 1n, optimism: 10n, gnosis: 100n, polygon: 137n, mantle: 5000n,
|
|
15
|
+
arbitrum: 42161n, avalanche: 43114n, fraxtal: 252n,
|
|
16
|
+
};
|
|
17
|
+
export const evmChainId = (chain) => {
|
|
18
|
+
if (/^\d+$/.test(chain)) {
|
|
19
|
+
const id = BigInt(chain);
|
|
20
|
+
return id > 0n ? id : undefined;
|
|
21
|
+
}
|
|
22
|
+
const chainEnum = normalizeChainEnum(chain);
|
|
23
|
+
return CHAIN_IDS[chainEnum ? CHAIN_ENUM_TO_SERVER_ID[chainEnum] : chain.toLowerCase()];
|
|
24
|
+
};
|
|
25
|
+
export const sourceChainMismatch = (expected, actual) => {
|
|
26
|
+
const expectedId = evmChainId(expected);
|
|
27
|
+
if (actual === undefined || expectedId === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
return (typeof actual === 'number' && !Number.isSafeInteger(actual)) ||
|
|
31
|
+
BigInt(actual) !== expectedId;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type AbiParam, type AbiValue } from './abi.js';
|
|
2
|
+
import type { ParsedBridgeCall } from './types.js';
|
|
3
|
+
export declare const destinationCall: AbiParam;
|
|
4
|
+
export declare const readTransfer: (target: string, data: string, value: bigint) => Partial<ParsedBridgeCall> | undefined;
|
|
5
|
+
export declare const readDestinationTransfers: (calls: AbiValue[]) => Partial<ParsedBridgeCall>;
|
|
6
|
+
export declare const applyAcrossMessage: (parsed: ParsedBridgeCall, message: string) => ParsedBridgeCall;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { BRIDGE_ERRORS, BridgeError, asBridgeError } from '../errors/index.js';
|
|
2
|
+
import { asArray, asBigint, asRecord, asString, decodeCall, decodeParameters } from './abi.js';
|
|
3
|
+
import { isZeroWord, readSelector } from './hex.js';
|
|
4
|
+
import { NATIVE_TOKEN, WRAPPED_NATIVE_TOKENS, subtractFees } from './protocols.js';
|
|
5
|
+
import { calldataDiagnostics } from './diagnostics.js';
|
|
6
|
+
import { ALLOWANCE_HOLDER, HYPEREVM_LIFI_ROUTER, readLifiSwap, readSettlerSwap } from './destinationSwap.js';
|
|
7
|
+
import { AVALANCHE_UNIVERSAL_ROUTER, readUniversalRouterSwap } from './destinationUniswap.js';
|
|
8
|
+
export const destinationCall = {
|
|
9
|
+
name: 'call', kind: 'tuple', components: [
|
|
10
|
+
{ name: 'target', kind: 'address' }, { name: 'data', kind: 'bytes' }, { name: 'value', kind: 'uint' },
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
export const readTransfer = (target, data, value) => {
|
|
14
|
+
if (data === '0x' && value > 0n)
|
|
15
|
+
return { receiver: target, outputToken: NATIVE_TOKEN, minOutputAmount: value };
|
|
16
|
+
if (value !== 0n || readSelector(data) !== '0xa9059cbb')
|
|
17
|
+
return undefined;
|
|
18
|
+
const args = decodeCall([{ name: 'to', kind: 'address' }, { name: 'amount', kind: 'uint' }], data);
|
|
19
|
+
return { receiver: asString(args.to), outputToken: target, minOutputAmount: asBigint(args.amount) };
|
|
20
|
+
};
|
|
21
|
+
export const readDestinationTransfers = (calls) => {
|
|
22
|
+
const diagnostics = { destinationCallCount: calls.length,
|
|
23
|
+
destinationSelectors: [...new Set(calls.map((call) => calldataDiagnostics(asRecord(call).data).selector)
|
|
24
|
+
.filter((selector) => Boolean(selector)))].slice(0, 16) };
|
|
25
|
+
const transfers = calls.map((call) => {
|
|
26
|
+
const c = asRecord(call);
|
|
27
|
+
return readTransfer(asString(c.target), asString(c.data), asBigint(c.value));
|
|
28
|
+
});
|
|
29
|
+
if (!transfers.length || transfers.some((transfer) => !transfer))
|
|
30
|
+
return { diagnostics };
|
|
31
|
+
const first = transfers[0];
|
|
32
|
+
if (!transfers.every((transfer) => transfer?.receiver === first.receiver))
|
|
33
|
+
return { diagnostics };
|
|
34
|
+
// Different assets can have one recipient without having one comparable output amount.
|
|
35
|
+
if (!transfers.every((transfer) => transfer?.outputToken === first.outputToken))
|
|
36
|
+
return { receiver: first.receiver, diagnostics };
|
|
37
|
+
return { ...first, diagnostics, minOutputAmount: transfers.reduce((sum, transfer) => sum + (transfer?.minOutputAmount ?? 0n), 0n) };
|
|
38
|
+
};
|
|
39
|
+
const instructions = {
|
|
40
|
+
name: 'instructions', kind: 'tuple', components: [
|
|
41
|
+
{ name: 'calls', kind: 'array', item: { name: 'call', kind: 'tuple', components: [
|
|
42
|
+
{ name: 'target', kind: 'address' }, { name: 'callData', kind: 'bytes' }, { name: 'value', kind: 'uint' },
|
|
43
|
+
] } },
|
|
44
|
+
{ name: 'fallbackRecipient', kind: 'address' },
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
const ACROSS_MESSAGE_HANDLERS = {
|
|
48
|
+
143: '0xec41f75c686e376ab2a4f18bde263ab5822c4511',
|
|
49
|
+
999: '0x5e7840e06faccb6d1c3b5f5e0d1d3d07f2829bba',
|
|
50
|
+
4663: '0xa8ad2e87e2043711d8bec77e8bc3e2683c0ab6bd',
|
|
51
|
+
9745: '0x5e7840e06faccb6d1c3b5f5e0d1d3d07f2829bba',
|
|
52
|
+
43114: '0x9610954acdca5ff7905f051a040ce33fe613c60e',
|
|
53
|
+
};
|
|
54
|
+
const readAcrossMessage = (parsed, message) => {
|
|
55
|
+
if (message === '0x') {
|
|
56
|
+
const wrapped = WRAPPED_NATIVE_TOKENS[parsed.destinationChainId?.toString() ?? ''];
|
|
57
|
+
if (parsed.outputToken !== undefined && (wrapped === undefined || parsed.outputToken === wrapped)) {
|
|
58
|
+
parsed.bridgeOutputToken = parsed.outputToken;
|
|
59
|
+
parsed.outputTokenReference = { protocol: 'across-native', token: parsed.outputToken,
|
|
60
|
+
destinationChainId: parsed.destinationChainId, destinationContract: parsed.receiver };
|
|
61
|
+
delete parsed.outputToken;
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
const handler = parsed.receiver;
|
|
66
|
+
parsed.bridgeReceiver = handler;
|
|
67
|
+
parsed.bridgeOutputToken = parsed.outputToken;
|
|
68
|
+
parsed.bridgeOutputAmount = parsed.minOutputAmount;
|
|
69
|
+
parsed.destinationCallData = message;
|
|
70
|
+
parsed.diagnostics = { hasDestinationCall: true, destinationCalldataBytes: calldataDiagnostics(message).calldataBytes };
|
|
71
|
+
delete parsed.receiver;
|
|
72
|
+
delete parsed.outputToken;
|
|
73
|
+
delete parsed.minOutputAmount;
|
|
74
|
+
// Other message handlers can implement arbitrary destination logic.
|
|
75
|
+
const expectedHandler = ACROSS_MESSAGE_HANDLERS[parsed.destinationChainId?.toString() ?? ''] ??
|
|
76
|
+
'0x0f7ae28de1c8532170ad4ee566b5801485c13a0e';
|
|
77
|
+
if (handler !== expectedHandler)
|
|
78
|
+
return parsed;
|
|
79
|
+
const emitter = parsed.destinationChainId === 43114n
|
|
80
|
+
? '0x25b15fc38d6a2c13feddd9cb51f184f7cf0d4c11' : '0xbf75133b48b0a42ab9374027902e83c5e2949034';
|
|
81
|
+
const args = asRecord(decodeParameters([instructions], message).instructions);
|
|
82
|
+
const calls = asArray(args.calls);
|
|
83
|
+
parsed.diagnostics.destinationCallCount = calls.length;
|
|
84
|
+
parsed.diagnostics.destinationSelectors = [...new Set(calls.map((call) => calldataDiagnostics(asRecord(call).callData).selector)
|
|
85
|
+
.filter((selector) => Boolean(selector)))].slice(0, 16);
|
|
86
|
+
const balances = new Map([[parsed.bridgeOutputToken, parsed.bridgeOutputAmount]]);
|
|
87
|
+
const payouts = [];
|
|
88
|
+
const directTransfers = [];
|
|
89
|
+
const approvals = new Map();
|
|
90
|
+
const lifiRouter = parsed.destinationChainId === 999n ? HYPEREVM_LIFI_ROUTER : undefined;
|
|
91
|
+
const possibleRefundTokens = new Set();
|
|
92
|
+
const refundReceivers = [];
|
|
93
|
+
for (let index = 0; index < calls.length; index++) {
|
|
94
|
+
const call = asRecord(calls[index]);
|
|
95
|
+
const target = asString(call.target);
|
|
96
|
+
const data = asString(call.callData);
|
|
97
|
+
const value = asBigint(call.value);
|
|
98
|
+
const selector = readSelector(data);
|
|
99
|
+
// emitData is telemetry; its amounts are not an enforced receive floor.
|
|
100
|
+
if (selector === '0xd836083e' && target === emitter && value === 0n)
|
|
101
|
+
continue;
|
|
102
|
+
if (selector === '0x095ea7b3' && value === 0n && balances.has(target)) {
|
|
103
|
+
const approval = decodeCall([{ name: 'spender', kind: 'address' }, { name: 'amount', kind: 'uint' }], data);
|
|
104
|
+
const amount = asBigint(approval.amount);
|
|
105
|
+
const spender = asString(approval.spender);
|
|
106
|
+
if ((spender !== ALLOWANCE_HOLDER && spender !== lifiRouter) ||
|
|
107
|
+
(amount > (balances.get(target) ?? 0n) && amount !== (1n << 256n) - 1n))
|
|
108
|
+
return parsed;
|
|
109
|
+
approvals.set(`${target}:${spender}`, amount);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if ((target === ALLOWANCE_HOLDER || target === lifiRouter) && value === 0n) {
|
|
113
|
+
const swap = target === ALLOWANCE_HOLDER
|
|
114
|
+
? readSettlerSwap(data, parsed.destinationChainId, handler, balances)
|
|
115
|
+
: readLifiSwap(data, parsed.destinationChainId, target, handler);
|
|
116
|
+
const approvalKey = `${swap?.inputToken}:${target}`;
|
|
117
|
+
if (!swap || (approvals.get(approvalKey) ?? 0n) < swap.inputAmount ||
|
|
118
|
+
(balances.get(swap.inputToken) ?? 0n) < swap.inputAmount)
|
|
119
|
+
return parsed;
|
|
120
|
+
approvals.set(approvalKey, (approvals.get(approvalKey) ?? 0n) - swap.inputAmount);
|
|
121
|
+
balances.set(swap.inputToken, (balances.get(swap.inputToken) ?? 0n) - swap.inputAmount);
|
|
122
|
+
balances.set(swap.outputToken, (balances.get(swap.outputToken) ?? 0n) + swap.minOutputAmount);
|
|
123
|
+
// GenericSwapFacet returns unused input to Handler; its later payout must agree too.
|
|
124
|
+
if (target === lifiRouter)
|
|
125
|
+
possibleRefundTokens.add(swap.inputToken);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (selector === '0xc41e8295' && target === handler) {
|
|
129
|
+
if (value !== 0n)
|
|
130
|
+
return parsed;
|
|
131
|
+
const nested = decodeCall([
|
|
132
|
+
{ name: 'target', kind: 'address' }, { name: 'data', kind: 'bytes' }, { name: 'value', kind: 'uint' },
|
|
133
|
+
{ name: 'replacements', kind: 'array', item: { name: 'replacement', kind: 'tuple', components: [
|
|
134
|
+
{ name: 'token', kind: 'address' }, { name: 'offset', kind: 'uint' },
|
|
135
|
+
] } },
|
|
136
|
+
], data);
|
|
137
|
+
const token = asString(nested.target);
|
|
138
|
+
const replacements = asArray(nested.replacements).map(asRecord);
|
|
139
|
+
const withdrawal = asString(nested.data);
|
|
140
|
+
if (readSelector(withdrawal) !== '0x2e1a7d4d' || replacements.length !== 1 ||
|
|
141
|
+
asBigint(decodeCall([{ name: 'amount', kind: 'uint' }], withdrawal).amount) !== 0n ||
|
|
142
|
+
asBigint(nested.value) !== 0n ||
|
|
143
|
+
asString(replacements[0].token) !== token || asBigint(replacements[0].offset) !== 4n || !balances.has(token) ||
|
|
144
|
+
WRAPPED_NATIVE_TOKENS[parsed.destinationChainId?.toString() ?? ''] !== token)
|
|
145
|
+
return parsed;
|
|
146
|
+
balances.set(NATIVE_TOKEN, (balances.get(NATIVE_TOKEN) ?? 0n) + (balances.get(token) ?? 0n));
|
|
147
|
+
balances.set(token, 0n);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (selector === '0xef8738d3' && target === handler) {
|
|
151
|
+
if (value !== 0n)
|
|
152
|
+
return parsed;
|
|
153
|
+
const drain = decodeCall([{ name: 'token', kind: 'address' }, { name: 'receiver', kind: 'address' }], data);
|
|
154
|
+
const token = asString(drain.token);
|
|
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
|
+
}
|
|
170
|
+
if (possibleRefundTokens.delete(token))
|
|
171
|
+
refundReceivers.push(asString(drain.receiver));
|
|
172
|
+
if (amount > 0n)
|
|
173
|
+
payouts.push({ outputToken: token, minOutputAmount: amount, receiver: asString(drain.receiver) });
|
|
174
|
+
balances.set(token, 0n);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const transfer = readTransfer(target, data, value);
|
|
178
|
+
if (!transfer || !balances.has(transfer.outputToken))
|
|
179
|
+
return parsed;
|
|
180
|
+
const token = transfer.outputToken;
|
|
181
|
+
balances.set(token, subtractFees(balances.get(token) ?? 0n, transfer.minOutputAmount ?? 0n));
|
|
182
|
+
directTransfers.push(transfer);
|
|
183
|
+
}
|
|
184
|
+
const fallback = asString(args.fallbackRecipient);
|
|
185
|
+
const hasDrain = payouts.length > 0;
|
|
186
|
+
// The Handler drains the original bridge asset to fallback even after successful calls.
|
|
187
|
+
const remaining = balances.get(parsed.bridgeOutputToken) ?? 0n;
|
|
188
|
+
if (!isZeroWord(fallback) && possibleRefundTokens.delete(parsed.bridgeOutputToken)) {
|
|
189
|
+
refundReceivers.push(fallback);
|
|
190
|
+
}
|
|
191
|
+
if (!isZeroWord(fallback) && remaining > 0n) {
|
|
192
|
+
payouts.push({ receiver: fallback, outputToken: parsed.bridgeOutputToken, minOutputAmount: remaining });
|
|
193
|
+
balances.set(parsed.bridgeOutputToken, 0n);
|
|
194
|
+
}
|
|
195
|
+
// Existing drain routes may contain explicit fee transfers; without a drain every
|
|
196
|
+
// direct transfer must agree on the recipient, and no tracked funds may be stranded.
|
|
197
|
+
const delivered = hasDrain
|
|
198
|
+
? [...payouts, ...directTransfers.filter((transfer) => transfer.receiver === payouts[0].receiver)]
|
|
199
|
+
: [...directTransfers, ...payouts];
|
|
200
|
+
if (delivered.length && !possibleRefundTokens.size && [...balances.values()].every((amount) => amount === 0n)) {
|
|
201
|
+
const receiver = delivered[0].receiver;
|
|
202
|
+
if (!delivered.every((payout) => payout.receiver === receiver) ||
|
|
203
|
+
!refundReceivers.every((recipient) => recipient === receiver))
|
|
204
|
+
return parsed;
|
|
205
|
+
if (!isZeroWord(fallback) && fallback !== receiver) {
|
|
206
|
+
throw new BridgeError(BRIDGE_ERRORS.ACROSS_FALLBACK_RECEIVER_MISMATCH);
|
|
207
|
+
}
|
|
208
|
+
parsed.receiver = receiver;
|
|
209
|
+
const token = delivered[0].outputToken;
|
|
210
|
+
if (delivered.every((payout) => payout.outputToken === token) &&
|
|
211
|
+
(isZeroWord(fallback) || token === parsed.bridgeOutputToken)) {
|
|
212
|
+
parsed.outputToken = token;
|
|
213
|
+
parsed.minOutputAmount = delivered.reduce((sum, payout) => sum + (payout.minOutputAmount ?? 0n), 0n);
|
|
214
|
+
}
|
|
215
|
+
// Different success/fallback assets do not invalidate a shared receiver, but
|
|
216
|
+
// neither asset nor a single minimum can be claimed for both outcomes.
|
|
217
|
+
}
|
|
218
|
+
return parsed;
|
|
219
|
+
};
|
|
220
|
+
export const applyAcrossMessage = (parsed, message) => {
|
|
221
|
+
try {
|
|
222
|
+
return readAcrossMessage(parsed, message);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw asBridgeError(error, BRIDGE_ERRORS.CALLDATA_DECODE_FAILED, parsed.diagnostics);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const ALLOWANCE_HOLDER = "0x0000000000001ff3684f28c67538d4d072c22734";
|
|
2
|
+
export declare const HYPEREVM_LIFI_ROUTER = "0x0a0758d937d1059c356d4714e57f5df0239bce1a";
|
|
3
|
+
export declare const readLifiSwap: (data: string, chain: bigint | undefined, target: string, recipient: string) => {
|
|
4
|
+
inputToken: string;
|
|
5
|
+
inputAmount: bigint;
|
|
6
|
+
outputToken: string;
|
|
7
|
+
minOutputAmount: bigint;
|
|
8
|
+
} | undefined;
|
|
9
|
+
export declare const readSettlerSwap: (data: string, chain: bigint | undefined, recipient: string, balances?: ReadonlyMap<string, bigint>) => {
|
|
10
|
+
inputToken: string;
|
|
11
|
+
inputAmount: bigint;
|
|
12
|
+
outputToken: string;
|
|
13
|
+
minOutputAmount: bigint;
|
|
14
|
+
} | undefined;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { asArray, asBigint, asRecord, asString, decodeCall } from './abi.js';
|
|
2
|
+
import { readSelector } from './hex.js';
|
|
3
|
+
import { isNative, NATIVE_TOKEN } from './protocols.js';
|
|
4
|
+
import { methods as lifiSwaps } from './lifi/swap/index.js';
|
|
5
|
+
export const ALLOWANCE_HOLDER = '0x0000000000001ff3684f28c67538d4d072c22734';
|
|
6
|
+
// Reviewed deployments, not dynamic registry lookups. Evidence and renewal limits:
|
|
7
|
+
// tests/fixtures/route-discovery-20261104/evidence/base-settler-verification.json
|
|
8
|
+
// tests/fixtures/reported-20261111-evidence.json (Ethereum)
|
|
9
|
+
const SETTLERS = {
|
|
10
|
+
1: '0xaddc26a3854feabfd20d6bf658254088d83a425a',
|
|
11
|
+
8453: '0x7d6839415693801e94cdc4a7d732c9a3fdbbd1d7',
|
|
12
|
+
999: '0xf58d4122251f2ace689f257eaf2688883e30fc6e',
|
|
13
|
+
};
|
|
14
|
+
export const HYPEREVM_LIFI_ROUTER = '0x0a0758d937d1059c356d4714e57f5df0239bce1a';
|
|
15
|
+
export const readLifiSwap = (data, chain, target, recipient) => {
|
|
16
|
+
if (chain !== 999n || target !== HYPEREVM_LIFI_ROUTER || readSelector(data) !== '0x4666fc80')
|
|
17
|
+
return undefined;
|
|
18
|
+
const method = lifiSwaps.find((item) => item.selector === '0x4666fc80');
|
|
19
|
+
const parsed = method?.decode(data, 0n);
|
|
20
|
+
if (!parsed || parsed.receiver !== recipient)
|
|
21
|
+
return undefined;
|
|
22
|
+
const { sourceToken: inputToken, sourceAmount: inputAmount, outputToken, minOutputAmount } = parsed;
|
|
23
|
+
if (!inputToken || isNative(inputToken) || !outputToken || isNative(outputToken) ||
|
|
24
|
+
inputAmount === undefined || inputAmount <= 0n || minOutputAmount === undefined || minOutputAmount <= 0n)
|
|
25
|
+
return undefined;
|
|
26
|
+
return { inputToken, inputAmount, outputToken, minOutputAmount };
|
|
27
|
+
};
|
|
28
|
+
export const readSettlerSwap = (data, chain, recipient, balances) => {
|
|
29
|
+
if (readSelector(data) !== '0x2213bc0b')
|
|
30
|
+
return undefined;
|
|
31
|
+
const args = decodeCall([
|
|
32
|
+
{ name: 'operator', kind: 'address' }, { name: 'token', kind: 'address' },
|
|
33
|
+
{ name: 'amount', kind: 'uint' }, { name: 'target', kind: 'address' }, { name: 'data', kind: 'bytes' },
|
|
34
|
+
], data);
|
|
35
|
+
const settler = SETTLERS[chain?.toString() ?? ''];
|
|
36
|
+
if (!settler || asString(args.target) !== settler || asString(args.operator) !== settler ||
|
|
37
|
+
isNative(asString(args.token)) || asBigint(args.amount) <= 0n)
|
|
38
|
+
return undefined;
|
|
39
|
+
const inner = asString(args.data);
|
|
40
|
+
if (readSelector(inner) !== '0x1fff991f')
|
|
41
|
+
return undefined;
|
|
42
|
+
const execution = decodeCall([
|
|
43
|
+
{ name: 'slippage', kind: 'tuple', components: [
|
|
44
|
+
{ name: 'recipient', kind: 'address' }, { name: 'buyToken', kind: 'address' }, { name: 'minAmountOut', kind: 'uint' },
|
|
45
|
+
] },
|
|
46
|
+
{ name: 'actions', kind: 'array', item: { name: 'action', kind: 'bytes' } },
|
|
47
|
+
{ name: 'zid', kind: 'bytes', size: 32 },
|
|
48
|
+
], inner);
|
|
49
|
+
const slippage = asRecord(execution.slippage);
|
|
50
|
+
if (asString(slippage.recipient) !== recipient || asString(slippage.buyToken) === NATIVE_TOKEN ||
|
|
51
|
+
asBigint(slippage.minAmountOut) <= 0n)
|
|
52
|
+
return undefined;
|
|
53
|
+
const actions = asArray(execution.actions).map(asString);
|
|
54
|
+
// These reviewed actions cannot mutate/zeroize slippage. CHECK_SLIPPAGE, SELECT
|
|
55
|
+
// and unreviewed actions must not bypass the final enforced transfer to Handler.
|
|
56
|
+
if (!actions.length || readSelector(actions[0]) !== '0xc1fb425e' ||
|
|
57
|
+
actions.slice(1).some((action) => !['0x38c9c147', '0x8d68a156', '0x5f80abae'].includes(readSelector(action) ?? '') &&
|
|
58
|
+
!(chain === 1n && readSelector(action) === '0xf61460f9')))
|
|
59
|
+
return undefined;
|
|
60
|
+
const funding = decodeCall([
|
|
61
|
+
{ name: 'recipient', kind: 'address' }, { name: 'token', kind: 'address' },
|
|
62
|
+
{ name: 'amount', kind: 'uint' }, { name: 'nonce', kind: 'uint' },
|
|
63
|
+
{ name: 'deadline', kind: 'uint' }, { name: 'signature', kind: 'bytes' },
|
|
64
|
+
], actions[0]);
|
|
65
|
+
if (asString(funding.recipient) !== settler || asString(funding.token) !== asString(args.token) ||
|
|
66
|
+
asBigint(funding.amount) !== asBigint(args.amount) || asString(funding.signature) !== '0x')
|
|
67
|
+
return undefined;
|
|
68
|
+
let inputAmount = asBigint(args.amount);
|
|
69
|
+
const maxUint256 = (1n << 256n) - 1n;
|
|
70
|
+
// shortcut: only reviewed Ethereum/HyperEVM 100%-balance encoding; review other ratios before support.
|
|
71
|
+
if ((chain === 1n || chain === 999n) && inputAmount > maxUint256 - 1000000n) {
|
|
72
|
+
if (inputAmount !== maxUint256)
|
|
73
|
+
return undefined;
|
|
74
|
+
inputAmount = balances?.get(asString(args.token)) ?? 0n;
|
|
75
|
+
if (inputAmount <= 0n)
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
inputToken: asString(args.token), inputAmount,
|
|
80
|
+
outputToken: isNative(asString(slippage.buyToken)) ? NATIVE_TOKEN : asString(slippage.buyToken),
|
|
81
|
+
minOutputAmount: asBigint(slippage.minAmountOut),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BridgeErrorContext } from '../errors/index.js';
|
|
2
|
+
/** Inspect call structure only. Never retain arguments or scan arbitrary payloads for selectors. */
|
|
3
|
+
export declare const calldataDiagnostics: (data: unknown) => BridgeErrorContext;
|
|
4
|
+
export declare const innerCallDiagnostics: (data: string, layer: "router" | "protocol") => BridgeErrorContext;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Inspect call structure only. Never retain arguments or scan arbitrary payloads for selectors. */
|
|
2
|
+
export const calldataDiagnostics = (data) => {
|
|
3
|
+
if (typeof data !== 'string')
|
|
4
|
+
return { calldataFormat: 'missing' };
|
|
5
|
+
const valid = /^0x(?:[0-9a-f]{2})*$/i.test(data);
|
|
6
|
+
const selector = /^0x[0-9a-f]{8}/i.test(data) ? data.slice(0, 10).toLowerCase() : undefined;
|
|
7
|
+
return {
|
|
8
|
+
selector,
|
|
9
|
+
calldataFormat: valid ? (data.length === 2 ? 'empty' : 'valid') : 'invalid',
|
|
10
|
+
...(valid ? { calldataBytes: (data.length - 2) / 2 } : {}),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export const innerCallDiagnostics = (data, layer) => {
|
|
14
|
+
const { selector, calldataBytes } = calldataDiagnostics(data);
|
|
15
|
+
return layer === 'router'
|
|
16
|
+
? { routerSelector: selector, routerCalldataBytes: calldataBytes }
|
|
17
|
+
: { protocolSelector: selector, protocolCalldataBytes: calldataBytes };
|
|
18
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BridgeErrorDefinition } from '../errors/index.js';
|
|
2
|
+
import type { BridgeCheck, ParsedBridgeCall } from './types.js';
|
|
3
|
+
export type BridgeCallExpectation = {
|
|
4
|
+
userAddress: string;
|
|
5
|
+
fromTokenId: string;
|
|
6
|
+
fromTokenRawAmount: string;
|
|
7
|
+
toTokenId: string;
|
|
8
|
+
/** 目标链 id。目标币是原生币时,用它识别 `hood` 这类链 id,而不是源链 id。 */
|
|
9
|
+
toChainId?: string;
|
|
10
|
+
toTokenRawAmount?: string;
|
|
11
|
+
/** Selected backend quote key; only protocol identifiers with known semantics are compared. */
|
|
12
|
+
quoteKey?: string | Record<string, unknown>;
|
|
13
|
+
/** Relay `protocol.v2.orderData` captured with the transaction being signed. */
|
|
14
|
+
orderData?: unknown;
|
|
15
|
+
/** 小数滑点。0.01 表示 1%。 */
|
|
16
|
+
slippage?: string | number;
|
|
17
|
+
};
|
|
18
|
+
export type CompareMask = {
|
|
19
|
+
fromToken: boolean;
|
|
20
|
+
fromAmount: boolean;
|
|
21
|
+
toToken: boolean;
|
|
22
|
+
toAmount: boolean;
|
|
23
|
+
receiver: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare const knownCompare: (parsed: ParsedBridgeCall) => CompareMask;
|
|
26
|
+
/** Source payment is capped at 100.1% of local intent; smaller payments are allowed. */
|
|
27
|
+
export declare const sourceAmountMatches: (actual: bigint | undefined, expected: bigint) => boolean;
|
|
28
|
+
export declare const expectationFailures: (parsed: ParsedBridgeCall, expectation: BridgeCallExpectation, chainId: string, mask: CompareMask) => BridgeErrorDefinition[];
|
|
29
|
+
export declare const finishCheck: (aggregatorId: string, parsed: ParsedBridgeCall, structural: BridgeErrorDefinition | undefined, expectation: BridgeCallExpectation | undefined, chainId: string, mask: CompareMask) => BridgeCheck;
|