Skip to content

Commit 8c4f781

Browse files
authored
updated liq check condition based on sr (#222)
1 parent e8bfda7 commit 8c4f781

File tree

4 files changed

+78
-62
lines changed

4 files changed

+78
-62
lines changed

spot-contracts/contracts/RolloverVault.sol

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { IVault } from "./_interfaces/IVault.sol";
66
import { IRolloverVault } from "./_interfaces/IRolloverVault.sol";
77
import { IERC20Burnable } from "./_interfaces/IERC20Burnable.sol";
88
import { TokenAmount, RolloverData, SubscriptionParams } from "./_interfaces/CommonTypes.sol";
9-
import { UnauthorizedCall, UnauthorizedTransferOut, UnexpectedDecimals, UnexpectedAsset, OutOfBounds, UnacceptableSwap, InsufficientDeployment, DeployedCountOverLimit, InvalidPerc, InsufficientLiquidity } from "./_interfaces/ProtocolErrors.sol";
9+
import { UnauthorizedCall, UnauthorizedTransferOut, UnexpectedDecimals, UnexpectedAsset, OutOfBounds, UnacceptableSwap, InsufficientDeployment, DeployedCountOverLimit, InsufficientLiquidity } from "./_interfaces/ProtocolErrors.sol";
1010

1111
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
1212
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
@@ -84,6 +84,10 @@ contract RolloverVault is
8484
/// @dev The maximum number of deployed assets that can be held in this vault at any given time.
8585
uint8 public constant MAX_DEPLOYED_COUNT = 47;
8686

87+
// Replicating value used here:
88+
// https://github.com/buttonwood-protocol/tranche/blob/main/contracts/BondController.sol
89+
uint256 private constant TRANCHE_RATIO_GRANULARITY = 1000;
90+
8791
/// @dev Immature redemption may result in some dust tranches when balances are not perfectly divisible by the tranche ratio.
8892
/// Based on current the implementation of `computeRedeemableTrancheAmounts`,
8993
/// the dust balances which remain after immature redemption will be *at most* {TRANCHE_RATIO_GRANULARITY} or 1000.
@@ -128,16 +132,24 @@ contract RolloverVault is
128132
/// @return The address of the keeper.
129133
address public keeper;
130134

131-
/// @notice The enforced minimum absolute balance of underlying tokens to be held by the vault.
132-
/// @dev On deployment only the delta greater than this balance is deployed.
133-
/// `minUnderlyingBal` is enforced on deployment and swapping operations which reduce the underlying balance.
134-
/// This parameter ensures that the vault's tvl is never too low, which guards against the "share" manipulation attack.
135-
uint256 public minUnderlyingBal;
136-
137-
/// @notice The enforced minimum percentage of the vault's value to be held as underlying tokens.
138-
/// @dev The percentage minimum is enforced after swaps which reduce the vault's underlying token liquidity.
139-
/// This ensures that the vault has sufficient liquid underlying tokens for upcoming rollovers.
140-
uint256 public minUnderlyingPerc;
135+
//--------------------------------------------------------------------------
136+
// The reserved liquidity is the subset of the vault's underlying tokens that it
137+
// does not deploy for rolling over (or used for swaps) and simply holds.
138+
// The existence of sufficient reserved liquidity ensures that
139+
// a) The vault's TVL never goes too low and guards against the "share" manipulation attack.
140+
// b) Not all of the vault's liquidity is locked up in tranches.
141+
142+
/// @notice The absolute amount of underlying tokens, reserved.
143+
/// @custom:oz-upgrades-renamed-from minUnderlyingBal
144+
uint256 public reservedUnderlyingBal;
145+
146+
/// @notice The percentage of the vault's "neutrally" subscribed TVL, reserved.
147+
/// @dev A neutral subscription state implies the vault's TVL is exactly enough to
148+
/// rollover over the entire supply of perp tokens.
149+
/// NOTE: A neutral subscription ratio of 1.0 is distinct from a deviation ratio (dr) of 1.0.
150+
/// For more details, refer to the fee policy documentation.
151+
/// @custom:oz-upgrades-renamed-from minUnderlyingPerc
152+
uint256 public reservedSubscriptionPerc;
141153

142154
//--------------------------------------------------------------------------
143155
// Modifiers
@@ -190,8 +202,8 @@ contract RolloverVault is
190202

191203
// setting initial parameter values
192204
minDeploymentAmt = 0;
193-
minUnderlyingBal = 0;
194-
minUnderlyingPerc = ONE / 3; // 33%
205+
reservedUnderlyingBal = 0;
206+
reservedSubscriptionPerc = 0;
195207

196208
// sync underlying
197209
_syncAsset(underlying);
@@ -247,19 +259,16 @@ contract RolloverVault is
247259
minDeploymentAmt = minDeploymentAmt_;
248260
}
249261

