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

Add apply/mint interest for redemptions and leave redeemed Troves open #102

Merged
merged 2 commits into from
Apr 10, 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
109 changes: 62 additions & 47 deletions contracts/src/TroveManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,22 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana
uint decayedBaseRate;
uint price;
uint totalBoldSupplyAtStart;
uint256 totalRedistDebtGains;
uint256 totalNewRecordedTroveDebts;
uint256 totalOldRecordedTroveDebts;
uint256 totalNewWeightedRecordedTroveDebts;
uint256 totalOldWeightedRecordedTroveDebts;
}


struct SingleRedemptionValues {
uint BoldLot;
uint ETHLot;
bool cancelledPartial;
uint256 redistDebtGain;
uint256 oldRecordedTroveDebt;
uint256 newRecordedTroveDebt;
uint256 oldWeightedRecordedTroveDebt;
uint256 newWeightedRecordedTroveDebt;
}

// --- Events ---
Expand Down Expand Up @@ -827,53 +837,46 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana
)
internal returns (SingleRedemptionValues memory singleRedemption)
{
singleRedemption.oldWeightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_troveId);
singleRedemption.oldRecordedTroveDebt = Troves[_troveId].debt;

(, singleRedemption.redistDebtGain) = _getAndApplyRedistributionGains(_contractsCache.activePool, _contractsCache.defaultPool, _troveId);

// TODO: Gas. We apply accrued interest here, but could gas optimize this, since all-but-one Trove in the sequence will have their
// debt zero'd by redemption. However, gas optimization for redemption is not as critical as for borrower & SP ops.
uint256 entireTroveDebt = getTroveEntireDebt(_troveId);
_updateTroveDebt(_troveId, entireTroveDebt);

// Determine the remaining amount (lot) to be redeemed, capped by the entire debt of the Trove minus the liquidation reserve
singleRedemption.BoldLot = LiquityMath._min(_maxBoldamount, Troves[_troveId].debt - BOLD_GAS_COMPENSATION);
// TODO: should we leave gas compensation (and corresponding debt) untouched for zombie Troves? Currently it's not touched.
singleRedemption.BoldLot = LiquityMath._min(_maxBoldamount, entireTroveDebt - BOLD_GAS_COMPENSATION);

// Get the ETHLot of equivalent value in USD
singleRedemption.ETHLot = singleRedemption.BoldLot * DECIMAL_PRECISION / _price;

// Decrease the debt and collateral of the current Trove according to the Bold lot and corresponding ETH to send
uint newDebt = Troves[_troveId].debt - singleRedemption.BoldLot;
uint newColl = Troves[_troveId].coll - singleRedemption.ETHLot;

// TODO: zombi troves
if (newDebt == BOLD_GAS_COMPENSATION) {
// No debt left in the Trove (except for the liquidation reserve), therefore the trove gets closed
_removeStake(_troveId);
_closeTrove(_troveId, Status.closedByRedemption);
_redeemCloseTrove(_contractsCache, _troveId, BOLD_GAS_COMPENSATION, newColl);
emit TroveUpdated(_troveId, 0, 0, 0, TroveManagerOperation.redeemCollateral);

} else {
Troves[_troveId].debt = newDebt;
Troves[_troveId].coll = newColl;
_updateStakeAndTotalStakes(_troveId);
singleRedemption.newRecordedTroveDebt = entireTroveDebt - singleRedemption.BoldLot;
uint newColl = Troves[_troveId].coll - singleRedemption.ETHLot;

emit TroveUpdated(
_troveId,
newDebt, newColl,
Troves[_troveId].stake,
TroveManagerOperation.redeemCollateral
);
if (singleRedemption.newRecordedTroveDebt <= MIN_NET_DEBT) {
// TODO: tag it as a zombie Trove and remove from Sorted List
}
Troves[_troveId].debt = singleRedemption.newRecordedTroveDebt;
Troves[_troveId].coll = newColl;

return singleRedemption;
}
singleRedemption.newWeightedRecordedTroveDebt = getTroveWeightedRecordedDebt(_troveId);

