@gearbox-protocol/sdk 14.12.0-next.57 → 14.12.0-next.59

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 (29) hide show
  1. package/dist/cjs/abi/ILiquidationCompressorV313.js +43 -1
  2. package/dist/cjs/abi/IWithdrawalCompressorV313.js +71 -41
  3. package/dist/cjs/dev/claimDSToken.js +85 -6
  4. package/dist/cjs/dev/withdrawalUtils.js +173 -119
  5. package/dist/cjs/sdk/accounts/liquidations/LiquidationsService.js +27 -43
  6. package/dist/cjs/sdk/accounts/liquidations/constants.js +1 -1
  7. package/dist/cjs/sdk/accounts/liquidations/helpers.js +4 -2
  8. package/dist/cjs/sdk/accounts/withdrawal-compressor/AbstractWithdrawalCompressorContract.js +12 -0
  9. package/dist/cjs/sdk/accounts/withdrawal-compressor/WithdrawalCompressorV313Contract.js +10 -0
  10. package/dist/cjs/sdk/accounts/withdrawal-compressor/addresses.js +1 -1
  11. package/dist/esm/abi/ILiquidationCompressorV313.js +43 -1
  12. package/dist/esm/abi/IWithdrawalCompressorV313.js +71 -41
  13. package/dist/esm/dev/claimDSToken.js +86 -7
  14. package/dist/esm/dev/withdrawalUtils.js +173 -119
  15. package/dist/esm/sdk/accounts/liquidations/LiquidationsService.js +29 -45
  16. package/dist/esm/sdk/accounts/liquidations/constants.js +1 -1
  17. package/dist/esm/sdk/accounts/liquidations/helpers.js +4 -2
  18. package/dist/esm/sdk/accounts/withdrawal-compressor/AbstractWithdrawalCompressorContract.js +12 -0
  19. package/dist/esm/sdk/accounts/withdrawal-compressor/WithdrawalCompressorV313Contract.js +10 -0
  20. package/dist/esm/sdk/accounts/withdrawal-compressor/addresses.js +1 -1
  21. package/dist/types/abi/ILiquidationCompressorV313.d.ts +36 -1
  22. package/dist/types/abi/IWithdrawalCompressorV313.d.ts +62 -38
  23. package/dist/types/dev/withdrawalUtils.d.ts +9 -5
  24. package/dist/types/sdk/accounts/liquidations/helpers.d.ts +8 -1
  25. package/dist/types/sdk/accounts/liquidations/types.d.ts +27 -5
  26. package/dist/types/sdk/accounts/withdrawal-compressor/AbstractWithdrawalCompressorContract.d.ts +3 -3
  27. package/dist/types/sdk/accounts/withdrawal-compressor/WithdrawalCompressorV313Contract.d.ts +62 -38
  28. package/dist/types/sdk/accounts/withdrawal-compressor/types.d.ts +10 -0
  29. package/package.json +1 -1
@@ -75,6 +75,11 @@ const iWithdrawalCompressorV313Abi = [
75
75
  internalType: "bytes"
76
76
  }]
77
77
  },
