Skip to content
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
2 changes: 1 addition & 1 deletion contracts/script/DeployLiquity2.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging {
troveManagers[0] = ITroveManager(troveManagerAddress);

r.collateralRegistry =
new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers, r.systemParams);
new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers, r.systemParams, makeAddr("liquidityStrategy"));
r.hintHelpers = new HintHelpers(r.collateralRegistry, r.systemParams);
r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry);

Expand Down
46 changes: 45 additions & 1 deletion contracts/src/CollateralRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,21 @@ contract CollateralRegistry is ICollateralRegistry {

uint256 public baseRate;

address public immutable liquidityStrategy;

// The timestamp of the latest fee operation (redemption or new Bold issuance)
uint256 public lastFeeOperationTime = block.timestamp;

event BaseRateUpdated(uint256 _baseRate);
event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime);
event LiquidityStrategyUpdated(address indexed _liquidityStrategy);

constructor(
IBoldToken _boldToken,
IERC20Metadata[] memory _tokens,
ITroveManager[] memory _troveManagers,
ISystemParams _systemParams
ISystemParams _systemParams,
address _liquidityStrategy
) {
uint256 numTokens = _tokens.length;
require(numTokens > 0, "Collateral list cannot be empty");
Expand Down Expand Up @@ -88,6 +92,10 @@ contract CollateralRegistry is ICollateralRegistry {
// Initialize the baseRate state variable
baseRate = _systemParams.INITIAL_BASE_RATE();
emit BaseRateUpdated(baseRate);

// Initialize the liquidityStrategy state variable
liquidityStrategy = _liquidityStrategy;
emit LiquidityStrategyUpdated(liquidityStrategy);
}

struct RedemptionTotals {
Expand All @@ -97,6 +105,34 @@ contract CollateralRegistry is ICollateralRegistry {
uint256 redeemedAmount;
}

/**
* @notice Redeems debt tokens with a fixed fee for the trove owner
* @dev This function is used during the rebalancing of a CDP pool and can only be called by the liquidity strategy
* @param _boldAmount The amount of bold to redeem
* @param _maxIterationsPerCollateral The maximum number of iterations per collateral
* @param _troveOwnerFee The fee to pay to the trove owner
*/
function redeemCollateralRebalancing(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _troveOwnerFee) external {
_requireCallerIsLiquidityStrategy();
_requireAmountGreaterThanZero(_boldAmount);
_requireValidTroveOwnerFee(_troveOwnerFee);
require(totalCollaterals == 1, "CollateralRegistry: Only one collateral supported for rebalancing");

ITroveManager troveManager = getTroveManager(0);
(, uint256 price, bool redeemable) =
troveManager.getUnbackedPortionPriceAndRedeemability();
require(redeemable, "CollateralRegistry: Collateral is not redeemable");
uint256 redeemedAmount = troveManager.redeemCollateral(
msg.sender,
_boldAmount,
price,
_troveOwnerFee,
_maxIterationsPerCollateral
);
require(redeemedAmount == _boldAmount, "CollateralRegistry: Redeemed amount does not match requested amount");
boldToken.burn(msg.sender, redeemedAmount);
}

function redeemCollateral(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _maxFeePercentage)
external
{
Expand Down Expand Up @@ -321,4 +357,12 @@ contract CollateralRegistry is ICollateralRegistry {
function _requireAmountGreaterThanZero(uint256 _amount) internal pure {
require(_amount > 0, "CollateralRegistry: Amount must be greater than zero");
}

function _requireCallerIsLiquidityStrategy() internal view {
require(msg.sender == address(liquidityStrategy), "CollateralRegistry: Caller is not LiquidityStrategy");
}

function _requireValidTroveOwnerFee(uint256 _troveOwnerFee) internal pure {
require(_troveOwnerFee <= DECIMAL_PRECISION, "CollateralRegistry: Trove owner fee must be between 0% and 100%");
}
}
3 changes: 2 additions & 1 deletion contracts/src/Interfaces/ICollateralRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import "./ITroveManager.sol";
interface ICollateralRegistry {
function baseRate() external view returns (uint256);
function lastFeeOperationTime() external view returns (uint256);

function liquidityStrategy() external view returns (address);
function redeemCollateralRebalancing(uint256 _boldamount, uint256 _maxIterationsPerCollateral, uint256 _troveOwnerFee) external;
function redeemCollateral(uint256 _boldamount, uint256 _maxIterations, uint256 _maxFeePercentage) external;
// getters
function totalCollaterals() external view returns (uint256);
Expand Down
3 changes: 2 additions & 1 deletion contracts/src/Interfaces/IStabilityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver {

function boldToken() external view returns (IBoldToken);
function troveManager() external view returns (ITroveManager);

function systemParams() external view returns (ISystemParams);

/* provideToSP():
* - Calculates depositor's Coll gain
* - Calculates the compounded deposit
Expand Down
4 changes: 2 additions & 2 deletions contracts/test/TestContracts/CollateralRegistryTester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import "src/Interfaces/ISystemParams.sol";
for testing the parent's internal functions. */

contract CollateralRegistryTester is CollateralRegistry {
constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, ISystemParams _systemParams)
CollateralRegistry(_boldToken, _tokens, _troveManagers, _systemParams)
constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, ISystemParams _systemParams, address _liquidityStrategy)
CollateralRegistry(_boldToken, _tokens, _troveManagers, _systemParams, _liquidityStrategy)
{}

function unprotectedDecayBaseRateFromBorrowing() external returns (uint256) {
Expand Down
2 changes: 1 addition & 1 deletion contracts/test/TestContracts/Deployment.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ contract TestDeployer is MetadataDeployment {
}

collateralRegistry =
new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParamsArray[0]);
new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParamsArray[0], makeAddr("liquidityStrategy"));
hintHelpers = new HintHelpers(collateralRegistry, systemParamsArray[0]);
multiTroveGetter = new MultiTroveGetter(collateralRegistry);

Expand Down
108 changes: 108 additions & 0 deletions contracts/test/rebalancingRedemptions.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "./TestContracts/DevTestSetup.sol";

contract RebalancingRedemptions is DevTestSetup {
using stdStorage for StdStorage;

function test_redeemCollateralRebalancing_whenCallerIsNotLiquidityStrategy_shouldRevert() public {
vm.expectRevert("CollateralRegistry: Caller is not LiquidityStrategy");
collateralRegistry.redeemCollateralRebalancing(100, 10, 1e18);
}

function test_redeemCollateralRebalancing_whenAmountIsZero_shouldRevert() public {
vm.startPrank(collateralRegistry.liquidityStrategy());
vm.expectRevert("CollateralRegistry: Amount must be greater than zero");
collateralRegistry.redeemCollateralRebalancing(0, 10, 1e18);
}

function test_redeemCollateralRebalancing_whenTroveOwnerFeeIsGreaterThan100_shouldRevert() public {
vm.startPrank(collateralRegistry.liquidityStrategy());
vm.expectRevert("CollateralRegistry: Trove owner fee must be between 0% and 100%");
collateralRegistry.redeemCollateralRebalancing(100, 10, 1e18 + 1);
}

function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategy_shouldRedeemAmountCorrectly() public {
(,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest();
uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A);
uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B);
uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C);

uint256 coll_A = troveManager.getTroveEntireColl(troveIDs.A);
uint256 coll_B = troveManager.getTroveEntireColl(troveIDs.B);
uint256 coll_C = troveManager.getTroveEntireColl(troveIDs.C);

uint256 debtToRedeem = debt_A + debt_B + debt_C/2;

deal(address(boldToken), address(collateralRegistry.liquidityStrategy()), debtToRedeem);

vm.startPrank(collateralRegistry.liquidityStrategy());
// redemption fee is 50 bps scaled to 1e18
collateralRegistry.redeemCollateralRebalancing(debtToRedeem, 10, 50 * 1e12);

assertEq(troveManager.getTroveEntireDebt(troveIDs.A), 0);
assertEq(troveManager.getTroveEntireDebt(troveIDs.B), 0);
// plus 1 because of rounding down when calculating the debt to redeem
assertEq(troveManager.getTroveEntireDebt(troveIDs.C), debt_C/2 + 1 );
}

function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategy_shouldLeaveCorrectFeeInTroves() public {
(,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest();
uint256 price = priceFeed.getPrice();

// redemption fee is 50 bps scaled to 1e18
uint256 fee = 50 * 1e12;

uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A);
uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B);
uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C);

uint256 coll_A = troveManager.getTroveEntireColl(troveIDs.A);
uint256 coll_B = troveManager.getTroveEntireColl(troveIDs.B);
uint256 coll_C = troveManager.getTroveEntireColl(troveIDs.C);

uint256 debtToRedeem = debt_A + debt_B + debt_C/2;

uint256 expectedColl_A_After = calculateCorrespondingCollAfterRedemption(debt_A, coll_A, price, 50 * 1e12);
uint256 expectedColl_B_After = calculateCorrespondingCollAfterRedemption(debt_B, coll_B, price, 50 * 1e12);
uint256 expectedColl_C_After = calculateCorrespondingCollAfterRedemption(debt_C/2, coll_C, price, 50 * 1e12);

deal(address(boldToken), address(collateralRegistry.liquidityStrategy()), debtToRedeem);

vm.startPrank(collateralRegistry.liquidityStrategy());
collateralRegistry.redeemCollateralRebalancing(debtToRedeem, 10, fee);


assertEq(troveManager.getTroveEntireColl(troveIDs.A), expectedColl_A_After);
assertEq(troveManager.getTroveEntireColl(troveIDs.B), expectedColl_B_After);
assertEq(troveManager.getTroveEntireColl(troveIDs.C), expectedColl_C_After);
}

