Skip to content
Open
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: 2 additions & 0 deletions contracts/SDCollateral.sol
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ contract SDCollateral is ISDCollateral, AccessControlUpgradeable, ReentrancyGuar
emit UpdatedStaderConfig(_staderConfig);
}



function updatePoolThreshold(
uint8 _poolId,
uint256 _minThreshold,
Expand Down
51 changes: 48 additions & 3 deletions contracts/SDUtilityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr

uint256 public constant MAX_PROTOCOL_FEE = 1e17; // 10%

uint256 public constant RISK_CONFIG_APPLY = 1 days;


// State variables

/// @notice Percentage of protocol fee expressed in gwei
Expand Down Expand Up @@ -80,6 +83,12 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr

/// @notice risk configuration
RiskConfig public riskConfig;
/// @notice pending risk configuration
RiskConfig public pendingRiskConfig;
/// @notice risk config propose time
uint public riskConfigProposeTime;



/// @notice chronological collection of liquidations
OperatorLiquidation[] public liquidations;
Expand Down Expand Up @@ -620,19 +629,55 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr
}

/**
* @notice Updates the risk configuration
* @notice Apply the queued risk configuration after timelock period
* @dev Reverts if no configuration is queued or timelock period hasn't elapsed
*/
function applyRiskConfig() external onlyRole(DEFAULT_ADMIN_ROLE) {
if(riskConfigProposeTime == 0) revert InvalidInput();
if(riskConfigProposeTime + RISK_CONFIG_APPLY > block.timestamp)
revert RiskConfigApplyTimeDontReach();
_updateRiskConfig(pendingRiskConfig.liquidationThreshold, pendingRiskConfig.liquidationBonusPercent, pendingRiskConfig.liquidationFeePercent, pendingRiskConfig.ltv);
riskConfigProposeTime = 0; // Reset after applying
}
/**
* @notice Queue a new risk configuration to be applied after timelock period
* @param liquidationThreshold The new liquidation threshold percent (1 - 100)
* @param liquidationBonusPercent The new liquidation bonus percent (0 - 100)
* @param liquidationFeePercent The new liquidation fee percent (0 - 100)
* @param ltv The new loan-to-value ratio (1 - 100)
*/
function updateRiskConfig(
function queueRiskConfig(
uint256 liquidationThreshold,
uint256 liquidationBonusPercent,
uint256 liquidationFeePercent,
uint256 ltv
) external onlyRole(DEFAULT_ADMIN_ROLE) {
_updateRiskConfig(liquidationThreshold, liquidationBonusPercent, liquidationFeePercent, ltv);
if (liquidationThreshold > 100 || liquidationThreshold == 0) revert InvalidInput();
if (liquidationBonusPercent > 100) revert InvalidInput();
if (liquidationFeePercent > 100) revert InvalidInput();
if (ltv > 100 || ltv == 0) revert InvalidInput();

pendingRiskConfig = RiskConfig({
liquidationThreshold: liquidationThreshold,
liquidationBonusPercent: liquidationBonusPercent,
liquidationFeePercent: liquidationFeePercent,
ltv: ltv
});

riskConfigProposeTime = block.timestamp;

emit RiskConfigQueued(liquidationThreshold, liquidationBonusPercent, liquidationFeePercent, ltv);
}

/**
* @notice Cancel a queued risk configuration
* @dev Only callable by admin, useful for emergency situations
*/
function cancelQueuedRiskConfig() external onlyRole(DEFAULT_ADMIN_ROLE) {
if (riskConfigProposeTime == 0) revert InvalidInput();
riskConfigProposeTime = 0;
delete pendingRiskConfig;
emit RiskConfigCancelled();
}

//Getters
Expand Down
8 changes: 8 additions & 0 deletions contracts/interfaces/ISDUtilityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface ISDUtilityPool {
error MaxLimitOnWithdrawRequestCountReached();
error RequestIdNotFinalized(uint256 requestId);
error AlreadyLiquidated();
error RiskConfigApplyTimeDontReach();

event WithdrawnProtocolFee(uint256 amount);
event ProtocolFeeFactorUpdated(uint256 protocolFeeFactor);
Expand Down Expand Up @@ -73,6 +74,13 @@ interface ISDUtilityPool {
uint256 liquidationFeePercent,
uint256 ltv
);
event RiskConfigQueued(
uint256 liquidationThreshold,
uint256 liquidationBonusPercent,
uint256 liquidationFeePercent,
uint256 ltv
);
event RiskConfigCancelled();
event AccruedFees(uint256 feeAccumulated, uint256 totalProtocolFee, uint256 totalUtilizedSD);
event WithdrawRequestReceived(address caller, uint256 nextRequestId, uint256 sdAmountToWithdraw);
event UpdatedConservativeEthPerKey(uint256 conservativeEthPerKey);
Expand Down
33 changes: 30 additions & 3 deletions test/foundry_tests/SDUtilityPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -777,10 +777,37 @@ contract SDUtilityPoolTest is Test {
function test_UpdateRiskConfig(uint256 randomSeed) public {
vm.assume(randomSeed > 0);
vm.assume(randomSeed < 100);
vm.expectRevert();
sdUtilityPool.updateRiskConfig(randomSeed, randomSeed, randomSeed, randomSeed);

// Queue the risk config
vm.prank(staderAdmin);
sdUtilityPool.queueRiskConfig(randomSeed, randomSeed, randomSeed, randomSeed);

// Wait for timelock period
vm.warp(block.timestamp + sdUtilityPool.RISK_CONFIG_APPLY());

// Apply the queued config
vm.prank(staderAdmin);
sdUtilityPool.updateRiskConfig(randomSeed, randomSeed, randomSeed, randomSeed);
sdUtilityPool.applyRiskConfig();

// Verify the config was applied
(uint liquidationThreshold, uint liquidationBonusPercent, uint liquidationFeePercent, uint ltv) = sdUtilityPool.riskConfig();
assertEq(randomSeed, liquidationThreshold);
assertEq(randomSeed, liquidationBonusPercent);
assertEq(randomSeed, liquidationFeePercent);
assertEq(randomSeed, ltv);
}

function test_CancelQueuedRiskConfig() public {
// Queue a risk config
vm.prank(staderAdmin);
sdUtilityPool.queueRiskConfig(50, 10, 5, 80);

// Cancel it
vm.prank(staderAdmin);
sdUtilityPool.cancelQueuedRiskConfig();

// Verify timelock was reset
assertEq(sdUtilityPool.riskConfigProposeTime(), 0);
}

function test_LiquidationCall(uint16 randomSeed) public {
Expand Down