@raac/rpc 2.0.0-beta.21 → 2.0.0-beta.22

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.
@@ -3,41 +3,47 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.minSharesOut = exports.sameSplit = exports.splitTotal = exports.SPLIT_KEYS = void 0;
6
+ exports.minSharesOut = exports.sharesAtNav = exports.valueAtNav = exports.curveQuote = exports.DEFAULT_PLAN_PARAMS = exports.sameSplit = exports.splitTotal = exports.SPLIT_KEYS = void 0;
7
7
  exports.readLiquidationContext = readLiquidationContext;
8
8
  exports.readLiquidationTarget = readLiquidationTarget;
9
- exports.quoteLiquidation = quoteLiquidation;
10
- exports.suggestSplit = suggestSplit;
11
- exports.simulateWithSplit = simulateWithSplit;
9
+ exports.toleranceNeeded = toleranceNeeded;
10
+ exports.priceImpact = priceImpact;
11
+ exports.largestClearingFloor = largestClearingFloor;
12
+ exports.planLiquidation = planLiquidation;
12
13
  /**
13
- * Liquidation simulation for StabilityPool.liquidateBorrower, mirroring LiquidationStrategyProxy:
14
+ * Planning StabilityPool.liquidateBorrower, mirroring LiquidationStrategyProxy:
14
15
  * 1. the seized collateral is deposited into the RWAVault for vault-token shares, unless it already is the vault token;
15
16
  * 2. the stability pool's whole vault-token balance (the proxy runs under delegatecall) is split by BPS into
16
- * mint (treasury fee), burn, swap and liquidity (fee collector);
17
- * 3. only the swap slice becomes crvUSD, through LiquidationSwap and its Curve pool;
18
- * 3b. LiquidationSwap reverts when slippage protection is on and get_dy falls below pricePerShare / oracle price
19
- * less its tolerance, which is the case when Curve trades below the vault's value;
20
- * 4. the crvUSD must cover the position's debt; any shortfall is withdrawn from the LendingPool with the pool's
21
- * rTokens, so stability pool depositors pay it, and the call reverts (InsufficientBalance) if they hold too few.
17
+ * mint (treasury), burn, swap and liquidity (fee collector);
18
+ * 3. the swap slice is sold on Curve through LiquidationSwap, whose floor is the vault's NAV less a tolerance;
19
+ * 4. the reserve asset the pool then holds (swap proceeds plus whatever it held before) repays the debt; a shortfall
20
+ * is drawn from the pool's rTokens through the LendingPool, and the call reverts (InsufficientBalance) if too few.
21
+ *
22
+ * The plan sizes the swap on Curve's get_dy and values every other leg at NAV. Any gap between the swap and the debt
23
+ * is reserve asset the Treasury sends to the pool beforehand, and the unsold vault tokens go to the Treasury for it.
22
24
  */
23
25
  const ethers_1 = require("ethers");
24
26
  const contracts_1 = require("../../utils/contracts");
25
27
  const artifacts_1 = require("../../utils/artifacts");
26
28
  const getAdapters_1 = __importDefault(require("../rwaVault/getAdapters"));
27
29
  const getParameters_1 = __importDefault(require("../lendingPool/getParameters"));
30
+ const getState_1 = __importDefault(require("../../contracts/crvUSDToUSDOracle/getState"));
28
31
  const BPS = BigInt(10_000);
29
32
  const WAD = BigInt(10) ** BigInt(18);
30
33
  const ZERO = BigInt(0);
34
+ /** Curve quotes its fee on a 1e10 scale. */
35
+ const CURVE_FEE_PER_BPS = BigInt(10) ** BigInt(6);
36
+ const LIQUIDITY_FEE_TYPE = ethers_1.ethers.id("RWA_INDEX_TOKEN_LIQUIDITY_REWARDS");
31
37
  const SWAP_ABI = [
32
38
  "function liquidityPool() view returns (address)",
33
- "function swapToken() view returns (address)",
34
39
  "function iIn() view returns (uint256)",
35
40
  "function iOut() view returns (uint256)",
36
41
  "function enableSlippageProtection() view returns (bool)",
37
42
  "function slippageProtectionBps() view returns (uint256)",
38
43
  "function crvusdToUSDOracle() view returns (address)",
44
+ "function owner() view returns (address)",
39
45
  ];
40
- const CURVE_ABI = ["function get_dy(uint256 i, uint256 j, uint256 dx) view returns (uint256)", "function balances(uint256 i) view returns (uint256)"];
46
+ const CURVE_ABI = ["function get_dy(uint256 i, uint256 j, uint256 dx) view returns (uint256)", "function balances(uint256 i) view returns (uint256)", "function fee() view returns (uint256)"];
41
47
  const VAULT_ABI = ["function underlyingVaultToken() view returns (address)", "function previewDeposit(uint256 assetAmount) view returns (uint256)", "function pricePerShare() view returns (uint256)"];