function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategyAndCollateralIsNotRedeemable_shouldRevert() public {
(,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest();
uint256 price = priceFeed.getPrice();

priceFeed.setPrice((price * 5e17)/1e18); // 50% below the initial price

vm.startPrank(collateralRegistry.liquidityStrategy());
vm.expectRevert("CollateralRegistry: Collateral is not redeemable");
collateralRegistry.redeemCollateralRebalancing(100, 10, 50 * 1e12);
}

function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategyAndFullAmountIsNotRedeemed_shouldRevert() public {
(,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest();

uint256 totalDebtSupply = boldToken.totalSupply();

vm.startPrank(collateralRegistry.liquidityStrategy());
vm.expectRevert("CollateralRegistry: Redeemed amount does not match requested amount");
collateralRegistry.redeemCollateralRebalancing(totalDebtSupply + 1, 10, 50 * 1e12);
}

function calculateCorrespondingCollAfterRedemption(uint256 debtRedeemed, uint256 collInitial, uint256 price, uint256 fee) public pure returns (uint256 collateralAfter) {
uint256 correspondingColl = debtRedeemed * DECIMAL_PRECISION / price;
uint256 correspondingCollFee = (debtRedeemed * fee * DECIMAL_PRECISION) / (1e18 * price);
return collInitial - correspondingColl + correspondingCollFee;
}
}