@morpho-org/bundler-sdk-viem 1.12.4

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/lib/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./actions.js";
2
+ export * from "./operations.js";
3
+ export * from "./errors.js";
4
+ export * from "./types/index.js";
@@ -0,0 +1,62 @@
1
+ import { type Address, type MarketId } from "@morpho-org/blue-sdk";
2
+ import { type MaybeDraft, type Operation, type Operations, type PublicAllocatorOptions, type SimulationResult, type SimulationState } from "@morpho-org/simulation-sdk";
3
+ import type { BundlerOperation, CallbackBundlerOperation, InputBundlerOperation } from "./types/index.js";
4
+ export interface BundlingOptions {
5
+ withSimplePermit?: Set<Address>;
6
+ publicAllocatorOptions?: PublicAllocatorOptions & {
7
+ supplyTargetUtilization?: Record<MarketId, bigint | undefined>;
8
+ };
9
+ getRequirementOperations?: (requiredTokenAmounts: {
10
+ token: Address;
11
+ required: bigint;
12
+ }[]) => BundlerOperation[];
13
+ }
14
+ export declare const populateInputTransfer: ({ address, args: { amount, from } }: Operations["Erc20_Transfer"], data: MaybeDraft<SimulationState>, { hasSimplePermit }?: {
15
+ hasSimplePermit?: boolean;
16
+ }) => Exclude<BundlerOperation, CallbackBundlerOperation>[];
17
+ /**
18
+ * Simulates the input operation on the given simulation data with args tweaked so the bundler operates on behalf of the sender.
19
+ * Then, populates a bundle of operations made of:
20
+ * - required approvals to the bundler
21
+ * - required input transfers to the bundler
22
+ * - required token wrapping
23
+ * - the given operation
24
+ * @param inputOperation The input operation to populate a bundle for.
25
+ * @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
26
+ * of an already populated bundle, the `Transfer` and `Wrap` operation are only populated if required.
27
+ * @param wrapSlippage The slippage simulated during wraps. Should never be 0.
28
+ * @return The bundle of operations to optimize and skim before being encoded.
29
+ */
30
+ export declare const populateSubBundle: (inputOperation: InputBundlerOperation, data: MaybeDraft<SimulationState>, options?: BundlingOptions) => BundlerOperation[];
31
+ /**
32
+ * Merges unnecessary duplicate `Erc20_Approve`, `Erc20_Transfer` and `Erc20_Wrap`.
33
+ * Also redirects `Blue_Borrow|Withdraw|WithdrawCollateral` & `MetaMorpho_Withdraw` operations from the bundler to the receiver,
34
+ * as long as the tokens received (possibly ERC4626 shares) are not used afterwards in the bundle.
35
+ * For all the other remaining tokens, appends `Erc20_Transfer` operations to the bundle, from the bundler to the receiver.
36
+ * @param operations The bundle to optimize.
37
+ * @param startData The start data from which to simulate th bundle.
38
+ * @param receiver The receiver of skimmed tokens.
39
+ * @param unwrapTokens The set of tokens to unwrap before transferring to the receiver.
40
+ * @param unwrapSlippage The slippage simulated during unwraps. Should never be 0.
41
+ * @return The optimized bundle.
42
+ */
43
+ export declare const finalizeBundle: (operations: BundlerOperation[], startData: SimulationState, receiver: Address, unwrapTokens?: Set<`0x${string}`>, unwrapSlippage?: bigint) => BundlerOperation[];
44
+ export declare const populateBundle: (inputOperations: InputBundlerOperation[], data: MaybeDraft<SimulationState>, options?: BundlingOptions) => {
45
+ operations: BundlerOperation[];
46
+ steps: SimulationResult;
47
+ };
48
+ export declare const getSimulatedBundlerOperation: (operation: BundlerOperation, { slippage }?: {
49
+ slippage?: bigint;
50
+ }) => Operation;
51
+ export declare const handleBundlerOperation: (options?: {
52
+ slippage?: bigint;
53
+ }) => (operation: BundlerOperation, startData: MaybeDraft<SimulationState>, index?: number) => MaybeDraft<SimulationState>;
54
+ export declare const handleBundlerOperations: (operations: BundlerOperation[], startData: MaybeDraft<SimulationState>, options?: {
55
+ slippage?: bigint;
56
+ }) => SimulationResult;
57
+ export declare const simulateBundlerOperation: (options?: {
58
+ slippage?: bigint;
59
+ }) => (operation: BundlerOperation, startData: MaybeDraft<SimulationState>, index?: number) => MaybeDraft<SimulationState>;
60
+ export declare const simulateBundlerOperations: (operations: BundlerOperation[], startData: MaybeDraft<SimulationState>, options?: {
61
+ slippage?: bigint;
62
+ }) => SimulationResult;
@@ -0,0 +1,595 @@
1
+ import { DEFAULT_SLIPPAGE_TOLERANCE, DEFAULT_SUPPLY_TARGET_UTILIZATION, 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 { handleOperation, handleOperations, produceImmutable, simulateOperation, } from "@morpho-org/simulation-sdk";
4
+ import { maxUint256 } from "viem";
5
+ import { BundlerErrors } from "./errors.js";
6
+ export const populateInputTransfer = ({ address, args: { amount, from } }, data, { hasSimplePermit = false } = {}) => {
7
+ const { bundler, permit2 } = getChainAddresses(data.chainId);
8
+ // If native token, it is expected to be sent along as call value.
9
+ if (address === NATIVE_ADDRESS)
10
+ return [
11
+ {
12
+ type: "Erc20_Transfer",
13
+ sender: from,
14
+ address,
15
+ args: {
16
+ amount,
17
+ from,
18
+ to: bundler,
19
+ },
20
+ },
21
+ ];
22
+ const { erc20Allowances, permit2Allowances, erc2612Nonce } = data.getHolding(from, address);
23
+ // ERC20 allowance to the bundler is enough, consume it.
24
+ if (erc20Allowances.bundler >= amount)
25
+ return [
26
+ {
27
+ type: "Erc20_Transfer",
28
+ sender: bundler,
29
+ address,
30
+ args: {
31
+ amount,
32
+ from,
33
+ to: bundler,
34
+ },
35
+ },
36
+ ];
37
+ const operations = [];
38
+ // Try using simple permit.
39
+ const useSimplePermit = erc2612Nonce != null &&
40
+ (data.tryGetVault(address) != null || // MetaMorpho vaults implement EIP-2612.
41
+ hasSimplePermit);
42
+ const isPermissioned = permissionedWrapperTokens[data.chainId].has(address) ||
43
+ permissionedBackedTokens[data.chainId].has(address);
44
+ if (useSimplePermit)
45
+ operations.push({
46
+ type: "Erc20_Permit",
47
+ sender: from,
48
+ address,
49
+ args: {
50
+ amount,
51
+ spender: bundler,
52
+ nonce: erc2612Nonce,
53
+ },
54
+ });
55
+ // Token is permissioned and Permit2 may not be authorized so Permit2 cannot be used.
56
+ else if (isPermissioned)
57
+ operations.push({
58
+ type: "Erc20_Approve",
59
+ sender: from,
60
+ address,
61
+ args: {
62
+ amount,
63
+ spender: bundler,
64
+ },
65
+ });
66
+ if (useSimplePermit || isPermissioned)
67
+ operations.push({
68
+ type: "Erc20_Transfer",
69
+ sender: bundler,
70
+ address,
71
+ args: {
72
+ amount,
73
+ from,
74
+ to: bundler,
75
+ },
76
+ });
77
+ // Simple permit is not supported and token is not permissioned: fallback to Permit2.
78
+ else {
79
+ if (erc20Allowances.permit2 < amount)
80
+ operations.push({
81
+ type: "Erc20_Approve",
82
+ sender: from,
83
+ address,
84
+ args: {
85
+ amount: MathLib.MAX_UINT_160, // Always approve infinite.
86
+ spender: permit2,
87
+ },
88
+ });
89
+ if (permit2Allowances.bundler.amount < amount ||
90
+ permit2Allowances.bundler.expiration < data.block.timestamp)
91
+ operations.push({
92
+ type: "Erc20_Permit2",
93
+ sender: from,
94
+ address,
95
+ args: {
96
+ amount,
97
+ spender: bundler,
98
+ expiration: MathLib.MAX_UINT_48, // Always approve indefinitely.
99
+ nonce: permit2Allowances.bundler.nonce,
100
+ },
101
+ });
102
+ operations.push({
103
+ type: "Erc20_Transfer2",
104
+ sender: bundler,
105
+ address,
106
+ args: {
107
+ amount,
108
+ from,
109
+ to: bundler,
110
+ },
111
+ });
112
+ }
113
+ return operations;
114
+ };
115
+ /**
116
+ * Simulates the input operation on the given simulation data with args tweaked so the bundler operates on behalf of the sender.
117
+ * Then, populates a bundle of operations made of:
118
+ * - required approvals to the bundler
119
+ * - required input transfers to the bundler
120
+ * - required token wrapping
121
+ * - the given operation
122
+ * @param inputOperation The input operation to populate a bundle for.
123
+ * @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
124
+ * of an already populated bundle, the `Transfer` and `Wrap` operation are only populated if required.
125
+ * @param wrapSlippage The slippage simulated during wraps. Should never be 0.
126
+ * @return The bundle of operations to optimize and skim before being encoded.
127
+ */
128
+ export const populateSubBundle = (inputOperation, data, options = {}) => {
129
+ const { sender } = inputOperation;
130
+ const { morpho, bundler } = getChainAddresses(data.chainId);
131
+ const { withSimplePermit = new Set(), publicAllocatorOptions, getRequirementOperations, } = options;
132
+ const operations = [];
133
+ const wrappedToken = inputOperation.type === "Erc20_Wrap"
134
+ ? data.getWrappedToken(inputOperation.address)
135
+ : undefined;
136
+ const isErc20Wrapper = !!wrappedToken &&
137
+ erc20WrapperTokens[data.chainId].has(wrappedToken.address);
138
+ // Transform input operation to act on behalf of the sender, via the bundler.
139
+ const mainOperation = produceImmutable(inputOperation, (draft) => {
140
+ draft.sender = bundler;
141
+ // Redirect MetaMorpho operation owner.
142
+ switch (draft.type) {
143
+ case "Erc20_Wrap": {
144
+ // ERC20Wrapper are skipped because tokens are sent to the caller, not the bundler.
145
+ if (isErc20Wrapper) {
146
+ draft.args.owner = sender;
147
+ break;
148
+ }
149
+ }
150
+ case "MetaMorpho_Deposit":
151
+ case "MetaMorpho_Withdraw":
152
+ // Only if sender is owner otherwise the owner would be lost.
153
+ if (draft.args.owner === sender)
154
+ draft.args.owner = bundler;
155
+ }
156
+ // Redirect operation targets.
157
+ switch (draft.type) {
158
+ case "Blue_Borrow":
159
+ case "Blue_Withdraw":
160
+ case "Blue_WithdrawCollateral":
161
+ draft.args.onBehalf = sender;
162
+ case "MetaMorpho_Withdraw":
163
+ // Only if sender is receiver otherwise the receiver would be lost.
164
+ if (draft.args.receiver === sender)
165
+ draft.args.receiver = bundler;
166
+ }
167
+ });
168
+ const needsBundlerAuthorization = mainOperation.type === "Blue_Borrow" ||
169
+ mainOperation.type === "Blue_Withdraw" ||
170
+ mainOperation.type === "Blue_WithdrawCollateral";
171
+ if (needsBundlerAuthorization && !data.getUser(sender).isBundlerAuthorized)
172
+ operations.push({
173
+ type: "Blue_SetAuthorization",
174
+ sender: bundler,
175
+ address: morpho,
176
+ args: {
177
+ owner: sender,
178
+ isBundlerAuthorized: true,
179
+ },
180
+ });
181
+ // Reallocate liquidity if necessary.
182
+ if (mainOperation.type === "Blue_Borrow" ||
183
+ mainOperation.type === "Blue_Withdraw") {
184
+ const market = data
185
+ .getMarket(mainOperation.args.id)
186
+ .accrueInterest(data.block.timestamp);
187
+ const borrowedAssets = mainOperation.type === "Blue_Borrow"
188
+ ? (mainOperation.args.assets ??
189
+ market.toBorrowAssets(mainOperation.args.shares))
190
+ : 0n;
191
+ const withdrawnAssets = mainOperation.type === "Blue_Withdraw"
192
+ ? (mainOperation.args.assets ??
193
+ market.toSupplyAssets(mainOperation.args.shares))
194
+ : 0n;
195
+ const newTotalSupplyAssets = market.totalSupplyAssets - withdrawnAssets;
196
+ const newTotalBorrowAssets = market.totalBorrowAssets + borrowedAssets;
197
+ const reallocations = {};
198
+ const supplyTargetUtilization = publicAllocatorOptions?.supplyTargetUtilization?.[market.config.id] ??
199
+ DEFAULT_SUPPLY_TARGET_UTILIZATION;
200
+ if (MarketUtils.getUtilization({
201
+ totalSupplyAssets: newTotalSupplyAssets,
202
+ totalBorrowAssets: newTotalBorrowAssets,
203
+ }) > supplyTargetUtilization) {
204
+ // Liquidity is insufficient: trigger a public reallocation and try to have a resulting utilization as low as possible, above the target.
205
+ // Solve: newTotalBorrowAssets / (newTotalSupplyAssets + reallocatedAssets) = supplyTargetUtilization
206
+ // Worst case is: there is not enough withdrawals available to fill reallocatedAssets, so utilization is above supplyTargetUtilization.
207
+ let requiredAssets = supplyTargetUtilization === 0n
208
+ ? MathLib.MAX_UINT_160
209
+ : MathLib.wDivDown(newTotalBorrowAssets, supplyTargetUtilization) -
210
+ newTotalSupplyAssets;
211
+ const { withdrawals } = data.getMarketPublicReallocations(market.id, publicAllocatorOptions);
212
+ for (const { vault, ...withdrawal } of withdrawals) {
213
+ const vaultReallocations = (reallocations[vault] ??= []);
214
+ if (withdrawal.assets > requiredAssets) {
215
+ vaultReallocations.push({
216
+ ...withdrawal,
217
+ assets: requiredAssets,
218
+ });
219
+ break;
220
+ }
221
+ requiredAssets -= withdrawal.assets;
222
+ vaultReallocations.push(withdrawal);
223
+ }
224
+ const fees = keys(reallocations).reduce((total, vault) => total + data.getVault(vault).publicAllocatorConfig.fee, 0n);
225
+ // Native input transfer of all fees.
226
+ if (fees > 0n)
227
+ operations.push({
228
+ type: "Erc20_Transfer",
229
+ sender,
230
+ address: NATIVE_ADDRESS,
231
+ args: {
232
+ amount: fees,
233
+ from: sender,
234
+ to: bundler,
235
+ },
236
+ });
237
+ }
238
+ // Reallocate each vault.
239
+ operations.push(...Object.entries(reallocations).map(([vault, vaultWithdrawals]) => ({
240
+ type: "MetaMorpho_PublicReallocate",
241
+ sender: bundler,
242
+ address: vault,
243
+ args: {
244
+ // Reallocation withdrawals must be sorted by market id in ascending alphabetical order.
245
+ withdrawals: vaultWithdrawals.sort(({ id: idA }, { id: idB }) => idA > idB ? 1 : -1),
246
+ supplyMarketId: market.id,
247
+ },
248
+ })));
249
+ }
250
+ const callback = getValue(mainOperation.args, "callback");
251
+ const simulatedOperation = {
252
+ ...mainOperation,
253
+ args: {
254
+ ...mainOperation.args,
255
+ ...(callback && {
256
+ callback: (data) => {
257
+ const operations = callback.flatMap((inputOperation) => {
258
+ const subBundleOperations = populateSubBundle(inputOperation, data, options);
259
+ // Handle to mutate data (not simulate).
260
+ handleBundlerOperations(subBundleOperations, data);
261
+ return subBundleOperations;
262
+ });
263
+ mainOperation.args.callback =
264
+ operations;
265
+ return [];
266
+ },
267
+ }),
268
+ },
269
+ };
270
+ // Operations with callbacks are populated recursively as a side-effect of the simulation, within the callback itself.
271
+ let requiredTokenAmounts = data.simulateRequiredTokenAmounts(operations.concat([simulatedOperation]));
272
+ const allOperations = operations.concat([
273
+ mainOperation,
274
+ ]);
275
+ // Skip approvals/transfers if operation only uses available balances (via maxUint256).
276
+ if (("amount" in mainOperation.args &&
277
+ mainOperation.args.amount === maxUint256) ||
278
+ ("assets" in mainOperation.args &&
279
+ mainOperation.args.assets === maxUint256) ||
280
+ ("shares" in mainOperation.args && mainOperation.args.shares === maxUint256)) {
281
+ if (mainOperation.type === "MetaMorpho_Withdraw")
282
+ mainOperation.args.owner = bundler;
283
+ return allOperations;
284
+ }
285
+ const requirementOperations = getRequirementOperations?.(requiredTokenAmounts) ?? [];
286
+ requiredTokenAmounts = data.simulateRequiredTokenAmounts(requirementOperations
287
+ .concat(allOperations)
288
+ .map((operation) => getSimulatedBundlerOperation(operation)));
289
+ // Append required input transfers.
290
+ requiredTokenAmounts.forEach(({ token, required }) => {
291
+ requirementOperations.push(...populateInputTransfer({
292
+ type: "Erc20_Transfer",
293
+ sender: bundler,
294
+ address: token,
295
+ args: {
296
+ amount: required,
297
+ from: sender,
298
+ to: bundler,
299
+ },
300
+ }, data, { hasSimplePermit: withSimplePermit.has(token) }));
301
+ });
302
+ return requirementOperations.concat(allOperations);
303
+ };
304
+ /**
305
+ * Merges unnecessary duplicate `Erc20_Approve`, `Erc20_Transfer` and `Erc20_Wrap`.
306
+ * Also redirects `Blue_Borrow|Withdraw|WithdrawCollateral` & `MetaMorpho_Withdraw` operations from the bundler to the receiver,
307
+ * as long as the tokens received (possibly ERC4626 shares) are not used afterwards in the bundle.
308
+ * For all the other remaining tokens, appends `Erc20_Transfer` operations to the bundle, from the bundler to the receiver.
309
+ * @param operations The bundle to optimize.
310
+ * @param startData The start data from which to simulate th bundle.
311
+ * @param receiver The receiver of skimmed tokens.
312
+ * @param unwrapTokens The set of tokens to unwrap before transferring to the receiver.
313
+ * @param unwrapSlippage The slippage simulated during unwraps. Should never be 0.
314
+ * @return The optimized bundle.
315
+ */
316
+ export const finalizeBundle = (operations, startData, receiver, unwrapTokens = new Set(), unwrapSlippage = DEFAULT_SLIPPAGE_TOLERANCE) => {
317
+ const nbOperations = operations.length;
318
+ if (nbOperations === 0)
319
+ return operations;
320
+ const { bundler } = getChainAddresses(startData.chainId);
321
+ if (receiver === bundler)
322
+ throw Error(`receiver is bundler`);
323
+ const approvals = [];
324
+ const permits = [];
325
+ const permit2s = [];
326
+ const inputTransfers = [];
327
+ const inputTransfer2s = [];
328
+ const others = [];
329
+ // TODO input transfers can be merged to the right-most position where transferred assets are still not used
330
+ // Merge together approvals, permits, permit2s & input transfers.
331
+ operations.forEach((operation) => {
332
+ switch (operation.type) {
333
+ case "Erc20_Approve": {
334
+ const duplicateApproval = approvals.find((approval) => approval.address === operation.address &&
335
+ approval.sender === operation.sender &&
336
+ approval.args.spender === operation.args.spender);
337
+ if (duplicateApproval == null)
338
+ return approvals.push(operation);
339
+ duplicateApproval.args.amount += operation.args.amount;
340
+ break;
341
+ }
342
+ case "Erc20_Permit": {
343
+ const duplicatePermit = permits.find((permit) => permit.address === operation.address &&
344
+ permit.sender === operation.sender &&
345
+ permit.args.spender === operation.args.spender);
346
+ if (duplicatePermit == null) {
347
+ const lastPermit = permits.findLast((permit) => permit.address === operation.address &&
348
+ permit.sender === operation.sender);
349
+ if (lastPermit)
350
+ operation.args.nonce = lastPermit.args.nonce + 1n;
351
+ permits.push(operation);
352
+ }
353
+ else
354
+ duplicatePermit.args.amount += operation.args.amount;
355
+ break;
356
+ }
357
+ case "Erc20_Permit2": {
358
+ const duplicatePermit2 = permit2s.find((permit2) => permit2.address === operation.address &&
359
+ permit2.sender === operation.sender &&
360
+ permit2.args.spender === operation.args.spender);
361
+ if (duplicatePermit2 == null) {
362
+ const lastPermit2 = permit2s.findLast((permit2) => permit2.address === operation.address &&
363
+ permit2.sender === operation.sender);
364
+ if (lastPermit2)
365
+ operation.args.nonce = lastPermit2.args.nonce + 1n;
366
+ permit2s.push(operation);
367
+ }
368
+ else
369
+ duplicatePermit2.args.amount += operation.args.amount;
370
+ break;
371
+ }
372
+ case "Erc20_Transfer": {
373
+ const { address, sender, args: { amount, from, to }, } = operation;
374
+ if (from !== bundler &&
375
+ to === bundler &&
376
+ !erc20WrapperTokens[startData.chainId].has(address)) {
377
+ const duplicateTransfer = inputTransfers.find((transfer) => transfer.address === address &&
378
+ transfer.sender === sender &&
379
+ transfer.args.from === from);
380
+ if (duplicateTransfer == null ||
381
+ // Don't merge the input transfer if from didn't have enough balance at the start.
382
+ startData.getHolding(from, address).balance < amount)
383
+ return inputTransfers.push(operation);
384
+ duplicateTransfer.args.amount += amount;
385
+ return;
386
+ }
387
+ others.push(operation);
388
+ break;
389
+ }
390
+ case "Erc20_Transfer2": {
391
+ const { address, sender, args: { amount, from, to }, } = operation;
392
+ if (from !== bundler && to === bundler) {
393
+ const duplicateTransfer2 = inputTransfer2s.find((transfer) => transfer.address === address &&
394
+ transfer.sender === sender &&
395
+ transfer.args.from === from);
396
+ if (duplicateTransfer2 == null ||
397
+ // Don't merge the input transfer if from didn't have enough balance at the start.
398
+ startData.getHolding(from, address).balance < amount)
399
+ return inputTransfer2s.push(operation);
400
+ duplicateTransfer2.args.amount += amount;
401
+ return;
402
+ }
403
+ others.push(operation);
404
+ break;
405
+ }
406
+ // Cannot factorize public reallocations because the liquidity may not always be available before other operations.
407
+ default:
408
+ others.push(operation);
409
+ }
410
+ });
411
+ operations = [
412
+ approvals,
413
+ permits,
414
+ permit2s,
415
+ inputTransfers,
416
+ inputTransfer2s,
417
+ others,
418
+ ].flat(1);
419
+ let steps = simulateBundlerOperations(operations, startData);
420
+ // Redirect MetaMorpho deposits.
421
+ operations.forEach((operation, index) => {
422
+ if (operation.type !== "MetaMorpho_Deposit" ||
423
+ operation.args.owner !== bundler)
424
+ return;
425
+ const token = operation.address;
426
+ // shares are not defined when depositing assets, so we rely on simulation steps.
427
+ const shares = steps[index + 1].getHolding(bundler, token).balance -
428
+ steps[index].getHolding(bundler, token).balance;
429
+ if (steps
430
+ .slice(index + 2)
431
+ .some((step) => step.getHolding(bundler, token).balance < shares))
432
+ // If the bundler's balance is at least once lower than assets, the bundler does need these assets.
433
+ return;
434
+ operation.args.owner = receiver;
435
+ });
436
+ // Redirect borrows, withdrawals & MetaMorpho withdrawals.
437
+ operations.forEach((operation, index) => {
438
+ let token;
439
+ switch (operation.type) {
440
+ case "Blue_Borrow":
441
+ case "Blue_Withdraw":
442
+ token = startData.getMarket(operation.args.id).config.loanToken;
443
+ break;
444
+ case "Blue_WithdrawCollateral":
445
+ token = startData.getMarket(operation.args.id).config.collateralToken;
446
+ break;
447
+ case "MetaMorpho_Withdraw":
448
+ token = startData.getVault(operation.address).asset;
449
+ break;
450
+ default:
451
+ return;
452
+ }
453
+ if (operation.args.receiver !== bundler || unwrapTokens.has(token))
454
+ return;
455
+ // assets are not defined when using shares, so we rely on simulation steps.
456
+ const assets = steps[index + 1].getHolding(bundler, token).balance -
457
+ steps[index].getHolding(bundler, token).balance;
458
+ if (steps
459
+ .slice(index + 2)
460
+ .some((step) => step.getHolding(bundler, token).balance < assets))
461
+ // If the bundler's balance is at least once lower than assets, the bundler does need these assets.
462
+ return;
463
+ operation.args.receiver = receiver;
464
+ });
465
+ // Simplify Erc20_Transfer(sender = bundler, to = bundler) + MetaMorpho_Withdraw(owner = bundler) = MetaMorpho_Withdraw(owner = from).
466
+ operations.forEach((operation, index) => {
467
+ if (operation.type !== "MetaMorpho_Withdraw" ||
468
+ operation.args.owner !== bundler)
469
+ return;
470
+ // shares are not defined when using assets, so we rely on simulation steps.
471
+ const shares = steps[index].getHolding(bundler, operation.address).balance -
472
+ steps[index + 1].getHolding(bundler, operation.address).balance;
473
+ const inputTransferIndex = operations.findIndex((candidate) => candidate.type === "Erc20_Transfer" &&
474
+ candidate.address === operation.address &&
475
+ candidate.sender === bundler &&
476
+ candidate.args.to === bundler &&
477
+ candidate.args.amount >= shares);
478
+ if (inputTransferIndex <= 0)
479
+ return;
480
+ const inputTransfer = operations[inputTransferIndex];
481
+ inputTransfer.args.amount -= shares;
482
+ operation.args.owner = inputTransfer.args.from;
483
+ });
484
+ // Filter out useless input transfers.
485
+ operations = operations.filter((operation, index) => {
486
+ if (operation.type !== "Erc20_Transfer")
487
+ return true;
488
+ const { amount, from, to } = operation.args;
489
+ if (from === bundler || to !== bundler)
490
+ return true;
491
+ const token = operation.address;
492
+ if (steps
493
+ .slice(index + 2)
494
+ .some((step) => step.getHolding(bundler, token).balance < amount))
495
+ // If the bundler's balance is at least once less than amount, the bundler does need these assets.
496
+ // Do not only keep the amount actually used in this case because some input transfers
497
+ // are expected to be larger to account for slippage.
498
+ return true;
499
+ return false;
500
+ });
501
+ // Simulate without slippage to skim the bundler of all possible surplus of shares & assets.
502
+ steps = simulateBundlerOperations(operations, startData, { slippage: 0n });
503
+ // Unwrap requested remaining wrapped tokens.
504
+ const unwraps = [];
505
+ const endBundlerTokenData = getLast(steps).holdings[bundler] ?? {};
506
+ unwrapTokens.forEach((wrappedToken) => {
507
+ const remaining = endBundlerTokenData[wrappedToken]?.balance ?? 0n;
508
+ if (remaining <= 5n)
509
+ return;
510
+ const unwrappedToken = getUnwrappedToken(wrappedToken, startData.chainId);
511
+ if (unwrappedToken == null)
512
+ return;
513
+ unwraps.push({
514
+ type: "Erc20_Unwrap",
515
+ address: wrappedToken,
516
+ sender: bundler,
517
+ args: {
518
+ amount: maxUint256,
519
+ receiver,
520
+ slippage: unwrapSlippage,
521
+ },
522
+ });
523
+ });
524
+ if (unwraps.length > 0)
525
+ steps = simulateBundlerOperations(operations.concat(unwraps), startData, {
526
+ slippage: 0n,
527
+ });
528
+ // Skim any token expected to be left on the bundler.
529
+ const skims = [];
530
+ {
531
+ const startBundlerTokenData = steps[0].holdings[bundler] ?? {};
532
+ const endBundlerTokenData = getLast(steps).holdings[bundler] ?? {};
533
+ skims.push(...entries(endBundlerTokenData)
534
+ .filter(([token, { balance }]) => balance - (startBundlerTokenData[token]?.balance ?? 0n) > 5n)
535
+ .map(([address]) => ({
536
+ type: "Erc20_Transfer",
537
+ address,
538
+ sender: bundler,
539
+ args: {
540
+ amount: maxUint256,
541
+ from: bundler,
542
+ to: receiver,
543
+ },
544
+ })));
545
+ }
546
+ return operations.concat(unwraps, skims);
547
+ };
548
+ export const populateBundle = (inputOperations, data, options) => {
549
+ const steps = [data];
550
+ let end = data;
551
+ const operations = inputOperations.flatMap((inputOperation, index) => {
552
+ try {
553
+ const subBundleOperations = populateSubBundle(inputOperation, end, options);
554
+ steps.push((end = getLast(simulateBundlerOperations(subBundleOperations, end))));
555
+ return subBundleOperations;
556
+ }
557
+ catch (error) {
558
+ if (!(error instanceof Error))
559
+ throw error;
560
+ throw new BundlerErrors.Bundle(error, index, inputOperation, steps);
561
+ }
562
+ });
563
+ return { operations, steps };
564
+ };
565
+ export const getSimulatedBundlerOperation = (operation, { slippage } = {}) => {
566
+ const callback = getValue(operation.args, "callback");
567
+ const simulatedOperation = {
568
+ ...operation,
569
+ args: {
570
+ ...operation.args,
571
+ ...(callback && {
572
+ callback: () => callback.map((operation) => getSimulatedBundlerOperation(operation, { slippage })),
573
+ }),
574
+ },
575
+ };
576
+ if (slippage != null) {
577
+ switch (simulatedOperation.type) {
578
+ case "Erc20_Wrap":
579
+ case "Erc20_Unwrap":
580
+ case "Blue_Supply":
581
+ case "Blue_Withdraw":
582
+ case "Blue_Borrow":
583
+ case "Blue_Repay":
584
+ case "MetaMorpho_Deposit":
585
+ case "MetaMorpho_Withdraw":
586
+ simulatedOperation.args.slippage = slippage;
587
+ break;
588
+ }
589
+ }
590
+ return simulatedOperation;
591
+ };
592
+ export const handleBundlerOperation = (options) => (operation, startData, index) => handleOperation(getSimulatedBundlerOperation(operation, options), startData, index);
593
+ export const handleBundlerOperations = (operations, startData, options) => handleOperations(operations, startData, handleBundlerOperation(options));
594
+ export const simulateBundlerOperation = (options) => (operation, startData, index) => simulateOperation(getSimulatedBundlerOperation(operation, options), startData, index);
595
+ export const simulateBundlerOperations = (operations, startData, options) => handleOperations(operations, startData, simulateBundlerOperation(options));