/*
* Called when a full redemption occurs, and closes the trove.
* The redeemer swaps (debt - liquidation reserve) Bold for (debt - liquidation reserve) worth of ETH, so the Bold liquidation reserve left corresponds to the remaining debt.
* In order to close the trove, the Bold liquidation reserve is burned, and the corresponding debt is removed from the active pool.
* The debt recorded on the trove's struct is zero'd elswhere, in _closeTrove.
* Any surplus ETH left in the trove, is sent to the Coll surplus pool, and can be later claimed by the borrower.
*/
function _redeemCloseTrove(ContractsCache memory _contractsCache, uint256 _troveId, uint _bold, uint _ETH) internal {
_contractsCache.boldToken.burn(gasPoolAddress, _bold);
// TODO: Gas optimize? We update totalStakes N times for a sequence of N Trovres(!).
_updateStakeAndTotalStakes(_troveId);

emit TroveUpdated(
_troveId,
singleRedemption.newRecordedTroveDebt, newColl,
Troves[_troveId].stake,
TroveManagerOperation.redeemCollateral
);

// send ETH from Active Pool to CollSurplus Pool
_contractsCache.collSurplusPool.accountSurplus(_troveId, _ETH);
_contractsCache.activePool.sendETH(address(_contractsCache.collSurplusPool), _ETH);
return singleRedemption;
}

