Skip to content

Commit

Permalink
feat: lower report costs (#76)
Browse files Browse the repository at this point in the history
* feat: only update unlock date

* feat: dont burn unlocked (#77)

* feat: burn shares once

* feat: dont convert twice

* fix: loss shares to burn

* chore: comments

* fix: report updates

* fix: dont burn unless there is shares

* chore: rebase to storage pointer

* chore: organize reporting

* chore: rebase
  • Loading branch information
Schlagonia authored Feb 3, 2024
1 parent a6cb558 commit 037b86c
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 90 deletions.
155 changes: 75 additions & 80 deletions src/TokenizedStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1066,8 +1066,6 @@ contract TokenizedStrategy {
// Cache storage pointer since its used repeatedly.
StrategyData storage S = _strategyStorage();

uint256 oldTotalAssets = S.totalAssets;

// Tell the strategy to report the real total assets it has.
// It should do all reward selling and redepositing now and
// account for deployed and loose `asset` so we can accurately
Expand All @@ -1076,8 +1074,10 @@ contract TokenizedStrategy {
uint256 newTotalAssets = IBaseStrategy(address(this))
.harvestAndReport();

// Burn unlocked shares.
_burnUnlockedShares(S);
uint256 oldTotalAssets = _totalAssets(S);

// Get the amount of shares we need to burn from previous reports.
uint256 sharesToBurn = _unlockedShares(S);

// Initialize variables needed throughout.
uint256 totalFees;
Expand All @@ -1089,85 +1089,98 @@ contract TokenizedStrategy {
// We have a profit.
unchecked {
profit = newTotalAssets - oldTotalAssets;
// Asses performance fees.
totalFees = (profit * S.performanceFee) / MAX_BPS;
}

address protocolFeesRecipient;
uint256 performanceFeeShares;
uint256 protocolFeeShares;
// If performance fees are 0 so will protocol fees.
if (totalFees != 0) {
// Get the config from the factory.
uint16 protocolFeeBps;
(protocolFeeBps, protocolFeesRecipient) = IFactory(FACTORY)
.protocol_fee_config();
// We need to get the equivalent amount of shares
// at the current PPS before any minting or burning.
sharesToLock = _convertToShares(S, profit, Math.Rounding.Down);

// Cache the performance fee.
uint16 fee = S.performanceFee;
uint256 totalFeeShares;
// If we are charging a performance fee
if (fee != 0) {
// Asses performance fees.
unchecked {
// Get in `asset` for the event.
totalFees = (profit * fee) / MAX_BPS;
// And in shares for the payment.
totalFeeShares = (sharesToLock * fee) / MAX_BPS;
}

// Get the protocol fee config from the factory.
(
uint16 protocolFeeBps,
address protocolFeesRecipient
) = IFactory(FACTORY).protocol_fee_config();

uint256 protocolFeeShares;
// Check if there is a protocol fee to charge.
if (protocolFeeBps != 0) {
// Calculate protocol fees based on the performance Fees.
protocolFees = (totalFees * protocolFeeBps) / MAX_BPS;
unchecked {
// Calculate protocol fees based on the performance Fees.
protocolFeeShares =
(totalFeeShares * protocolFeeBps) /
MAX_BPS;
// Need amount in underlying for event.
protocolFees = (totalFees * protocolFeeBps) / MAX_BPS;
}

// Mint the protocol fees to the recipient.
_mint(S, protocolFeesRecipient, protocolFeeShares);
}

// We need to get the shares to issue for the fees at
// current PPS before any minting or burning.
// Mint the difference to the strategy fee recipient.
unchecked {
performanceFeeShares = _convertToShares(
_mint(
S,
totalFees - protocolFees,
Math.Rounding.Down
);
}
if (protocolFees != 0) {
protocolFeeShares = _convertToShares(
S,
protocolFees,
Math.Rounding.Down
S.performanceFeeRecipient,
totalFeeShares - protocolFeeShares
);
}
}

// we have a net profit. Check if we are locking profit.
// Check if we are locking profit.
if (_profitMaxUnlockTime != 0) {
// lock (profit - fees)
unchecked {
sharesToLock = _convertToShares(
S,
profit - totalFees,
Math.Rounding.Down
);
sharesToLock -= totalFeeShares;
}
// Mint the shares to lock the strategy.
_mint(S, address(this), sharesToLock);
}

// Mint fees shares to recipients.
if (performanceFeeShares != 0) {
_mint(S, S.performanceFeeRecipient, performanceFeeShares);
}

if (protocolFeeShares != 0) {
_mint(S, protocolFeesRecipient, protocolFeeShares);
// If we are burning more than re-locking.
if (sharesToBurn > sharesToLock) {
// Burn the difference
unchecked {
_burn(S, address(this), sharesToBurn - sharesToLock);
}
} else if (sharesToLock > sharesToBurn) {
// Mint the shares to lock the strategy.
unchecked {
_mint(S, address(this), sharesToLock - sharesToBurn);
}
}
}
} else {
// We have a loss.
// Expect we have a loss.
unchecked {
loss = oldTotalAssets - newTotalAssets;
}

// Check in case else was due to being equal.
// Check in case `else` was due to being equal.
if (loss != 0) {
// We will try and burn shares from any pending profit still unlocking
// to offset the loss to prevent any PPS decline post report.
uint256 sharesToBurn = Math.min(
// We will try and burn the unlocked shares and as much from any
// pending profit still unlocking to offset the loss to prevent any PPS decline post report.
sharesToBurn = Math.min(
// Cannot burn more than we have.
S.balances[address(this)],
_convertToShares(S, loss, Math.Rounding.Down)
// Try and burn both the shares already unlocked and the amount for the loss.
_convertToShares(S, loss, Math.Rounding.Down) + sharesToBurn
);
}

// Check if there is anything to burn.
if (sharesToBurn != 0) {
_burn(S, address(this), sharesToBurn);
}
// Check if there is anything to burn.
if (sharesToBurn != 0) {
_burn(S, address(this), sharesToBurn);
}
}

Expand Down Expand Up @@ -1204,8 +1217,8 @@ contract TokenizedStrategy {
);
} else {
// Only setting this to 0 will turn in the desired effect,
// no need to update fullProfitUnlockDate.
S.profitUnlockingRate = 0;
// no need to update profitUnlockingRate.
S.fullProfitUnlockDate = 0;
}

// Update the new total assets value.
Expand All @@ -1221,27 +1234,6 @@ contract TokenizedStrategy {
);
}

/**
* @dev Called during reports to burn shares that have been unlocked
* since the last report.
*
* Will reset the `lastReport` if haven't unlocked the full amount yet
* so future calculations remain correct.
*/
function _burnUnlockedShares(StrategyData storage S) internal {
uint256 unlocked = _unlockedShares(S);
if (unlocked == 0) {
return;
}

// update variables (done here to keep _unlockedShares() as a view function)
if (S.fullProfitUnlockDate > block.timestamp) {
S.lastReport = uint96(block.timestamp);
}

_burn(S, address(this), unlocked);
}

/**
* @notice Get how many shares have been unlocked since last report.
* @return . The amount of shares that have unlocked.
Expand Down Expand Up @@ -1589,8 +1581,11 @@ contract TokenizedStrategy {

// If we are setting to 0 we need to adjust amounts.
if (_profitMaxUnlockTime == 0) {
// Burn all shares if applicable.
_burn(S, address(this), S.balances[address(this)]);
uint256 shares = S.balances[address(this)];
if (shares != 0) {
// Burn all shares if applicable.
_burn(S, address(this), shares);
}
// Reset unlocking variables
S.profitUnlockingRate = 0;
S.fullProfitUnlockDate = 0;
Expand Down
127 changes: 117 additions & 10 deletions src/test/ProfitLocking.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -780,9 +780,9 @@ contract ProfitLockingTest is Setup {

uint256 newAmount = _amount + profit;

uint256 secondExpectedSharesForFees = strategy.convertToShares(
expectedProtocolFee + expectedPerformanceFee
);
uint256 secondExpectedSharesForFees = (strategy.convertToShares(
profit
) * performanceFee) / MAX_BPS;

createAndCheckProfit(
strategy,
Expand Down Expand Up @@ -920,9 +920,9 @@ contract ProfitLockingTest is Setup {

uint256 newAmount = _amount + profit;

uint256 secondExpectedSharesForFees = strategy.convertToShares(
expectedPerformanceFee
) + strategy.convertToShares(expectedProtocolFee);
uint256 secondExpectedSharesForFees = (strategy.convertToShares(
profit
) * performanceFee) / MAX_BPS;

createAndCheckProfit(
strategy,
Expand All @@ -938,10 +938,7 @@ contract ProfitLockingTest is Setup {
0,
newAmount -
((profit - totalExpectedFees) / 2) +
strategy.previewWithdraw(
profit - (expectedProtocolFee + expectedPerformanceFee)
) +
secondExpectedSharesForFees
strategy.convertToShares(profit)
);

increaseTimeAndCheckBuffer(strategy, profitMaxUnlockTime, 0);
Expand Down Expand Up @@ -1869,6 +1866,116 @@ contract ProfitLockingTest is Setup {
assertEq(strategy.pricePerShare(), wad, "pps reset");
}

function test_buffer_noGainReport(
address _address,
uint256 _amount,
uint16 _profitFactor
) public {
_amount = bound(_amount, minFuzzAmount, maxFuzzAmount);
_profitFactor = uint16(bound(uint256(_profitFactor), 10, MAX_BPS));
vm.assume(
_address != address(0) &&
_address != address(strategy) &&
_address != protocolFeeRecipient &&
_address != performanceFeeRecipient &&
_address != address(yieldSource)
);
// set fees to 0
uint16 protocolFee = 0;
uint16 performanceFee = 0;
setFees(protocolFee, performanceFee);

assertEq(strategy.profitUnlockingRate(), 0, "!rate");
assertEq(strategy.fullProfitUnlockDate(), 0, "date");

mintAndDepositIntoStrategy(strategy, _address, _amount);

// Increase time to simulate interest being earned
increaseTimeAndCheckBuffer(strategy, profitMaxUnlockTime, 0);

uint256 profit = (_amount * _profitFactor) / MAX_BPS;

uint256 expectedPerformanceFee = (profit * performanceFee) / MAX_BPS;
uint256 expectedProtocolFee = (expectedPerformanceFee * protocolFee) /
MAX_BPS;

uint256 totalExpectedFees = expectedPerformanceFee +
expectedProtocolFee;
createAndCheckProfit(
strategy,
profit,
expectedProtocolFee,
expectedPerformanceFee
);

assertEq(strategy.pricePerShare(), wad, "!pps");

checkStrategyTotals(
strategy,
_amount + profit,
_amount + profit,
0,
_amount + profit
);

increaseTimeAndCheckBuffer(
strategy,
profitMaxUnlockTime / 2,
(profit - totalExpectedFees) / 2
);

checkStrategyTotals(
strategy,
_amount + profit,
_amount + profit,
0,
_amount + profit - ((profit - totalExpectedFees) / 2)
);

// Make sure we have active unlocking
assertGt(strategy.profitUnlockingRate(), 0);
assertGt(strategy.fullProfitUnlockDate(), 0);
assertGt(strategy.balanceOf(address(strategy)), 0);
uint256 pps = strategy.pricePerShare();

// Report with no profit or loss
vm.prank(keeper);
strategy.report();

// Should be the same as before
assertEq(strategy.pricePerShare(), pps, "pps");
checkStrategyTotals(
strategy,
_amount + profit,
_amount + profit,
0,
_amount + profit - ((profit - totalExpectedFees) / 2)
);

increaseTimeAndCheckBuffer(strategy, profitMaxUnlockTime / 2, 0);

// Everything should be unlocked now.
assertRelApproxEq(
strategy.pricePerShare(),
wad + ((wad * _profitFactor) / MAX_BPS),
MAX_BPS
);
checkStrategyTotals(
strategy,
_amount + profit,
_amount + profit,
0,
_amount
);

vm.prank(_address);
strategy.redeem(_amount, _address, _address);

checkStrategyTotals(strategy, 0, 0, 0, 0);

assertEq(strategy.pricePerShare(), wad, "pps reset");
}

function test_loss_NoFeesNoBuffer_noUnlock(
address _address,
uint256 _amount,
Expand Down

0 comments on commit 037b86c

Please sign in to comment.