@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
|
|
118
|
+
const mintHash = await issueDSTokens({
|
|
119
|
+
anvil,
|
|
40
120
|
account,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
28
|
-
* @param
|
|
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
|
|
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 = (
|
|
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},
|
|
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
|
|
77
|
-
*
|
|
78
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
128
|
+
const redeemers = await anvil.readContract({
|
|
94
129
|
address: gateway,
|
|
95
130
|
abi: require_dev_withdrawalAbi.midasGatewayAbi,
|
|
96
|
-
functionName: "
|
|
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
|
|
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:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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},
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
117
|
+
const mintHash = await issueDSTokens({
|
|
118
|
+
anvil,
|
|
39
119
|
account,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
27
|
-
* @param
|
|
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
|
|
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 =
|
|
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},
|
|
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
|
|
76
|
-
*
|
|
77
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
127
|
+
const redeemers = await anvil.readContract({
|
|
93
128
|
address: gateway,
|
|
94
129
|
abi: midasGatewayAbi,
|
|
95
|
-
functionName: "
|
|
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
|
|
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:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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},
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
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
|
|
19
|
-
* @param
|
|
22
|
+
* @param address credit account or redeemer
|
|
23
|
+
* @param options
|
|
20
24
|
*/
|
|
21
|
-
declare function makePendingWithdrawalsClaimable(anvil: AnvilClient,
|
|
25
|
+
declare function makePendingWithdrawalsClaimable(anvil: AnvilClient, address: Address, options?: MakePendingWithdrawalsClaimableOptions): Promise<void>;
|
|
22
26
|
//#endregion
|
|
23
27
|
export { MakePendingWithdrawalsClaimableOptions, makePendingWithdrawalsClaimable };
|