From 8c4f7812588adcb67c323d2b1b379c7f977e77f6 Mon Sep 17 00:00:00 2001 From: Aalavandhan <6264334+aalavandhan@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:08:28 -0400 Subject: [PATCH] updated liq check condition based on sr (#222) --- spot-contracts/contracts/RolloverVault.sol | 88 +++++++++++-------- .../test/rollover-vault/RolloverVault.ts | 28 +++--- .../rollover-vault/RolloverVault_deploy.ts | 14 +-- .../test/rollover-vault/RolloverVault_swap.ts | 10 +-- 4 files changed, 78 insertions(+), 62 deletions(-) diff --git a/spot-contracts/contracts/RolloverVault.sol b/spot-contracts/contracts/RolloverVault.sol index 101b76b7..c17c8478 100644 --- a/spot-contracts/contracts/RolloverVault.sol +++ b/spot-contracts/contracts/RolloverVault.sol @@ -6,7 +6,7 @@ import { IVault } from "./_interfaces/IVault.sol"; import { IRolloverVault } from "./_interfaces/IRolloverVault.sol"; import { IERC20Burnable } from "./_interfaces/IERC20Burnable.sol"; import { TokenAmount, RolloverData, SubscriptionParams } from "./_interfaces/CommonTypes.sol"; -import { UnauthorizedCall, UnauthorizedTransferOut, UnexpectedDecimals, UnexpectedAsset, OutOfBounds, UnacceptableSwap, InsufficientDeployment, DeployedCountOverLimit, InvalidPerc, InsufficientLiquidity } from "./_interfaces/ProtocolErrors.sol"; +import { UnauthorizedCall, UnauthorizedTransferOut, UnexpectedDecimals, UnexpectedAsset, OutOfBounds, UnacceptableSwap, InsufficientDeployment, DeployedCountOverLimit, InsufficientLiquidity } from "./_interfaces/ProtocolErrors.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; @@ -84,6 +84,10 @@ contract RolloverVault is /// @dev The maximum number of deployed assets that can be held in this vault at any given time. uint8 public constant MAX_DEPLOYED_COUNT = 47; + // Replicating value used here: + // https://github.com/buttonwood-protocol/tranche/blob/main/contracts/BondController.sol + uint256 private constant TRANCHE_RATIO_GRANULARITY = 1000; + /// @dev Immature redemption may result in some dust tranches when balances are not perfectly divisible by the tranche ratio. /// Based on current the implementation of `computeRedeemableTrancheAmounts`, /// the dust balances which remain after immature redemption will be *at most* {TRANCHE_RATIO_GRANULARITY} or 1000. @@ -128,16 +132,24 @@ contract RolloverVault is /// @return The address of the keeper. address public keeper; - /// @notice The enforced minimum absolute balance of underlying tokens to be held by the vault. - /// @dev On deployment only the delta greater than this balance is deployed. - /// `minUnderlyingBal` is enforced on deployment and swapping operations which reduce the underlying balance. - /// This parameter ensures that the vault's tvl is never too low, which guards against the "share" manipulation attack. - uint256 public minUnderlyingBal; - - /// @notice The enforced minimum percentage of the vault's value to be held as underlying tokens. - /// @dev The percentage minimum is enforced after swaps which reduce the vault's underlying token liquidity. - /// This ensures that the vault has sufficient liquid underlying tokens for upcoming rollovers. - uint256 public minUnderlyingPerc; + //-------------------------------------------------------------------------- + // The reserved liquidity is the subset of the vault's underlying tokens that it + // does not deploy for rolling over (or used for swaps) and simply holds. + // The existence of sufficient reserved liquidity ensures that + // a) The vault's TVL never goes too low and guards against the "share" manipulation attack. + // b) Not all of the vault's liquidity is locked up in tranches. + + /// @notice The absolute amount of underlying tokens, reserved. + /// @custom:oz-upgrades-renamed-from minUnderlyingBal + uint256 public reservedUnderlyingBal; + + /// @notice The percentage of the vault's "neutrally" subscribed TVL, reserved. + /// @dev A neutral subscription state implies the vault's TVL is exactly enough to + /// rollover over the entire supply of perp tokens. + /// NOTE: A neutral subscription ratio of 1.0 is distinct from a deviation ratio (dr) of 1.0. + /// For more details, refer to the fee policy documentation. + /// @custom:oz-upgrades-renamed-from minUnderlyingPerc + uint256 public reservedSubscriptionPerc; //-------------------------------------------------------------------------- // Modifiers @@ -190,8 +202,8 @@ contract RolloverVault is // setting initial parameter values minDeploymentAmt = 0; - minUnderlyingBal = 0; - minUnderlyingPerc = ONE / 3; // 33% + reservedUnderlyingBal = 0; + reservedSubscriptionPerc = 0; // sync underlying _syncAsset(underlying); @@ -247,19 +259,16 @@ contract RolloverVault is minDeploymentAmt = minDeploymentAmt_; } - /// @notice Updates the minimum underlying balance requirement (Absolute number of underlying tokens). - /// @param minUnderlyingBal_ The new minimum underlying balance. - function updateMinUnderlyingBal(uint256 minUnderlyingBal_) external onlyKeeper { - minUnderlyingBal = minUnderlyingBal_; + /// @notice Updates absolute reserved underlying balance. + /// @param reservedUnderlyingBal_ The new reserved underlying balance. + function updateReservedUnderlyingBal(uint256 reservedUnderlyingBal_) external onlyKeeper { + reservedUnderlyingBal = reservedUnderlyingBal_; } - /// @notice Updates the minimum underlying percentage requirement (Expressed as a percentage). - /// @param minUnderlyingPerc_ The new minimum underlying percentage. - function updateMinUnderlyingPerc(uint256 minUnderlyingPerc_) external onlyKeeper { - if (minUnderlyingPerc_ > ONE) { - revert InvalidPerc(); - } - minUnderlyingPerc = minUnderlyingPerc_; + /// @notice Updates the reserved subscription percentage. + /// @param reservedSubscriptionPerc_ The new reserved subscription percentage. + function updateReservedSubscriptionPerc(uint256 reservedSubscriptionPerc_) external onlyKeeper { + reservedSubscriptionPerc = reservedSubscriptionPerc_; } //-------------------------------------------------------------------------- @@ -274,20 +283,19 @@ contract RolloverVault is /// @inheritdoc IVault /// @dev Its safer to call `recover` before `deploy` so the full available balance can be deployed. - /// The vault holds `minUnderlyingBal` as underlying tokens and deploys the rest. + /// The vault holds the reserved balance of underlying tokens and deploys the rest. /// Reverts if no funds are rolled over or enforced deployment threshold is not reached. function deploy() public override nonReentrant whenNotPaused { IERC20Upgradeable underlying_ = underlying; IPerpetualTranche perp_ = perp; - // `minUnderlyingBal` worth of underlying liquidity is excluded from the usable balance - uint256 usableBal = underlying_.balanceOf(address(this)); - if (usableBal <= minUnderlyingBal) { - revert InsufficientLiquidity(); - } - usableBal -= minUnderlyingBal; + // We calculate the usable underlying balance. + uint256 underlyingBal = underlying_.balanceOf(address(this)); + uint256 reservedBal = _totalReservedBalance(perp_.getTVL(), perp_.getDepositTrancheRatio()); + uint256 usableBal = (underlyingBal > reservedBal) ? underlyingBal - reservedBal : 0; // We ensure that at-least `minDeploymentAmt` amount of underlying tokens are deployed + // (i.e used productively for rollovers). if (usableBal <= minDeploymentAmt) { revert InsufficientDeployment(); } @@ -469,13 +477,12 @@ contract RolloverVault is // NOTE: In case this operation mints slightly more perps than that are required for the swap, // The vault continues to hold the perp dust until the subsequent `swapPerpsForUnderlying` or manual `recover(perp)`. - // If vault liquidity has reduced, revert if it reduced too much. - // - Absolute balance is strictly greater than `minUnderlyingBal`. - // - Ratio of the balance to the vault's TVL is strictly greater than `minUnderlyingPerc`. + // We ensure that the vault's underlying token liquidity + // remains above the reserved level after swap. uint256 underlyingBalPost = underlying_.balanceOf(address(this)); if ( - underlyingBalPost < underlyingBalPre && - (underlyingBalPost <= minUnderlyingBal || underlyingBalPost.mulDiv(ONE, s.vaultTVL) <= minUnderlyingPerc) + (underlyingBalPost < underlyingBalPre) && + (underlyingBalPost <= _totalReservedBalance((s.perpTVL + underlyingAmtIn), s.seniorTR)) ) { revert InsufficientLiquidity(); } @@ -973,4 +980,13 @@ contract RolloverVault is (uint256 trancheClaim, uint256 trancheSupply) = tranche.getTrancheCollateralization(collateralToken); return trancheClaim.mulDiv(trancheAmt, trancheSupply, MathUpgradeable.Rounding.Up); } + + /// @dev Computes the balance of underlying tokens to NOT be used for any operation. + function _totalReservedBalance(uint256 perpTVL, uint256 seniorTR) private view returns (uint256) { + return + MathUpgradeable.max( + reservedUnderlyingBal, + perpTVL.mulDiv(TRANCHE_RATIO_GRANULARITY - seniorTR, seniorTR).mulDiv(reservedSubscriptionPerc, ONE) + ); + } } diff --git a/spot-contracts/test/rollover-vault/RolloverVault.ts b/spot-contracts/test/rollover-vault/RolloverVault.ts index c811e225..7a23860b 100644 --- a/spot-contracts/test/rollover-vault/RolloverVault.ts +++ b/spot-contracts/test/rollover-vault/RolloverVault.ts @@ -72,9 +72,9 @@ describe("RolloverVault", function () { }); it("should set initial param values", async function () { - expect(await vault.minUnderlyingPerc()).to.eq(toPercFixedPtAmt("0.33333333")); expect(await vault.minDeploymentAmt()).to.eq("0"); - expect(await vault.minUnderlyingBal()).to.eq("0"); + expect(await vault.reservedUnderlyingBal()).to.eq("0"); + expect(await vault.reservedSubscriptionPerc()).to.eq("0"); }); it("should initialize lists", async function () { @@ -291,7 +291,7 @@ describe("RolloverVault", function () { }); }); - describe("#updateMinDeploymentAmt", function () { + describe("#updateReservedUnderlyingBal", function () { let tx: Transaction; beforeEach(async function () { await vault.connect(deployer).updateKeeper(await otherUser.getAddress()); @@ -299,7 +299,7 @@ describe("RolloverVault", function () { describe("when triggered by non-keeper", function () { it("should revert", async function () { - await expect(vault.connect(deployer).updateMinDeploymentAmt(0)).to.be.revertedWithCustomError( + await expect(vault.connect(deployer).updateReservedUnderlyingBal(0)).to.be.revertedWithCustomError( vault, "UnauthorizedCall", ); @@ -308,16 +308,16 @@ describe("RolloverVault", function () { describe("when triggered by keeper", function () { beforeEach(async function () { - tx = await vault.connect(otherUser).updateMinDeploymentAmt(toFixedPtAmt("1000")); + tx = await vault.connect(otherUser).updateReservedUnderlyingBal(toFixedPtAmt("1000")); await tx; }); it("should update the min deployment amount", async function () { - expect(await vault.minDeploymentAmt()).to.eq(toFixedPtAmt("1000")); + expect(await vault.reservedUnderlyingBal()).to.eq(toFixedPtAmt("1000")); }); }); }); - describe("#updateMinUnderlyingBal", function () { + describe("#updateReservedUnderlyingBal", function () { let tx: Transaction; beforeEach(async function () { await vault.connect(deployer).updateKeeper(await otherUser.getAddress()); @@ -325,7 +325,7 @@ describe("RolloverVault", function () { describe("when triggered by non-keeper", function () { it("should revert", async function () { - await expect(vault.connect(deployer).updateMinUnderlyingBal(0)).to.be.revertedWithCustomError( + await expect(vault.connect(deployer).updateReservedUnderlyingBal(0)).to.be.revertedWithCustomError( vault, "UnauthorizedCall", ); @@ -334,16 +334,16 @@ describe("RolloverVault", function () { describe("when triggered by keeper", function () { beforeEach(async function () { - tx = await vault.connect(otherUser).updateMinUnderlyingBal(toFixedPtAmt("1000")); + tx = await vault.connect(otherUser).updateReservedUnderlyingBal(toFixedPtAmt("1000")); await tx; }); it("should update the min underlying balance", async function () { - expect(await vault.minUnderlyingBal()).to.eq(toFixedPtAmt("1000")); + expect(await vault.reservedUnderlyingBal()).to.eq(toFixedPtAmt("1000")); }); }); }); - describe("#updateMinUnderlyingPerc", function () { + describe("#updateReservedSubscriptionPerc", function () { let tx: Transaction; beforeEach(async function () { await vault.connect(deployer).updateKeeper(await otherUser.getAddress()); @@ -351,7 +351,7 @@ describe("RolloverVault", function () { describe("when triggered by non-keeper", function () { it("should revert", async function () { - await expect(vault.connect(deployer).updateMinUnderlyingPerc(0)).to.be.revertedWithCustomError( + await expect(vault.connect(deployer).updateReservedSubscriptionPerc(0)).to.be.revertedWithCustomError( vault, "UnauthorizedCall", ); @@ -360,11 +360,11 @@ describe("RolloverVault", function () { describe("when triggered by keeper", function () { beforeEach(async function () { - tx = await vault.connect(otherUser).updateMinUnderlyingPerc(toPercFixedPtAmt("0.1")); + tx = await vault.connect(otherUser).updateReservedSubscriptionPerc(toPercFixedPtAmt("0.1")); await tx; }); it("should update the min underlying balance", async function () { - expect(await vault.minUnderlyingPerc()).to.eq(toPercFixedPtAmt("0.1")); + expect(await vault.reservedSubscriptionPerc()).to.eq(toPercFixedPtAmt("0.1")); }); }); }); diff --git a/spot-contracts/test/rollover-vault/RolloverVault_deploy.ts b/spot-contracts/test/rollover-vault/RolloverVault_deploy.ts index c2e356c4..3f85a0eb 100644 --- a/spot-contracts/test/rollover-vault/RolloverVault_deploy.ts +++ b/spot-contracts/test/rollover-vault/RolloverVault_deploy.ts @@ -110,13 +110,13 @@ describe("RolloverVault", function () { describe("#deploy", function () { describe("when usable balance is zero", function () { it("should revert", async function () { - await expect(vault.deploy()).to.be.revertedWithCustomError(vault, "InsufficientLiquidity"); + await expect(vault.deploy()).to.be.revertedWithCustomError(vault, "InsufficientDeployment"); }); }); - describe("when minUnderlyingBal is not set", function () { + describe("when reservedUnderlyingBal is not set", function () { beforeEach(async function () { - await vault.updateMinUnderlyingBal(toFixedPtAmt("0")); + await vault.updateReservedUnderlyingBal(toFixedPtAmt("0")); }); describe("when usable balance is lower than the min deployment", function () { @@ -140,18 +140,18 @@ describe("RolloverVault", function () { }); }); - describe("when minUnderlyingBal is set", function () { + describe("when reservedUnderlyingBal is set", function () { beforeEach(async function () { - await vault.updateMinUnderlyingBal(toFixedPtAmt("25")); + await vault.updateReservedUnderlyingBal(toFixedPtAmt("25")); }); - describe("when usable balance is lower than the minUnderlyingBal", function () { + describe("when usable balance is lower than the reservedUnderlyingBal", function () { beforeEach(async function () { await collateralToken.transfer(vault.address, toFixedPtAmt("20")); await vault.updateMinDeploymentAmt(toFixedPtAmt("1")); }); it("should revert", async function () { - await expect(vault.deploy()).to.be.revertedWithCustomError(vault, "InsufficientLiquidity"); + await expect(vault.deploy()).to.be.revertedWithCustomError(vault, "InsufficientDeployment"); }); }); diff --git a/spot-contracts/test/rollover-vault/RolloverVault_swap.ts b/spot-contracts/test/rollover-vault/RolloverVault_swap.ts index 08c5ff06..fa686e38 100644 --- a/spot-contracts/test/rollover-vault/RolloverVault_swap.ts +++ b/spot-contracts/test/rollover-vault/RolloverVault_swap.ts @@ -303,8 +303,8 @@ describe("RolloverVault", function () { describe("when absolute liquidity is too low", function () { beforeEach(async function () { - await vault.updateMinUnderlyingBal(toFixedPtAmt("1000")); - await vault.updateMinUnderlyingPerc(0); + await vault.updateReservedUnderlyingBal(toFixedPtAmt("1000")); + await vault.updateReservedSubscriptionPerc(0); }); it("should be reverted", async function () { await expect(vault.swapUnderlyingForPerps(toFixedPtAmt("50"))).to.be.revertedWithCustomError( @@ -317,15 +317,15 @@ describe("RolloverVault", function () { describe("when percentage of liquidity is too low", function () { beforeEach(async function () { - await vault.updateMinUnderlyingBal(0); - await vault.updateMinUnderlyingPerc(toPercFixedPtAmt("0.40")); + await vault.updateReservedUnderlyingBal(0); + await vault.updateReservedSubscriptionPerc(toPercFixedPtAmt("0.25")); }); it("should be reverted", async function () { await expect(vault.swapUnderlyingForPerps(toFixedPtAmt("100"))).to.be.revertedWithCustomError( vault, "InsufficientLiquidity", ); - await expect(vault.swapUnderlyingForPerps(toFixedPtAmt("99"))).not.to.be.reverted; + await expect(vault.swapUnderlyingForPerps(toFixedPtAmt("50"))).not.to.be.reverted; }); });