@gearbox-protocol/sdk 14.12.0-next.58 → 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.
@@ -6,6 +6,85 @@ const require_dev_kycUtils = require("./kycUtils.js");
6
6
  let viem = require("viem");
7
7
  let viem_accounts = require("viem/accounts");
8
8
  //#region src/dev/claimDSToken.ts
9
+ /**
10
+ * `COMPLIANCE_CONFIGURATION_SERVICE` id in the DS protocol service registry
11
+ */
12
+ const COMPLIANCE_CONFIGURATION_SERVICE = 256n;
13
+ const iDSComplianceConfigurationServiceAbi = (0, viem.parseAbi)(["function getUSLockPeriod() external view returns (uint256)", "function getNonUSLockPeriod() external view returns (uint256)"]);
14
+ /**
15
+ * Returns issuance time that is old enough for the issued tokens to be past
16
+ * both US and non-US compliance lock periods, or `undefined` when the token
17
+ * has no compliance configuration service (e.g. MockDSToken)
18
+ */
19
+ async function getUnlockedIssuanceTime({ anvil, token, logger }) {
20
+ try {
21
+ const complianceConfiguration = await anvil.readContract({
22
+ address: token,
23
+ abi: require_abi_rwa_iDSToken.iDSTokenAbi,
24
+ functionName: "getDSService",
25
+ args: [COMPLIANCE_CONFIGURATION_SERVICE]
26
+ });
27
+ if ((0, viem.isAddressEqual)(complianceConfiguration, viem.zeroAddress)) return;
28
+ const [usLockPeriod, nonUSLockPeriod] = await anvil.multicall({
29
+ contracts: [{
30
+ address: complianceConfiguration,
31
+ abi: iDSComplianceConfigurationServiceAbi,
32
+ functionName: "getUSLockPeriod"
33
+ }, {
34
+ address: complianceConfiguration,
35
+ abi: iDSComplianceConfigurationServiceAbi,
36
+ functionName: "getNonUSLockPeriod"
37
+ }],
38
+ allowFailure: false
39
+ });
40
+ const lockPeriod = usLockPeriod > nonUSLockPeriod ? usLockPeriod : nonUSLockPeriod;
41
+ const { timestamp } = await anvil.getBlock();
42
+ logger?.debug(`Lock periods: US ${usLockPeriod}, non-US ${nonUSLockPeriod} (compliance configuration service ${complianceConfiguration})`);
43
+ return timestamp > lockPeriod ? timestamp - lockPeriod : 0n;
44
+ } catch (e) {
45
+ logger?.debug(`Failed to get compliance lock periods: ${e}`);
46
+ return;
47
+ }
48
+ }
49
+ /**
50
+ * Issues tokens to investor via `issueTokensCustom` with issuance time that
51
+ * bypasses compliance lock periods, falling back to plain `issueTokens` for
52
+ * tokens that do not support it (e.g. MockDSToken)
53
+ *
54
+ * ACRED tokens do not have this lock, but STAC has
55
+ */
56
+ async function issueDSTokens(props) {
57
+ const { anvil, account, token, investor, amount, logger } = props;
58
+ const issuanceTime = await getUnlockedIssuanceTime(props);
59
+ if (issuanceTime !== void 0) try {
60
+ return await require_dev_kycUtils.writeAndWait(anvil, {
61
+ account,
62
+ chain: anvil.chain,
63
+ address: token,
64
+ abi: require_abi_rwa_iDSToken.iDSTokenAbi,
65
+ functionName: "issueTokensCustom",
66
+ args: [
67
+ investor,
68
+ amount,
69
+ issuanceTime,
70
+ 0n,
71
+ "",
72
+ 0n
73
+ ]
74
+ });
75
+ } catch (e) {
76
+ logger?.debug(`issueTokensCustom failed: ${e}`);
77
+ }
78
+ logger?.debug("Falling back to issueTokens");
79
+ return require_dev_kycUtils.writeAndWait(anvil, {
80
+ account,
81
+ chain: anvil.chain,
82
+ address: token,
83
+ abi: require_abi_rwa_iDSToken.iDSTokenAbi,
84
+ functionName: "issueTokens",
85
+ args: [investor, amount]
86
+ });
87
+ }
9
88
  async function claimDSToken(props) {
10
89
  const { anvil, investor, adminPrivateKey, token, marketConfigurators, rwaFactories, usdAmount: usdAmountProp = "100000" } = props;
11
90
  const account = (0, viem_accounts.privateKeyToAccount)(adminPrivateKey);
@@ -36,13 +115,13 @@ async function claimDSToken(props) {
36
115
  logger
37
116
  });
