Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dont burn unlocked #77

Merged
merged 3 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
98 changes: 50 additions & 48 deletions src/TokenizedStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1028,8 +1028,8 @@ contract TokenizedStrategy {
uint256 newTotalAssets = IBaseStrategy(address(this))
.harvestAndReport();

// Burn unlocked shares.
_burnUnlockedShares();
// Get the amount of shares we need to burn
uint256 sharesToBurn = _unlockedShares();

// Initialize variables needed throughout.
uint256 totalFees;
Expand All @@ -1046,30 +1046,29 @@ contract TokenizedStrategy {
}

address protocolFeesRecipient;
uint256 performanceFeeShares;
uint256 totalFeeShares;
uint256 protocolFeeShares;
// If performance fees are 0 so will protocol fees.
if (totalFees != 0) {
// We need to get the shares to issue for the fees at
// current PPS before any minting or burning.
totalFeeShares = convertToShares(totalFees);

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

// 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;
}

// We need to get the shares to issue for the fees at
// current PPS before any minting or burning.
unchecked {
performanceFeeShares = convertToShares(
totalFees - protocolFees
);
}
if (protocolFees != 0) {
protocolFeeShares = convertToShares(protocolFees);
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;
}
}
}

Expand All @@ -1079,37 +1078,56 @@ contract TokenizedStrategy {
unchecked {
sharesToLock = convertToShares(profit - totalFees);
}
// Mint the shares to lock the strategy.
_mint(address(this), sharesToLock);

// If we are burning more than re-locking.
if (sharesToBurn > sharesToLock) {
// Burn the difference
unchecked {
_burn(address(this), sharesToBurn - sharesToLock);
}
} else if (sharesToLock > sharesToBurn) {
// Mint the shares to lock the strategy.
unchecked {
_mint(address(this), sharesToLock - sharesToBurn);
}
}
}

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

if (protocolFeeShares != 0) {
_mint(protocolFeesRecipient, protocolFeeShares);
}
} else {
// We have a loss.
// Expect we have a loss.
unchecked {
loss = oldTotalAssets - newTotalAssets;
}

// 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(
// Add the equivalent shares to the amount to try and burn.
// 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(loss)
// Try and burn both the shares unlocked and the amount for the loss.
convertToShares(loss) + sharesToBurn
);
}

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

Expand Down Expand Up @@ -1157,6 +1175,9 @@ contract TokenizedStrategy {
S.totalIdle = newIdle;
S.totalDebt = newTotalAssets - newIdle;

// Reset lastReport.
_strategyStorage().lastReport = uint128(block.timestamp);

// Emit event with info
emit Reported(
profit,
Expand All @@ -1166,25 +1187,6 @@ contract TokenizedStrategy {
);
}

/**
* @dev Called during reports to burn shares that have been unlocked
* since the last report.
*
* Will reset the `lastReport` since this is only called during reports.
*/
function _burnUnlockedShares() private {
uint256 unlocked = _unlockedShares();

// Reset lastReport no matter what.
_strategyStorage().lastReport = uint128(block.timestamp);

if (unlocked == 0) {
return;
}

_burn(address(this), unlocked);
}

/**
* @notice Get how many shares have been unlocked since last report.
* @return . The amount of shares that have unlocked.
Expand Down
114 changes: 112 additions & 2 deletions src/test/ProfitLocking.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -921,8 +921,8 @@ contract ProfitLockingTest is Setup {
uint256 newAmount = _amount + profit;

uint256 secondExpectedSharesForFees = strategy.convertToShares(
expectedPerformanceFee
) + strategy.convertToShares(expectedProtocolFee);
expectedPerformanceFee + expectedProtocolFee
);

createAndCheckProfit(
strategy,
Expand Down Expand Up @@ -1869,6 +1869,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
Loading