Skip to content

Commit

Permalink
updated liq check condition based on sr (#222)
Browse files Browse the repository at this point in the history
  • Loading branch information
aalavandhan authored Sep 13, 2024
1 parent e8bfda7 commit 8c4f781
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 62 deletions.
88 changes: 52 additions & 36 deletions spot-contracts/contracts/RolloverVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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_;
}

//--------------------------------------------------------------------------
Expand All @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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)
);
}
}
28 changes: 14 additions & 14 deletions spot-contracts/test/rollover-vault/RolloverVault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -291,15 +291,15 @@ describe("RolloverVault", function () {
});
});

describe("#updateMinDeploymentAmt", function () {
describe("#updateReservedUnderlyingBal", function () {
let tx: Transaction;
beforeEach(async function () {
await vault.connect(deployer).updateKeeper(await otherUser.getAddress());
});

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",
);
Expand All @@ -308,24 +308,24 @@ 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());
});

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",
);
Expand All @@ -334,24 +334,24 @@ 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());
});

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",
);
Expand All @@ -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"));
});
});
});
Expand Down
14 changes: 7 additions & 7 deletions spot-contracts/test/rollover-vault/RolloverVault_deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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");
});
});

Expand Down
10 changes: 5 additions & 5 deletions spot-contracts/test/rollover-vault/RolloverVault_swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
});
});

Expand Down

0 comments on commit 8c4f781

Please sign in to comment.