42
48
  const VAULT_ADAPTER_ABI = ["function getAssetToken() view returns (address)", "function getAssetValue(bytes data) view returns (uint256)"];
43
49
  const ADAPTER_ABI = [
@@ -47,64 +53,103 @@ const ADAPTER_ABI = [
47
53
  "function balanceOf(address user) view returns (uint256)",
48
54
  "function getDepositedTokens(address user) view returns (uint256[])",
49
55
  ];
50
- const ERC20_ABI = ["function balanceOf(address) view returns (uint256)"];
56
+ const ERC20_ABI = ["function balanceOf(address) view returns (uint256)", "function totalSupply() view returns (uint256)", "function symbol() view returns (string)"];
57
+ const YIELD_VAULT_ABI = ["function maxWithdraw(address owner, uint256 maxLoss, address[] strategies) view returns (uint256)"];
58
+ const TREASURY_ABI = ["function isTokenSupported(address) view returns (bool)"];
51
59
  exports.SPLIT_KEYS = ["mintBps", "burnBps", "swapBps", "liquidityBps"];
52
60
  const splitTotal = (s) => s.mintBps + s.burnBps + s.swapBps + s.liquidityBps;
53
61
  exports.splitTotal = splitTotal;
54
62
  const sameSplit = (a, b) => exports.SPLIT_KEYS.every((k) => a[k] === b[k]);
55
63
  exports.sameSplit = sameSplit;
64
+ exports.DEFAULT_PLAN_PARAMS = { burnBps: BigInt(250), treasuryBps: BigInt(250), liquidityCapBps: BigInt(1500), impactCapBps: BigInt(100), toleranceCeilingBps: BigInt(500), toleranceCushionBps: BigInt(10), marginBps: BigInt(100) };
56
65
  const contract = (address, abi, provider) => new ethers_1.ethers.Contract(address, abi, provider);
57
66
  const decodeAmount = (data) => ethers_1.ethers.AbiCoder.defaultAbiCoder().decode(["uint256"], data)[0];
58
- /** Reads the stability pool's liquidation route, split, balances and the LendingPool grace period. */
67
+ const min = (...values) => values.reduce((a, b) => (a < b ? a : b));
68
+ const ceilDiv = (a, b) => (a + b - BigInt(1)) / b;
69
+ /** Reads the stability pool's liquidation route, balances, Curve, NAV, floor and leg prerequisites at one block. */
59
70
  async function readLiquidationContext(chainId, provider) {
60
71
  const stabilityPool = (0, contracts_1.getContractAddress)(chainId, "stabilitypool");
72
+ const lendingPool = (0, contracts_1.getContractAddress)(chainId, "lendingpool");
61
73
  const sp = contract(stabilityPool, (0, artifacts_1.getABI)("stabilitypool"), provider);
62
- const [liquidationSwap, rwaVault, split, rToken, params] = await Promise.all([
74
+ const [liquidationSwap, rwaVault, split, rToken, deToken, reserveAsset, treasury, feeCollector, params, yieldVault, reserve] = await Promise.all([
63
75
  sp.liquidationSwap(),
64
76
  sp.rwaVault(),
65
77
  sp.liquidationSplit(),
66
78
  sp.rToken(),
79
+ sp.deToken(),
80
+ sp.crvUSDToken(),
81
+ sp.treasury(),
82
+ sp.feeCollector(),
67
83
  (0, getParameters_1.default)(chainId, provider),
84
+ contract(lendingPool, (0, artifacts_1.getABI)("lendingpool"), provider).vault(),
85
+ contract(lendingPool, (0, artifacts_1.getABI)("lendingpool"), provider).reserve(),
68
86
  ]);
69
87
  if (liquidationSwap === ethers_1.ethers.ZeroAddress)
70
88
  throw new Error("The stability pool has no LiquidationSwap set, so the swap leg reverts.");
71
89
  const swap = contract(liquidationSwap, SWAP_ABI, provider);
72
90
  const vault = contract(rwaVault, VAULT_ABI, provider);
73
- const [liquidityPool, iIn, iOut, vaultToken, protect] = await Promise.all([
91
+ const [liquidityPool, iIn, iOut, vaultToken, enabled, bps, owner, oracle, pricePerShare] = await Promise.all([
74
92
  swap.liquidityPool(),
75
93
  swap.iIn(),
76
94
  swap.iOut(),
77
95
  vault.underlyingVaultToken(),
78
96
  swap.enableSlippageProtection(),
97
+ swap.slippageProtectionBps(),
98
+ swap.owner(),
99
+ swap.crvusdToUSDOracle(),
100
+ vault.pricePerShare(),
79
101
  ]);
80
- const slippage = protect
81
- ? await Promise.all([
82
- swap.slippageProtectionBps(),
83
- vault.pricePerShare(),
84
- swap.crvusdToUSDOracle().then((o) => contract(o, ["function getPrice() view returns (uint256)"], provider).getPrice()),
85
- ]).then(([bps, pricePerShare, oraclePrice]) => ({ bps, pricePerShare, oraclePrice }))
86
- : null;
87
102
  if (liquidityPool === ethers_1.ethers.ZeroAddress)
88
103
  throw new Error("LiquidationSwap has no Curve pool set, so the swap leg reverts.");
89
- const [vaultSymbol, poolVaultTokens, rTokens] = await Promise.all([
90
- contract(vaultToken, ["function symbol() view returns (string)"], provider).symbol().catch(() => "vault token"),
91
- contract(vaultToken, ERC20_ABI, provider).balanceOf(stabilityPool),
92
- contract(rToken, ERC20_ABI, provider).balanceOf(stabilityPool),
104
+ const curve = contract(liquidityPool, CURVE_ABI, provider);
105
+ const erc20 = (token) => contract(token, ERC20_ABI, provider);
106
+ const fc = feeCollector === ethers_1.ethers.ZeroAddress ? null : contract(feeCollector, (0, artifacts_1.getABI)("feecollector"), provider);
107
+ const [vaultSymbol, poolVaultTokens, vaultTokenSupply, rTokens, poolReserve, deTokenSupply, buffer, vaultWithdrawable, spot, depth, curveFee, oraclePrice, oracleState, fcPaused, feeType, treasurySupportsVaultToken] = await Promise.all([
108
+ erc20(vaultToken).symbol().catch(() => "vault token"),
109
+ erc20(vaultToken).balanceOf(stabilityPool),
110
+ erc20(vaultToken).totalSupply(),
111
+ erc20(rToken).balanceOf(stabilityPool),
112
+ erc20(reserveAsset).balanceOf(stabilityPool),
113
+ erc20(deToken).totalSupply(),
114
+ erc20(reserveAsset).balanceOf(rToken),
115
+ yieldVault === ethers_1.ethers.ZeroAddress ? ZERO : contract(yieldVault, YIELD_VAULT_ABI, provider).maxWithdraw(lendingPool, 0, []).catch(() => null),
116
+ curve.get_dy(iIn, iOut, WAD),
117
+ curve.balances(iOut),
118
+ curve.fee(),
119
+ contract(oracle, ["function getPrice() view returns (uint256)"], provider).getPrice(),
120
+ (0, getState_1.default)(chainId, provider, oracle).catch(() => null),
121
+ fc ? fc.paused() : false,
122
+ fc ? fc.getFeeType(LIQUIDITY_FEE_TYPE) : null,
123
+ treasury === ethers_1.ethers.ZeroAddress ? false : contract(treasury, TREASURY_ABI, provider).isTokenSupported(vaultToken),
93
124
  ]);
94
125
  return {
95
126
  stabilityPool,
127
+ lendingPool,
96
128
  liquidationSwap,
97
129
  liquidityPool,
98
130
  rwaVault,
131
+ treasury,
132
+ feeCollector,
99
133
  vaultToken,
100
134
  vaultSymbol,
135
+ reserveAsset,
136
+ rToken,
101
137
  iIn,
102
138
  iOut,
103
139
  split: { mintBps: split[0], burnBps: split[1], swapBps: split[2], liquidityBps: split[3] },
104
140
  poolVaultTokens,
141
+ poolReserve,
105
142
  rTokens,
143
+ deTokenSupply,
144
+ idleCash: { buffer, vaultWithdrawable },
145
+ lendingPoolLiquidity: { total: reserve.totalLiquidity, bufferRatioBps: params.liquidityBufferRatio, minRebalance: params.minVaultRebalanceAmount.value },
146
+ curve: { spot, depth, feeBps: curveFee / CURVE_FEE_PER_BPS },
147
+ nav: { pricePerShare, oraclePrice, priceTimestamp: oracleState?.priceTimestamp ?? null },
148
+ vaultTokenSupply,
149
+ floor: { enabled, bps, owner },
150
+ feeCollectorState: { paused: fcPaused, liquidityFeeTypeRegistered: feeType !== null && feeType.feeType !== ethers_1.ethers.ZeroHash },
151
+ treasurySupportsVaultToken,
106
152
  gracePeriod: Number(params.liquidationGracePeriod),
107
- slippage,
108
153
  };
109
154
  }
110
155
  /**
@@ -147,13 +192,18 @@ async function findVaultAdapter(chainId, provider, assetToken, vaultToken) {
147
192
  async function readLiquidationTarget(chainId, provider, ctx, positionId) {
148
193
  const [user, poolAdapter, positionKey] = positionId.split(":");
149
194
  const { data, assetType, assetToken } = await readLiquidationData(provider, poolAdapter, user, positionKey);
150
- const pool = contract((0, contracts_1.getContractAddress)(chainId, "lendingpool"), (0, artifacts_1.getABI)("lendingpool"), provider);
195
+ const pool = contract(ctx.lendingPool, (0, artifacts_1.getABI)("lendingpool"), provider);
196
+ const isVaultToken = assetToken.toLowerCase() === ctx.vaultToken.toLowerCase();
151
197
  const [vaultAdapter, collateralValue, debt, position] = await Promise.all([
152
198
  findVaultAdapter(chainId, provider, assetToken, ctx.vaultToken),
153
199
  contract(poolAdapter, ADAPTER_ABI, provider).getAssetValue(user, data),
154
200
  pool.getPositionScaledDebt(poolAdapter, user, data),
155
201
  pool.getPosition(poolAdapter, user, data),
156
202
  ]);
203
+ // poolDepositAsset mints from the vault adapter's own valuation, which may use a different oracle than the pool's.
204
+ const sharesFromCollateral = isVaultToken
205
+ ? decodeAmount(data)
206
+ : await contract(ctx.rwaVault, VAULT_ABI, provider).previewDeposit(await contract(vaultAdapter, VAULT_ADAPTER_ABI, provider).getAssetValue(data));
157
207
  return {
158
208
  poolAdapter: ethers_1.ethers.getAddress(poolAdapter),
159
209
  vaultAdapter,
@@ -161,137 +211,160 @@ async function readLiquidationTarget(chainId, provider, ctx, positionId) {
161
211
  data,
162
212
  assetType,
163
213
  assetToken,
164
- isVaultToken: assetToken.toLowerCase() === ctx.vaultToken.toLowerCase(),
214
+ isVaultToken,
165
215
  collateralValue,
216
+ sharesFromCollateral,
166
217
  debt,
167
218
  isUnderLiquidation: position.isUnderLiquidation,
168
219
  isInsured: position.isInsured,
169
220
  liquidationStartTime: Number(position.liquidationStartTime),
170
221
  };
171
222
  }
172
- async function quoteDy(provider, ctx, amount) {
173
- if (amount <= ZERO)
174
- return ZERO;
175
- return contract(ctx.liquidityPool, CURVE_ABI, provider).get_dy(ctx.iIn, ctx.iOut, amount).catch(() => null);
223
+ /** get_dy on the context's Curve pool. */
224
+ const curveQuote = (provider, ctx) => (amount) => amount <= ZERO ? Promise.resolve(ZERO) : contract(ctx.liquidityPool, CURVE_ABI, provider).get_dy(ctx.iIn, ctx.iOut, amount).catch(() => null);
225
+ exports.curveQuote = curveQuote;
226
+ /** Reserve asset that `shares` vault tokens are worth at NAV, as LiquidationSwap computes its expected output. */
227
+ const valueAtNav = (ctx, shares) => (ctx.nav.pricePerShare * shares) / ctx.nav.oraclePrice;
228
+ exports.valueAtNav = valueAtNav;
229
+ /** Vault tokens worth `value` of reserve asset at NAV, rounded up. */
230
+ const sharesAtNav = (ctx, value) => ceilDiv(value * ctx.nav.oraclePrice, ctx.nav.pricePerShare);
231
+ exports.sharesAtNav = sharesAtNav;
232
+ /**
233
+ * The floor tolerance a swap of `amount` returning `out` needs: how far `out` falls below NAV, in bps, rounded up.
234
+ * The proxy burns the burn leg before it swaps, which lifts the vault's price per share by the burned share of the
235
+ * supply, so the floor LiquidationSwap applies is computed on that higher NAV; `burned` accounts for it.
236
+ */
237
+ function toleranceNeeded(ctx, amount, out, burned = ZERO) {
238
+ const supplyAfter = ctx.vaultTokenSupply - burned;
239
+ const expected = supplyAfter > ZERO && burned > ZERO ? ((0, exports.valueAtNav)(ctx, amount) * ctx.vaultTokenSupply) / supplyAfter : (0, exports.valueAtNav)(ctx, amount);
240
+ return expected <= out || expected === ZERO ? ZERO : ceilDiv((expected - out) * BPS, expected);
176
241
  }
177
- /** Prices the liquidation with `split`, using the deployed Curve pool's get_dy rather than the vault's oracle price. */
178
- async function quoteLiquidation(provider, ctx, target, split) {
179
- // poolDepositAsset mints from the vault adapter's own valuation, which may use a different oracle than the pool's.
180
- const sharesFromCollateral = target.isVaultToken
181
- ? decodeAmount(target.data)
182
- : await contract(ctx.rwaVault, VAULT_ABI, provider).previewDeposit(await contract(target.vaultAdapter, VAULT_ADAPTER_ABI, provider).getAssetValue(target.data));
183
- const totalVaultTokens = sharesFromCollateral + ctx.poolVaultTokens;
184
- const swapAmount = (totalVaultTokens * split.swapBps) / BPS;
185
- const [swapOut, maxSwapOut, spotOut, poolReserveOut] = await Promise.all([
186
- quoteDy(provider, ctx, swapAmount),
187
- quoteDy(provider, ctx, totalVaultTokens),
188
- quoteDy(provider, ctx, WAD),
189
- contract(ctx.liquidityPool, CURVE_ABI, provider).balances(ctx.iOut).catch(() => null),
190
- ]);
191
- let priceImpactBps = null;
192
- if (swapOut !== null && spotOut && swapAmount > ZERO) {
193
- const realised = (swapOut * WAD) / swapAmount;
194
- priceImpactBps = realised >= spotOut ? ZERO : ((spotOut - realised) * BPS) / spotOut;
195
- }
196
- return {
197
- split,
198
- sharesFromCollateral,
199
- totalVaultTokens,
200
- swapAmount,
201
- swapOut,
202
- minSwapOut: ctx.slippage && swapAmount > ZERO ? ((ctx.slippage.pricePerShare * swapAmount) / ctx.slippage.oraclePrice) * (BPS - ctx.slippage.bps) / BPS : null,
203
- shortfall: swapOut === null ? target.debt : swapOut >= target.debt ? ZERO : target.debt - swapOut,
204
- coversDebt: swapOut !== null && swapOut >= target.debt,
205
- badDebt: target.collateralValue < target.debt ? target.debt - target.collateralValue : ZERO,
206
- maxSwapOut,
207
- priceImpactBps,
208
- poolReserveOut,
209
- };
242
+ /** How far the realised rate of a swap falls below Curve's spot rate, in bps. */
243
+ function priceImpact(ctx, amount, out) {
244
+ if (amount === ZERO || ctx.curve.spot === ZERO)
245
+ return ZERO;
246
+ const realised = (out * WAD) / amount;
247
+ return realised >= ctx.curve.spot ? ZERO : ((ctx.curve.spot - realised) * BPS) / ctx.curve.spot;
210
248
  }
249
+ const shareOf = (total, bps) => (total * bps) / BPS;
211
250
  /**
212
- * The smallest swap share whose get_dy still covers the debt plus `bufferBps`, with the rest given back to mint,
213
- * burn and liquidity in their current proportion. get_dy grows with dx, so a binary search over BPS is sound.
251
+ * Binary search over the swap share in bps of `total`, which is exact because the proxy computes the swap amount the
252
+ * same way. `holds` must be monotone in the share: true up to some share for "largest", true from some share on for
253
+ * "smallest". A reverted quote counts as not holding. Returns the share, or null when no share holds.
214
254
  */
215
- async function suggestSplit(provider, ctx, target, quote, bufferBps = BigInt(100)) {
216
- if (quote.totalVaultTokens <= ZERO)
217
- return null;
218
- const required = (target.debt * (BPS + bufferBps)) / BPS;
219
- const spread = (swapBps, projectedOut, bestEffort) => {
220
- const rest = BPS - swapBps;
221
- const others = ctx.split.mintBps + ctx.split.burnBps + ctx.split.liquidityBps;
222
- const mintBps = others === ZERO ? ZERO : (rest * ctx.split.mintBps) / others;
223
- const burnBps = others === ZERO ? ZERO : (rest * ctx.split.burnBps) / others;
224
- return { mintBps, burnBps, swapBps, liquidityBps: rest - mintBps - burnBps, projectedOut, bestEffort };
255
+ async function searchShare(total, quote, holds, want) {
256
+ const test = async (bps) => {
257
+ const amount = shareOf(total, bps);
258
+ const out = await quote(amount);
259
+ return out !== null && holds(amount, out);
225
260
  };
226
- const full = quote.maxSwapOut ?? (await quoteDy(provider, ctx, quote.totalVaultTokens));
227
- if (full === null)
261
+ if (want === "largest") {
262
+ if (!(await test(ZERO)))
263
+ return null;
264
+ let low = ZERO;
265
+ let high = BPS;
266
+ while (low < high) {
267
+ const mid = (low + high + BigInt(1)) / BigInt(2);
268
+ if (await test(mid))
269
+ low = mid;
270
+ else
271
+ high = mid - BigInt(1);
272
+ }
273
+ return low;
274
+ }
275
+ if (!(await test(BPS)))
228
276
  return null;
229
- if (full < required)
230
- return spread(BPS, full, true);
231
277
  let low = ZERO;
232
278
  let high = BPS;
233
- let best = full;
234
279
  while (low < high) {
235
280
  const mid = (low + high) / BigInt(2);
236
- const out = await quoteDy(provider, ctx, (quote.totalVaultTokens * mid) / BPS);
237
- if (out !== null && out >= required) {
281
+ if (await test(mid))
238
282
  high = mid;
239
- best = out;
240
- }
241
- else {
283
+ else
242
284
  low = mid + BigInt(1);
243
- }
244
285
  }
245
- return spread(low, best, false);
286
+ return low;
287
+ }
288
+ /** The largest swap share (bps) whose output clears the floor at `toleranceBps` once `burned` vault tokens are gone; the whole balance when the floor is off. */
289
+ async function largestClearingFloor(ctx, quote, total, toleranceBps, burned = ZERO) {
290
+ if (!ctx.floor.enabled)
291
+ return BPS;
292
+ return (await searchShare(total, quote, (amount, out) => toleranceNeeded(ctx, amount, out, burned) <= toleranceBps, "largest")) ?? ZERO;
246
293
  }
247
- /** minSharesOut for the RWAVault deposit: previewDeposit less `slippageBps`. Zero when nothing is deposited. */
248
- const minSharesOut = (target, quote, slippageBps) => (target.isVaultToken ? ZERO : (quote.sharesFromCollateral * (BPS - slippageBps)) / BPS);
249
- exports.minSharesOut = minSharesOut;
250
- // StabilityPoolStorage is inherited first and the OpenZeppelin parents use namespaced storage, so liquidationSplit
251
- // takes slots 10 to 13. Checked against the live split before it is relied on.
252
- const SPLIT_SLOT = 10;
253
294
  /**
254
- * Simulates liquidateBorrower from `sender` with the split overridden in storage (an eth_call state override), so a
255
- * split change in the same batch is simulated rather than only checked. Returns null when the RPC or the storage
256
- * layout cannot be used; otherwise the revert reason, or "" when it succeeds.
295
+ * Sizes the swap, derives the split and says what the Treasury must send. `quote` is Curve's get_dy, so a test can
296
+ * pass a synthetic curve. In "otc" mode nothing is swapped and the Treasury funds the whole uncovered debt.
257
297
  */
258
- async function simulateWithSplit(provider, ctx, sender, data, split) {
259
- const send = provider.send?.bind(provider);
260
- if (!send)
261
- return null;
262
- const slot = (i) => ethers_1.ethers.toBeHex(SPLIT_SLOT + i, 32);
263
- try {
264
- const live = await Promise.all(exports.SPLIT_KEYS.map((_, i) => provider.getStorage(ctx.stabilityPool, SPLIT_SLOT + i)));
265
- if (exports.SPLIT_KEYS.some((k, i) => BigInt(live[i]) !== ctx.split[k]))
266
- return null;
267
- }
268
- catch {
269
- return null;
270
- }
271
- const stateDiff = Object.fromEntries(exports.SPLIT_KEYS.map((k, i) => [slot(i), ethers_1.ethers.toBeHex(split[k], 32)]));
272
- try {
273
- await send("eth_call", [{ from: sender, to: ctx.stabilityPool, data }, "latest", { [ctx.stabilityPool]: { stateDiff } }]);
274
- return "";
275
- }
276
- catch (error) {
277
- if (!ethers_1.ethers.isError(error, "CALL_EXCEPTION"))
278
- return null;
279
- return (error.data && attemptParse(error.data)) || error.reason || error.shortMessage;
280
- }
281
- }
282
- function attemptParse(data) {
283
- for (const key of ["stabilitypool", "lendingpool"]) {
284
- try {
285
- const e = new ethers_1.ethers.Interface((0, artifacts_1.getABI)(key)).parseError(data);
286
- if (e)
287
- return e.name === "Error" ? String(e.args[0]) : e.name;
288
- }
289
- catch { }
290
- }
291
- try {
292
- return ethers_1.ethers.AbiCoder.defaultAbiCoder().decode(["string"], ethers_1.ethers.dataSlice(data, 4))[0];
298
+ async function planLiquidation(ctx, target, quote, params = exports.DEFAULT_PLAN_PARAMS, mode = "defi") {
299
+ const total = target.sharesFromCollateral + ctx.poolVaultTokens;
300
+ const required = (target.debt * (BPS + params.marginBps)) / BPS;
301
+ const toCover = required > ctx.poolReserve ? required - ctx.poolReserve : ZERO;
302
+ const floorCap = ctx.floor.enabled ? (params.toleranceCeilingBps > params.toleranceCushionBps ? params.toleranceCeilingBps - params.toleranceCushionBps : ZERO) : BPS;
303
+ // The burn runs before the swap; the policy burn is close enough to size the floor searches on.
304
+ const burnEstimate = shareOf(total, min(params.burnBps, BPS));
305
+ const [underImpact, atCurrent, atCeiling, needed] = total === ZERO ? [ZERO, ZERO, ZERO, ZERO] : await Promise.all([
306
+ searchShare(total, quote, (amount, out) => priceImpact(ctx, amount, out) <= params.impactCapBps, "largest").then((s) => s ?? ZERO),
307
+ largestClearingFloor(ctx, quote, total, ctx.floor.bps, burnEstimate),
308
+ largestClearingFloor(ctx, quote, total, floorCap, burnEstimate),
309
+ toCover === ZERO ? ZERO : searchShare(total, quote, (_, out) => out >= toCover, "smallest"),
310
+ ]);
311
+ let swapBps;
312
+ let bound;
313
+ if (mode === "otc") {
314
+ swapBps = ZERO;
315
+ bound = "mode";
293
316
  }
294
- catch {
295
- return null;
317
+ else {
318
+ const wanted = needed ?? BPS;
319
+ swapBps = min(wanted, underImpact, atCeiling);
320
+ bound = swapBps === wanted ? (needed === null ? "balance" : "debt") : swapBps === underImpact ? "impact" : "floor";
296
321
  }
322
+ const swapAmount = shareOf(total, swapBps);
323
+ const swapOut = swapAmount === ZERO ? ZERO : ((await quote(swapAmount)) ?? ZERO);
324
+ const gap = toCover > swapOut ? toCover - swapOut : ZERO;
325
+ const liquidityDisabled = ctx.feeCollectorState.paused || !ctx.feeCollectorState.liquidityFeeTypeRegistered;
326
+ const burnBps = min(params.burnBps, BPS - swapBps);
327
+ const treasuryBaseBps = min(params.treasuryBps, BPS - swapBps - burnBps);
328
+ const remainderBps = BPS - swapBps - burnBps - treasuryBaseBps;
329
+ const gapBps = gap === ZERO || total === ZERO ? ZERO : ceilDiv((0, exports.sharesAtNav)(ctx, gap) * BPS, total);
330
+ const treasuryGapBps = min(gapBps, remainderBps);
331
+ const leftoverBps = remainderBps - treasuryGapBps;
332
+ const liquidityBps = liquidityDisabled ? ZERO : gap > ZERO ? min(leftoverBps, params.liquidityCapBps) : leftoverBps;
333
+ const split = { mintBps: treasuryBaseBps + treasuryGapBps + leftoverBps - liquidityBps, burnBps, swapBps, liquidityBps };
334
+ const legs = { swap: swapAmount, burn: shareOf(total, split.burnBps), treasury: shareOf(total, split.mintBps), liquidity: shareOf(total, split.liquidityBps) };
335
+ const needsBps = toleranceNeeded(ctx, swapAmount, swapOut, legs.burn);
336
+ const raise = ctx.floor.enabled && swapAmount > ZERO && needsBps > ctx.floor.bps;
337
+ const treasuryValue = (0, exports.valueAtNav)(ctx, legs.treasury);
338
+ const idle = ctx.idleCash.vaultWithdrawable === null ? null : ctx.idleCash.buffer + ctx.idleCash.vaultWithdrawable;
339
+ const wanted = ceilDiv((ctx.lendingPoolLiquidity.total + target.debt) * ctx.lendingPoolLiquidity.bufferRatioBps, BPS);
340
+ const shortage = wanted > ctx.idleCash.buffer ? wanted - ctx.idleCash.buffer : ZERO;
341
+ const rebalances = shortage >= ctx.lendingPoolLiquidity.minRebalance && shortage > ZERO;
342
+ const rebalanceFeasible = !rebalances ? true : ctx.idleCash.vaultWithdrawable === null ? null : shortage <= ctx.idleCash.vaultWithdrawable;
343
+ return {
344
+ mode,
345
+ params,
346
+ totalVaultTokens: total,
347
+ required,
348
+ toCover,
349
+ swapAmount,
350
+ swapOut,
351
+ priceImpactBps: priceImpact(ctx, swapAmount, swapOut),
352
+ toleranceNeededBps: needsBps,
353
+ proposedToleranceBps: raise ? min(needsBps + params.toleranceCushionBps, params.toleranceCeilingBps > needsBps ? params.toleranceCeilingBps : needsBps) : null,
354
+ bound,
355
+ ceilings: { atCurrent: shareOf(total, atCurrent), atCeiling: shareOf(total, atCeiling), underImpact: shareOf(total, underImpact) },
356
+ split,
357
+ legs,
358
+ gap,
359
+ treasuryValue,
360
+ treasuryNet: treasuryValue - gap,
361
+ liquidityCapped: !liquidityDisabled && gap > ZERO && leftoverBps > params.liquidityCapBps,
362
+ liquidityDisabled,
363
+ route: gap === ZERO ? "covered" : swapAmount > ZERO ? "partial" : "otc",
364
+ absorb: { feasible: idle === null ? null : ctx.rTokens >= gap && idle >= gap, rTokensAfter: ctx.rTokens - gap, inject: idle !== null && idle < gap ? gap - idle : ZERO },
365
+ rebalance: { shortage, feasible: rebalanceFeasible, inject: rebalanceFeasible === false ? shortage : ZERO },
366
+ };
297
367
  }
368
+ /** minSharesOut for the RWAVault deposit: previewDeposit less `slippageBps`. Zero when nothing is deposited. */
369
+ const minSharesOut = (target, slippageBps) => (target.isVaultToken ? ZERO : (target.sharesFromCollateral * (BPS - slippageBps)) / BPS);
370
+ exports.minSharesOut = minSharesOut;
@@ -140,7 +140,7 @@ import getRoles from "./contracts/getRoles";
140
140
  import getTreasuryState from "./contracts/treasury/getState";
141
141
  import getComplianceStatuses from "./contracts/complianceRegistry/getStatuses";
142
142
  import getStabilityPoolAdminState from "./pools/stabilityPool/getAdminState";
143
- import { readLiquidationContext, readLiquidationTarget, quoteLiquidation, suggestSplit, simulateWithSplit, minSharesOut } from "./pools/stabilityPool/liquidation";
143
+ import { readLiquidationContext, readLiquidationTarget, planLiquidation, curveQuote, largestClearingFloor, toleranceNeeded, priceImpact, valueAtNav, sharesAtNav, minSharesOut, DEFAULT_PLAN_PARAMS } from "./pools/stabilityPool/liquidation";
144
144
  import { getEmissionSchedule, getSchedulePosition, getEmissionPerSecondForWeek, getMintingWeekForTimestamp } from "./minter/emissionSchedule";
145
145
  import getRWAVaultRegisteredAdapters from "./pools/rwaVault/getRegisteredAdapters";
146
146
  import getRWAVaultSeedStatus from "./pools/rwaVault/getSeedStatus";
@@ -707,10 +707,15 @@ declare class RPCLibrary {
707
707
  liquidation: {
708
708
  getContext: typeof readLiquidationContext;
709
709
  getTarget: typeof readLiquidationTarget;
710
- quote: typeof quoteLiquidation;
711
- suggestSplit: typeof suggestSplit;
712
- simulateWithSplit: typeof simulateWithSplit;
710
+ plan: typeof planLiquidation;
711
+ curveQuote: typeof curveQuote;
712
+ largestClearingFloor: typeof largestClearingFloor;
713
+ toleranceNeeded: typeof toleranceNeeded;
714
+ priceImpact: typeof priceImpact;
715
+ valueAtNav: typeof valueAtNav;
716
+ sharesAtNav: typeof sharesAtNav;
713
717
  minSharesOut: typeof minSharesOut;
718
+ DEFAULT_PLAN_PARAMS: typeof DEFAULT_PLAN_PARAMS;
714
719
  };
715
720
  };
716
721
  lendingPool: {
@@ -7,8 +7,6 @@ export interface UserGaugeTokenState {
7
7
  symbol?: string;
8
8
  /** From the token registry, or read from the token when the registry does not list it. */
9
9
  decimals: number;
10
- /** Wallet balance, in this token's base units. */
11
- balance: TokenAmount;
12
10
  /** Allowance to `spender`, in this token's base units. */
13
11
  allowance: TokenAmount;
14
12
  /** The contract that pulls the token: the gauge for the staking token, the wrapper for the underlying. */
@@ -23,8 +21,9 @@ export interface UserGaugeState {
23
21
  underlying: UserGaugeTokenState | null;
24
22
  }
25
23
  /**
26
- * What a wallet needs to stake in, wrap for, or unstake from a gauge: its stake, its balance of the staking token and its
27
- * allowance to the gauge, and for a wrapped gauge its balance of the underlying and allowance to the wrapper.
24
+ * What a wallet needs to stake in, wrap for, or unstake from a gauge: its stake, its allowance of the staking token to the
25
+ * gauge, and for a wrapped gauge its allowance of the underlying to the wrapper. Read the wallet's token balances with
26
+ * `wallet.assets.getBalance`.
28
27
  *
29
28
  * @param chainId - The chain/network to use.
30
29
  * @param gaugeAddressOrId - Address of the gauge contract or its config ID.
@@ -1,8 +1,8 @@
1
1
  import { Provider } from "ethers";
2
2
  import { ChainId } from "../../configs/chains";
3
3
  import { TokenAmount, ScaledValue } from "../../utils/amount";
4
+ /** A wallet's open redeem request and allowances to the vault. Token balances, shares included, come from `wallet.assets.getBalance`. */
4
5
  export interface PsmVaultUserInfo {
5
- shareBalance: TokenAmount;
6
6
  redeemRequest: {
7
7
  shares: TokenAmount;
8
8
  exitTime: number;