Skip to content

Commit 3c14851

Browse files
committed
fix: cap automatic DEX quote amounts by available liquidity
1 parent bf74be4 commit 3c14851

10 files changed

Lines changed: 174 additions & 44 deletions

File tree

docs/ACTIONS.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ runner works.
1313
1414
Cron times are UTC. Enable state is managed in the database, not here.
1515

16-
The mainnet `setPrices*` actions use `--amount` as the DEX swap amount when
17-
fetching the reference price quote. This is separate from `--buy-amount` and
16+
The mainnet `setPrices*` actions use `--amount` as an explicit override for the
17+
DEX swap amount when fetching the reference price quote. This is separate from `--buy-amount` and
1818
`--sell-amount`, which set the buy-side liquidity-asset and sell-side base-asset
1919
liquidity remaining on the Ethena, USDC, and WETH ARMs. If omitted, each limit is
2020
set to the maximum `uint128` value. Liquidity amounts are integer native token
2121
units (for example, `100000000` is 100 tokens for an asset with 6 decimals).
22+
When `--amount` is omitted, the DEX quote amount is the smaller of the
23+
withdrawable ARM/market reserves and the corresponding price liquidity limit.
24+
An explicit `--amount` is used unchanged.
2225

2326
`--buy-price` and `--sell-price` bypass DEX-derived pricing and set an exact
2427
pair. Both must be supplied together; `--amount` is not used in this mode.
25-
When the Ethena or USDC action derives `--amount` from withdrawable liquidity,
26-
it rounds the available amount up to the minimum DEX quote size of 1,000 USDe
27-
or 1,000 USDC respectively; an explicit `--amount` override is used as supplied.
2828

2929
`setPricesWETH` uses the Lido pricing profile and 1Inch for `STETH,WSTETH`, and
3030
the EtherFi pricing profile and Kyber for `EETH,WEETH`. It processes all four

src/js/tasks/actions/setPricesEthena.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { types } from "hardhat/config";
44
import { action } from "../lib/action";
55
import { setPrices } from "../armPrices";
66
import { setPricesForBases } from "../../utils/priceActionUtils";
7-
import { resolveEthenaAggregatorAmount } from "../../utils/ethenaPricing";
87
import { mainnet } from "../../utils/addresses";
98
const ethenaARMAbi = require("../../../abis/EthenaARM.json");
109

