Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/nicka/lp' into clement/invariant…
Browse files Browse the repository at this point in the history
…-testing
  • Loading branch information
clement-ux committed Oct 3, 2024
2 parents 257468e + 19b55d7 commit 084a0ba
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 168 deletions.
103 changes: 42 additions & 61 deletions src/contracts/AbstractARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// 2,000 = 20% performance fee
/// 500 = 5% performance fee
uint16 public fee;
/// @notice The performance fees accrued but not collected.
/// This is removed from the available assets.
uint112 public feesAccrued;
/// @notice The available assets at the last time performance fees were calculated.
/// This can only go up so is a high watermark.
uint128 public lastAvailableAssets;
/// @notice The available assets the the last time performance fees were collected and adjusted
/// for liquidity assets (WETH) deposited and redeemed.
/// This can be negative if there were asset gains and then all the liquidity providers redeemed.
int128 public lastAvailableAssets;
/// @notice The account that can collect the performance fee
address public feeCollector;

Expand All @@ -118,7 +116,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp
);
event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets);
event FeeCalculated(uint256 newFeesAccrued, uint256 assetIncrease);
event FeeCollected(address indexed feeCollector, uint256 fee);
event FeeUpdated(uint256 fee);
event FeeCollectorUpdated(address indexed newFeeCollector);
Expand Down Expand Up @@ -168,7 +165,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {

// Initialize the last available assets to the current available assets
// This ensures no performance fee is accrued when the performance fee is calculated when the fee is set
lastAvailableAssets = SafeCast.toUint128(_availableAssets());
lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(_availableAssets()));
_setFee(_fee);
_setFeeCollector(_feeCollector);

Expand Down Expand Up @@ -406,10 +403,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// @param assets The amount of liquidity assets to deposit
/// @return shares The amount of shares that were minted
function deposit(uint256 assets) external returns (uint256 shares) {
// Accrue any performance fees based on the increase in available assets before
// the liquidity asset from the deposit is transferred into the ARM
_accruePerformanceFee();

// Calculate the amount of shares to mint after the performance fees have been accrued
// which reduces the available assets, and before new assets are deposited.
shares = convertToShares(assets);
Expand All @@ -421,7 +414,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
_mint(msg.sender, shares);

// Add the deposited assets to the last available assets
lastAvailableAssets = SafeCast.toUint128(lastAvailableAssets + assets);
lastAvailableAssets += SafeCast.toInt128(SafeCast.toInt256(assets));

// Check the liquidity provider caps after the new assets have been deposited
if (liquidityProviderController != address(0)) {
Expand All @@ -443,10 +436,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// @return requestId The index of the withdrawal request
/// @return assets The amount of liquidity assets that will be claimable by the redeemer
function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) {
// Accrue any performance fees based on the increase in available assets before
// the liquidity asset from the redeem is reserved for the ARM withdrawal queue
_accruePerformanceFee();

// Calculate the amount of assets to transfer to the redeemer
assets = convertToAssets(shares);

Expand All @@ -471,7 +460,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
_burn(msg.sender, shares);

// Remove the redeemed assets from the last available assets
lastAvailableAssets = SafeCast.toUint128(lastAvailableAssets - assets);
lastAvailableAssets -= SafeCast.toInt128(SafeCast.toInt256(assets));

emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp);
}
Expand Down Expand Up @@ -566,21 +555,17 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// @notice The total amount of assets in the ARM and external withdrawal queue,
/// less the liquidity assets reserved for the ARM's withdrawal queue and accrued fees.
function totalAssets() public view virtual returns (uint256) {
uint256 availableAssetsBeforeFees = _availableAssets();

// If the available assets have decreased, then we don't charge a performance fee
if (availableAssetsBeforeFees <= lastAvailableAssets) return availableAssetsBeforeFees;
(uint256 fees, uint256 newAvailableAssets) = _feesAccrued();

// Calculate the increase in assets since the last time fees were calculated
uint256 assetIncrease = availableAssetsBeforeFees - lastAvailableAssets;
if (fees > newAvailableAssets) return 0;

// Calculate the performance fee and remove from the available assets before new fees are removed
return availableAssetsBeforeFees - ((assetIncrease * fee) / FEE_SCALE);
// Calculate the performance fee and remove from the available assets
return newAvailableAssets - fees;
}

