@wayne-zhang/ovault-evm 0.0.1
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 +141 -0
- package/artifacts/IVaultComposerSync.sol/IVaultComposerSync.json +1126 -0
- package/artifacts/IVaultComposerSyncNative.sol/IVaultComposerSyncNative.json +323 -0
- package/artifacts/VaultComposerSync.sol/VaultComposerSync.json +1399 -0
- package/artifacts/VaultComposerSyncNative.sol/VaultComposerSyncNative.json +1540 -0
- package/dist/contracts/EIP2612.d.ts +14 -0
- package/dist/contracts/EIP2612.d.ts.map +1 -0
- package/dist/contracts/EIP2612.js +38 -0
- package/dist/contracts/ERC20.d.ts +169 -0
- package/dist/contracts/ERC20.d.ts.map +1 -0
- package/dist/contracts/ERC20.js +222 -0
- package/dist/contracts/ERC4626.d.ts +2020 -0
- package/dist/contracts/ERC4626.d.ts.map +1 -0
- package/dist/contracts/ERC4626.js +2638 -0
- package/dist/contracts/OFT.d.ts +1736 -0
- package/dist/contracts/OFT.d.ts.map +1 -0
- package/dist/contracts/OFT.js +2251 -0
- package/dist/contracts/OVaultComposer.d.ts +94 -0
- package/dist/contracts/OVaultComposer.d.ts.map +1 -0
- package/dist/contracts/OVaultComposer.js +1629 -0
- package/dist/contracts/OVaultComposerSync.d.ts +528 -0
- package/dist/contracts/OVaultComposerSync.d.ts.map +1 -0
- package/dist/contracts/OVaultComposerSync.js +682 -0
- package/dist/contracts/OVaultComposerSyncNative.d.ts +560 -0
- package/dist/contracts/OVaultComposerSyncNative.d.ts.map +1 -0
- package/dist/contracts/OVaultComposerSyncNative.js +723 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/oVaultSync/index.d.ts +3 -0
- package/dist/oVaultSync/index.d.ts.map +1 -0
- package/dist/oVaultSync/index.js +2 -0
- package/dist/oVaultSync/tracking.d.ts +39 -0
- package/dist/oVaultSync/tracking.d.ts.map +1 -0
- package/dist/oVaultSync/tracking.js +458 -0
- package/dist/oVaultSync/transfer.d.ts +94 -0
- package/dist/oVaultSync/transfer.d.ts.map +1 -0
- package/dist/oVaultSync/transfer.js +431 -0
- package/dist/types.d.ts +149 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/package.json +87 -0
- package/src/contracts/EIP2612.ts +38 -0
- package/src/contracts/ERC20.ts +222 -0
- package/src/contracts/ERC4626.ts +2638 -0
- package/src/contracts/OFT.ts +2251 -0
- package/src/contracts/OVaultComposer.ts +1629 -0
- package/src/contracts/OVaultComposerSync.ts +682 -0
- package/src/index.ts +2 -0
- package/src/oVaultSync/index.ts +2 -0
- package/src/oVaultSync/tracking.ts +623 -0
- package/src/oVaultSync/transfer.ts +615 -0
- package/src/types.ts +201 -0
- package/test/sdk/tracker.test.ts +18 -0
- package/test/sdk/transfer.test.ts +330 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPublicClient,
|
|
3
|
+
encodeAbiParameters,
|
|
4
|
+
erc20Abi,
|
|
5
|
+
hexToBigInt,
|
|
6
|
+
http,
|
|
7
|
+
pad,
|
|
8
|
+
stringToHex,
|
|
9
|
+
} from "viem";
|
|
10
|
+
import { OFTAbi } from "../contracts/OFT";
|
|
11
|
+
import { OVaultComposerSyncAbi } from "../contracts/OVaultComposerSync";
|
|
12
|
+
import { ERC4626_ABI } from "../contracts/ERC4626";
|
|
13
|
+
import { Options } from "@layerzerolabs/lz-v2-utilities";
|
|
14
|
+
import { ERC20Abi } from "../contracts/ERC20";
|
|
15
|
+
import { EIP2612_ABI } from "../contracts/EIP2612";
|
|
16
|
+
import {
|
|
17
|
+
OVaultSyncInputs,
|
|
18
|
+
SendParams,
|
|
19
|
+
SendParamsInput,
|
|
20
|
+
GenerateOVaultSyncInputsProps,
|
|
21
|
+
OVaultSyncOperations,
|
|
22
|
+
} from "../types";
|
|
23
|
+
|
|
24
|
+
const EXTRA_GAS_COMPOSE = 300_000n;
|
|
25
|
+
|
|
26
|
+
export class OVaultSyncMessageBuilder {
|
|
27
|
+
private static getFormattedRefCode(code?: string): `0x${string}` {
|
|
28
|
+
return code
|
|
29
|
+
? (stringToHex(code, { size: 32 }) as `0x${string}`)
|
|
30
|
+
: ("0x" as `0x${string}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a string amount to BigInt with specified decimals, always truncating (never rounding up)
|
|
35
|
+
* This is critical for LayerZero operations to avoid precision issues
|
|
36
|
+
*/
|
|
37
|
+
private static parseUnitsTruncate(value: string, decimals: number): bigint {
|
|
38
|
+
const strValue = String(value);
|
|
39
|
+
const [integerPart, decimalPart = ""] = strValue.split(".");
|
|
40
|
+
const truncatedDecimalPart = decimalPart
|
|
41
|
+
.slice(0, decimals)
|
|
42
|
+
.padEnd(decimals, "0");
|
|
43
|
+
const combined = integerPart + truncatedDecimalPart;
|
|
44
|
+
return BigInt(combined);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* LayerZero uses 6 shared decimals for cross-chain transfers.
|
|
49
|
+
* This function removes dust below the 6 decimal precision that LayerZero won't transfer.
|
|
50
|
+
* For tokens with >6 decimals, this prevents quoteSend failures due to precision loss.
|
|
51
|
+
*/
|
|
52
|
+
private static removeLzDust(amount: bigint, decimals: number): bigint {
|
|
53
|
+
if (decimals <= 6) return amount;
|
|
54
|
+
const dust = 10n ** BigInt(decimals - 6);
|
|
55
|
+
return (amount / dust) * dust;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static buildComposeArgument(input: SendParamsInput, messageFee: bigint) {
|
|
59
|
+
// For the default OVault, the compose argument is just the hub to dst chain send params
|
|
60
|
+
// However, if you are using a custom OVault, you can have a custom compose argument
|
|
61
|
+
return encodeAbiParameters(
|
|
62
|
+
[
|
|
63
|
+
{
|
|
64
|
+
type: "tuple",
|
|
65
|
+
name: "sendParams",
|
|
66
|
+
components: [
|
|
67
|
+
{ name: "dstEid", type: "uint32" },
|
|
68
|
+
{ name: "to", type: "bytes32" },
|
|
69
|
+
{ name: "amountLD", type: "uint256" },
|
|
70
|
+
{ name: "minAmountLD", type: "uint256" },
|
|
71
|
+
{ name: "extraOptions", type: "bytes" },
|
|
72
|
+
{ name: "composeMsg", type: "bytes" },
|
|
73
|
+
{ name: "oftCmd", type: "bytes" },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: "uint256",
|
|
78
|
+
name: "minMsgValue",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
[this.buildHubToDstChainSendParams(input), messageFee],
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the send params for the hub chain to the dst chain.
|
|
87
|
+
*
|
|
88
|
+
* @param input - The input parameters for the OVault.
|
|
89
|
+
* @returns The send params for the hub chain to the dst chain.
|
|
90
|
+
*/
|
|
91
|
+
static buildHubToDstChainSendParams(input: SendParamsInput) {
|
|
92
|
+
const {
|
|
93
|
+
srcEid,
|
|
94
|
+
hubEid,
|
|
95
|
+
dstEid,
|
|
96
|
+
dstAddress,
|
|
97
|
+
dstAmount,
|
|
98
|
+
minDstAmount,
|
|
99
|
+
referralCode,
|
|
100
|
+
} = input;
|
|
101
|
+
|
|
102
|
+
const srcIsHubChain = srcEid === hubEid;
|
|
103
|
+
|
|
104
|
+
// This is for a basic OFT send, so the enforced options should be enough
|
|
105
|
+
const options = Options.newOptions();
|
|
106
|
+
return {
|
|
107
|
+
dstEid: dstEid,
|
|
108
|
+
to: pad(dstAddress, { size: 32 }),
|
|
109
|
+
amountLD: dstAmount,
|
|
110
|
+
minAmountLD: minDstAmount,
|
|
111
|
+
extraOptions: options.toHex() as `0x${string}`,
|
|
112
|
+
composeMsg: "0x" as `0x${string}`,
|
|
113
|
+
oftCmd: srcIsHubChain
|
|
114
|
+
? this.getFormattedRefCode(referralCode)
|
|
115
|
+
: ("0x" as `0x${string}`),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Quote the amount of shares or assets that will be received from the OVault.
|
|
121
|
+
*
|
|
122
|
+
* @param input - The input parameters for the OVault.
|
|
123
|
+
* @returns The amount of shares or assets that will be received from the OVault.
|
|
124
|
+
*/
|
|
125
|
+
static async quoteOVaultOutput(
|
|
126
|
+
input: GenerateOVaultSyncInputsProps,
|
|
127
|
+
): Promise<{
|
|
128
|
+
dstAmount: bigint;
|
|
129
|
+
minDstAmount: bigint;
|
|
130
|
+
}> {
|
|
131
|
+
const {
|
|
132
|
+
operation,
|
|
133
|
+
vaultAddress,
|
|
134
|
+
hubChain,
|
|
135
|
+
slippage,
|
|
136
|
+
srcEid,
|
|
137
|
+
dstEid,
|
|
138
|
+
hubEid,
|
|
139
|
+
} = input;
|
|
140
|
+
|
|
141
|
+
const client = createPublicClient({
|
|
142
|
+
chain: hubChain,
|
|
143
|
+
transport: http(),
|
|
144
|
+
});
|
|
145
|
+
if (input.amount == null || input.tokenHubDecimals == null) {
|
|
146
|
+
throw new Error("inputs cannot be null");
|
|
147
|
+
}
|
|
148
|
+
let amountInHubDecimals = this.parseUnitsTruncate(
|
|
149
|
+
input.amount,
|
|
150
|
+
input.tokenHubDecimals,
|
|
151
|
+
);
|
|
152
|
+
// Only remove LayerZero dust if this is a cross-chain operation
|
|
153
|
+
// LayerZero is involved if source or destination is not the hub chain
|
|
154
|
+
const isCrossChain = srcEid !== hubEid || dstEid !== hubEid;
|
|
155
|
+
if (isCrossChain) {
|
|
156
|
+
amountInHubDecimals = this.removeLzDust(
|
|
157
|
+
amountInHubDecimals,
|
|
158
|
+
input.tokenHubDecimals,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const outputAmount = await client.readContract({
|
|
163
|
+
address: vaultAddress,
|
|
164
|
+
abi: ERC4626_ABI,
|
|
165
|
+
functionName:
|
|
166
|
+
operation === OVaultSyncOperations.DEPOSIT
|
|
167
|
+
? "previewDeposit"
|
|
168
|
+
: "previewRedeem",
|
|
169
|
+
args: [amountInHubDecimals],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (slippage < 0.001)
|
|
173
|
+
throw new Error("Slippage must be greater or equal to 0.001 (0.1%)");
|
|
174
|
+
if (outputAmount === 0n) throw new Error("Output amount is too small");
|
|
175
|
+
|
|
176
|
+
const slippageAmount =
|
|
177
|
+
(outputAmount * BigInt(Number(slippage.toFixed(3)) * 1000)) / 1000n;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
dstAmount: outputAmount,
|
|
181
|
+
minDstAmount: outputAmount - slippageAmount,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Calculate the message fee for the hub chain. This is the fee that it costs to send a message from the hub chain to the dst chain.
|
|
187
|
+
*
|
|
188
|
+
* @param input - The input parameters for the OVault.
|
|
189
|
+
* @param useWalletAddress
|
|
190
|
+
* @returns The message fee for the hub chain.
|
|
191
|
+
*/
|
|
192
|
+
static async calculateHubChainFee(
|
|
193
|
+
input: SendParamsInput,
|
|
194
|
+
useWalletAddress = true,
|
|
195
|
+
) {
|
|
196
|
+
const {
|
|
197
|
+
srcEid,
|
|
198
|
+
operation,
|
|
199
|
+
amount,
|
|
200
|
+
composerAddress,
|
|
201
|
+
hubChain,
|
|
202
|
+
dstEid,
|
|
203
|
+
hubEid,
|
|
204
|
+
oftHubAddress,
|
|
205
|
+
vaultAddress,
|
|
206
|
+
buffer,
|
|
207
|
+
} = input;
|
|
208
|
+
|
|
209
|
+
// If the dst chain is the same as the hub chain, then we don't need to calculate the fee
|
|
210
|
+
// as we are already on the hub chain, so "send" on the OFT will not be called.
|
|
211
|
+
if (dstEid === hubEid) {
|
|
212
|
+
return {
|
|
213
|
+
nativeFee: 0n,
|
|
214
|
+
lzTokenFee: 0n,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const client = createPublicClient({
|
|
219
|
+
chain: hubChain,
|
|
220
|
+
transport: http(),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const hubSendParams = this.buildHubToDstChainSendParams(input);
|
|
224
|
+
|
|
225
|
+
// Determine the target OFT based on operation:
|
|
226
|
+
// - For deposits: shares are sent via share OFT
|
|
227
|
+
// - For redeems: assets are sent via asset OFT
|
|
228
|
+
const targetOft =
|
|
229
|
+
operation === OVaultSyncOperations.DEPOSIT ? vaultAddress : oftHubAddress;
|
|
230
|
+
|
|
231
|
+
if (srcEid === hubEid) {
|
|
232
|
+
// For same-chain operations, use wallet address. For cross-chain, we could use
|
|
233
|
+
// the OFT address since the wallet may not have the tokens yet.
|
|
234
|
+
if (!useWalletAddress) {
|
|
235
|
+
throw new Error("Wallet address is required");
|
|
236
|
+
}
|
|
237
|
+
const quote = await client.readContract({
|
|
238
|
+
address: composerAddress,
|
|
239
|
+
abi: OVaultComposerSyncAbi,
|
|
240
|
+
functionName: "quoteSend",
|
|
241
|
+
args: [input.walletAddress, targetOft, amount, hubSendParams],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
nativeFee: this.increaseByBuffer(quote.nativeFee, buffer),
|
|
246
|
+
lzTokenFee: this.increaseByBuffer(quote.lzTokenFee, buffer),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// We check directly on the OFT to get the message fee as the vault address or the share OFT address
|
|
251
|
+
// won't have shares or assets balance as we burn them.
|
|
252
|
+
// This is different to the original LZ implementation, where they use
|
|
253
|
+
// OFT adapter for the shares token in accounting chain (the tokens are locked in that case,
|
|
254
|
+
// not burned). In their case, they always call VaultComposer, for maxDeposit/maxRedeem checks.
|
|
255
|
+
const quote = await client.readContract({
|
|
256
|
+
address: targetOft,
|
|
257
|
+
abi: OFTAbi,
|
|
258
|
+
functionName: "quoteSend",
|
|
259
|
+
args: [hubSendParams as never, false],
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
nativeFee: this.increaseByBuffer(quote.nativeFee, buffer),
|
|
264
|
+
lzTokenFee: this.increaseByBuffer(quote.lzTokenFee, buffer),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static async getMessageFee(sendParams: SendParams, input: SendParamsInput) {
|
|
269
|
+
const {
|
|
270
|
+
operation,
|
|
271
|
+
oftAddress,
|
|
272
|
+
vaultTokenAddress,
|
|
273
|
+
sourceChain,
|
|
274
|
+
srcEid,
|
|
275
|
+
hubEid,
|
|
276
|
+
buffer,
|
|
277
|
+
} = input;
|
|
278
|
+
|
|
279
|
+
const client = createPublicClient({
|
|
280
|
+
chain: sourceChain,
|
|
281
|
+
transport: http(),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// If the src chain is the same as the hub chain, then the way to calculate the message fee is the same
|
|
285
|
+
// as calculating the hub chain fee as we are already on the hub chain.
|
|
286
|
+
if (srcEid === hubEid) {
|
|
287
|
+
return this.calculateHubChainFee(input, true);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const sendingOftAddress =
|
|
291
|
+
operation === OVaultSyncOperations.DEPOSIT
|
|
292
|
+
? oftAddress
|
|
293
|
+
: vaultTokenAddress;
|
|
294
|
+
|
|
295
|
+
// If we are not on the hub chain, then we need to call the OFT's quoteSend function to get the message fee.
|
|
296
|
+
const messageFee = await client.readContract({
|
|
297
|
+
address: sendingOftAddress,
|
|
298
|
+
abi: OFTAbi,
|
|
299
|
+
functionName: "quoteSend",
|
|
300
|
+
args: [sendParams as never, false],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
nativeFee: this.increaseByBuffer(messageFee.nativeFee, buffer),
|
|
305
|
+
lzTokenFee: this.increaseByBuffer(messageFee.lzTokenFee, buffer),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
static increaseByBuffer(amount: bigint, buffer: number = 0) {
|
|
310
|
+
if (!buffer || buffer <= 0) {
|
|
311
|
+
return amount;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const flatNumber = BigInt((buffer * 100).toFixed(0));
|
|
315
|
+
|
|
316
|
+
const bufferAmount = (amount * flatNumber) / 100n;
|
|
317
|
+
return amount + bufferAmount;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
static async buildSendParams(input: SendParamsInput) {
|
|
321
|
+
const {
|
|
322
|
+
amount,
|
|
323
|
+
srcEid,
|
|
324
|
+
composerAddress,
|
|
325
|
+
hubEid,
|
|
326
|
+
dstAmount,
|
|
327
|
+
minDstAmount,
|
|
328
|
+
referralCode,
|
|
329
|
+
} = input;
|
|
330
|
+
|
|
331
|
+
const options = Options.newOptions();
|
|
332
|
+
|
|
333
|
+
const srcIsHubChain = srcEid === hubEid;
|
|
334
|
+
|
|
335
|
+
if (srcIsHubChain) {
|
|
336
|
+
return this.buildHubToDstChainSendParams(input);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// If the src chain is not the same as the hub chain, then the first message will be an LzCompose
|
|
340
|
+
// so we need to add the executor option
|
|
341
|
+
const hubMessageFee = await this.calculateHubChainFee(
|
|
342
|
+
{
|
|
343
|
+
...input,
|
|
344
|
+
dstAmount,
|
|
345
|
+
minDstAmount,
|
|
346
|
+
},
|
|
347
|
+
false,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const extraGas = input.hubLzComposeGasLimit ?? EXTRA_GAS_COMPOSE;
|
|
351
|
+
options.addExecutorComposeOption(0, extraGas, hubMessageFee.nativeFee);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
// If the src chain is not the same as the hub chain, then the first message will be to the hub chain
|
|
355
|
+
// so we need to set the dstEid to the hub chain EID
|
|
356
|
+
dstEid: hubEid,
|
|
357
|
+
to: pad(composerAddress, { size: 32 }),
|
|
358
|
+
amountLD: amount,
|
|
359
|
+
// TODO: find out why no slippage
|
|
360
|
+
minAmountLD: amount,
|
|
361
|
+
extraOptions: options.toHex() as `0x${string}`,
|
|
362
|
+
composeMsg: this.buildComposeArgument(input, hubMessageFee.nativeFee),
|
|
363
|
+
oftCmd: this.getFormattedRefCode(referralCode),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
static async buildApproval(input: SendParamsInput) {
|
|
368
|
+
const {
|
|
369
|
+
tokenAddress,
|
|
370
|
+
sourceChain,
|
|
371
|
+
composerAddress,
|
|
372
|
+
srcEid,
|
|
373
|
+
hubEid,
|
|
374
|
+
amount,
|
|
375
|
+
walletAddress,
|
|
376
|
+
vaultTokenAddress,
|
|
377
|
+
oftAddress,
|
|
378
|
+
operation,
|
|
379
|
+
supportsEip2612,
|
|
380
|
+
requiresZeroApprovalReset,
|
|
381
|
+
} = input;
|
|
382
|
+
|
|
383
|
+
let spender: `0x${string}` | null = null;
|
|
384
|
+
let approvingTokenAddress: `0x${string}` | null = null;
|
|
385
|
+
const srcIsHubChain = srcEid === hubEid;
|
|
386
|
+
|
|
387
|
+
// 1. We are locking the base token on a spoke chain (UnderlyingOFTAdapter)
|
|
388
|
+
// In this case, we need to approve the OFTAdapter to spend the base token.
|
|
389
|
+
if (!srcIsHubChain && operation == OVaultSyncOperations.DEPOSIT) {
|
|
390
|
+
approvingTokenAddress = tokenAddress;
|
|
391
|
+
spender = oftAddress;
|
|
392
|
+
// 2. We are depositing the base token on the hub chain
|
|
393
|
+
// In this case, we need to approve the VaultComposer to spend the base token (depositAndSend).
|
|
394
|
+
} else if (srcIsHubChain && operation == OVaultSyncOperations.DEPOSIT) {
|
|
395
|
+
approvingTokenAddress = tokenAddress;
|
|
396
|
+
spender = composerAddress;
|
|
397
|
+
// 3. We are requesting a withdrawal on a spoke chain (VaultToken)
|
|
398
|
+
// No need, as it's handled by OFT.send in VaultToken
|
|
399
|
+
} else if (!srcIsHubChain && operation == OVaultSyncOperations.REDEEM) {
|
|
400
|
+
return;
|
|
401
|
+
// 4. We are requesting a withdrawal on the hub chain (Vault)
|
|
402
|
+
// In this case, we need to approve the Composer to spend the shares.
|
|
403
|
+
} else if (srcIsHubChain && operation == OVaultSyncOperations.REDEEM) {
|
|
404
|
+
approvingTokenAddress = vaultTokenAddress;
|
|
405
|
+
spender = composerAddress;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// If we couldn't determine spender/token, there's nothing to approve
|
|
409
|
+
if (!approvingTokenAddress || !spender) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const client = createPublicClient({
|
|
414
|
+
chain: sourceChain,
|
|
415
|
+
transport: http(),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const allowance = await client.readContract({
|
|
419
|
+
address: approvingTokenAddress,
|
|
420
|
+
abi: ERC20Abi,
|
|
421
|
+
functionName: "allowance",
|
|
422
|
+
args: [walletAddress, spender],
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
if (allowance >= amount) {
|
|
426
|
+
// We have enough allowance, so we don't need to approve
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Check if the asset supports EIP-2612 permit
|
|
431
|
+
// Only use permit for deposits with the base token (not vault shares)
|
|
432
|
+
const usePermit =
|
|
433
|
+
operation === OVaultSyncOperations.DEPOSIT && supportsEip2612 === true;
|
|
434
|
+
|
|
435
|
+
if (usePermit) {
|
|
436
|
+
try {
|
|
437
|
+
const [nonceResult, tokenNameResult, tokenVersionResult] =
|
|
438
|
+
await client.multicall({
|
|
439
|
+
contracts: [
|
|
440
|
+
{
|
|
441
|
+
address: approvingTokenAddress as `0x${string}`,
|
|
442
|
+
abi: EIP2612_ABI,
|
|
443
|
+
functionName: "nonces",
|
|
444
|
+
args: [walletAddress],
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
address: approvingTokenAddress as `0x${string}`,
|
|
448
|
+
abi: erc20Abi,
|
|
449
|
+
functionName: "name",
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
address: approvingTokenAddress as `0x${string}`,
|
|
453
|
+
abi: EIP2612_ABI,
|
|
454
|
+
functionName: "version",
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (
|
|
460
|
+
nonceResult.status !== "success" ||
|
|
461
|
+
tokenNameResult.status !== "success"
|
|
462
|
+
) {
|
|
463
|
+
throw new Error("Failed to fetch permit data");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const tokenVersion =
|
|
467
|
+
tokenVersionResult.status !== "success"
|
|
468
|
+
? "1"
|
|
469
|
+
: (tokenVersionResult.result as string);
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
tokenAddress: approvingTokenAddress,
|
|
473
|
+
amount,
|
|
474
|
+
spender,
|
|
475
|
+
usePermit: true,
|
|
476
|
+
nonce: nonceResult.result as bigint,
|
|
477
|
+
tokenName: tokenNameResult.result as string,
|
|
478
|
+
tokenVersion,
|
|
479
|
+
};
|
|
480
|
+
} catch (error) {
|
|
481
|
+
// If nonce fetch fails, fall back to regular approval
|
|
482
|
+
console.warn(
|
|
483
|
+
"Failed to fetch nonce for permit, falling back to approval:",
|
|
484
|
+
error,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Some tokens (like USDT) require approval to be set to 0 before changing to a new value
|
|
490
|
+
// If there's an existing non-zero allowance, we need a two-step approval
|
|
491
|
+
const needsResetApproval =
|
|
492
|
+
requiresZeroApprovalReset && allowance > 0n && allowance < amount;
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
tokenAddress: approvingTokenAddress,
|
|
496
|
+
amount,
|
|
497
|
+
spender,
|
|
498
|
+
usePermit: false,
|
|
499
|
+
needsResetApproval,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Generate the inputs for the OVault.
|
|
505
|
+
*
|
|
506
|
+
* @param input - The input parameters for the OVault.
|
|
507
|
+
* @returns Inputs to call contracts to perform Deposit or Redeem on the OVault.
|
|
508
|
+
*
|
|
509
|
+
*/
|
|
510
|
+
static async generateOVaultInputs(
|
|
511
|
+
input: GenerateOVaultSyncInputsProps,
|
|
512
|
+
): Promise<OVaultSyncInputs> {
|
|
513
|
+
const {
|
|
514
|
+
srcEid,
|
|
515
|
+
hubEid,
|
|
516
|
+
dstEid,
|
|
517
|
+
operation,
|
|
518
|
+
amount, // amount should be not scaled (string)
|
|
519
|
+
dstAddress,
|
|
520
|
+
walletAddress: refundAddress,
|
|
521
|
+
composerAddress,
|
|
522
|
+
oftAddress, // OFT/OFTAdapter on the source chain
|
|
523
|
+
vaultTokenAddress, // VaultToken address on the source chain
|
|
524
|
+
tokenLocalDecimals,
|
|
525
|
+
} = input;
|
|
526
|
+
|
|
527
|
+
// Calculates the minimum amount based on the slippage
|
|
528
|
+
const outputAmount = await this.quoteOVaultOutput(input);
|
|
529
|
+
|
|
530
|
+
// Parse the amount
|
|
531
|
+
let amountInLocalDecimals = this.parseUnitsTruncate(
|
|
532
|
+
amount,
|
|
533
|
+
tokenLocalDecimals,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
// Only remove LayerZero dust if this is a cross-chain operation
|
|
537
|
+
// LayerZero is involved if source or destination is not the hub chain
|
|
538
|
+
const isCrossChain = srcEid !== hubEid || dstEid !== hubEid;
|
|
539
|
+
if (isCrossChain) {
|
|
540
|
+
amountInLocalDecimals = this.removeLzDust(
|
|
541
|
+
amountInLocalDecimals,
|
|
542
|
+
tokenLocalDecimals,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (amountInLocalDecimals === 0n) {
|
|
547
|
+
throw new Error("Amount is too small");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const fullInputParams = {
|
|
551
|
+
...input,
|
|
552
|
+
amount: amountInLocalDecimals,
|
|
553
|
+
dstAmount: outputAmount.dstAmount,
|
|
554
|
+
minDstAmount: outputAmount.minDstAmount,
|
|
555
|
+
dstAddress: dstAddress ?? refundAddress,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const sendParams = await this.buildSendParams(fullInputParams);
|
|
559
|
+
const messageFee = await this.getMessageFee(sendParams, fullInputParams);
|
|
560
|
+
|
|
561
|
+
const srcIsHubChain = srcEid === hubEid;
|
|
562
|
+
|
|
563
|
+
const approvalData = await this.buildApproval(fullInputParams);
|
|
564
|
+
|
|
565
|
+
if (!srcIsHubChain) {
|
|
566
|
+
// Spoke chain: use 'send' or 'sendWithPermit' for deposits
|
|
567
|
+
const usePermit =
|
|
568
|
+
approvalData?.usePermit && operation === OVaultSyncOperations.DEPOSIT;
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
messageFee,
|
|
572
|
+
dstAmount: {
|
|
573
|
+
amount: outputAmount.dstAmount,
|
|
574
|
+
minAmount: outputAmount.minDstAmount,
|
|
575
|
+
},
|
|
576
|
+
approval: approvalData,
|
|
577
|
+
txArgs: {
|
|
578
|
+
base: [sendParams, messageFee, refundAddress],
|
|
579
|
+
permitParams: usePermit ? ["deadline", "v", "r", "s"] : null,
|
|
580
|
+
},
|
|
581
|
+
contractFunctionName: usePermit ? "sendWithPermit" : "send",
|
|
582
|
+
contractAddress:
|
|
583
|
+
operation == OVaultSyncOperations.DEPOSIT
|
|
584
|
+
? oftAddress
|
|
585
|
+
: vaultTokenAddress,
|
|
586
|
+
abi: OFTAbi,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Hub chain: use 'depositAndSend' or 'depositAndSendWithPermit' for deposits
|
|
591
|
+
const usePermit =
|
|
592
|
+
approvalData?.usePermit && operation === OVaultSyncOperations.DEPOSIT;
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
messageFee,
|
|
596
|
+
dstAmount: {
|
|
597
|
+
amount: outputAmount.dstAmount,
|
|
598
|
+
minAmount: outputAmount.minDstAmount,
|
|
599
|
+
},
|
|
600
|
+
approval: approvalData,
|
|
601
|
+
txArgs: {
|
|
602
|
+
base: [amountInLocalDecimals, sendParams, refundAddress ?? dstAddress],
|
|
603
|
+
permitParams: usePermit ? ["deadline", "v", "r", "s"] : null,
|
|
604
|
+
},
|
|
605
|
+
contractFunctionName:
|
|
606
|
+
operation === OVaultSyncOperations.DEPOSIT
|
|
607
|
+
? usePermit
|
|
608
|
+
? "depositAndSendWithPermit"
|
|
609
|
+
: "depositAndSend"
|
|
610
|
+
: "redeemAndSend",
|
|
611
|
+
contractAddress: composerAddress,
|
|
612
|
+
abi: OVaultComposerSyncAbi,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|