@@ -118,17 +117,6 @@ action({
118117
const arm = new ethers.Contract(mainnet.ethenaARM, ethenaARMAbi, signer);
119118

120119
log.info("Setting prices for Ethena ARM");
121-
const exactPrices =
122-
args.buyPrice !== undefined && args.sellPrice !== undefined;
123-
let amount = args.amount;
124-
if (!exactPrices && amount === undefined) {
125-
amount = await resolveEthenaAggregatorAmount({
126-
arm,
127-
log,
128-
blockTag: "latest",
129-
});
130-
}
131-
132120
await setPricesForBases({
133121
setPrices,
134122
bases: ["SUSDE"],
@@ -146,7 +134,7 @@ action({
146134
minBuyPrice: args.minBuyPrice,
147135
kyber: args.kyber,
148136
inch: args.inch,
149-
amount,
137+
amount: args.amount,
150138
tolerance: args.tolerance,
151139
fee: args.fee,
152140
offset: args.offset,

src/js/tasks/actions/setPricesEtherFi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ action({
5555
)
5656
.addOptionalParam(
5757
"amount",
58-
"DEX swap amount used to fetch the reference price quote.",
59-
20,
58+
"Override the automatically detected DEX swap amount used to fetch the reference price quote.",
59+
undefined,
6060
types.float,
6161
)
6262
.addOptionalParam(

src/js/tasks/actions/setPricesLido.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ action({
5555
)
5656
.addOptionalParam(
5757
"amount",
58-
"DEX swap amount used to fetch the reference price quote.",
59-
20,
58+
"Override the automatically detected DEX swap amount used to fetch the reference price quote.",
59+
undefined,
6060
types.float,
6161
)
6262
.addOptionalParam(

src/js/tasks/actions/setPricesOETH.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ action({
5555
)
5656
.addOptionalParam(
5757
"amount",
58-
"DEX swap amount used to fetch the reference price quote.",
59-
10,
58+
"Override the automatically detected DEX swap amount used to fetch the reference price quote.",
59+
undefined,
6060
types.float,
6161
)
6262
.addOptionalParam(

src/js/tasks/actions/setPricesUSDC.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { types } from "hardhat/config";
44
import { action } from "../lib/action";
55
import { setPrices } from "../armPrices";
66
import { setPricesForBases } from "../../utils/priceActionUtils";
7-
import { resolveUsdAggregatorAmount } from "../../utils/usdPricing";
87
import { mainnet } from "../../utils/addresses";
98
const multiAssetARMAbi = require("../../../abis/MultiAssetARM.json");
109

@@ -122,17 +121,6 @@ action({
122121
const arm = new ethers.Contract(mainnet.usdcARM, multiAssetARMAbi, signer);
123122

124123
log.info("Setting prices for USDC ARM");
125-
const exactPrices =
126-
args.buyPrice !== undefined && args.sellPrice !== undefined;
127-
let amount = args.amount;
128-
if (!exactPrices && amount === undefined) {
129-
amount = await resolveUsdAggregatorAmount({
130-
arm,
131-
log,
132-
blockTag: "latest",
133-
});
134-
}
135-
136124
await setPricesForBases({
137125
setPrices,
138126
bases: String(args.bases).split(","),
@@ -150,7 +138,7 @@ action({
150138
minBuyPrice: args.minBuyPrice,
151139
kyber: args.kyber,
152140
inch: args.inch,
153-
amount,
141+
amount: args.amount,
154142
tolerance: args.tolerance,
155143
fee: args.fee,
156144
offset: args.offset,

src/js/tasks/actions/setPricesWETH.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ action({
178178
minBuyPrice: args.minBuyPrice ?? defaults.minBuyPrice,
179179
kyber: aggregatorOverridden ? Boolean(args.kyber) : defaults.kyber,
180180
inch: aggregatorOverridden ? Boolean(args.inch) : defaults.inch,
181-
amount: args.amount ?? 20,
181+
amount: args.amount,
182182
tolerance: args.tolerance ?? defaults.tolerance,
183183
fee: args.fee,
184184
offset: args.offset,

src/js/tasks/armPrices.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ const {
1818
rangeSellPrice,
1919
rangeBuyPrice,
2020
} = require("../utils/pricing");
21-
const { haveSwapCapsChanged } = require("../utils/priceUpdate");
21+
const {
22+
haveSwapCapsChanged,
23+
resolveDexQuoteAmount,
24+
} = require("../utils/priceUpdate");
2225

2326
const log = require("../utils/logger")("task:prices");
2427

@@ -133,11 +136,38 @@ const setPrices = async (options) => {
133136
if (curve && options.armName !== "Lido")
134137
throw new Error(`Curve prices only available for Lido`);
135138

139+
let reserves;
140+
if (options.amount === undefined || options.amount === null) {
141+
if (baseContext.version !== "multiBase") {
142+
throw new Error(
143+
`--amount is required when pricing a legacy ${options.armName} ARM`,
144+
);
145+
}
146+
reserves = await baseContext.arm.getReserves(baseAddress, {
147+
blockTag: options.blockTag ?? "latest",
148+
});
149+
}
150+
151+
const dexAmount = resolveDexQuoteAmount({
152+
amount: options.amount,
153+
liquidityAssets: reserves?.liquidityAssets ?? reserves?.[0],
154+
baseAssetReserve: reserves?.baseAssetReserve ?? reserves?.[1],
155+
buyLiquidity: parseSwapCap(buyAmount),
156+
sellLiquidity: parseSwapCap(sellAmount),
157+
liquidityDecimals,
158+
baseDecimals,
159+
});
160+
if (options.amount === undefined || options.amount === null) {
161+
log(
162+
`Using ${dexAmount} as the DEX quote amount based on available reserves and price liquidity`,
163+
);
164+
}
165+
136166
// 2.1 Get latest market prices if no midPrice is provided
137167
referencePrices = inch
138168
? // 2.1.b Otherwise, get prices from 1Inch
139169
await get1InchPrices(
140-
options.amount,
170+
dexAmount,
141171
assets,
142172
inchFee,
143173
1,
@@ -147,25 +177,26 @@ const setPrices = async (options) => {
147177
: kyber
148178
? // 2.1.c Or from Kyber if specified
149179
await getKyberPrices(
150-
options.amount,
180+
dexAmount,
151181
assets,
152182
baseDecimals,
153183
liquidityDecimals,
154184
)
155185
: // 2.1.d Or from Curve if specified
156186
await getCurvePrices({
157187
...options,
188+
amount: dexAmount,
158189
poolAddress: addresses.mainnet.CurveNgStEthPool,
159190
});
160191

161192
// Adjust price down if a wrapped asset like sUSDe or wstETH
162193
if (shouldAdjustWrapped) {
163-
const amountIn = parseUnits(options.amount.toString(), baseDecimals);
194+
const amountIn = parseUnits(dexAmount, baseDecimals);
164195
// The legacy convertToAsset path returns 18 decimals while the adapter
165196
// converts a base decimals input to liquidity decimals
166197
const convertedAssets =
167198
config.adapter === ZeroAddress
168-
? await convertToAsset(baseAddress, options.amount, signer)
199+
? await convertToAsset(baseAddress, dexAmount, signer)
169200
: await (
170201
await adapterContract(config.adapter, signer)
171202
).convertToAssets(amountIn);

src/js/utils/priceUpdate.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,52 @@
1+
const { formatUnits, parseUnits } = require("ethers");
2+
3+
const capDexAmountBySwapLiquidity = ({
4+
amount,
5+
buyLiquidity,
6+
sellLiquidity,
7+
liquidityDecimals,
8+
baseDecimals,
9+
}) => {
10+
let cappedAmount = amount.toString();
11+
12+
if (parseUnits(cappedAmount, liquidityDecimals) > buyLiquidity) {
13+
cappedAmount = formatUnits(buyLiquidity, liquidityDecimals);
14+
}
15+
if (parseUnits(cappedAmount, baseDecimals) > sellLiquidity) {
16+
cappedAmount = formatUnits(sellLiquidity, baseDecimals);
17+
}
18+
19+
return cappedAmount;
20+
};
21+
22+
const resolveDexQuoteAmount = ({
23+
amount,
24+
liquidityAssets,
25+
baseAssetReserve,
26+
buyLiquidity,
27+
sellLiquidity,
28+
liquidityDecimals,
29+
baseDecimals,
30+
}) => {
31+
if (amount !== undefined && amount !== null) return amount.toString();
32+
33+
return capDexAmountBySwapLiquidity({
34+
amount: formatUnits(liquidityAssets, liquidityDecimals),
35+
buyLiquidity,
36+
sellLiquidity:
37+
baseAssetReserve < sellLiquidity ? baseAssetReserve : sellLiquidity,
38+
liquidityDecimals,
39+
baseDecimals,
40+
});
41+
};
42+
143
const haveSwapCapsChanged = (baseContext, buyAmount, sellAmount) =>
244
baseContext.version === "multiBase" &&
345
(buyAmount !== baseContext.config.buyLiquidityRemaining ||
446
sellAmount !== baseContext.config.sellLiquidityRemaining);
547

648
module.exports = {
49+
capDexAmountBySwapLiquidity,
750
haveSwapCapsChanged,
51+
resolveDexQuoteAmount,
852
};

test/js/armPrices.test.js

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const assert = require("assert");
22

3-
const { haveSwapCapsChanged } = require("../../src/js/utils/priceUpdate");
3+
const {
4+
capDexAmountBySwapLiquidity,
5+
haveSwapCapsChanged,
6+
resolveDexQuoteAmount,
7+
} = require("../../src/js/utils/priceUpdate");
48

59
const multiBaseContext = (buyLiquidityRemaining, sellLiquidityRemaining) => ({
610
version: "multiBase",
@@ -34,4 +38,79 @@ assert.strictEqual(
3438
"legacy ARMs do not support buy and sell amounts",
3539
);
3640

41+
assert.strictEqual(
42+
capDexAmountBySwapLiquidity({
43+
amount: 100,
44+
buyLiquidity: 50000000n,
45+
sellLiquidity: (1n << 128n) - 1n,
46+
liquidityDecimals: 6,
47+
baseDecimals: 18,
48+
}),
49+
"50.0",
50+
"buy quote amount should not exceed buy liquidity",
51+
);
52+
53+
assert.strictEqual(
54+
capDexAmountBySwapLiquidity({
55+
amount: 100,
56+
buyLiquidity: (1n << 128n) - 1n,
57+
sellLiquidity: 25000000000000000000n,
58+
liquidityDecimals: 6,
59+
baseDecimals: 18,
60+
}),
61+
"25.0",
62+
"sell quote amount should not exceed sell liquidity",
63+
);
64+
65+
assert.strictEqual(
66+
capDexAmountBySwapLiquidity({
67+
amount: 20,
68+
buyLiquidity: 30000000n,
69+
sellLiquidity: 10000000000000000000n,
70+
liquidityDecimals: 6,
71+
baseDecimals: 18,
72+
}),
73+
"10.0",
74+
"quote amount should use the lower of buy and sell liquidity",
75+
);
76+
77+
assert.strictEqual(
78+
capDexAmountBySwapLiquidity({
79+
amount: 20,
80+
buyLiquidity: 30000000n,
81+
sellLiquidity: 40000000000000000000n,
82+
liquidityDecimals: 6,
83+
baseDecimals: 18,
84+
}),
85+
"20",
86+
"quote amount within both liquidity limits should be unchanged",
87+
);
88+
89+
assert.strictEqual(
90+
resolveDexQuoteAmount({
91+
amount: 100,
92+
liquidityAssets: 50000000n,
93+
baseAssetReserve: 25000000000000000000n,
94+
buyLiquidity: 30000000n,
95+
sellLiquidity: 10000000000000000000n,
96+
liquidityDecimals: 6,
97+
baseDecimals: 18,
98+
}),
99+
"100",
100+
"an explicit quote amount should override reserves and price liquidity",
101+
);
102+
103+
assert.strictEqual(
104+
resolveDexQuoteAmount({
105+
liquidityAssets: 50000000n,
106+
baseAssetReserve: 40000000000000000000n,
107+
buyLiquidity: 30000000n,
108+
sellLiquidity: 20000000000000000000n,
109+
liquidityDecimals: 6,
110+
baseDecimals: 18,
111+
}),
112+
"20.0",
113+
"an automatic quote amount should use the smallest reserve or price liquidity limit",
114+
);
115+
37116
console.log("ARM price tests passed");

0 commit comments

Comments
 (0)