38
117
  logger?.debug(`Issuing ${amount} tokens to ${investor}...`);
39
- const mintHash = await require_dev_kycUtils.writeAndWait(anvil, {
118
+ const mintHash = await issueDSTokens({
119
+ anvil,
40
120
  account,
41
- chain: anvil.chain,
42
- address: token,
43
- abi: require_abi_rwa_iDSToken.iDSTokenAbi,
44
- functionName: "issueTokens",
45
- args: [investor, amount]
121
+ token,
122
+ investor,
123
+ amount,
124
+ logger
46
125
  });
47
126
  logger?.debug(`Done! tx: ${mintHash}`);
48
127
  const balance = await anvil.readContract({
@@ -12,23 +12,56 @@ let viem = require("viem");
12
12
  const MIDAS_VAULT_ADMIN = "0x2ACB4BdCbEf02f81BF713b696Ac26390d7f79A12";
13
13
  const iVersionAbi = (0, viem.parseAbi)(["function contractType() external view returns (bytes32)"]);
14
14
  /**
15
+ * Common part of Midas and Securitize redeemers
16
+ */
17
+ const iRedeemerAbi = (0, viem.parseAbi)(["function gateway() external view returns (address)"]);
18
+ /**
15
19
  * Functions of external Midas redemption vault contract that are not declared
16
20
  * in integrations-v3's IMidasRedemptionVault interface
17
21
  */
18
22
  const iMidasRedemptionVaultExtAbi = (0, viem.parseAbi)(["function requestRedeemer() external view returns (address)", "function safeApproveRequest(uint256 requestId, uint256 newMTokenRate) external"]);
19
23
  /**
20
- * Makes pending delayed withdrawals of a credit account claimable on an anvil fork.
24
+ * Makes pending delayed withdrawals claimable on an anvil fork.
25
+ *
26
+ * Accepts either a credit account, in which case all its pending withdrawals are
27
+ * fulfilled, or a single redeemer. The latter is the only way to reach redeemers
28
+ * that were transferred away from a credit account during a liquidation: they are
29
+ * dropped from the gateway's pending sets, so the compressor no longer reports them.
21
30
  *
22
31
  * Replicates the `_fulfillWithdrawal` logic from periphery-v3 Foundry tests
23
32
  * (WithdrawalCompressorMidasRWA.t.sol, WithdrawalCompressorSecuritize.t.sol)
24
33
  * using anvil cheatcodes. Assumes the version 313 of WithdrawalCompressor.
25
34
  *
26
35
  * @param anvil
27
- * @param creditAccount
28
- * @param logger
36
+ * @param address credit account or redeemer
37
+ * @param options
38
+ */
39
+ async function makePendingWithdrawalsClaimable(anvil, address, options) {
40
+ const { logger } = options || {};
41
+ if (await getContractType(anvil, address) === "CREDIT_ACCOUNT") await fulfillCreditAccountWithdrawals(anvil, address, logger);
42
+ else await fulfillRedeemer(anvil, address, logger);
43
+ }
44
+ /**
45
+ * Returns the decoded `contractType` of a contract, or `undefined` when it does
46
+ * not implement it (redeemers, for example)
47
+ */
48
+ async function getContractType(anvil, address) {
49
+ try {
50
+ const cType = await anvil.readContract({
51
+ address,
52
+ abi: iVersionAbi,
53
+ functionName: "contractType"
54
+ });
55
+ return (0, viem.hexToString)(cType, { size: 32 });
56
+ } catch {
57
+ return;
58
+ }
59
+ }
60
+ /**
61
+ * Fulfills all pending withdrawals of a credit account, as reported by the
62
+ * withdrawal compressor
29
63
  */
30
- async function makePendingWithdrawalsClaimable(anvil, creditAccount, options) {
31
- const { logger, timeWarp = false } = options || {};
64
+ async function fulfillCreditAccountWithdrawals(anvil, creditAccount, logger) {
32
65
  const compressor = require_sdk_accounts_withdrawal_compressor_addresses.getWithdrawalCompressorAddress(require_sdk_chain_chains.getNetworkType(anvil.chain.id));
33
66
  if (!compressor) throw new Error(`no withdrawal compressor for chain ${anvil.chain.id}`);
34
67
  if (compressor.version !== 313) logger?.warn(`withdrawal compressor version is ${compressor.version}, this helper assumes 313`);
@@ -40,19 +73,8 @@ async function makePendingWithdrawalsClaimable(anvil, creditAccount, options) {
40
73
  });
41
74
  logger?.debug(`found ${pending.length} pending withdrawals for credit account ${creditAccount}`);
42
75
  if (pending.length === 0) return;
43
- if (timeWarp) {
44
- const maxClaimableAt = pending.reduce((max, p) => p.claimableAt > max ? p.claimableAt : max, 0n);
45
- if (maxClaimableAt > 0n) {
46
- logger?.debug(`warping time to ${maxClaimableAt + 1n}`);
47
- await anvil.evmMineDetailed(maxClaimableAt + 1n);
48
- }
49
- }
50
76
  for (const w of pending) {
51
- const cType = (0, viem.hexToString)(await anvil.readContract({
52
- address: w.withdrawalPhantomToken,
53
- abi: iVersionAbi,
54
- functionName: "contractType"
55
- }), { size: 32 });
77
+ const cType = await getContractType(anvil, w.withdrawalPhantomToken);
56
78
  logger?.debug(`fulfilling withdrawal in phantom token ${w.withdrawalPhantomToken} with contract type ${cType}`);
57
79
  switch (cType) {
58
80
  case "PHANTOM_TOKEN::MIDAS_REDEMPTION":
@@ -61,7 +83,7 @@ async function makePendingWithdrawalsClaimable(anvil, creditAccount, options) {
61
83
  case "PHANTOM_TOKEN::SECURITIZE_RD":
62
84
  await fulfillSecuritizeWithdrawal(anvil, creditAccount, w.withdrawalPhantomToken, logger);
63
85
  break;
64
- default: logger?.warn(`unsupported withdrawal phantom token type ${cType}, only time warp was applied`);
86
+ default: logger?.warn(`unsupported withdrawal phantom token type ${cType}, skipping`);
65
87
  }
66
88
  }
67
89
  const [claimableAfter, pendingAfter] = await anvil.readContract({
@@ -73,29 +95,72 @@ async function makePendingWithdrawalsClaimable(anvil, creditAccount, options) {
73
95
  logger?.debug(`after fulfillment: ${claimableAfter.length} claimable, ${pendingAfter.length} still pending withdrawals`);
74
96
  }
75
97
  /**
76
- * Fulfills a Midas redemption request:
77
- * funds the vault's request redeemer with tokenOut and approves the request
78
- * on the Midas redemption vault on behalf of the vault admin
98
+ * Fulfills the redemption request held by a single redeemer, dispatching on the
99
+ * type of the gateway that deployed it
100
+ */
101
+ async function fulfillRedeemer(anvil, redeemer, logger) {
102
+ const gateway = await anvil.readContract({
103
+ address: redeemer,
104
+ abi: iRedeemerAbi,
105
+ functionName: "gateway"
106
+ });
107
+ const cType = await getContractType(anvil, gateway);
108
+ logger?.debug(`fulfilling redeemer ${redeemer} of gateway ${gateway} with contract type ${cType}`);
109
+ switch (cType) {
110
+ case "GATEWAY::MIDAS":
111
+ await fulfillMidasRedeemer(anvil, redeemer, logger);
112
+ break;
113
+ case "GATEWAY::SECURITIZE_REDEMPTION":
114
+ await fulfillSecuritizeRedeemer(anvil, redeemer, logger);
115
+ break;
116
+ default: throw new Error(`unsupported gateway type ${cType} of redeemer ${redeemer}`);
117
+ }
118
+ }
119
+ /**
120
+ * Fulfills all pending Midas redemption requests of a credit account
79
121
  */
80
122
  async function fulfillMidasWithdrawal(anvil, creditAccount, withdrawalPhantomToken, logger) {
81
- const [gateway, tokenOut] = await anvil.multicall({
82
- allowFailure: false,
83
- contracts: [{
84
- address: withdrawalPhantomToken,
85
- abi: require_dev_withdrawalAbi.midasRedemptionVaultPhantomTokenAbi,
86
- functionName: "gateway"
87
- }, {
88
- address: withdrawalPhantomToken,
89
- abi: require_dev_withdrawalAbi.midasRedemptionVaultPhantomTokenAbi,
90
- functionName: "underlying"
91
- }]
123
+ const gateway = await anvil.readContract({
124
+ address: withdrawalPhantomToken,
125
+ abi: require_dev_withdrawalAbi.midasRedemptionVaultPhantomTokenAbi,
126
+ functionName: "gateway"
92
127
  });
93
- const midasRedemptionVault = await anvil.readContract({
128
+ const redeemers = await anvil.readContract({
94
129
  address: gateway,
95
130
  abi: require_dev_withdrawalAbi.midasGatewayAbi,
96
- functionName: "midasRedemptionVault"
131
+ functionName: "pendingRedeemers",
132
+ args: [creditAccount]
133
+ });
134
+ logger?.debug(`midas: gateway ${gateway}, ${redeemers.length} pending redeemers`);
135
+ for (const redeemer of redeemers) await fulfillMidasRedeemer(anvil, redeemer, logger);
136
+ }
137
+ /**
138
+ * Fulfills a single Midas redemption request:
139
+ * funds the vault's request redeemer with tokenOut and approves the request
140
+ * on the Midas redemption vault on behalf of the vault admin
141
+ */
142
+ async function fulfillMidasRedeemer(anvil, redeemer, logger) {
143
+ const [midasRedemptionVault, tokenOut, requestId] = await anvil.multicall({
144
+ allowFailure: false,
145
+ contracts: [
146
+ {
147
+ address: redeemer,
148
+ abi: require_dev_withdrawalAbi.midasRedeemerAbi,
149
+ functionName: "midasRedemptionVault"
150
+ },
151
+ {
152
+ address: redeemer,
153
+ abi: require_dev_withdrawalAbi.midasRedeemerAbi,
154
+ functionName: "quoteToken"
155
+ },
156
+ {
157
+ address: redeemer,
158
+ abi: require_dev_withdrawalAbi.midasRedeemerAbi,
159
+ functionName: "requestId"
160
+ }
161
+ ]
97
162
  });
98
- const [mTokenDataFeed, requestRedeemer, tokenOutDecimals, redeemers] = await anvil.multicall({
163
+ const [mTokenDataFeed, requestRedeemer, tokenOutDecimals] = await anvil.multicall({
99
164
  allowFailure: false,
100
165
  contracts: [
101
166
  {
@@ -112,12 +177,6 @@ async function fulfillMidasWithdrawal(anvil, creditAccount, withdrawalPhantomTok
112
177
  address: tokenOut,
113
178
  abi: viem.erc20Abi,
114
179
  functionName: "decimals"
115
- },
116
- {
117
- address: gateway,
118
- abi: require_dev_withdrawalAbi.midasGatewayAbi,
119
- functionName: "pendingRedeemers",
120
- args: [creditAccount]
121
180
  }
122
181
  ]
123
182
  });
@@ -126,63 +185,48 @@ async function fulfillMidasWithdrawal(anvil, creditAccount, withdrawalPhantomTok
126
185
  abi: require_dev_withdrawalAbi.iMidasDataFeedAbi,
127
186
  functionName: "getDataInBase18"
128
187
  });
129
- logger?.debug(`midas: gateway ${gateway}, vault ${midasRedemptionVault}, tokenOut ${tokenOut}, mToken rate ${mTokenRate}, ${redeemers.length} pending redeemers`);
130
- for (const redeemer of redeemers) {
131
- const topUp = 1000000n * 10n ** BigInt(tokenOutDecimals);
132
- const requestRedeemerBalance = await anvil.readContract({
133
- address: tokenOut,
134
- abi: viem.erc20Abi,
135
- functionName: "balanceOf",
136
- args: [requestRedeemer]
137
- });
138
- logger?.debug(`midas: dealing ${topUp} of tokenOut ${tokenOut} to request redeemer ${requestRedeemer}`);
139
- await anvil.deal({
140
- erc20: tokenOut,
141
- account: requestRedeemer,
142
- amount: requestRedeemerBalance + topUp
143
- });
144
- const requestId = await anvil.readContract({
145
- address: redeemer,
146
- abi: require_dev_withdrawalAbi.midasRedeemerAbi,
147
- functionName: "requestId"
148
- });
149
- logger?.debug(`midas: approving request ${requestId} of redeemer ${redeemer} as vault admin ${MIDAS_VAULT_ADMIN}`);
150
- await anvil.impersonateAccount({ address: MIDAS_VAULT_ADMIN });
151
- await anvil.setBalance({
152
- address: MIDAS_VAULT_ADMIN,
153
- value: (0, viem.parseEther)("100")
154
- });
155
- const hash = await anvil.writeContract({
156
- chain: anvil.chain,
157
- address: midasRedemptionVault,
158
- account: MIDAS_VAULT_ADMIN,
159
- abi: iMidasRedemptionVaultExtAbi,
160
- functionName: "safeApproveRequest",
161
- args: [requestId, mTokenRate]
162
- });
163
- if ((await anvil.waitForTransactionReceipt({
164
- hash,
165
- pollingInterval: 100
166
- })).status !== "success") throw new Error(`midas: safeApproveRequest tx ${hash} reverted`);
167
- await anvil.stopImpersonatingAccount({ address: MIDAS_VAULT_ADMIN });
168
- }
188
+ logger?.debug(`midas: redeemer ${redeemer}, vault ${midasRedemptionVault}, tokenOut ${tokenOut}, mToken rate ${mTokenRate}`);
189
+ const topUp = 1000000n * 10n ** BigInt(tokenOutDecimals);
190
+ const requestRedeemerBalance = await anvil.readContract({
191
+ address: tokenOut,
192
+ abi: viem.erc20Abi,
193
+ functionName: "balanceOf",
194
+ args: [requestRedeemer]
195
+ });
196
+ logger?.debug(`midas: dealing ${topUp} of tokenOut ${tokenOut} to request redeemer ${requestRedeemer}`);
197
+ await anvil.deal({
198
+ erc20: tokenOut,
199
+ account: requestRedeemer,
200
+ amount: requestRedeemerBalance + topUp
201
+ });
202
+ logger?.debug(`midas: approving request ${requestId} of redeemer ${redeemer} as vault admin ${MIDAS_VAULT_ADMIN}`);
203
+ await anvil.impersonateAccount({ address: MIDAS_VAULT_ADMIN });
204
+ await anvil.setBalance({
205
+ address: MIDAS_VAULT_ADMIN,
206
+ value: (0, viem.parseEther)("100")
207
+ });
208
+ const hash = await anvil.writeContract({
209
+ chain: anvil.chain,
210
+ address: midasRedemptionVault,
211
+ account: MIDAS_VAULT_ADMIN,
212
+ abi: iMidasRedemptionVaultExtAbi,
213
+ functionName: "safeApproveRequest",
214
+ args: [requestId, mTokenRate]
215
+ });
216
+ if ((await anvil.waitForTransactionReceipt({
217
+ hash,
218
+ pollingInterval: 100
219
+ })).status !== "success") throw new Error(`midas: safeApproveRequest tx ${hash} reverted`);
220
+ await anvil.stopImpersonatingAccount({ address: MIDAS_VAULT_ADMIN });
169
221
  }
170
222
  /**
171
- * Fulfills a Securitize redemption request:
172
- * funds the first redeemer of the credit account with stablecoins
223
+ * Fulfills all unclaimed Securitize redemption requests of a credit account
173
224
  */
174
225
  async function fulfillSecuritizeWithdrawal(anvil, creditAccount, withdrawalPhantomToken, logger) {
175
- const [redemptionGateway, stableCoinToken] = await anvil.multicall({
176
- allowFailure: false,
177
- contracts: [{
178
- address: withdrawalPhantomToken,
179
- abi: require_dev_withdrawalAbi.securitizeRedemptionPhantomTokenAbi,
180
- functionName: "redemptionGateway"
181
- }, {
182
- address: withdrawalPhantomToken,
183
- abi: require_dev_withdrawalAbi.securitizeRedemptionPhantomTokenAbi,
184
- functionName: "stableCoinToken"
185
- }]
226
+ const redemptionGateway = await anvil.readContract({
227
+ address: withdrawalPhantomToken,
228
+ abi: require_dev_withdrawalAbi.securitizeRedemptionPhantomTokenAbi,
229
+ functionName: "redemptionGateway"
186
230
  });
187
231
  const redeemers = await anvil.readContract({
188
232
  address: redemptionGateway,
@@ -190,36 +234,46 @@ async function fulfillSecuritizeWithdrawal(anvil, creditAccount, withdrawalPhant
190
234
  functionName: "getUnclaimedRedeemers",
191
235
  args: [creditAccount]
192
236
  });
193
- logger?.debug(`securitize: gateway ${redemptionGateway}, stablecoin ${stableCoinToken}, ${redeemers.length} unclaimed redeemers`);
237
+ logger?.debug(`securitize: gateway ${redemptionGateway}, ${redeemers.length} unclaimed redeemers`);
194
238
  if (redeemers.length === 0) {
195
239
  logger?.warn(`securitize: no unclaimed redeemers found for credit account ${creditAccount}`);
196
240
  return;
197
241
  }
198
- for (const redeemer of redeemers) {
199
- const [redemptionValue, redeemerBalance] = await anvil.multicall({
200
- allowFailure: false,
201
- contracts: [{
202
- address: redeemer,
203
- abi: require_dev_withdrawalAbi.securitizeRedeemerAbi,
204
- functionName: "getCurrentRedemptionValue"
205
- }, {
206
- address: stableCoinToken,
207
- abi: viem.erc20Abi,
208
- functionName: "balanceOf",
209
- args: [redeemer]
210
- }]
211
- });
212
- if (redemptionValue === 0n) {
213
- logger?.debug(`securitize: skipping redeemer ${redeemer} with zero redemption value`);
214
- continue;
215
- }
216
- logger?.debug(`securitize: dealing ${redemptionValue} of stablecoin ${stableCoinToken} to redeemer ${redeemer}`);
217
- await anvil.deal({
218
- erc20: stableCoinToken,
219
- account: redeemer,
220
- amount: redeemerBalance + redemptionValue
221
- });
242
+ for (const redeemer of redeemers) await fulfillSecuritizeRedeemer(anvil, redeemer, logger);
243
+ }
244
+ /**
245
+ * Fulfills a single Securitize redemption request:
246
+ * funds the redeemer with stablecoins
247
+ */
248
+ async function fulfillSecuritizeRedeemer(anvil, redeemer, logger) {
249
+ const [stableCoinToken, redemptionValue] = await anvil.multicall({
250
+ allowFailure: false,
251
+ contracts: [{
252
+ address: redeemer,
253
+ abi: require_dev_withdrawalAbi.securitizeRedeemerAbi,
254
+ functionName: "stableCoinToken"
255
+ }, {
256
+ address: redeemer,
257
+ abi: require_dev_withdrawalAbi.securitizeRedeemerAbi,
258
+ functionName: "getCurrentRedemptionValue"
259
+ }]
260
+ });
261
+ if (redemptionValue === 0n) {
262
+ logger?.debug(`securitize: skipping redeemer ${redeemer} with zero redemption value`);
263
+ return;
222
264
  }
265
+ const redeemerBalance = await anvil.readContract({
266
+ address: stableCoinToken,
267
+ abi: viem.erc20Abi,
268
+ functionName: "balanceOf",
269
+ args: [redeemer]
270
+ });
271
+ logger?.debug(`securitize: dealing ${redemptionValue} of stablecoin ${stableCoinToken} to redeemer ${redeemer}`);
272
+ await anvil.deal({
273
+ erc20: stableCoinToken,
274
+ account: redeemer,
275
+ amount: redeemerBalance + redemptionValue
276
+ });
223
277
  }
224
278
  //#endregion
225
279
  exports.makePendingWithdrawalsClaimable = makePendingWithdrawalsClaimable;
@@ -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 };
@@ -5,19 +5,23 @@ import { Address } from "viem";
5
5
  //#region src/dev/withdrawalUtils.d.ts
6
6
  interface MakePendingWithdrawalsClaimableOptions {
7
7
  logger?: ILogger;
8
- timeWarp?: boolean;
9
8
  }
10
9
  /**
11
- * Makes pending delayed withdrawals of a credit account claimable on an anvil fork.
10
+ * Makes pending delayed withdrawals claimable on an anvil fork.
11
+ *
12
+ * Accepts either a credit account, in which case all its pending withdrawals are
13
+ * fulfilled, or a single redeemer. The latter is the only way to reach redeemers
14
+ * that were transferred away from a credit account during a liquidation: they are
15
+ * dropped from the gateway's pending sets, so the compressor no longer reports them.
12
16
  *
13
17
  * Replicates the `_fulfillWithdrawal` logic from periphery-v3 Foundry tests
14
18
  * (WithdrawalCompressorMidasRWA.t.sol, WithdrawalCompressorSecuritize.t.sol)
15
19
  * using anvil cheatcodes. Assumes the version 313 of WithdrawalCompressor.
16
20
  *
17
21
  * @param anvil
18
- * @param creditAccount
19
- * @param logger
22
+ * @param address credit account or redeemer
23
+ * @param options
20
24
  */
21
- declare function makePendingWithdrawalsClaimable(anvil: AnvilClient, creditAccount: Address, options?: MakePendingWithdrawalsClaimableOptions): Promise<void>;
25
+ declare function makePendingWithdrawalsClaimable(anvil: AnvilClient, address: Address, options?: MakePendingWithdrawalsClaimableOptions): Promise<void>;
22
26
  //#endregion
23
27
  export { MakePendingWithdrawalsClaimableOptions, makePendingWithdrawalsClaimable };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gearbox-protocol/sdk",
3
- "version": "14.12.0-next.58",
3
+ "version": "14.12.0-next.59",
4
4
  "description": "Gearbox SDK",
5
5
  "license": "MIT",
6
6
  "repository": {