Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
54877f0
updated unit tests to use ethers v6, using custom mocking library
aalavandhan Mar 5, 2025
a7a5f41
updated tasks and deps
aalavandhan Mar 6, 2025
e8a35ed
updated perp mint fees to be paid to the vault
aalavandhan Mar 8, 2025
eff36d7
removed perp share of fees from flash swaps, all fees go to the vault
aalavandhan Mar 8, 2025
433263f
added configurable dr soft bounds (curve cutoff point) to flash swap …
aalavandhan Mar 8, 2025
28db923
updated unit tests
aalavandhan Mar 8, 2025
293c25d
removed rollover fees from perp
aalavandhan Mar 9, 2025
3d11ca3
Daily Rebalance
aalavandhan Mar 11, 2025
061aaf9
updated unit tests
aalavandhan Mar 11, 2025
e11e203
constant rate enrichment/debasement
aalavandhan Mar 12, 2025
0459829
mint2, redeem2
aalavandhan Mar 12, 2025
2c6b1f2
added linked library to limit contract size
aalavandhan Mar 14, 2025
5a28255
unit tests
aalavandhan Mar 14, 2025
272be4a
melding perps immediately after rebalance
aalavandhan Mar 14, 2025
8b7a29d
Apply suggestions from code review
aalavandhan Mar 24, 2025
f1ed255
Update spot-contracts/contracts/FeePolicy.sol
aalavandhan Mar 24, 2025
0fc7733
code review fixes
aalavandhan Mar 24, 2025
70ea8d1
code review fixes v2
aalavandhan Mar 27, 2025
654d290
code review fixes v3
aalavandhan Mar 27, 2025
68ad680
code review v4
aalavandhan Mar 31, 2025
8e9f016
during rebalance, vault pays perps by transferring tranches into perp…
aalavandhan Mar 31, 2025
e461e1e
rebalance eql
aalavandhan Mar 31, 2025
76f3831
configurable rebalance freq
aalavandhan Apr 1, 2025
f9fdff0
code review v5
aalavandhan Apr 3, 2025
3ef2756
Apply suggestions from code review
aalavandhan Apr 9, 2025
b23ad28
code review fixes
aalavandhan Apr 16, 2025
2403fb5
Code review fixes
aalavandhan Apr 17, 2025
2c97ec7
Merge branch 'rollover-fee-removal' into rebal-param-update
aalavandhan Apr 18, 2025
4a5ce29
Merge pull request #249 from ampleforth/rebal-param-update
aalavandhan Apr 18, 2025
5bdecd3
Merge branch 'rollover-fee-removal' into pair-ops
aalavandhan Apr 18, 2025
6779862
Merge pull request #247 from ampleforth/pair-ops
aalavandhan Apr 18, 2025
abc1164
Merge pull request #246 from ampleforth/rollover-fee-removal
aalavandhan Apr 18, 2025
eed0d39
Merge pull request #243 from ampleforth/fee-restructure
aalavandhan Apr 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 196 additions & 105 deletions spot-contracts/contracts/FeePolicy.sol

Large diffs are not rendered by default.