78
+ {
79
+ name: "redeemer",
80
+ type: "address",
81
+ internalType: "address"
82
+ },
78
83
  {
79
84
  name: "extraData",
80
85
  type: "bytes",
@@ -123,6 +128,11 @@ const iWithdrawalCompressorV313Abi = [
123
128
  type: "uint256",
124
129
  internalType: "uint256"
125
130
  },
131
+ {
132
+ name: "redeemer",
133
+ type: "address",
134
+ internalType: "address"
135
+ },
126
136
  {
127
137
  name: "extraData",
128
138
  type: "bytes",
@@ -136,9 +146,9 @@ const iWithdrawalCompressorV313Abi = [
136
146
  type: "function",
137
147
  name: "getExternalAccountCurrentWithdrawals",
138
148
  inputs: [{
139
- name: "withdrawalToken",
140
- type: "address",
141
- internalType: "address"
149
+ name: "withdrawalTokens",
150
+ type: "address[]",
151
+ internalType: "address[]"
142
152
  }, {
143
153
  name: "account",
144
154
  type: "address",
@@ -200,6 +210,11 @@ const iWithdrawalCompressorV313Abi = [
200
210
  internalType: "bytes"
201
211
  }]
202
212
  },
213
+ {
214
+ name: "redeemer",
215
+ type: "address",
216
+ internalType: "address"
217
+ },
203
218
  {
204
219
  name: "extraData",
205
220
  type: "bytes",
@@ -248,6 +263,11 @@ const iWithdrawalCompressorV313Abi = [
248
263
  type: "uint256",
249
264
  internalType: "uint256"
250
265
  },
266
+ {
267
+ name: "redeemer",
268
+ type: "address",
269
+ internalType: "address"
270
+ },
251
271
  {
252
272
  name: "extraData",
253
273
  type: "bytes",
@@ -261,9 +281,9 @@ const iWithdrawalCompressorV313Abi = [
261
281
  type: "function",
262
282
  name: "getExternalAccountCurrentWithdrawals",
263
283
  inputs: [{
264
- name: "withdrawalTokens",
265
- type: "address[]",
266
- internalType: "address[]"
284
+ name: "withdrawalToken",
285
+ type: "address",
286
+ internalType: "address"
267
287
  }, {
268
288
  name: "account",
269
289
  type: "address",
@@ -325,6 +345,11 @@ const iWithdrawalCompressorV313Abi = [
325
345
  internalType: "bytes"
326
346
  }]
327
347
  },
348
+ {
349
+ name: "redeemer",
350
+ type: "address",
351
+ internalType: "address"
352
+ },
328
353
  {
329
354
  name: "extraData",
330
355
  type: "bytes",
@@ -373,6 +398,11 @@ const iWithdrawalCompressorV313Abi = [
373
398
  type: "uint256",
374
399
  internalType: "uint256"
375
400
  },
401
+ {
402
+ name: "redeemer",
403
+ type: "address",
404
+ internalType: "address"
405
+ },
376
406
  {
377
407
  name: "extraData",
378
408
  type: "bytes",
@@ -382,36 +412,6 @@ const iWithdrawalCompressorV313Abi = [
382
412
  }],
383
413
  stateMutability: "view"
384
414
  },
385
- {
386
- type: "function",
387
- name: "getWithdrawalStatus",
388
- inputs: [{
389
- name: "redeemer",
390
- type: "address",
391
- internalType: "address"
392
- }],
393
- outputs: [{
394
- name: "",
395
- type: "uint8",
396
- internalType: "enum WithdrawalStatus"
397
- }],
398
- stateMutability: "view"
399
- },
400
- {
401
- type: "function",
402
- name: "getWithdrawalStatus",
403
- inputs: [{
404
- name: "redeemers",
405
- type: "address[]",
406
- internalType: "address[]"
407
- }],
408
- outputs: [{
409
- name: "",
410
- type: "uint8[]",
411
- internalType: "enum WithdrawalStatus[]"
412
- }],
413
- stateMutability: "view"
414
- },
415
415
  {
416
416
  type: "function",
417
417
  name: "getWithdrawableAssets",
@@ -558,11 +558,6 @@ const iWithdrawalCompressorV313Abi = [
558
558
  type: "address",
559
559
  internalType: "address"
560
560
  },
561
- {
562
- name: "withdrawalToken",
563
- type: "address",
564
- internalType: "address"
565
- },
566
561
  {
567
562
  name: "amount",
568
563
  type: "uint256",
@@ -643,6 +638,11 @@ const iWithdrawalCompressorV313Abi = [
643
638
  type: "address",
644
639
  internalType: "address"
645
640
  },
641
+ {
642
+ name: "withdrawalToken",
643
+ type: "address",
644
+ internalType: "address"
645
+ },
646
646
  {
647
647
  name: "amount",
648
648
  type: "uint256",
@@ -709,6 +709,36 @@ const iWithdrawalCompressorV313Abi = [
709
709
  }],
710
710
  stateMutability: "view"
711
711
  },
712
+ {
713
+ type: "function",
714
+ name: "getWithdrawalStatus",
715
+ inputs: [{
716
+ name: "redeemers",
717
+ type: "address[]",
718
+ internalType: "address[]"
719
+ }],
720
+ outputs: [{
721
+ name: "",
722
+ type: "uint8[]",
723
+ internalType: "enum WithdrawalStatus[]"
724
+ }],
725
+ stateMutability: "view"
726
+ },
727
+ {
728
+ type: "function",
729
+ name: "getWithdrawalStatus",
730
+ inputs: [{
731
+ name: "redeemer",
732
+ type: "address",
733
+ internalType: "address"
734
+ }],
735
+ outputs: [{
736
+ name: "",
737
+ type: "uint8",
738
+ internalType: "enum WithdrawalStatus"
739
+ }],
740
+ stateMutability: "view"
741
+ },
712
742
  {
713
743
  type: "function",
714
744
  name: "version",
@@ -2,9 +2,88 @@ import { OnchainSDK } from "../sdk/OnchainSDK.js";
2
2
  import "../sdk/index.js";
3
3
  import { iDSTokenAbi } from "../abi/rwa/iDSToken.js";
4
4
  import { registerSecuritizeInvestor, writeAndWait } from "./kycUtils.js";
5
- import { erc20Abi } from "viem";
5
+ import { erc20Abi, isAddressEqual, parseAbi, zeroAddress } from "viem";
6
6
  import { privateKeyToAccount } from "viem/accounts";
7
7
  //#region src/dev/claimDSToken.ts
8
+ /**
9
+ * `COMPLIANCE_CONFIGURATION_SERVICE` id in the DS protocol service registry
10
+ */
11
+ const COMPLIANCE_CONFIGURATION_SERVICE = 256n;
12
+ const iDSComplianceConfigurationServiceAbi = parseAbi(["function getUSLockPeriod() external view returns (uint256)", "function getNonUSLockPeriod() external view returns (uint256)"]);
13
+ /**
14
+ * Returns issuance time that is old enough for the issued tokens to be past
15
+ * both US and non-US compliance lock periods, or `undefined` when the token
16
+ * has no compliance configuration service (e.g. MockDSToken)
17
+ */
18
+ async function getUnlockedIssuanceTime({ anvil, token, logger }) {
19
+ try {
20
+ const complianceConfiguration = await anvil.readContract({
21
+ address: token,
22
+ abi: iDSTokenAbi,
23
+ functionName: "getDSService",
24
+ args: [COMPLIANCE_CONFIGURATION_SERVICE]
25
+ });
26
+ if (isAddressEqual(complianceConfiguration, zeroAddress)) return;
27
+ const [usLockPeriod, nonUSLockPeriod] = await anvil.multicall({
28
+ contracts: [{
29
+ address: complianceConfiguration,
30
+ abi: iDSComplianceConfigurationServiceAbi,
31
+ functionName: "getUSLockPeriod"
32
+ }, {
33
+ address: complianceConfiguration,
34
+ abi: iDSComplianceConfigurationServiceAbi,
35
+ functionName: "getNonUSLockPeriod"
36
+ }],
37
+ allowFailure: false
38
+ });
39
+ const lockPeriod = usLockPeriod > nonUSLockPeriod ? usLockPeriod : nonUSLockPeriod;
40
+ const { timestamp } = await anvil.getBlock();
41
+ logger?.debug(`Lock periods: US ${usLockPeriod}, non-US ${nonUSLockPeriod} (compliance configuration service ${complianceConfiguration})`);
42
+ return timestamp > lockPeriod ? timestamp - lockPeriod : 0n;
43
+ } catch (e) {
44
+ logger?.debug(`Failed to get compliance lock periods: ${e}`);
45
+ return;
46
+ }
47
+ }
48
+ /**
49
+ * Issues tokens to investor via `issueTokensCustom` with issuance time that
50
+ * bypasses compliance lock periods, falling back to plain `issueTokens` for
51
+ * tokens that do not support it (e.g. MockDSToken)
52
+ *
53
+ * ACRED tokens do not have this lock, but STAC has
54
+ */
55
+ async function issueDSTokens(props) {
56
+ const { anvil, account, token, investor, amount, logger } = props;
57
+ const issuanceTime = await getUnlockedIssuanceTime(props);
58
+ if (issuanceTime !== void 0) try {
59
+ return await writeAndWait(anvil, {
60
+ account,
61
+ chain: anvil.chain,
62
+ address: token,
63
+ abi: iDSTokenAbi,
64
+ functionName: "issueTokensCustom",
65
+ args: [
66
+ investor,
67
+ amount,
68
+ issuanceTime,
69
+ 0n,
70
+ "",
71
+ 0n
72
+ ]
73
+ });
74
+ } catch (e) {
75
+ logger?.debug(`issueTokensCustom failed: ${e}`);
76
+ }
77
+ logger?.debug("Falling back to issueTokens");
78
+ return writeAndWait(anvil, {
79
+ account,
80
+ chain: anvil.chain,
81
+ address: token,
82
+ abi: iDSTokenAbi,
83
+ functionName: "issueTokens",
84
+ args: [investor, amount]
85
+ });
86
+ }
8
87
  async function claimDSToken(props) {
9
88
  const { anvil, investor, adminPrivateKey, token, marketConfigurators, rwaFactories, usdAmount: usdAmountProp = "100000" } = props;
10
89
  const account = privateKeyToAccount(adminPrivateKey);
@@ -35,13 +114,13 @@ async function claimDSToken(props) {
35
114
  logger
36
115
  });
37
116
  logger?.debug(`Issuing ${amount} tokens to ${investor}...`);
38
- const mintHash = await writeAndWait(anvil, {
117
+ const mintHash = await issueDSTokens({
118
+ anvil,
39
119
  account,
40
- chain: anvil.chain,
41
- address: token,
42
- abi: iDSTokenAbi,
43
- functionName: "issueTokens",
44
- args: [investor, amount]
120
+ token,
121
+ investor,
122
+ amount,
123
+ logger
45
124
  });
46
125
  logger?.debug(`Done! tx: ${mintHash}`);
47
126
  const balance = await anvil.readContract({
@@ -11,23 +11,56 @@ import { erc20Abi, hexToString, parseAbi, parseEther } from "viem";
11
11
  const MIDAS_VAULT_ADMIN = "0x2ACB4BdCbEf02f81BF713b696Ac26390d7f79A12";
12
12
  const iVersionAbi = parseAbi(["function contractType() external view returns (bytes32)"]);
13
13
  /**
14
+ * Common part of Midas and Securitize redeemers
15
+ */
16
+ const iRedeemerAbi = parseAbi(["function gateway() external view returns (address)"]);
17
+ /**
14
18
  * Functions of external Midas redemption vault contract that are not declared
15
19
  * in integrations-v3's IMidasRedemptionVault interface
16
20
  */
17
21
  const iMidasRedemptionVaultExtAbi = parseAbi(["function requestRedeemer() external view returns (address)", "function safeApproveRequest(uint256 requestId, uint256 newMTokenRate) external"]);
18
22
  /**
19
- * Makes pending delayed withdrawals of a credit account claimable on an anvil fork.
23
+ * Makes pending delayed withdrawals claimable on an anvil fork.
24
+ *
25
+ * Accepts either a credit account, in which case all its pending withdrawals are
26
+ * fulfilled, or a single redeemer. The latter is the only way to reach redeemers
27
+ * that were transferred away from a credit account during a liquidation: they are
28
+ * dropped from the gateway's pending sets, so the compressor no longer reports them.
20
29
  *
21
30
  * Replicates the `_fulfillWithdrawal` logic from periphery-v3 Foundry tests
22
31
  * (WithdrawalCompressorMidasRWA.t.sol, WithdrawalCompressorSecuritize.t.sol)
23
32
  * using anvil cheatcodes. Assumes the version 313 of WithdrawalCompressor.
24
33
  *
25
34
  * @param anvil
26
- * @param creditAccount
27
- * @param logger
35
+ * @param address credit account or redeemer
36
+ * @param options
37
+ */
38
+ async function makePendingWithdrawalsClaimable(anvil, address, options) {
39
+ const { logger } = options || {};
40
+ if (await getContractType(anvil, address) === "CREDIT_ACCOUNT") await fulfillCreditAccountWithdrawals(anvil, address, logger);
41
+ else await fulfillRedeemer(anvil, address, logger);
42
+ }
43
+ /**
44
+ * Returns the decoded `contractType` of a contract, or `undefined` when it does
45
+ * not implement it (redeemers, for example)
46
+ */
47
+ async function getContractType(anvil, address) {
48
+ try {
49
+ const cType = await anvil.readContract({
50
+ address,
51
+ abi: iVersionAbi,
52
+ functionName: "contractType"
53
+ });
54
+ return hexToString(cType, { size: 32 });
55
+ } catch {
56
+ return;
57
+ }
58
+ }
59
+ /**
60
+ * Fulfills all pending withdrawals of a credit account, as reported by the
61
+ * withdrawal compressor
28
62
  */
29
- async function makePendingWithdrawalsClaimable(anvil, creditAccount, options) {
30
- const { logger, timeWarp = false } = options || {};
63
+ async function fulfillCreditAccountWithdrawals(anvil, creditAccount, logger) {
31
64
  const compressor = getWithdrawalCompressorAddress(getNetworkType(anvil.chain.id));
32
65
  if (!compressor) throw new Error(`no withdrawal compressor for chain ${anvil.chain.id}`);
33
66
  if (compressor.version !== 313) logger?.warn(`withdrawal compressor version is ${compressor.version}, this helper assumes 313`);
@@ -39,19 +72,8 @@ async function makePendingWithdrawalsClaimable(anvil, creditAccount, options) {
39
72
  });
40
73
  logger?.debug(`found ${pending.length} pending withdrawals for credit account ${creditAccount}`);
41
74
  if (pending.length === 0) return;
42
- if (timeWarp) {
43
- const maxClaimableAt = pending.reduce((max, p) => p.claimableAt > max ? p.claimableAt : max, 0n);
44
- if (maxClaimableAt > 0n) {
45
- logger?.debug(`warping time to ${maxClaimableAt + 1n}`);
46
- await anvil.evmMineDetailed(maxClaimableAt + 1n);
47
- }
48
- }
49
75
  for (const w of pending) {
50
- const cType = hexToString(await anvil.readContract({
51
- address: w.withdrawalPhantomToken,
52
- abi: iVersionAbi,
53
- functionName: "contractType"
54
- }), { size: 32 });
76
+ const cType = await getContractType(anvil, w.withdrawalPhantomToken);
55
77
  logger?.debug(`fulfilling withdrawal in phantom token ${w.withdrawalPhantomToken} with contract type ${cType}`);
56
78
  switch (cType) {
57
79
  case "PHANTOM_TOKEN::MIDAS_REDEMPTION":
@@ -60,7 +82,7 @@ async function makePendingWithdrawalsClaimable(anvil, creditAccount, options) {
60
82
  case "PHANTOM_TOKEN::SECURITIZE_RD":
61
83
  await fulfillSecuritizeWithdrawal(anvil, creditAccount, w.withdrawalPhantomToken, logger);
62
84
  break;
63
- default: logger?.warn(`unsupported withdrawal phantom token type ${cType}, only time warp was applied`);
85
+ default: logger?.warn(`unsupported withdrawal phantom token type ${cType}, skipping`);
64
86
  }
65
87
  }
66
88
  const [claimableAfter, pendingAfter] = await anvil.readContract({
@@ -72,29 +94,72 @@ async function makePendingWithdrawalsClaimable(anvil, creditAccount, options) {
72
94
  logger?.debug(`after fulfillment: ${claimableAfter.length} claimable, ${pendingAfter.length} still pending withdrawals`);
73
95
  }
74
96
  /**
75
- * Fulfills a Midas redemption request:
76
- * funds the vault's request redeemer with tokenOut and approves the request
77
- * on the Midas redemption vault on behalf of the vault admin
97
+ * Fulfills the redemption request held by a single redeemer, dispatching on the
98
+ * type of the gateway that deployed it
99
+ */
100
+ async function fulfillRedeemer(anvil, redeemer, logger) {
101
+ const gateway = await anvil.readContract({
102
+ address: redeemer,
103
+ abi: iRedeemerAbi,
104
+ functionName: "gateway"
105
+ });
106
+ const cType = await getContractType(anvil, gateway);
107
+ logger?.debug(`fulfilling redeemer ${redeemer} of gateway ${gateway} with contract type ${cType}`);
108
+ switch (cType) {
109
+ case "GATEWAY::MIDAS":
110
+ await fulfillMidasRedeemer(anvil, redeemer, logger);
111
+ break;
112
+ case "GATEWAY::SECURITIZE_REDEMPTION":
113
+ await fulfillSecuritizeRedeemer(anvil, redeemer, logger);
114
+ break;
115
+ default: throw new Error(`unsupported gateway type ${cType} of redeemer ${redeemer}`);
116
+ }
117
+ }
118
+ /**
119
+ * Fulfills all pending Midas redemption requests of a credit account
78
120
  */
79
121
  async function fulfillMidasWithdrawal(anvil, creditAccount, withdrawalPhantomToken, logger) {
80
- const [gateway, tokenOut] = await anvil.multicall({
81
- allowFailure: false,
82
- contracts: [{
83
- address: withdrawalPhantomToken,
84
- abi: midasRedemptionVaultPhantomTokenAbi,
85
- functionName: "gateway"
86
- }, {
87
- address: withdrawalPhantomToken,
88
- abi: midasRedemptionVaultPhantomTokenAbi,
89
- functionName: "underlying"
90
- }]
122
+ const gateway = await anvil.readContract({
123
+ address: withdrawalPhantomToken,
124
+ abi: midasRedemptionVaultPhantomTokenAbi,
125
+ functionName: "gateway"
91
126
  });
92
- const midasRedemptionVault = await anvil.readContract({
127
+ const redeemers = await anvil.readContract({
93
128
  address: gateway,
94
129
  abi: midasGatewayAbi,
95
- functionName: "midasRedemptionVault"
130
+ functionName: "pendingRedeemers",
131
+ args: [creditAccount]
132
+ });
133
+ logger?.debug(`midas: gateway ${gateway}, ${redeemers.length} pending redeemers`);
134
+ for (const redeemer of redeemers) await fulfillMidasRedeemer(anvil, redeemer, logger);
135
+ }
136
+ /**
137
+ * Fulfills a single Midas redemption request:
138
+ * funds the vault's request redeemer with tokenOut and approves the request
139
+ * on the Midas redemption vault on behalf of the vault admin
140
+ */
141
+ async function fulfillMidasRedeemer(anvil, redeemer, logger) {
142
+ const [midasRedemptionVault, tokenOut, requestId] = await anvil.multicall({
143
+ allowFailure: false,
144
+ contracts: [
145
+ {
146
+ address: redeemer,
147
+ abi: midasRedeemerAbi,
148
+ functionName: "midasRedemptionVault"
149
+ },
150
+ {
151
+ address: redeemer,
152
+ abi: midasRedeemerAbi,
153
+ functionName: "quoteToken"
154
+ },
155
+ {
156
+ address: redeemer,
157
+ abi: midasRedeemerAbi,
158
+ functionName: "requestId"
159
+ }
160
+ ]
96
161
  });
97
- const [mTokenDataFeed, requestRedeemer, tokenOutDecimals, redeemers] = await anvil.multicall({
162
+ const [mTokenDataFeed, requestRedeemer, tokenOutDecimals] = await anvil.multicall({
98
163
  allowFailure: false,
99
164
  contracts: [
100
165
  {
@@ -111,12 +176,6 @@ async function fulfillMidasWithdrawal(anvil, creditAccount, withdrawalPhantomTok
111
176
  address: tokenOut,
112
177
  abi: erc20Abi,
113
178
  functionName: "decimals"
114
- },
115
- {
116
- address: gateway,
117
- abi: midasGatewayAbi,
118
- functionName: "pendingRedeemers",
119
- args: [creditAccount]
120
179
  }
121
180
  ]
122
181
  });
@@ -125,63 +184,48 @@ async function fulfillMidasWithdrawal(anvil, creditAccount, withdrawalPhantomTok
125
184
  abi: iMidasDataFeedAbi,
126
185
  functionName: "getDataInBase18"
127
186
  });
128
- logger?.debug(`midas: gateway ${gateway}, vault ${midasRedemptionVault}, tokenOut ${tokenOut}, mToken rate ${mTokenRate}, ${redeemers.length} pending redeemers`);
129
- for (const redeemer of redeemers) {
130
- const topUp = 1000000n * 10n ** BigInt(tokenOutDecimals);
131
- const requestRedeemerBalance = await anvil.readContract({
132
- address: tokenOut,
133
- abi: erc20Abi,
134
- functionName: "balanceOf",
135
- args: [requestRedeemer]
136
- });
137
- logger?.debug(`midas: dealing ${topUp} of tokenOut ${tokenOut} to request redeemer ${requestRedeemer}`);
138
- await anvil.deal({
139
- erc20: tokenOut,
140
- account: requestRedeemer,
141
- amount: requestRedeemerBalance + topUp
142
- });
143
- const requestId = await anvil.readContract({
144
- address: redeemer,
145
- abi: midasRedeemerAbi,
146
- functionName: "requestId"
147
- });
148
- logger?.debug(`midas: approving request ${requestId} of redeemer ${redeemer} as vault admin ${MIDAS_VAULT_ADMIN}`);
149
- await anvil.impersonateAccount({ address: MIDAS_VAULT_ADMIN });
150
- await anvil.setBalance({
151
- address: MIDAS_VAULT_ADMIN,
152
- value: parseEther("100")
153
- });
154
- const hash = await anvil.writeContract({
155
- chain: anvil.chain,
156
- address: midasRedemptionVault,
157
- account: MIDAS_VAULT_ADMIN,
158
- abi: iMidasRedemptionVaultExtAbi,
159
- functionName: "safeApproveRequest",
160
- args: [requestId, mTokenRate]
161
- });
162
- if ((await anvil.waitForTransactionReceipt({
163
- hash,
164
- pollingInterval: 100
165
- })).status !== "success") throw new Error(`midas: safeApproveRequest tx ${hash} reverted`);
166
- await anvil.stopImpersonatingAccount({ address: MIDAS_VAULT_ADMIN });
167
- }
187
+ logger?.debug(`midas: redeemer ${redeemer}, vault ${midasRedemptionVault}, tokenOut ${tokenOut}, mToken rate ${mTokenRate}`);
188
+ const topUp = 1000000n * 10n ** BigInt(tokenOutDecimals);
189
+ const requestRedeemerBalance = await anvil.readContract({
190
+ address: tokenOut,
191
+ abi: erc20Abi,
192
+ functionName: "balanceOf",
193
+ args: [requestRedeemer]
194
+ });
195
+ logger?.debug(`midas: dealing ${topUp} of tokenOut ${tokenOut} to request redeemer ${requestRedeemer}`);
196
+ await anvil.deal({
197
+ erc20: tokenOut,
198
+ account: requestRedeemer,
199
+ amount: requestRedeemerBalance + topUp
200
+ });
201
+ logger?.debug(`midas: approving request ${requestId} of redeemer ${redeemer} as vault admin ${MIDAS_VAULT_ADMIN}`);
202
+ await anvil.impersonateAccount({ address: MIDAS_VAULT_ADMIN });
203
+ await anvil.setBalance({
204
+ address: MIDAS_VAULT_ADMIN,
205
+ value: parseEther("100")
206
+ });
207
+ const hash = await anvil.writeContract({
208
+ chain: anvil.chain,
209
+ address: midasRedemptionVault,
210
+ account: MIDAS_VAULT_ADMIN,
211
+ abi: iMidasRedemptionVaultExtAbi,
212
+ functionName: "safeApproveRequest",
213
+ args: [requestId, mTokenRate]
214
+ });
215
+ if ((await anvil.waitForTransactionReceipt({
216
+ hash,
217
+ pollingInterval: 100
218
+ })).status !== "success") throw new Error(`midas: safeApproveRequest tx ${hash} reverted`);
219
+ await anvil.stopImpersonatingAccount({ address: MIDAS_VAULT_ADMIN });
168
220
  }
169
221
  /**
170
- * Fulfills a Securitize redemption request:
171
- * funds the first redeemer of the credit account with stablecoins
222
+ * Fulfills all unclaimed Securitize redemption requests of a credit account
172
223
  */
173
224
  async function fulfillSecuritizeWithdrawal(anvil, creditAccount, withdrawalPhantomToken, logger) {
174
- const [redemptionGateway, stableCoinToken] = await anvil.multicall({
175
- allowFailure: false,
176
- contracts: [{
177
- address: withdrawalPhantomToken,
178
- abi: securitizeRedemptionPhantomTokenAbi,
179
- functionName: "redemptionGateway"
180
- }, {
181
- address: withdrawalPhantomToken,
182
- abi: securitizeRedemptionPhantomTokenAbi,
183
- functionName: "stableCoinToken"
184
- }]
225
+ const redemptionGateway = await anvil.readContract({
226
+ address: withdrawalPhantomToken,
227
+ abi: securitizeRedemptionPhantomTokenAbi,
228
+ functionName: "redemptionGateway"
185
229
  });
186
230
  const redeemers = await anvil.readContract({
187
231
  address: redemptionGateway,
@@ -189,36 +233,46 @@ async function fulfillSecuritizeWithdrawal(anvil, creditAccount, withdrawalPhant
189
233
  functionName: "getUnclaimedRedeemers",
190
234
  args: [creditAccount]
191
235
  });
192
- logger?.debug(`securitize: gateway ${redemptionGateway}, stablecoin ${stableCoinToken}, ${redeemers.length} unclaimed redeemers`);
236
+ logger?.debug(`securitize: gateway ${redemptionGateway}, ${redeemers.length} unclaimed redeemers`);
193
237
  if (redeemers.length === 0) {
194
238
  logger?.warn(`securitize: no unclaimed redeemers found for credit account ${creditAccount}`);
195
239
  return;
196
240
  }
197
- for (const redeemer of redeemers) {
198
- const [redemptionValue, redeemerBalance] = await anvil.multicall({
199
- allowFailure: false,
200
- contracts: [{
201
- address: redeemer,
202
- abi: securitizeRedeemerAbi,
203
- functionName: "getCurrentRedemptionValue"
204
- }, {
205
- address: stableCoinToken,
206
- abi: erc20Abi,
207
- functionName: "balanceOf",
208
- args: [redeemer]
209
- }]
210
- });
211
- if (redemptionValue === 0n) {
212
- logger?.debug(`securitize: skipping redeemer ${redeemer} with zero redemption value`);
213
- continue;
214
- }
215
- logger?.debug(`securitize: dealing ${redemptionValue} of stablecoin ${stableCoinToken} to redeemer ${redeemer}`);
216
- await anvil.deal({
217
- erc20: stableCoinToken,
218
- account: redeemer,
219
- amount: redeemerBalance + redemptionValue
220
- });
241
+ for (const redeemer of redeemers) await fulfillSecuritizeRedeemer(anvil, redeemer, logger);
242
+ }
243
+ /**
244
+ * Fulfills a single Securitize redemption request:
245
+ * funds the redeemer with stablecoins
246
+ */
247
+ async function fulfillSecuritizeRedeemer(anvil, redeemer, logger) {
248
+ const [stableCoinToken, redemptionValue] = await anvil.multicall({
249
+ allowFailure: false,
250
+ contracts: [{
251
+ address: redeemer,
252
+ abi: securitizeRedeemerAbi,
253
+ functionName: "stableCoinToken"
254
+ }, {
255
+ address: redeemer,
256
+ abi: securitizeRedeemerAbi,
257
+ functionName: "getCurrentRedemptionValue"
258
+ }]
259
+ });
260
+ if (redemptionValue === 0n) {
261
+ logger?.debug(`securitize: skipping redeemer ${redeemer} with zero redemption value`);
262
+ return;
221
263
  }
264
+ const redeemerBalance = await anvil.readContract({
265
+ address: stableCoinToken,
266
+ abi: erc20Abi,
267
+ functionName: "balanceOf",
268
+ args: [redeemer]
269
+ });
270
+ logger?.debug(`securitize: dealing ${redemptionValue} of stablecoin ${stableCoinToken} to redeemer ${redeemer}`);
271
+ await anvil.deal({
272
+ erc20: stableCoinToken,
273
+ account: redeemer,
274
+ amount: redeemerBalance + redemptionValue
275
+ });
222
276
  }
223
277
  //#endregion
224
278
  export { makePendingWithdrawalsClaimable };