@morpho-org/bundler-sdk-viem 4.1.4 → 4.3.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.
Files changed (48) hide show
  1. package/lib/{actions.js → cjs/actions.js} +3 -0
  2. package/lib/cjs/package.json +1 -0
  3. package/lib/esm/ActionBundle.d.ts +37 -0
  4. package/lib/esm/ActionBundle.js +35 -0
  5. package/lib/esm/BundlerAction.d.ts +470 -0
  6. package/lib/esm/BundlerAction.js +1657 -0
  7. package/lib/esm/abis.d.ts +3014 -0
  8. package/lib/esm/abis.js +2047 -0
  9. package/lib/esm/actions.d.ts +10 -0
  10. package/lib/esm/actions.js +793 -0
  11. package/lib/esm/bundle.d.ts +12 -0
  12. package/lib/esm/bundle.js +11 -0
  13. package/lib/esm/errors.d.ts +30 -0
  14. package/lib/esm/errors.js +54 -0
  15. package/lib/esm/index.d.ts +8 -0
  16. package/lib/esm/index.js +8 -0
  17. package/lib/esm/operations.d.ts +78 -0
  18. package/lib/esm/operations.js +800 -0
  19. package/lib/esm/package.json +1 -0
  20. package/lib/esm/types/actions.d.ts +355 -0
  21. package/lib/esm/types/actions.js +1 -0
  22. package/lib/esm/types/index.d.ts +2 -0
  23. package/lib/esm/types/index.js +2 -0
  24. package/lib/esm/types/operations.d.ts +86 -0
  25. package/lib/esm/types/operations.js +55 -0
  26. package/package.json +25 -16
  27. package/src/index.ts +8 -0
  28. /package/lib/{ActionBundle.d.ts → cjs/ActionBundle.d.ts} +0 -0
  29. /package/lib/{ActionBundle.js → cjs/ActionBundle.js} +0 -0
  30. /package/lib/{BundlerAction.d.ts → cjs/BundlerAction.d.ts} +0 -0
  31. /package/lib/{BundlerAction.js → cjs/BundlerAction.js} +0 -0
  32. /package/lib/{abis.d.ts → cjs/abis.d.ts} +0 -0
  33. /package/lib/{abis.js → cjs/abis.js} +0 -0
  34. /package/lib/{actions.d.ts → cjs/actions.d.ts} +0 -0
  35. /package/lib/{bundle.d.ts → cjs/bundle.d.ts} +0 -0
  36. /package/lib/{bundle.js → cjs/bundle.js} +0 -0
  37. /package/lib/{errors.d.ts → cjs/errors.d.ts} +0 -0
  38. /package/lib/{errors.js → cjs/errors.js} +0 -0
  39. /package/lib/{index.d.ts → cjs/index.d.ts} +0 -0
  40. /package/lib/{index.js → cjs/index.js} +0 -0
  41. /package/lib/{operations.d.ts → cjs/operations.d.ts} +0 -0
  42. /package/lib/{operations.js → cjs/operations.js} +0 -0
  43. /package/lib/{types → cjs/types}/actions.d.ts +0 -0
  44. /package/lib/{types → cjs/types}/actions.js +0 -0
  45. /package/lib/{types → cjs/types}/index.d.ts +0 -0
  46. /package/lib/{types → cjs/types}/index.js +0 -0
  47. /package/lib/{types → cjs/types}/operations.d.ts +0 -0
  48. /package/lib/{types → cjs/types}/operations.js +0 -0
