@raac/rpc 2.0.0-beta.20 → 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.
- package/dist/RPCLibrary.js +10 -3
- package/dist/artifacts/core/pools/StabilityPool/LiquidationSwap.sol/LiquidationSwap.json +472 -0
- package/dist/contracts/governance/gauge/getUserGaugeState.js +11 -11
- package/dist/contracts/psm/getVaultInfo.js +1 -7
- package/dist/contracts/resolveTokenInfo.js +65 -0
- package/dist/contracts/yearn/vault/getUserInfo.js +4 -6
- package/dist/contracts/zap/aladdin/inspectCurvePool.js +3 -4
- package/dist/pools/rwaVault/getRegisteredAdapters.js +11 -6
- package/dist/pools/stabilityPool/liquidation.js +211 -138
- package/dist/prices/getCurveLPPriceUSD.js +7 -7
- package/dist/types/RPCLibrary.d.ts +11 -4
- package/dist/types/contracts/governance/gauge/getUserGaugeState.d.ts +6 -7
- package/dist/types/contracts/psm/getVaultInfo.d.ts +1 -3
- package/dist/types/contracts/resolveTokenInfo.d.ts +18 -0
- package/dist/types/contracts/yearn/vault/getUserInfo.d.ts +3 -4
- package/dist/types/pools/stabilityPool/liquidation.d.ts +166 -52
- package/dist/types/utils/artifacts.d.ts +113 -0
- package/dist/types/wallet/assets/getBalance.d.ts +5 -4
- package/dist/utils/artifacts.js +2 -0
- package/dist/wallet/assets/getBalance.js +19 -8
- package/package.json +2 -1
|
@@ -6,17 +6,17 @@ const amount_1 = require("../../../utils/amount");
|
|
|
6
6
|
const rpcError_1 = require("../../../utils/rpcError");
|
|
7
7
|
const _helpers_1 = require("./_helpers");
|
|
8
8
|
const ERC20_READ_ABI = [
|
|
9
|
-
"function balanceOf(address) view returns (uint256)",
|
|
10
9
|
"function allowance(address owner, address spender) view returns (uint256)",
|
|
11
10
|
];
|
|
12
11
|
/**
|
|
13
|
-
* Gets a user's position in a Yearn V3 vault, plus their vault asset balance
|
|
12
|
+
* Gets a user's position in a Yearn V3 vault, plus their vault asset allowance. Read the wallet's asset balance with
|
|
13
|
+
* `wallet.assets.getBalance`.
|
|
14
14
|
*
|
|
15
15
|
* @param chainId - The chain/network to use.
|
|
16
16
|
* @param vaultAddressOrId - The vault's key in the chain config's `yearn` section (e.g. "spmusd"), or its address.
|
|
17
17
|
* @param account - Address of the user.
|
|
18
18
|
* @param provider - Optional ethers.js Provider instance.
|
|
19
|
-
* @returns The user's shares, their asset value,
|
|
19
|
+
* @returns The user's shares, their asset value, allowance and limits, in base units.
|
|
20
20
|
*/
|
|
21
21
|
async function getUserInfo(chainId, vaultAddressOrId, account, provider) {
|
|
22
22
|
const { entry, contract } = (0, _helpers_1.getYearnVaultContract)(chainId, vaultAddressOrId, provider);
|
|
@@ -24,9 +24,8 @@ async function getUserInfo(chainId, vaultAddressOrId, account, provider) {
|
|
|
24
24
|
const shares = (value) => (0, amount_1.toTokenAmount)(chainId, entry.contract, value);
|
|
25
25
|
try {
|
|
26
26
|
const token = new ethers_1.ethers.Contract(await contract.asset(), ERC20_READ_ABI, contract.runner);
|
|
27
|
-
const [userShares,
|
|
27
|
+
const [userShares, assetAllowance, maxDeposit, maxWithdraw, maxRedeem] = await Promise.all([
|
|
28
28
|
contract.balanceOf(account),
|
|
29
|
-
token.balanceOf(account),
|
|
30
29
|
token.allowance(account, entry.contract),
|
|
31
30
|
contract.maxDeposit(account),
|
|
32
31
|
contract.maxWithdraw(account),
|
|
@@ -35,7 +34,6 @@ async function getUserInfo(chainId, vaultAddressOrId, account, provider) {
|
|
|
35
34
|
return {
|
|
36
35
|
shares: shares(userShares),
|
|
37
36
|
assets: asset(await contract.convertToAssets(userShares)),
|
|
38
|
-
assetBalance: asset(assetBalance),
|
|
39
37
|
assetAllowance: asset(assetAllowance),
|
|
40
38
|
maxDeposit: asset(maxDeposit),
|
|
41
39
|
maxWithdraw: asset(maxWithdraw),
|
|
@@ -7,6 +7,7 @@ exports.inspectCurvePool = void 0;
|
|
|
7
7
|
const ethers_1 = require("ethers");
|
|
8
8
|
const chains_1 = __importDefault(require("../../../configs/chains"));
|
|
9
9
|
const rpcError_1 = require("../../../utils/rpcError");
|
|
10
|
+
const resolveTokenInfo_1 = require("../../resolveTokenInfo");
|
|
10
11
|
const _types_1 = require("./_types");
|
|
11
12
|
/**
|
|
12
13
|
* The zap's Curve swap calls by pool type (TokenZapLogic.swap), for pools indexed by plain `coins(i)`. The base, A, Y,
|
|
@@ -19,7 +20,6 @@ const SWAP_SIGNATURES = [
|
|
|
19
20
|
[_types_1.AladdinPoolType.CurveBasePool, "exchange(int128,int128,uint256,uint256)"],
|
|
20
21
|
];
|
|
21
22
|
const MAX_COINS = 8;
|
|
22
|
-
const ERC20 = ["function symbol() view returns (string)", "function decimals() view returns (uint8)"];
|
|
23
23
|
/**
|
|
24
24
|
* Reads a Curve pool that may not be in the chain config, so a zap route can be built through it: its coins in index
|
|
25
25
|
* order with their symbols, and which zap pool types it can swap as. The types come from the swap function selectors in
|
|
@@ -53,9 +53,8 @@ const inspectCurvePool = async (chainId, pool, provider) => {
|
|
|
53
53
|
if (!addresses.length)
|
|
54
54
|
throw new Error(`${address} has no coins(0); it is not a Curve pool this can read`);
|
|
55
55
|
const coins = await Promise.all(addresses.map(async (coin) => {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
return { address: coin, symbol, decimals };
|
|
56
|
+
const { symbol, decimals } = await (0, resolveTokenInfo_1.resolveTokenInfo)(chainId, coin, provider);
|
|
57
|
+
return { address: coin, symbol: symbol ?? "?", decimals };
|
|
59
58
|
}));
|
|
60
59
|
const poolTypes = SWAP_SIGNATURES.filter(([, sig]) => code.toLowerCase().includes(ethers_1.ethers.id(sig).slice(2, 10))).map(([type]) => type);
|
|
61
60
|
return { pool: address, coins, poolTypes };
|
|
@@ -8,6 +8,7 @@ const getContractAddress_1 = __importDefault(require("../../contracts/getContrac
|
|
|
8
8
|
const artifacts_1 = require("../../utils/artifacts");
|
|
9
9
|
const amount_1 = require("../../utils/amount");
|
|
10
10
|
const rpcError_1 = require("../../utils/rpcError");
|
|
11
|
+
const resolveTokenInfo_1 = require("../../contracts/resolveTokenInfo");
|
|
11
12
|
/**
|
|
12
13
|
* Walks a public address array (`adapters(i)`) until the index reverts. Any other error (network, RPC) is thrown,
|
|
13
14
|
* so a flaky node never returns a shortened list as if it were complete.
|
|
@@ -30,15 +31,19 @@ async function readList(vault, method) {
|
|
|
30
31
|
}
|
|
31
32
|
return out;
|
|
32
33
|
}
|
|
33
|
-
async function readAdapter(address, provider) {
|
|
34
|
+
async function readAdapter(chainId, address, provider) {
|
|
34
35
|
const erc20 = new ethers_1.ethers.Contract(address, (0, artifacts_1.getABI)("erc20vaultadapter"), provider);
|
|
35
36
|
const [type, token] = await Promise.all([erc20.getAssetType(), erc20.getAssetToken()]);
|
|
36
37
|
const erc721 = type === "ERC721";
|
|
37
38
|
const adapter = erc721 ? new ethers_1.ethers.Contract(address, (0, artifacts_1.getABI)("erc721vaultadapter"), provider) : erc20;
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
// An NFT collection has no decimals, so only an ERC20 goes through resolveTokenInfo.
|
|
40
|
+
const assetInfo = erc721
|
|
41
|
+
? new ethers_1.ethers.Contract(token, ["function symbol() view returns (string)"], provider)
|
|
42
|
+
.symbol()
|
|
43
|
+
.then((symbol) => ({ decimals: 0, symbol }), () => ({ decimals: 0, symbol: "NFTs" }))
|
|
44
|
+
: (0, resolveTokenInfo_1.resolveTokenInfo)(chainId, token, provider).then(({ decimals, symbol }) => ({ decimals, symbol: symbol ?? "tokens" }));
|
|
45
|
+
const [{ symbol: tokenSymbol, decimals: tokenDecimals }, value, tokenIds] = await Promise.all([
|
|
46
|
+
assetInfo,
|
|
42
47
|
adapter.totalValue().catch(() => null),
|
|
43
48
|
erc721 ? adapter.getDepositedTokens() : Promise.resolve([]),
|
|
44
49
|
]);
|
|
@@ -63,7 +68,7 @@ async function getRegisteredAdapters(chainId, provider) {
|
|
|
63
68
|
return Promise.all(all.map(async (address) => {
|
|
64
69
|
const flags = { address, depositable: depositable.includes(address), redeemable: redeemable.includes(address) };
|
|
65
70
|
try {
|
|
66
|
-
return { ...flags, ...(await readAdapter(address, provider)) };
|
|
71
|
+
return { ...flags, ...(await readAdapter(chainId, address, provider)) };
|
|
67
72
|
}
|
|
68
73
|
catch (error) {
|
|
69
74
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -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.
|
|
10
|
-
exports.
|
|
11
|
-
exports.
|
|
9
|
+
exports.toleranceNeeded = toleranceNeeded;
|
|
10
|
+
exports.priceImpact = priceImpact;
|
|
11
|
+
exports.largestClearingFloor = largestClearingFloor;
|
|
12
|
+
exports.planLiquidation = planLiquidation;
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
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
|
|
17
|
-
* 3.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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,
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
*
|
|
213
|
-
*
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
237
|
-
if (out !== null && out >= required) {
|
|
281
|
+
if (await test(mid))
|
|
238
282
|
high = mid;
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
283
|
+
else
|
|
242
284
|
low = mid + BigInt(1);
|
|
243
|
-
}
|
|
244
285
|
}
|
|
245
|
-
return
|
|
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
|
-
*
|
|
255
|
-
*
|
|
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
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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;
|
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.getCurveLPPriceUSD = getCurveLPPriceUSD;
|
|
7
7
|
const ethers_1 = require("ethers");
|
|
8
8
|
const chains_1 = __importDefault(require("../configs/chains"));
|
|
9
|
-
const
|
|
9
|
+
const resolveTokenInfo_1 = require("../contracts/resolveTokenInfo");
|
|
10
10
|
const tokenPricesUSD_1 = require("./tokenPricesUSD");
|
|
11
11
|
const CURVE_POOL_ABI = [
|
|
12
12
|
"function coins(uint256) view returns (address)",
|
|
@@ -31,13 +31,13 @@ async function getCurveLPPriceUSD(chainId, token, provider) {
|
|
|
31
31
|
]);
|
|
32
32
|
if (supply === 0n)
|
|
33
33
|
return 0;
|
|
34
|
-
const [
|
|
35
|
-
(0,
|
|
36
|
-
(0,
|
|
34
|
+
const [info0, info1] = await Promise.all([
|
|
35
|
+
(0, resolveTokenInfo_1.resolveTokenInfo)(chainId, c0, provider),
|
|
36
|
+
(0, resolveTokenInfo_1.resolveTokenInfo)(chainId, c1, provider),
|
|
37
37
|
]);
|
|
38
|
-
const p0 = (0, tokenPricesUSD_1.getTokenPriceUSD)(
|
|
39
|
-
const p1 = (0, tokenPricesUSD_1.getTokenPriceUSD)(
|
|
40
|
-
const tvl = Number(ethers_1.ethers.
|
|
38
|
+
const p0 = (0, tokenPricesUSD_1.getTokenPriceUSD)(info0.symbol);
|
|
39
|
+
const p1 = (0, tokenPricesUSD_1.getTokenPriceUSD)(info1.symbol);
|
|
40
|
+
const tvl = Number(ethers_1.ethers.formatUnits(b0, info0.decimals)) * p0 + Number(ethers_1.ethers.formatUnits(b1, info1.decimals)) * p1;
|
|
41
41
|
return tvl / Number(ethers_1.ethers.formatEther(supply));
|
|
42
42
|
}
|
|
43
43
|
catch {
|