/// @dev Calculate the available assets which is the assets in the ARM, external withdrawal queue,
/// less liquidity assets reserved for the ARM's withdrawal queue and accrued fees.
/// The accrued fees are from the last time fees were calculated.
/// less liquidity assets reserved for the ARM's withdrawal queue.
/// This does not exclude any accrued performance fees.
function _availableAssets() internal view returns (uint256) {
// Get the assets in the ARM and external withdrawal queue
uint256 assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue();
Expand All @@ -591,13 +576,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {

// If the ARM becomes insolvent enough that the available assets in the ARM and external withdrawal queue
// is less than the outstanding withdrawals and accrued fees.
if (assets + claimedMem < queuedMem + feesAccrued) {
if (assets + claimedMem < queuedMem) {
return 0;
}

// Need to remove the liquidity assets that have been reserved for the withdrawal queue
// and any accrued fees
return assets + claimedMem - queuedMem - feesAccrued;
return assets + claimedMem - queuedMem;
}

/// @dev Hook for calculating the amount of assets in an external withdrawal queue like Lido or OETH
Expand Down Expand Up @@ -627,27 +611,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
/// Performance Fee Functions
////////////////////////////////////////////////////

/// @dev Accrues the performance fee based on the increase in available assets
/// Needs to be called before any action that changes the liquidity provider shares. eg deposit and redeem
function _accruePerformanceFee() internal {
uint256 newAvailableAssets = _availableAssets();

// Do not accrued a performance fee if the available assets has decreased
if (newAvailableAssets <= lastAvailableAssets) return;

uint256 assetIncrease = newAvailableAssets - lastAvailableAssets;
uint256 newFeesAccrued = (assetIncrease * fee) / FEE_SCALE;

// Save the new accrued fees back to storage
feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued);
// Save the new available assets back to storage less the new accrued fees.
// This is be updated again in the post deposit and post withdraw hooks to include
// the assets deposited or withdrawn
lastAvailableAssets = SafeCast.toUint128(newAvailableAssets - newFeesAccrued);

emit FeeCalculated(newFeesAccrued, assetIncrease);
}

/// @notice Owner sets the performance fee on increased assets
/// @param _fee The performance fee measured in basis points (1/100th of a percent)
/// 10,000 = 100% performance fee
Expand All @@ -664,8 +627,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {
function _setFee(uint256 _fee) internal {
require(_fee <= FEE_SCALE, "ARM: fee too high");

// Accrued any performance fees up to this point using the old fee
_accruePerformanceFee();
// Collect any performance fees up to this point using the old fee
collectFees();

fee = SafeCast.toUint16(_fee);

Expand All @@ -682,19 +645,37 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable {

/// @notice Transfer accrued performance fees to the fee collector
/// This requires enough liquidity assets in the ARM to cover the accrued fees.
function collectFees() external returns (uint256 fees) {
function collectFees() public returns (uint256 fees) {
uint256 newAvailableAssets;
// Accrue any performance fees up to this point
_accruePerformanceFee();
(fees, newAvailableAssets) = _feesAccrued();

// Read the updated accrued fees from storage
fees = feesAccrued;
require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity");
if (fee == 0) return fees;

// Reset the accrued fees in storage
feesAccrued = 0;
require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity");

IERC20(liquidityAsset).transfer(feeCollector, fees);

// Save the new available assets back to storage less the collected fees.
lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(newAvailableAssets) - SafeCast.toInt256(fees));

emit FeeCollected(feeCollector, fees);
}

/// @notice Calculates the performance fees accrued since the last time fees were collected
function feesAccrued() public view returns (uint256 fees) {
(fees,) = _feesAccrued();
}

function _feesAccrued() public view returns (uint256 fees, uint256 newAvailableAssets) {
newAvailableAssets = _availableAssets();

// Calculate the increase in assets since the last time fees were calculated
int256 assetIncrease = SafeCast.toInt256(newAvailableAssets) - lastAvailableAssets;

// Do not accrued a performance fee if the available assets has decreased
if (assetIncrease <= 0) return (0, newAvailableAssets);

fees = SafeCast.toUint256(assetIncrease) * fee / FEE_SCALE;
}
}
10 changes: 5 additions & 5 deletions test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ {
assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT);
assertEq(lidoARM.outstandingEther(), 0);
assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees
assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY);
assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY));
assertEq(lidoARM.balanceOf(address(this)), 0);
assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY);
assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0);
Expand All @@ -142,7 +142,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ {
assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY);
assertEq(lidoARM.outstandingEther(), 0);
assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees
assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY);
assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY));
assertEq(lidoARM.balanceOf(address(this)), 0);
assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY);
assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0);
Expand Down Expand Up @@ -186,7 +186,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ {
assertEq(weth.balanceOf(address(lidoARM)), 0);
assertEq(lidoARM.outstandingEther(), 0);
assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees
assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY);
assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY));
assertEq(lidoARM.balanceOf(address(this)), 0);
assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY);
assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0);
Expand All @@ -210,7 +210,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ {
assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2);
assertEq(lidoARM.outstandingEther(), 0);
assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees
assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY);
assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY));
assertEq(lidoARM.balanceOf(address(this)), 0);
assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY);
assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0);
Expand All @@ -233,7 +233,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ {
assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY);
assertEq(lidoARM.outstandingEther(), 0);
assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees
assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY);
assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY));
assertEq(lidoARM.balanceOf(address(this)), 0);
assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY);
assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0);
Expand Down
2 changes: 1 addition & 1 deletion test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ contract Fork_Concrete_LidoARM_Constructor_Test is Fork_Shared_Test_ {
assertEq(lidoARM.operator(), operator);
assertEq(lidoARM.feeCollector(), feeCollector);
assertEq(lidoARM.fee(), 2000);
assertEq(lidoARM.lastAvailableAssets(), 1e12);
assertEq(lidoARM.lastAvailableAssets(), int256(1e12));
assertEq(lidoARM.feesAccrued(), 0);
// the 20% performance fee is removed on initialization
assertEq(lidoARM.totalAssets(), 1e12);
Expand Down
Loading

0 comments on commit 084a0ba

Please sign in to comment.