@@ -0,0 +1,800 @@
1
+ import { DEFAULT_SLIPPAGE_TOLERANCE, Holding, MarketUtils, MathLib, NATIVE_ADDRESS, erc20WrapperTokens, getChainAddresses, getUnwrappedToken, permissionedBackedTokens, permissionedWrapperTokens, } from "@morpho-org/blue-sdk";
2
+ import { entries, getLast, getValue, keys } from "@morpho-org/morpho-ts";
3
+ import { APPROVE_ONLY_ONCE_TOKENS, handleOperation, handleOperations, produceImmutable, simulateOperation, simulateOperations, } from "@morpho-org/simulation-sdk";
4
+ import { isAddressEqual, maxUint256 } from "viem";
5
+ import { BundlerErrors } from "./errors.js";
6
+ /**
7
+ * The default target utilization above which the shared liquidity algorithm is triggered (scaled by WAD).
8
+ */
9
+ export const DEFAULT_SUPPLY_TARGET_UTILIZATION = 905000000000000000n;
10
+ export const populateInputTransfer = ({ address, args: { amount, from } }, data, { hasSimplePermit = false } = {}) => {
11
+ const { bundler3: { generalAdapter1 }, permit2, } = getChainAddresses(data.chainId);
12
+ // If native token, it is expected to be sent along as call value.
13
+ if (address === NATIVE_ADDRESS)
14
+ return [
15
+ {
16
+ type: "Erc20_Transfer",
17
+ sender: from,
18
+ address,
19
+ args: {
20
+ amount,
21
+ from,
22
+ to: generalAdapter1,
23
+ },
24
+ },
25
+ ];
26
+ const { erc20Allowances, permit2BundlerAllowance, erc2612Nonce } = data.getHolding(from, address);
27
+ // ERC20 allowance to the bundler is enough, consume it.
28
+ if (erc20Allowances["bundler3.generalAdapter1"] >= amount)
29
+ return [
30
+ {
31
+ type: "Erc20_Transfer",
32
+ sender: generalAdapter1,
33
+ address,
34
+ args: {
35
+ amount,
36
+ from,
37
+ to: generalAdapter1,
38
+ },
39
+ },
40
+ ];
41
+ const operations = [];
42
+ // Try using simple permit.
43
+ const useSimplePermit = erc2612Nonce != null &&
44
+ (data.tryGetVault(address) != null || // MetaMorpho vaults implement EIP-2612.
45
+ data.tryGetVaultV2(address) != null || // Vaults V2 implement EIP-2612.
46
+ hasSimplePermit);
47
+ const useSimpleTransfer = permit2 == null ||
48
+ // Token is permissioned and Permit2 may not be authorized so Permit2 cannot be used.
49
+ !!permissionedWrapperTokens[data.chainId]?.has(address) ||
50
+ !!permissionedBackedTokens[data.chainId]?.has(address);
51
+ if (useSimplePermit)
52
+ operations.push({
53
+ type: "Erc20_Permit",
54
+ sender: from,
55
+ address,
56
+ args: {
57
+ amount,
58
+ spender: generalAdapter1,
59
+ nonce: erc2612Nonce,
60
+ },
61
+ });
62
+ else if (useSimpleTransfer) {
63
+ if (APPROVE_ONLY_ONCE_TOKENS[data.chainId]?.includes(address) &&
64
+ erc20Allowances["bundler3.generalAdapter1"] > 0n)
65
+ operations.push({
66
+ type: "Erc20_Approve",
67
+ sender: from,
68
+ address,
69
+ args: {
70
+ amount: 0n,
71
+ spender: generalAdapter1,
72
+ },
73
+ });
74
+ operations.push({
75
+ type: "Erc20_Approve",
76
+ sender: from,
77
+ address,
78
+ args: {
79
+ amount,
80
+ spender: generalAdapter1,
81
+ },
82
+ });
83
+ }
84
+ if (useSimplePermit || useSimpleTransfer)
85
+ operations.push({
86
+ type: "Erc20_Transfer",
87
+ sender: generalAdapter1,
88
+ address,
89
+ args: {
90
+ amount,
91
+ from,
92
+ to: generalAdapter1,
93
+ },
94
+ });
95
+ // Simple permit is not supported: fallback to Permit2.
96
+ else {
97
+ if (erc20Allowances.permit2 < amount) {
98
+ if (APPROVE_ONLY_ONCE_TOKENS[data.chainId]?.includes(address) &&
99
+ erc20Allowances.permit2 > 0n)
100
+ operations.push({
101
+ type: "Erc20_Approve",
102
+ sender: from,
103
+ address,
104
+ args: {
105
+ amount: 0n,
106
+ spender: permit2,
107
+ },
108
+ });
109
+ operations.push({
110
+ type: "Erc20_Approve",
111
+ sender: from,
112
+ address,
113
+ args: {
114
+ amount: MathLib.MAX_UINT_160, // Always approve infinite.
115
+ spender: permit2,
116
+ },
117
+ });
118
+ }
119
+ if (permit2BundlerAllowance.amount < amount ||
120
+ permit2BundlerAllowance.expiration < data.block.timestamp)
121
+ operations.push({
122
+ type: "Erc20_Permit2",
123
+ sender: from,
124
+ address,
125
+ args: {
126
+ amount,
127
+ expiration: MathLib.MAX_UINT_48, // Always approve indefinitely.
128
+ nonce: permit2BundlerAllowance.nonce,
129
+ },
130
+ });
131
+ operations.push({
132
+ type: "Erc20_Transfer2",
133
+ sender: generalAdapter1,
134
+ address,
135
+ args: {
136
+ amount,
137
+ from,
138
+ to: generalAdapter1,
139
+ },
140
+ });
141
+ }
142
+ return operations;
143
+ };
144
+ /**
145
+ * Simulates the input operation on the given simulation data with args tweaked so the bundler operates on behalf of the sender.
146
+ * Then, populates a bundle of operations made of:
147
+ * - required approvals to the bundler
148
+ * - required input transfers to the bundler
149
+ * - required token wrapping
150
+ * - the given operation
151
+ * @param inputOperation The input operation to populate a bundle for.
152
+ * @param data The simulation data to determine the required steps of the bundle to populate. If the provided simulation data is the result of a simulation
153
+ * of an already populated bundle, the `Transfer` and `Wrap` operation are only populated if required.
154
+ * @param wrapSlippage The slippage simulated during wraps. Should never be 0.
155
+ * @return The bundle of operations to optimize and skim before being encoded.
156
+ */
157
+ export const populateSubBundle = (inputOperation, data, options = {}) => {
158
+ const { sender } = inputOperation;
159
+ const { bundler3: { bundler3, generalAdapter1 }, } = getChainAddresses(data.chainId);
160
+ const { withSimplePermit = new Set(), publicAllocatorOptions, getRequirementOperations, } = options;
161
+ const operations = [];
162
+ const wrappedToken = inputOperation.type === "Erc20_Wrap"
163
+ ? data.getWrappedToken(inputOperation.address)
164
+ : undefined;
165
+ const isErc20Wrapper = !!wrappedToken &&
166
+ !!erc20WrapperTokens[data.chainId]?.has(wrappedToken.address);
167
+ const mainOperation = produceImmutable(inputOperation, (draft) => {
168
+ if (draft.type === "Erc20_Wrap" && isErc20Wrapper)
169
+ // ERC20Wrapper wrapped tokens are sent to the caller, not the bundler.
170
+ draft.args.owner = sender;
171
+ // Transform input operation to act on behalf of the sender, when sender is not the bundler.
172
+ if (sender !== generalAdapter1) {
173
+ draft.sender = generalAdapter1;
174
+ // Redirect MetaMorpho operation owner.
175
+ switch (draft.type) {
176
+ case "Erc20_Wrap":
177
+ if (isErc20Wrapper)
178
+ break;
179
+ case "MetaMorpho_Deposit":
180
+ case "MetaMorpho_Withdraw": {
181
+ // Only if sender is owner otherwise the owner would be lost.
182
+ if (draft.args.owner === sender)
183
+ draft.args.owner = generalAdapter1;
184
+ break;
185
+ }
186
+ }
187
+ // Redirect operation targets.
188
+ switch (draft.type) {
189
+ case "Blue_Borrow":
190
+ case "Blue_Withdraw":
191
+ case "Blue_WithdrawCollateral":
192
+ draft.args.onBehalf = sender;
193
+ case "MetaMorpho_Withdraw":
194
+ case "Paraswap_Buy":
195
+ case "Paraswap_Sell":
196
+ case "Blue_Paraswap_BuyDebt":
197
+ // Only if sender is receiver otherwise the receiver would be lost.
198
+ if (draft.args.receiver === sender)
199
+ draft.args.receiver = generalAdapter1;
200
+ }
201
+ }
202
+ });
203
+ if (mainOperation.type === "Blue_Borrow" ||
204
+ mainOperation.type === "Blue_Withdraw" ||
205
+ mainOperation.type === "Blue_WithdrawCollateral") {
206
+ // Either sender === generalAdapter1 or sender === onBehalf.
207
+ const { onBehalf } = mainOperation.args;
208
+ if (!data.getUser(onBehalf).isBundlerAuthorized)
209
+ operations.push({
210
+ type: "Blue_SetAuthorization",
211
+ sender: bundler3,
212
+ args: {
213
+ owner: onBehalf,
214
+ isAuthorized: true,
215
+ authorized: generalAdapter1,
216
+ },
217
+ });
218
+ }
219
+ // Reallocate liquidity if necessary.
220
+ if (!!publicAllocatorOptions?.enabled &&
221
+ (mainOperation.type === "Blue_Borrow" ||
222
+ mainOperation.type === "Blue_Withdraw")) {
223
+ const market = data
224
+ .getMarket(mainOperation.args.id)
225
+ .accrueInterest(data.block.timestamp);
226
+ const borrowedAssets = mainOperation.type === "Blue_Borrow"
227
+ ? (mainOperation.args.assets ??
228
+ market.toBorrowAssets(mainOperation.args.shares))
229
+ : 0n;
230
+ const withdrawnAssets = mainOperation.type === "Blue_Withdraw"
231
+ ? (mainOperation.args.assets ??
232
+ market.toSupplyAssets(mainOperation.args.shares))
233
+ : 0n;
234
+ const newTotalSupplyAssets = market.totalSupplyAssets - withdrawnAssets;
235
+ const newTotalBorrowAssets = market.totalBorrowAssets + borrowedAssets;
236
+ const reallocations = {};
237
+ const supplyTargetUtilization = publicAllocatorOptions.supplyTargetUtilization?.[market.params.id] ??
238
+ publicAllocatorOptions.defaultSupplyTargetUtilization ??
239
+ DEFAULT_SUPPLY_TARGET_UTILIZATION;
240
+ if (MarketUtils.getUtilization({
241
+ totalSupplyAssets: newTotalSupplyAssets,
242
+ totalBorrowAssets: newTotalBorrowAssets,
243
+ }) > supplyTargetUtilization) {
244
+ // Liquidity is insufficient: trigger a public reallocation and try to have a resulting utilization as low as possible, above the target.
245
+ // Solve: newTotalBorrowAssets / (newTotalSupplyAssets + reallocatedAssets) = supplyTargetUtilization
246
+ // We first try to find public reallocations that respect every markets targets.
247
+ // If this is not enough, the first market to be pushed above target is the supply market. Then we fully withdraw from every market.
248
+ let requiredAssets = supplyTargetUtilization === 0n
249
+ ? MathLib.MAX_UINT_160
250
+ : MathLib.wDivDown(newTotalBorrowAssets, supplyTargetUtilization) -
251
+ newTotalSupplyAssets;
252
+ const { withdrawals, data: friendlyReallocationData } = data.getMarketPublicReallocations(market.id, publicAllocatorOptions);
253
+ const friendlyReallocationMarket = friendlyReallocationData.getMarket(market.id);
254
+ if (friendlyReallocationMarket.totalBorrowAssets + borrowedAssets >
255
+ friendlyReallocationMarket.totalSupplyAssets - withdrawnAssets) {
256
+ // If the "friendly" reallocations are not enough, we fully withdraw from every market.
257
+ requiredAssets = newTotalBorrowAssets - newTotalSupplyAssets;
258
+ withdrawals.push(...friendlyReallocationData.getMarketPublicReallocations(market.id, {
259
+ ...publicAllocatorOptions,
260
+ defaultMaxWithdrawalUtilization: MathLib.WAD,
261
+ maxWithdrawalUtilization: {},
262
+ }).withdrawals);
263
+ }
264
+ for (const { vault, ...withdrawal } of withdrawals) {
265
+ const vaultReallocations = (reallocations[vault] ??= []);
266
+ const vaultMarketReallocation = vaultReallocations.find((item) => item.id === withdrawal.id);
267
+ const reallocatedAssets = MathLib.min(withdrawal.assets, requiredAssets);
268
+ if (vaultMarketReallocation != null)
269
+ vaultMarketReallocation.assets += reallocatedAssets;
270
+ else
271
+ vaultReallocations.push({
272
+ ...withdrawal,
273
+ assets: reallocatedAssets,
274
+ });
275
+ requiredAssets -= reallocatedAssets;
276
+ if (requiredAssets === 0n)
277
+ break;
278
+ }
279
+ // TODO: we know there are no unwrap native in the middle
280
+ // of the bundle so we are certain we need to add an input transfer.
281
+ // This could be handled by `simulateRequiredTokenAmounts` below.
282
+ const fees = keys(reallocations).reduce((total, vault) => total + data.getVault(vault).publicAllocatorConfig.fee, 0n);
283
+ // Native input transfer of all fees.
284
+ if (fees > 0n)
285
+ operations.push({
286
+ type: "Erc20_Transfer",
287
+ sender,
288
+ address: NATIVE_ADDRESS,
289
+ args: {
290
+ amount: fees,
291
+ from: sender,
292
+ to: bundler3,
293
+ },
294
+ });
295
+ }
296
+ // Reallocate each vault.
297
+ operations.push(...Object.entries(reallocations).map(([vault, vaultWithdrawals]) => ({
298
+ type: "MetaMorpho_PublicReallocate",
299
+ sender: bundler3,
300
+ address: vault,
301
+ args: {
302
+ // Reallocation withdrawals must be sorted by market id in ascending alphabetical order.
303
+ withdrawals: vaultWithdrawals.sort(({ id: idA }, { id: idB }) => idA > idB ? 1 : -1),
304
+ supplyMarketId: market.id,
305
+ },
306
+ })));
307
+ }
308
+ const callback = getValue(mainOperation.args, "callback");
309
+ const simulatedOperation = {
310
+ ...mainOperation,
311
+ args: {
312
+ ...mainOperation.args,
313
+ ...(callback && {
314
+ callback: (data) => {
315
+ const operations = callback.flatMap((inputOperation) => {
316
+ const subBundleOperations = populateSubBundle({
317
+ ...inputOperation,
318
+ // Inside a callback, the sender is forced to be the generalAdapter1.
319
+ sender: generalAdapter1,
320
+ }, data, options);
321
+ // Handle to mutate data (not simulate).
322
+ handleBundlerOperations(subBundleOperations, data);
323
+ return subBundleOperations;
324
+ });
325
+ mainOperation.args.callback =
326
+ operations;
327
+ return [];
328
+ },
329
+ }),
330
+ },
331
+ };
332
+ let requiredTokenAmounts = simulateRequiredTokenAmounts(
333
+ // Safe cast because operations do not contain callbacks.
334
+ operations.concat([simulatedOperation]), data);
335
+ // Safe cast because operations do not contain callbacks.
336
+ const allOperations = operations.concat([
337
+ // Safe cast because mainOperation, if including a callback, was transformed to a BundlerOperation
338
+ // within the callback executed through the simulation `simulateRequiredTokenAmounts`.
339
+ mainOperation,
340
+ ]);
341
+ // Skip approvals/transfers if operation only uses available balances (via maxUint256).
342
+ if (("amount" in mainOperation.args &&
343
+ mainOperation.args.amount === maxUint256) ||
344
+ ("assets" in mainOperation.args &&
345
+ mainOperation.args.assets === maxUint256) ||
346
+ ("shares" in mainOperation.args && mainOperation.args.shares === maxUint256)) {
347
+ if (mainOperation.type === "MetaMorpho_Withdraw")
348
+ mainOperation.args.owner = generalAdapter1;
349
+ return allOperations;
350
+ }
351
+ const requirementOperations = getRequirementOperations?.(requiredTokenAmounts) ?? [];
352
+ requiredTokenAmounts = simulateRequiredTokenAmounts(requirementOperations
353
+ .concat(allOperations)
354
+ .map((operation) => getSimulatedBundlerOperation(operation)), data);
355
+ // Append required input transfers.
356
+ requiredTokenAmounts.forEach(({ token, required }) => {
357
+ requirementOperations.push(...populateInputTransfer({
358
+ type: "Erc20_Transfer",
359
+ sender: generalAdapter1,
360
+ address: token,
361
+ args: {
362
+ amount: required,
363
+ from: sender,
364
+ to: generalAdapter1,
365
+ },
366
+ }, data, { hasSimplePermit: withSimplePermit.has(token) }));
367
+ });
368
+ return requirementOperations.concat(allOperations);
369
+ };
370
+ /**
371
+ * Merges unnecessary duplicate `Erc20_Approve`, `Erc20_Transfer` and `Erc20_Wrap`.
372
+ * Also redirects `Blue_Borrow|Withdraw|WithdrawCollateral` & `MetaMorpho_Withdraw` operations from the bundler to the receiver,
373
+ * as long as the tokens received (possibly ERC4626 shares) are not used afterwards in the bundle.
374
+ * For all the other remaining tokens, appends `Erc20_Transfer` operations to the bundle, from the bundler to the receiver.
375
+ * @param operations The bundle to optimize.
376
+ * @param startData The start data from which to simulate th bundle.
377
+ * @param receiver The receiver of skimmed tokens.
378
+ * @param unwrapTokens The set of tokens to unwrap before transferring to the receiver.
379
+ * @param unwrapSlippage The slippage simulated during unwraps. Should never be 0.
380
+ * @return The optimized bundle.
381
+ */
382
+ export const finalizeBundle = (operations, startData, receiver, unwrapTokens = new Set(), unwrapSlippage = DEFAULT_SLIPPAGE_TOLERANCE) => {
383
+ const nbOperations = operations.length;
384
+ if (nbOperations === 0)
385
+ return operations;
386
+ const { bundler3: { bundler3, generalAdapter1 }, dai, } = getChainAddresses(startData.chainId);
387
+ if (isAddressEqual(receiver, bundler3) ||
388
+ isAddressEqual(receiver, generalAdapter1))
389
+ throw Error(`receiver is bundler`);
390
+ const approvals = [];
391
+ const permits = [];
392
+ const permit2s = [];
393
+ const inputTransfers = [];
394
+ const inputTransfer2s = [];
395
+ const others = [];
396
+ // TODO input transfers can be merged to the right-most position where transferred assets are still not used
397
+ // Merge together approvals, permits, permit2s & input transfers.
398
+ operations.forEach((operation) => {
399
+ switch (operation.type) {
400
+ case "Erc20_Approve": {
401
+ const duplicateApproval = approvals.find((approval) => approval.address === operation.address &&
402
+ approval.sender === operation.sender &&
403
+ approval.args.spender === operation.args.spender &&
404
+ (!APPROVE_ONLY_ONCE_TOKENS[startData.chainId]?.includes(approval.address) ||
405
+ (approval.args.amount === 0n) === (operation.args.amount === 0n)));
406
+ if (duplicateApproval == null)
407
+ return approvals.push(operation);
408
+ duplicateApproval.args.amount += operation.args.amount;
409
+ break;
410
+ }
411
+ case "Erc20_Permit": {
412
+ const duplicatePermit = permits.find((permit) => permit.address === operation.address &&
413
+ permit.sender === operation.sender &&
414
+ permit.args.spender === operation.args.spender);
415
+ if (duplicatePermit == null) {
416
+ const lastPermit = permits.findLast((permit) => permit.address === operation.address &&
417
+ permit.sender === operation.sender);
418
+ if (lastPermit)
419
+ operation.args.nonce = lastPermit.args.nonce + 1n;
420
+ permits.push(operation);
421
+ }
422
+ else
423
+ duplicatePermit.args.amount += operation.args.amount;
424
+ break;
425
+ }
426
+ case "Erc20_Permit2": {
427
+ const duplicatePermit2 = permit2s.find((permit2) => permit2.address === operation.address &&
428
+ permit2.sender === operation.sender);
429
+ if (duplicatePermit2 == null) {
430
+ const lastPermit2 = permit2s.findLast((permit2) => permit2.address === operation.address &&
431
+ permit2.sender === operation.sender);
432
+ if (lastPermit2)
433
+ operation.args.nonce = lastPermit2.args.nonce + 1n;
434
+ permit2s.push(operation);
435
+ }
436
+ else
437
+ duplicatePermit2.args.amount += operation.args.amount;
438
+ break;
439
+ }
440
+ case "Erc20_Transfer": {
441
+ const { address, sender, args: { amount, from, to }, } = operation;
442
+ if (from !== generalAdapter1 &&
443
+ to === generalAdapter1 &&
444
+ !erc20WrapperTokens[startData.chainId]?.has(address)) {
445
+ const duplicateTransfer = inputTransfers.find((transfer) => transfer.address === address &&
446
+ transfer.sender === sender &&
447
+ transfer.args.from === from);
448
+ if (duplicateTransfer == null ||
449
+ // Don't merge the input transfer if from didn't have enough balance at the start.
450
+ startData.getHolding(from, address).balance < amount)
451
+ return inputTransfers.push(operation);
452
+ duplicateTransfer.args.amount += amount;
453
+ return;
454
+ }
455
+ others.push(operation);
456
+ break;
457
+ }
458
+ case "Erc20_Transfer2": {
459
+ const { address, sender, args: { amount, from, to }, } = operation;
460
+ if (from !== generalAdapter1 && to === generalAdapter1) {
461
+ const duplicateTransfer2 = inputTransfer2s.find((transfer) => transfer.address === address &&
462
+ transfer.sender === sender &&
463
+ transfer.args.from === from);
464
+ if (duplicateTransfer2 == null ||
465
+ // Don't merge the input transfer if from didn't have enough balance at the start.
466
+ startData.getHolding(from, address).balance < amount)
467
+ return inputTransfer2s.push(operation);
468
+ duplicateTransfer2.args.amount += amount;
469
+ return;
470
+ }
471
+ others.push(operation);
472
+ break;
473
+ }
474
+ // Cannot factorize public reallocations because the liquidity may not always be available before other operations.
475
+ default:
476
+ others.push(operation);
477
+ }
478
+ });
479
+ operations = [
480
+ approvals,
481
+ permits,
482
+ permit2s,
483
+ inputTransfers,
484
+ inputTransfer2s,
485
+ others,
486
+ ].flat(1);
487
+ let steps = simulateBundlerOperations(operations, startData);
488
+ // Redirect MetaMorpho deposits.
489
+ operations.forEach((operation, index) => {
490
+ switch (operation.type) {
491
+ case "MetaMorpho_Deposit": {
492
+ if (operation.args.owner !== generalAdapter1)
493
+ return;
494
+ break;
495
+ }
496
+ default:
497
+ return;
498
+ }
499
+ const token = operation.address;
500
+ // shares are not defined when depositing assets, so we rely on simulation steps.
501
+ const shares = steps[index + 1].getHolding(generalAdapter1, token).balance -
502
+ steps[index].getHolding(generalAdapter1, token).balance;
503
+ if (steps
504
+ .slice(index + 2)
505
+ .some((step) => step.getHolding(generalAdapter1, token).balance < shares))
506
+ // If the bundler's balance is at least once lower than assets, the bundler does need these assets.
507
+ return;
508
+ if (operation.type === "MetaMorpho_Deposit")
509
+ operation.args.owner = receiver;
510
+ });
511
+ // Redirect borrows, withdrawals, MetaMorpho withdrawals & Vault V2 withdrawals.
512
+ operations.forEach((operation, index) => {
513
+ let token;
514
+ switch (operation.type) {
515
+ case "Blue_Borrow":
516
+ case "Blue_Withdraw":
517
+ token = startData.getMarket(operation.args.id).params.loanToken;
518
+ break;
519
+ case "Blue_WithdrawCollateral":
520
+ token = startData.getMarket(operation.args.id).params.collateralToken;
521
+ break;
522
+ case "MetaMorpho_Withdraw":
523
+ token = startData.getVault(operation.address).asset;
524
+ break;
525
+ default:
526
+ return;
527
+ }
528
+ if (operation.args.receiver !== generalAdapter1 || unwrapTokens.has(token))
529
+ return;
530
+ // assets are not defined when using shares, so we rely on simulation steps.
531
+ const assets = steps[index + 1].getHolding(generalAdapter1, token).balance -
532
+ steps[index].getHolding(generalAdapter1, token).balance;
533
+ if (steps
534
+ .slice(index + 2)
535
+ .some((step) => step.getHolding(generalAdapter1, token).balance < assets))
536
+ // If the bundler's balance is at least once lower than assets, the bundler does need these assets.
537
+ return;
538
+ operation.args.receiver = receiver;
539
+ });
540
+ // Simplify Erc20_Transfer(sender = bundler, to = bundler) + MetaMorpho_Withdraw(owner = bundler) = MetaMorpho_Withdraw(owner = from).
541
+ operations.forEach((operation, index) => {
542
+ switch (operation.type) {
543
+ case "MetaMorpho_Withdraw": {
544
+ if (operation.args.owner !== generalAdapter1)
545
+ return;
546
+ break;
547
+ }
548
+ default:
549
+ return;
550
+ }
551
+ // shares are not defined when using assets, so we rely on simulation steps.
552
+ const shares = steps[index].getHolding(generalAdapter1, operation.address).balance -
553
+ steps[index + 1].getHolding(generalAdapter1, operation.address).balance;
554
+ const inputTransferIndex = operations.findIndex((candidate) => candidate.type === "Erc20_Transfer" &&
555
+ candidate.address === operation.address &&
556
+ candidate.sender === generalAdapter1 &&
557
+ candidate.args.to === generalAdapter1 &&
558
+ candidate.args.amount >= shares);
559
+ if (inputTransferIndex <= 0)
560
+ return;
561
+ const inputTransfer = operations[inputTransferIndex];
562
+ inputTransfer.args.amount -= shares;
563
+ if (operation.type === "MetaMorpho_Withdraw")
564
+ operation.args.owner = inputTransfer.args.from;
565
+ });
566
+ // Filter out useless input transfers.
567
+ operations = operations.filter((operation, index) => {
568
+ if (operation.type !== "Erc20_Transfer")
569
+ return true;
570
+ const { amount, from, to } = operation.args;
571
+ if (from === generalAdapter1 || to !== generalAdapter1)
572
+ return true;
573
+ const token = operation.address;
574
+ if (steps
575
+ .slice(index + 2)
576
+ .some((step) => step.getHolding(generalAdapter1, token).balance < amount))
577
+ // If the bundler's balance is at least once less than amount, the bundler does need these assets.
578
+ // Do not only keep the amount actually used in this case because some input transfers
579
+ // are expected to be larger to account for slippage.
580
+ return true;
581
+ return false;
582
+ });
583
+ // Simulate without slippage to skim the bundler of all possible surplus of shares & assets.
584
+ steps = simulateBundlerOperations(operations, startData, { slippage: 0n });
585
+ const daiPermit = dai != null
586
+ ? operations.find(
587
+ // There should exist only one dai permit operation in the bundle thanks to the first optimization step.
588
+ (operation) => operation.type === "Erc20_Permit" && operation.address === dai)
589
+ : undefined;
590
+ // If the bundle approves dai, reset the dai allowance at the end of the bundle.
591
+ if (daiPermit != null)
592
+ operations.push({
593
+ ...daiPermit,
594
+ args: {
595
+ amount: 0n,
596
+ spender: daiPermit.args.spender,
597
+ nonce: daiPermit.args.nonce + 1n,
598
+ },
599
+ });
600
+ // Unwrap requested remaining wrapped tokens.
601
+ const unwraps = [];
602
+ let endBundlerTokenData = getLast(steps).holdings[generalAdapter1] ?? {};
603
+ unwrapTokens.forEach((wrappedToken) => {
604
+ const remaining = endBundlerTokenData[wrappedToken]?.balance ?? 0n;
605
+ if (remaining <= 5n)
606
+ return;
607
+ const unwrappedToken = getUnwrappedToken(wrappedToken, startData.chainId);
608
+ if (unwrappedToken == null)
609
+ return;
610
+ unwraps.push({
611
+ type: "Erc20_Unwrap",
612
+ address: wrappedToken,
613
+ sender: generalAdapter1,
614
+ args: {
615
+ amount: maxUint256,
616
+ receiver,
617
+ slippage: unwrapSlippage,
618
+ },
619
+ });
620
+ });
621
+ if (unwraps.length > 0) {
622
+ steps = simulateBundlerOperations(operations.concat(unwraps), startData, {
623
+ slippage: 0n,
624
+ });
625
+ endBundlerTokenData = getLast(steps).holdings[generalAdapter1] ?? {};
626
+ }
627
+ // Skim any token expected to be left on the bundler.
628
+ const skims = [];
629
+ const uniqueSkimTokens = new Set();
630
+ const pushSkim = (operation) => {
631
+ // Paraswap does not guarantee that the amount effectively bought (resp. sold) corresponds to
632
+ // the requested amount to buy (resp. sell), so we force skim the possible surplus of bought (resp. sold) token.
633
+ switch (operation.type) {
634
+ case "Blue_Borrow":
635
+ case "Blue_Repay":
636
+ case "Blue_Supply":
637
+ case "Blue_Withdraw":
638
+ uniqueSkimTokens.add(startData.getMarket(operation.args.id).params.loanToken);
639
+ break;
640
+ case "Blue_WithdrawCollateral":
641
+ case "Blue_SupplyCollateral":
642
+ uniqueSkimTokens.add(startData.getMarket(operation.args.id).params.collateralToken);
643
+ break;
644
+ case "Blue_FlashLoan":
645
+ uniqueSkimTokens.add(operation.args.token);
646
+ break;
647
+ case "Paraswap_Buy":
648
+ uniqueSkimTokens.add(operation.address);
649
+ uniqueSkimTokens.add(operation.args.srcToken);
650
+ break;
651
+ case "Blue_Paraswap_BuyDebt":
652
+ uniqueSkimTokens.add(startData.getMarket(operation.args.id).params.loanToken);
653
+ uniqueSkimTokens.add(operation.args.srcToken);
654
+ break;
655
+ case "Paraswap_Sell":
656
+ uniqueSkimTokens.add(operation.address);
657
+ uniqueSkimTokens.add(operation.args.dstToken);
658
+ break;
659
+ case "Erc20_Transfer":
660
+ case "Erc20_Transfer2":
661
+ uniqueSkimTokens.add(operation.address);
662
+ break;
663
+ case "Blue_SetAuthorization":
664
+ case "Erc20_Approve":
665
+ case "Erc20_Permit":
666
+ case "Erc20_Permit2":
667
+ break;
668
+ case "Erc20_Wrap":
669
+ case "Erc20_Unwrap":
670
+ uniqueSkimTokens.add(operation.address);
671
+ uniqueSkimTokens.add(startData.getWrappedToken(operation.address).underlying);
672
+ break;
673
+ case "MetaMorpho_Deposit":
674
+ case "MetaMorpho_Withdraw":
675
+ uniqueSkimTokens.add(operation.address);
676
+ uniqueSkimTokens.add(startData.getVault(operation.address).asset);
677
+ break;
678
+ case "MetaMorpho_PublicReallocate":
679
+ uniqueSkimTokens.add(NATIVE_ADDRESS);
680
+ break;
681
+ default:
682
+ //@ts-ignore This is dead code but acts as a guard in case a new operation is added
683
+ throw new BundlerErrors.MissingSkimHandler(operation.type);
684
+ }
685
+ if ("callback" in operation.args)
686
+ operation.args.callback?.forEach(pushSkim);
687
+ };
688
+ operations.concat(unwraps).forEach(pushSkim);
689
+ skims.push(...Array.from(uniqueSkimTokens, (address) => ({
690
+ type: "Erc20_Transfer",
691
+ address,
692
+ sender: generalAdapter1,
693
+ args: {
694
+ amount: maxUint256,
695
+ from: generalAdapter1,
696
+ to: receiver,
697
+ },
698
+ })));
699
+ const finalizedOperations = operations.concat(unwraps, skims);
700
+ const finalizedSteps = simulateBundlerOperations(finalizedOperations, startData);
701
+ const firstHoldings = finalizedSteps[0].holdings[generalAdapter1] ?? {};
702
+ const lastHoldings = getLast(finalizedSteps).holdings[generalAdapter1] ?? {};
703
+ for (const token of keys(lastHoldings)) {
704
+ const lastHolding = lastHoldings[token];
705
+ const firstHolding = firstHoldings[token];
706
+ if (!lastHolding)
707
+ continue;
708
+ // compute delta balance between final and initial, default first holding to 0n
709
+ const delta = lastHolding.balance - (firstHolding?.balance ?? 0n);
710
+ if (delta > 0n) {
711
+ throw new BundlerErrors.UnskimedToken(lastHolding.token);
712
+ }
713
+ }
714
+ return finalizedOperations;
715
+ };
716
+ export const populateBundle = (inputOperations, data, options) => {
717
+ const steps = [data];
718
+ let end = data;
719
+ const operations = inputOperations.flatMap((inputOperation, index) => {
720
+ try {
721
+ const subBundleOperations = populateSubBundle(inputOperation, end, options);
722
+ steps.push((end = getLast(simulateBundlerOperations(subBundleOperations, end))));
723
+ return subBundleOperations;
724
+ }
725
+ catch (error) {
726
+ if (!(error instanceof Error))
727
+ throw error;
728
+ throw new BundlerErrors.Bundle(error, index, inputOperation, steps);
729
+ }
730
+ });
731
+ return { operations, steps };
732
+ };
733
+ class VirtualHolding extends Holding {
734
+ required = 0n;
735
+ get balance() {
736
+ return this._balance;
737
+ }
738
+ set balance(value) {
739
+ if (value < 0n) {
740
+ this.required += -value;
741
+ this._balance = 0n;
742
+ }
743
+ else
744
+ this._balance = value;
745
+ }
746
+ }
747
+ export const simulateRequiredTokenAmounts = (operations, data) => {
748
+ const { bundler3: { generalAdapter1 }, } = getChainAddresses(data.chainId);
749
+ const virtualBundlerData = produceImmutable(data, (draft) => {
750
+ const bundlerHoldings = draft.holdings[generalAdapter1];
751
+ if (bundlerHoldings == null)
752
+ return;
753
+ entries(bundlerHoldings).map(([token, holding]) => {
754
+ if (holding == null)
755
+ return;
756
+ bundlerHoldings[token] = new VirtualHolding(holding);
757
+ });
758
+ });
759
+ // Simulate the operations to calculate the required token amounts.
760
+ const steps = simulateOperations(operations, virtualBundlerData);
761
+ return (entries(getLast(steps).holdings[generalAdapter1] ?? {})
762
+ // Safe cast because the holding was transformed to a VirtualHolding.
763
+ .filter((entry) => entry[1] != null)
764
+ .map(([token, { required }]) => ({ token, required }))
765
+ .filter(({ required }) => required > 0n));
766
+ };
767
+ export const getSimulatedBundlerOperation = (operation, { slippage } = {}) => {
768
+ const callback = getValue(operation.args, "callback");
769
+ const simulatedOperation = {
770
+ ...operation,
771
+ args: {
772
+ ...operation.args,
773
+ ...(callback && {
774
+ callback: () => callback.map((operation) => getSimulatedBundlerOperation(operation, { slippage })),
775
+ }),
776
+ },
777
+ };
778
+ if (slippage != null) {
779
+ switch (simulatedOperation.type) {
780
+ case "Erc20_Wrap":
781
+ case "Erc20_Unwrap":
782
+ case "Blue_Supply":
783
+ case "Blue_Withdraw":
784
+ case "Blue_Borrow":
785
+ case "Blue_Repay":
786
+ case "MetaMorpho_Deposit":
787
+ case "MetaMorpho_Withdraw":
788
+ case "Paraswap_Buy":
789
+ case "Paraswap_Sell":
790
+ case "Blue_Paraswap_BuyDebt":
791
+ simulatedOperation.args.slippage = slippage;
792
+ break;
793
+ }
794
+ }
795
+ return simulatedOperation;
796
+ };
797
+ export const handleBundlerOperation = (options) => (operation, startData, index) => handleOperation(getSimulatedBundlerOperation(operation, options), startData, index);
798
+ export const handleBundlerOperations = (operations, startData, options) => handleOperations(operations, startData, handleBundlerOperation(options));
799
+ export const simulateBundlerOperation = (options) => (operation, startData, index) => simulateOperation(getSimulatedBundlerOperation(operation, options), startData, index);
800
+ export const simulateBundlerOperations = (operations, startData, options) => handleOperations(operations, startData, simulateBundlerOperation(options));