130 changes: 60 additions & 70 deletions spot-contracts/contracts/PerpetualTranche.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { BondHelpers } from "./_utils/BondHelpers.sol";
* into the reserve. At any time, the reserve holds at most 2 classes of tokens
* i.e) the seniors and the underlying collateral.
*
* Incentivized parties can "rollover" tranches approaching maturity or the underlying collateral,
* The rollover vault can "rollover" tranches approaching maturity or the underlying collateral,
* for newer seniors (which expire further out in the future) that belong to the updated "depositBond".
*
*
Expand All @@ -45,6 +45,8 @@ import { BondHelpers } from "./_utils/BondHelpers.sol";
* This brings the system storage state up to date.
*
* CRITICAL: On the 3 main system operations: deposit, redeem and rollover;
*
* The system charges a fee for minting and burning perp tokens, which are paid to the vault.
* We first compute fees before executing any transfers in or out of the system.
* The ordering of operations is very important as the fee computation logic,
* requires the system TVL as an input and which should be recorded prior to any value
Expand All @@ -57,6 +59,12 @@ import { BondHelpers } from "./_utils/BondHelpers.sol";
* When computing the value of assets in the system, the code always over-values by
* rounding up. When computing the value of incoming assets, the code rounds down.
*
* @dev Demand imbalance between perp and the vault
* is restored through a "rebalancing" mechanism similar to a funding rate. When value needs to flow from perp to the vault,
* the system debases the value of perp tokens by minting perp tokens to the vault.
* When value needs to flow from the vault to perp, the underlying collateral tokens are
* transferred from the vault into perp's reserve thereby enriching the value of perp tokens.
*
*/
contract PerpetualTranche is
ERC20BurnableUpgradeable,
Expand Down Expand Up @@ -381,8 +389,8 @@ contract PerpetualTranche is
}

// Calculates the fee adjusted amount of perp tokens minted when depositing `trancheInAmt` of tranche tokens
// NOTE: This operation should precede any token transfers.
uint256 perpAmtMint = _computeMintAmt(trancheIn, trancheInAmt);
// NOTE: This calculation should precede any token transfers.
(uint256 perpAmtMint, uint256 perpFeeAmt) = _computeMintAmt(trancheIn, trancheInAmt);
if (trancheInAmt <= 0 || perpAmtMint <= 0) {
return 0;
}
Expand All @@ -393,6 +401,9 @@ contract PerpetualTranche is
// mints perp tokens to the sender
_mint(msg.sender, perpAmtMint);

// mint fees are paid to the vault by minting perp tokens.
_mint(address(vault), perpFeeAmt);

// post-deposit checks
_enforceMintCaps(trancheIn);

Expand All @@ -401,19 +412,22 @@ contract PerpetualTranche is

/// @inheritdoc IPerpetualTranche
function redeem(
uint256 perpAmtBurnt
uint256 perpAmt
) external override afterStateUpdate nonReentrant whenNotPaused returns (TokenAmount[] memory) {
// verifies if burn amount is acceptable
if (perpAmtBurnt <= 0) {
if (perpAmt <= 0) {
return new TokenAmount[](0);
}

// Calculates the fee adjusted share of reserve tokens to be redeemed
// NOTE: This operation should precede any token transfers.
TokenAmount[] memory tokensOut = _computeRedemptionAmts(perpAmtBurnt);
// NOTE: This calculation should precede any token transfers.
(TokenAmount[] memory tokensOut, uint256 perpFeeAmt) = _computeRedemptionAmts(perpAmt);

// burns perp tokens from the sender
_burn(msg.sender, perpAmtBurnt);
_burn(msg.sender, perpAmt - perpFeeAmt);

// redemption fees are paid to transferring perp tokens
transfer(address(vault), perpFeeAmt);

// transfers reserve tokens out
uint8 tokensOutCount = uint8(tokensOut.length);
Expand All @@ -437,8 +451,8 @@ contract PerpetualTranche is
revert UnacceptableRollover();
}

// Calculates the fee adjusted amount of tranches exchanged during a rolled over
// NOTE: This operation should precede any token transfers.
// Calculates the amount of tranches exchanged during a rolled over
// NOTE: This calculation should precede any token transfers.
RolloverData memory r = _computeRolloverAmt(trancheIn, tokenOut, trancheInAmtAvailable);

// Verifies if rollover amount is acceptable
Expand All @@ -455,6 +469,15 @@ contract PerpetualTranche is
return r;
}

/// @inheritdoc IPerpetualTranche
/// @dev CAUTION: Only the whitelisted vault can call this function.
/// The logic controlling the frequency and magnitude of debasement should be vetted.
function rebalanceToVault(
uint256 underlyingAmtToTransfer
) external override onlyVault afterStateUpdate nonReentrant whenNotPaused {
_mint(address(vault), underlyingAmtToTransfer.mulDiv(totalSupply(), _reserveValue()));
}