250-
/// @notice Updates the minimum underlying balance requirement (Absolute number of underlying tokens).
251-
/// @param minUnderlyingBal_ The new minimum underlying balance.
252-
function updateMinUnderlyingBal(uint256 minUnderlyingBal_) external onlyKeeper {
253-
minUnderlyingBal = minUnderlyingBal_;
262+
/// @notice Updates absolute reserved underlying balance.
263+
/// @param reservedUnderlyingBal_ The new reserved underlying balance.
264+
function updateReservedUnderlyingBal(uint256 reservedUnderlyingBal_) external onlyKeeper {
265+
reservedUnderlyingBal = reservedUnderlyingBal_;
254266
}
255267

256-
/// @notice Updates the minimum underlying percentage requirement (Expressed as a percentage).
257-
/// @param minUnderlyingPerc_ The new minimum underlying percentage.
258-
function updateMinUnderlyingPerc(uint256 minUnderlyingPerc_) external onlyKeeper {
259-
if (minUnderlyingPerc_ > ONE) {
260-
revert InvalidPerc();
261-
}
262-
minUnderlyingPerc = minUnderlyingPerc_;
268+
/// @notice Updates the reserved subscription percentage.
269+
/// @param reservedSubscriptionPerc_ The new reserved subscription percentage.
270+
function updateReservedSubscriptionPerc(uint256 reservedSubscriptionPerc_) external onlyKeeper {
271+
reservedSubscriptionPerc = reservedSubscriptionPerc_;
263272
}
264273

265274
//--------------------------------------------------------------------------
@@ -274,20 +283,19 @@ contract RolloverVault is
274283

275284
/// @inheritdoc IVault
276285
/// @dev Its safer to call `recover` before `deploy` so the full available balance can be deployed.
277-
/// The vault holds `minUnderlyingBal` as underlying tokens and deploys the rest.
286+
/// The vault holds the reserved balance of underlying tokens and deploys the rest.
278287
/// Reverts if no funds are rolled over or enforced deployment threshold is not reached.
279288
function deploy() public override nonReentrant whenNotPaused {
280289
IERC20Upgradeable underlying_ = underlying;
281290
IPerpetualTranche perp_ = perp;
282291

283-
// `minUnderlyingBal` worth of underlying liquidity is excluded from the usable balance
284-
uint256 usableBal = underlying_.balanceOf(address(this));
285-
if (usableBal <= minUnderlyingBal) {
286-
revert InsufficientLiquidity();
287-
}
288-
usableBal -= minUnderlyingBal;
292+
// We calculate the usable underlying balance.
293+
uint256 underlyingBal = underlying_.balanceOf(address(this));
294+
uint256 reservedBal = _totalReservedBalance(perp_.getTVL(), perp_.getDepositTrancheRatio());
295+
uint256 usableBal = (underlyingBal > reservedBal) ? underlyingBal - reservedBal : 0;
289296

290297
// We ensure that at-least `minDeploymentAmt` amount of underlying tokens are deployed
298+
// (i.e used productively for rollovers).
291299
if (usableBal <= minDeploymentAmt) {
292300
revert InsufficientDeployment();
293301
}
@@ -469,13 +477,12 @@ contract RolloverVault is
469477
// NOTE: In case this operation mints slightly more perps than that are required for the swap,
470478
// The vault continues to hold the perp dust until the subsequent `swapPerpsForUnderlying` or manual `recover(perp)`.
471479

472-
// If vault liquidity has reduced, revert if it reduced too much.
473-
// - Absolute balance is strictly greater than `minUnderlyingBal`.
474-
// - Ratio of the balance to the vault's TVL is strictly greater than `minUnderlyingPerc`.
480+
// We ensure that the vault's underlying token liquidity
481+
// remains above the reserved level after swap.
475482
uint256 underlyingBalPost = underlying_.balanceOf(address(this));
476483
if (
477-
underlyingBalPost < underlyingBalPre &&
478-
(underlyingBalPost <= minUnderlyingBal || underlyingBalPost.mulDiv(ONE, s.vaultTVL) <= minUnderlyingPerc)
484+
(underlyingBalPost < underlyingBalPre) &&
485+
(underlyingBalPost <= _totalReservedBalance((s.perpTVL + underlyingAmtIn), s.seniorTR))
479486
) {
480487
revert InsufficientLiquidity();
481488
}
@@ -973,4 +980,13 @@ contract RolloverVault is
973980
(uint256 trancheClaim, uint256 trancheSupply) = tranche.getTrancheCollateralization(collateralToken);
974981
return trancheClaim.mulDiv(trancheAmt, trancheSupply, MathUpgradeable.Rounding.Up);
975982
}
983+
984+
/// @dev Computes the balance of underlying tokens to NOT be used for any operation.
985+
function _totalReservedBalance(uint256 perpTVL, uint256 seniorTR) private view returns (uint256) {
986+
return
987+
MathUpgradeable.max(
988+
reservedUnderlyingBal,
989+
perpTVL.mulDiv(TRANCHE_RATIO_GRANULARITY - seniorTR, seniorTR).mulDiv(reservedSubscriptionPerc, ONE)
990+
);
991+
}
976992
}

spot-contracts/test/rollover-vault/RolloverVault.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ describe("RolloverVault", function () {
7272
});
7373

7474
it("should set initial param values", async function () {
75-
expect(await vault.minUnderlyingPerc()).to.eq(toPercFixedPtAmt("0.33333333"));
7675
expect(await vault.minDeploymentAmt()).to.eq("0");
77-
expect(await vault.minUnderlyingBal()).to.eq("0");
76+
expect(await vault.reservedUnderlyingBal()).to.eq("0");
77+
expect(await vault.reservedSubscriptionPerc()).to.eq("0");
7878
});
7979

8080
it("should initialize lists", async function () {
@@ -291,15 +291,15 @@ describe("RolloverVault", function () {
291291
});
292292
});
293293

294-
describe("#updateMinDeploymentAmt", function () {
294+
describe("#updateReservedUnderlyingBal", function () {
295295
let tx: Transaction;
296296
beforeEach(async function () {
297297
await vault.connect(deployer).updateKeeper(await otherUser.getAddress());
298298
});
299299

300300
describe("when triggered by non-keeper", function () {
301301
it("should revert", async function () {
302-
await expect(vault.connect(deployer).updateMinDeploymentAmt(0)).to.be.revertedWithCustomError(
302+
await expect(vault.connect(deployer).updateReservedUnderlyingBal(0)).to.be.revertedWithCustomError(
303303
vault,
304304
"UnauthorizedCall",
305305
);
@@ -308,24 +308,24 @@ describe("RolloverVault", function () {
308308

309309
describe("when triggered by keeper", function () {
310310
beforeEach(async function () {
311-
tx = await vault.connect(otherUser).updateMinDeploymentAmt(toFixedPtAmt("1000"));
311+
tx = await vault.connect(otherUser).updateReservedUnderlyingBal(toFixedPtAmt("1000"));
312312
await tx;
313313
});
314314
it("should update the min deployment amount", async function () {
315-
expect(await vault.minDeploymentAmt()).to.eq(toFixedPtAmt("1000"));
315+
expect(await vault.reservedUnderlyingBal()).to.eq(toFixedPtAmt("1000"));
316316
});
317317
});
318318
});
319319

320-
describe("#updateMinUnderlyingBal", function () {
320+
describe("#updateReservedUnderlyingBal", function () {
321321
let tx: Transaction;
322322
beforeEach(async function () {
323323
await vault.connect(deployer).updateKeeper(await otherUser.getAddress());
324324
});
325325

326326
describe("when triggered by non-keeper", function () {
327327
it("should revert", async function () {
328-
await expect(vault.connect(deployer).updateMinUnderlyingBal(0)).to.be.revertedWithCustomError(
328+
await expect(vault.connect(deployer).updateReservedUnderlyingBal(0)).to.be.revertedWithCustomError(
329329
vault,
330330
"UnauthorizedCall",
331331
);
@@ -334,24 +334,24 @@ describe("RolloverVault", function () {
334334

335335
describe("when triggered by keeper", function () {
336336
beforeEach(async function () {
337-
tx = await vault.connect(otherUser).updateMinUnderlyingBal(toFixedPtAmt("1000"));
337+
tx = await vault.connect(otherUser).updateReservedUnderlyingBal(toFixedPtAmt("1000"));
338338
await tx;
339339
});
340340
it("should update the min underlying balance", async function () {
341-
expect(await vault.minUnderlyingBal()).to.eq(toFixedPtAmt("1000"));
341+
expect(await vault.reservedUnderlyingBal()).to.eq(toFixedPtAmt("1000"));
342342
});
343343
});
344344
});
345345

346-
describe("#updateMinUnderlyingPerc", function () {
346+
describe("#updateReservedSubscriptionPerc", function () {
347347
let tx: Transaction;
348348
beforeEach(async function () {
349349
await vault.connect(deployer).updateKeeper(await otherUser.getAddress());
350350
});
351351

352352
describe("when triggered by non-keeper", function () {
353353
it("should revert", async function () {
354-
await expect(vault.connect(deployer).updateMinUnderlyingPerc(0)).to.be.revertedWithCustomError(
354+
await expect(vault.connect(deployer).updateReservedSubscriptionPerc(0)).to.be.revertedWithCustomError(
355355
vault,
356356
"UnauthorizedCall",
357357
);
@@ -360,11 +360,11 @@ describe("RolloverVault", function () {
360360

361361
describe("when triggered by keeper", function () {
362362
beforeEach(async function () {
363-
tx = await vault.connect(otherUser).updateMinUnderlyingPerc(toPercFixedPtAmt("0.1"));
363+
tx = await vault.connect(otherUser).updateReservedSubscriptionPerc(toPercFixedPtAmt("0.1"));
364364
await tx;
365365
});
366366
it("should update the min underlying balance", async function () {
367-
expect(await vault.minUnderlyingPerc()).to.eq(toPercFixedPtAmt("0.1"));
367+
expect(await vault.reservedSubscriptionPerc()).to.eq(toPercFixedPtAmt("0.1"));
368368
});
369369
});
370370
});

spot-contracts/test/rollover-vault/RolloverVault_deploy.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@ describe("RolloverVault", function () {
110110
describe("#deploy", function () {
111111
describe("when usable balance is zero", function () {
112112
it("should revert", async function () {
113-
await expect(vault.deploy()).to.be.revertedWithCustomError(vault, "InsufficientLiquidity");
113+
await expect(vault.deploy()).to.be.revertedWithCustomError(vault, "InsufficientDeployment");
114114
});
115115
});
116116

117-
describe("when minUnderlyingBal is not set", function () {
117+
describe("when reservedUnderlyingBal is not set", function () {
118118
beforeEach(async function () {
119-
await vault.updateMinUnderlyingBal(toFixedPtAmt("0"));
119+
await vault.updateReservedUnderlyingBal(toFixedPtAmt("0"));
120120
});
121121

122122
describe("when usable balance is lower than the min deployment", function () {
@@ -140,18 +140,18 @@ describe("RolloverVault", function () {
140140
});
141141
});
142142

143-
describe("when minUnderlyingBal is set", function () {
143+
describe("when reservedUnderlyingBal is set", function () {
144144
beforeEach(async function () {
145-
await vault.updateMinUnderlyingBal(toFixedPtAmt("25"));
145+
await vault.updateReservedUnderlyingBal(toFixedPtAmt("25"));
146146
});
147147

148-
describe("when usable balance is lower than the minUnderlyingBal", function () {
148+
describe("when usable balance is lower than the reservedUnderlyingBal", function () {
149149
beforeEach(async function () {
150150
await collateralToken.transfer(vault.address, toFixedPtAmt("20"));
151151
await vault.updateMinDeploymentAmt(toFixedPtAmt("1"));
152152
});
153153
it("should revert", async function () {
154-
await expect(vault.deploy()).to.be.revertedWithCustomError(vault, "InsufficientLiquidity");
154+
await expect(vault.deploy()).to.be.revertedWithCustomError(vault, "InsufficientDeployment");
155155
});
156156
});
157157

spot-contracts/test/rollover-vault/RolloverVault_swap.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,8 @@ describe("RolloverVault", function () {
303303

304304
describe("when absolute liquidity is too low", function () {
305305
beforeEach(async function () {
306-
await vault.updateMinUnderlyingBal(toFixedPtAmt("1000"));
307-
await vault.updateMinUnderlyingPerc(0);
306+
await vault.updateReservedUnderlyingBal(toFixedPtAmt("1000"));
307+
await vault.updateReservedSubscriptionPerc(0);
308308
});
309309
it("should be reverted", async function () {
310310
await expect(vault.swapUnderlyingForPerps(toFixedPtAmt("50"))).to.be.revertedWithCustomError(
@@ -317,15 +317,15 @@ describe("RolloverVault", function () {
317317

318318
describe("when percentage of liquidity is too low", function () {
319319
beforeEach(async function () {
320-
await vault.updateMinUnderlyingBal(0);
321-
await vault.updateMinUnderlyingPerc(toPercFixedPtAmt("0.40"));
320+
await vault.updateReservedUnderlyingBal(0);
321+
await vault.updateReservedSubscriptionPerc(toPercFixedPtAmt("0.25"));
322322
});
323323
it("should be reverted", async function () {
324324
await expect(vault.swapUnderlyingForPerps(toFixedPtAmt("100"))).to.be.revertedWithCustomError(
325325
vault,
326326
"InsufficientLiquidity",
327327
);
328-
await expect(vault.swapUnderlyingForPerps(toFixedPtAmt("99"))).not.to.be.reverted;
328+
await expect(vault.swapUnderlyingForPerps(toFixedPtAmt("50"))).not.to.be.reverted;
329329
});
330330
});
331331

0 commit comments

Comments
 (0)