/* Send _boldamount Bold to the system and redeem the corresponding amount of collateral from as many Troves as are needed to fill the redemption
Expand Down Expand Up @@ -935,31 +938,36 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana
if (_maxIterations == 0) { _maxIterations = type(uint256).max; }
while (currentTroveId != 0 && totals.remainingBold > 0 && _maxIterations > 0) {
_maxIterations--;
// Save the uint256 of the Trove preceding the current one, before potentially modifying the list
// Save the uint256 of the Trove preceding the current one
uint256 nextUserToCheck = contractsCache.sortedTroves.getPrev(currentTroveId);
// Skip if ICR < 100%, to make sure that redemptions always improve the CR of hit Troves
if (getCurrentICR(currentTroveId, totals.price) < _100pct) {
currentTroveId = nextUserToCheck;
continue;
}

_getAndApplyRedistributionGains(contractsCache.activePool, contractsCache.defaultPool, currentTroveId);

SingleRedemptionValues memory singleRedemption = _redeemCollateralFromTrove(
contractsCache,
currentTroveId,
totals.remainingBold,
totals.price
);

if (singleRedemption.cancelledPartial) break; // Partial redemption was cancelled (out-of-date hint, or new net debt < minimum), therefore we could not redeem from the last Trove


totals.totalBoldToRedeem = totals.totalBoldToRedeem + singleRedemption.BoldLot;
totals.totalETHDrawn = totals.totalETHDrawn + singleRedemption.ETHLot;
totals.totalRedistDebtGains = totals.totalRedistDebtGains + singleRedemption.redistDebtGain;
// For recorded and weighted recorded debt totals, we need to capture the increases and decreases,
// since the net debt change for a given Trove could be positive or negative: redemptions decrease a Trove's recorded
// (and weighted recorded) debt, but the accrued interest increases it.
totals.totalNewRecordedTroveDebts = totals.totalNewRecordedTroveDebts + singleRedemption.newRecordedTroveDebt;
totals.totalOldRecordedTroveDebts = totals.totalOldRecordedTroveDebts + singleRedemption.oldRecordedTroveDebt;
totals.totalNewWeightedRecordedTroveDebts = totals.totalNewWeightedRecordedTroveDebts + singleRedemption.newWeightedRecordedTroveDebt;
totals.totalOldWeightedRecordedTroveDebts = totals.totalOldWeightedRecordedTroveDebts + singleRedemption.oldWeightedRecordedTroveDebt;

totals.totalETHDrawn = totals.totalETHDrawn + singleRedemption.ETHLot;
totals.remainingBold = totals.remainingBold - singleRedemption.BoldLot;
currentTroveId = nextUserToCheck;
}

require(totals.totalETHDrawn > 0, "TroveManager: Unable to redeem any amount");

// Decay the baseRate due to time passed, and then increase it according to the size of this redemption.
Expand All @@ -976,10 +984,17 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana

emit Redemption(_boldamount, totals.totalBoldToRedeem, totals.totalETHDrawn, totals.ETHFee);

activePool.mintAggInterest(
totals.totalRedistDebtGains,
totals.totalBoldToRedeem,
totals.totalNewRecordedTroveDebts,
totals.totalOldRecordedTroveDebts,
totals.totalNewWeightedRecordedTroveDebts,
totals.totalOldWeightedRecordedTroveDebts
);

// Burn the total Bold that is cancelled with debt, and send the redeemed ETH to msg.sender
contractsCache.boldToken.burn(msg.sender, totals.totalBoldToRedeem);
// Update Active Pool Bold, and send ETH to account
contractsCache.activePool.decreaseRecordedDebtSum(totals.totalBoldToRedeem);
contractsCache.activePool.sendETH(msg.sender, totals.ETHToSendToRedeemer);
}

Expand Down Expand Up @@ -1108,7 +1123,7 @@ contract TroveManager is ERC721, LiquityBase, Ownable, CheckContract, ITroveMana
entireColl = recordedColl + pendingETHReward;
}

function getTroveEntireDebt(uint256 _troveId) external view returns (uint256) {
function getTroveEntireDebt(uint256 _troveId) public view returns (uint256) {
(uint256 entireTroveDebt, , , , ) = getEntireDebtAndColl(_troveId);
return entireTroveDebt;
}
Expand Down
17 changes: 17 additions & 0 deletions contracts/src/test/TestContracts/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,35 @@ contract BaseTest is Test {
uint256 A;
uint256 B;
uint256 C;
uint256 D;
}

struct TroveIDs {
uint256 A;
uint256 B;
uint256 C;
uint256 D;
}

struct TroveCollAmounts {
uint256 A;
uint256 B;
uint256 C;
uint256 D;
}

struct TroveInterestRates {
uint256 A;
uint256 B;
uint256 C;
uint256 D;
}

struct TroveAccruedInterests {
uint256 A;
uint256 B;
uint256 C;
uint256 D;
}

// --- functions ---
Expand Down Expand Up @@ -226,6 +237,12 @@ contract BaseTest is Test {
vm.stopPrank();
}


function redeem(address _from, uint256 _boldAmount) public {
vm.startPrank(_from);
troveManager.redeemCollateral(_boldAmount, MAX_UINT256, 1e18);
vm.stopPrank();
}
function logContractAddresses() public view {
console.log("ActivePool addr: ", address(activePool));
console.log("BorrowerOps addr: ", address(borrowerOperations));
Expand Down
29 changes: 29 additions & 0 deletions contracts/src/test/TestContracts/DevTestSetup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,33 @@ contract DevTestSetup is BaseTest {

return (ATroveId, BTroveId, CTroveId, DTroveId);
}

function _setupForRedemption() public returns (uint256, uint256, TroveIDs memory) {
TroveIDs memory troveIDs;

priceFeed.setPrice(2000e18);

uint256 interestRate_A = 10e16;
uint256 interestRate_B = 20e16;
uint256 interestRate_C = 30e16;
uint256 interestRate_D = 40e16;
uint256 coll = 20 ether;
uint256 debtRequest = 20000e18;
// Open in increasing order of interst rate
troveIDs.A = openTroveNoHints100pctMaxFee(A, coll, debtRequest, interestRate_A);
troveIDs.B = openTroveNoHints100pctMaxFee(B, coll, debtRequest, interestRate_B);
troveIDs.C = openTroveNoHints100pctMaxFee(C, coll, debtRequest, interestRate_C);
troveIDs.D = openTroveNoHints100pctMaxFee(D, coll, debtRequest, interestRate_D);

// fast-forward to pass bootstrap phase
vm.warp(block.timestamp + 14 days);

// A, B, C, D transfer all their Bold to E
transferBold(A, E, boldToken.balanceOf(A));
transferBold(B, E, boldToken.balanceOf(B));
transferBold(C, E, boldToken.balanceOf(C));
transferBold(D, E, boldToken.balanceOf(D));

return (coll, debtRequest, troveIDs);
}
}
Loading
Loading