/// @inheritdoc IPerpetualTranche
function getDepositBond() external override afterStateUpdate returns (IBondController) {
return _depositBond;
Expand Down Expand Up @@ -564,14 +587,14 @@ contract PerpetualTranche is
if (!_isDepositTranche(trancheIn)) {
revert UnexpectedAsset();
}
return _computeMintAmt(trancheIn, trancheInAmt);
(uint256 perpAmtMint, ) = _computeMintAmt(trancheIn, trancheInAmt);
return perpAmtMint;
}

/// @inheritdoc IPerpetualTranche
function computeRedemptionAmts(
uint256 perpAmtBurnt
) external override afterStateUpdate returns (TokenAmount[] memory) {
return _computeRedemptionAmts(perpAmtBurnt);
function computeRedemptionAmts(uint256 perpAmt) external override afterStateUpdate returns (TokenAmount[] memory) {
(TokenAmount[] memory tokensOut, ) = _computeRedemptionAmts(perpAmt);
return tokensOut;
}

/// @inheritdoc IPerpetualTranche
Expand Down Expand Up @@ -724,7 +747,10 @@ contract PerpetualTranche is
}

/// @dev Computes the fee adjusted perp mint amount for given amount of tranche tokens deposited into the reserve.
function _computeMintAmt(ITranche trancheIn, uint256 trancheInAmt) private view returns (uint256) {
function _computeMintAmt(
ITranche trancheIn,
uint256 trancheInAmt
) private view returns (uint256 perpAmtMint, uint256 perpFeeAmt) {
uint256 valueIn = _computeReserveTrancheValue(
trancheIn,
_depositBond,
Expand All @@ -740,68 +766,56 @@ contract PerpetualTranche is

// Compute mint amt
uint256 perpSupply = totalSupply();
uint256 perpAmtMint = valueIn;
perpAmtMint = valueIn;
if (perpSupply > 0) {
perpAmtMint = perpAmtMint.mulDiv(perpSupply, _reserveValue());
}

// The mint fees are settled by simply minting fewer perps.
// Compute the fee amount
if (feePerc > 0) {
perpAmtMint = perpAmtMint.mulDiv(ONE - feePerc, ONE);
perpFeeAmt = perpAmtMint.mulDiv(feePerc, ONE, MathUpgradeable.Rounding.Up);
perpAmtMint -= perpFeeAmt;
}

return perpAmtMint;
}

/// @dev Computes the reserve token amounts redeemed when a given number of perps are burnt.
function _computeRedemptionAmts(uint256 perpAmtBurnt) private view returns (TokenAmount[] memory) {
function _computeRedemptionAmts(
uint256 perpAmt
) private view returns (TokenAmount[] memory reserveTokens, uint256 perpFeeAmt) {
uint256 perpSupply = totalSupply();

//-----------------------------------------------------------------------------
// We charge no burn fee when interacting with other parts of the system.
uint256 feePerc = _isProtocolCaller() ? 0 : feePolicy.computePerpBurnFeePerc();
//-----------------------------------------------------------------------------

// Compute the fee amount
if (feePerc > 0) {
perpFeeAmt = perpAmt.mulDiv(feePerc, ONE, MathUpgradeable.Rounding.Up);
perpAmt -= perpFeeAmt;
}

// Compute redemption amounts
uint8 reserveCount = uint8(_reserves.length());
TokenAmount[] memory reserveTokens = new TokenAmount[](reserveCount);
reserveTokens = new TokenAmount[](reserveCount);
for (uint8 i = 0; i < reserveCount; ++i) {
IERC20Upgradeable tokenOut = _reserveAt(i);
reserveTokens[i] = TokenAmount({
token: tokenOut,
amount: tokenOut.balanceOf(address(this)).mulDiv(perpAmtBurnt, perpSupply)
amount: tokenOut.balanceOf(address(this)).mulDiv(perpAmt, perpSupply)
});

// The burn fees are settled by simply redeeming for fewer tranches.
if (feePerc > 0) {
reserveTokens[i].amount = reserveTokens[i].amount.mulDiv(ONE - feePerc, ONE);
}
}

return (reserveTokens);
return (reserveTokens, perpFeeAmt);
}

/// @dev Computes the amount of reserve tokens that can be rolled out for the given amount of tranches deposited.
/// The relative ratios of tokens In/Out are adjusted based on the current rollover fee perc.
function _computeRolloverAmt(
ITranche trancheIn,
IERC20Upgradeable tokenOut,
uint256 trancheInAmtAvailable
) private view returns (RolloverData memory) {
//-----------------------------------------------------------------------------
// The rollover fees are settled by, adjusting the exchange rate
// between `trancheInAmt` and `tokenOutAmt`.
//
int256 feePerc = feePolicy.computePerpRolloverFeePerc(
feePolicy.computeDeviationRatio(
SubscriptionParams({
perpTVL: _reserveValue(),
vaultTVL: vault.getTVL(),
seniorTR: _depositBond.getSeniorTrancheRatio()
})
)
);
//-----------------------------------------------------------------------------

// We compute "price" as the value of a unit token.
// The perp, tranche tokens and the underlying are denominated as fixed point numbers
Expand Down Expand Up @@ -832,8 +846,8 @@ contract PerpetualTranche is
return RolloverData({ trancheInAmt: 0, tokenOutAmt: 0 });
}
//-----------------------------------------------------------------------------
// Basic rollover with fees:
// (1 +/- f) . (trancheInAmt . trancheInPrice) = (tokenOutAmt . tokenOutPrice)
// Basic rollover:
// (trancheInAmt . trancheInPrice) = (tokenOutAmt . tokenOutPrice)
//-----------------------------------------------------------------------------

// Using perp's tokenOutBalance, we calculate the amount of tokens in to rollover
Expand All @@ -844,17 +858,6 @@ contract PerpetualTranche is
trancheInAmt: tokenOutBalance.mulDiv(tokenOutPrice, trancheInPrice, MathUpgradeable.Rounding.Up)
});

// A positive fee percentage implies that perp charges rotators by
// offering tranchesOut for a premium, i.e) more tranches in.
if (feePerc > 0) {
r.trancheInAmt = r.trancheInAmt.mulDiv(ONE, ONE - feePerc.toUint256(), MathUpgradeable.Rounding.Up);
}
// A negative fee percentage (or a reward) implies that perp pays the rotators by
// offering tranchesOut at a discount, i.e) fewer tranches in.
else if (feePerc < 0) {
r.trancheInAmt = r.trancheInAmt.mulDiv(ONE, ONE + feePerc.abs(), MathUpgradeable.Rounding.Up);
}

//-----------------------------------------------------------------------------

// When the trancheInAmt exceeds trancheInAmtAvailable:
Expand All @@ -864,19 +867,6 @@ contract PerpetualTranche is
// Given the amount of tranches In, we compute the amount of tokens out
r.trancheInAmt = trancheInAmtAvailable;
r.tokenOutAmt = trancheInAmtAvailable.mulDiv(trancheInPrice, tokenOutPrice);

// A positive fee percentage implies that perp charges rotators by
// accepting tranchesIn at a discount, i.e) fewer tokens out.
// This results in perp enrichment.
if (feePerc > 0) {
r.tokenOutAmt = r.tokenOutAmt.mulDiv(ONE - feePerc.abs(), ONE);
}
// A negative fee percentage (or a reward) implies that perp pays the rotators by
// accepting tranchesIn at a premium, i.e) more tokens out.
// This results in perp debasement.
else if (feePerc < 0) {
r.tokenOutAmt = r.tokenOutAmt.mulDiv(ONE + feePerc.abs(), ONE);
}
}

return r;
Expand Down
Loading