Skip to content

Commit

Permalink
Small fix and unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
dulguun-staderlabs committed Jan 7, 2024
1 parent c93e196 commit e9251e7
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 43 deletions.
8 changes: 1 addition & 7 deletions contracts/OperatorRewardsCollector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,7 @@ contract OperatorRewardsCollector is IOperatorRewardsCollector, AccessControlUpg
if (amount > 0) {
address rewardsAddress = UtilLib.getOperatorRewardAddress(operator, staderConfig);
weth.deposit{value: amount}();
if (
weth.transferFrom(
address(this),
rewardsAddress,
amount
) == false
) revert WethTransferFailed();
if (weth.transferFrom(address(this), rewardsAddress, amount) == false) revert WethTransferFailed();
emit Claimed(rewardsAddress, amount);
}
}
Expand Down
64 changes: 33 additions & 31 deletions contracts/SDIncentiveController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ contract SDIncentiveController is ISDIncentiveController, AccessControlUpgradeab
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
}

/// @notice Starts the reward period.
/// @dev only `MANAGER` role can call
/// @param rewardAmount The amount of reward tokens to distribute.
/// @param duration The duration of the reward period in blocks.
function start(uint256 rewardAmount, uint256 duration) external override {
UtilLib.onlyManagerRole(msg.sender, staderConfig);

if (rewardEndBlock > block.number) revert ExistingRewardPeriod();
if (rewardAmount == 0) revert InvalidRewardAmount();
if (duration == 0) revert InvalidEndBlock();
if (rewardAmount % duration != 0) revert InvalidEndBlock();

updateReward(address(0));

emissionPerBlock = rewardAmount / duration;
rewardEndBlock = block.number + duration;
if (!IERC20(staderConfig.getStaderToken()).transferFrom(msg.sender, address(this), rewardAmount)) {
revert SDTransferFailed();
}

emit EmissionRateUpdated(emissionPerBlock);
emit RewardEndBlockUpdated(rewardEndBlock);
}

/// @notice Claims the accrued rewards for an account.
/// @param account The address of the account claiming rewards.
function claim(address account) external override {
Expand Down Expand Up @@ -83,42 +107,17 @@ contract SDIncentiveController is ISDIncentiveController, AccessControlUpgradeab
emit UpdatedStaderConfig(_staderConfig);
}

/// @notice Updates the emission rate of the reward tokens per block.
/// @dev only `MANAGER` role can call
/// @param newEmissionRate The new emission rate per block.
function updateEmissionRate(uint256 newEmissionRate) external override {
UtilLib.onlyManagerRole(msg.sender, staderConfig);

if (newEmissionRate == 0) revert InvalidEmissionRate();
emissionPerBlock = newEmissionRate;
emit EmissionRateUpdated(newEmissionRate);
}

/// @notice Updates the end block of the reward period.
/// @dev only `MANAGER` role can call
/// @param _newEndBlock The new end block.
function updateEndBlock(uint256 _newEndBlock) external override {
UtilLib.onlyManagerRole(msg.sender, staderConfig);

if (_newEndBlock <= block.number) revert InvalidEndBlock();
rewardEndBlock = _newEndBlock;
emit RewardEndBlockUpdated(_newEndBlock);
}

/// @notice Calculates the current reward per token.
/// @return The calculated reward per token.
function rewardPerToken() public view override returns (uint256) {
if (block.number >= rewardEndBlock) {
return rewardPerTokenStored;
}

uint256 totalSupply = ISDUtilityPool(staderConfig.getSDUtilityPool()).cTokenTotalSupply();
if (totalSupply == 0) {
return rewardPerTokenStored;
}

return
rewardPerTokenStored +
(((block.number - lastUpdateBlockNumber) * emissionPerBlock * DECIMAL) / totalSupply);
(((lastRewardTime() - lastUpdateBlockNumber) * emissionPerBlock * DECIMAL) / totalSupply);
}

/// @notice Calculates the total accrued reward for an account.
Expand All @@ -128,17 +127,15 @@ contract SDIncentiveController is ISDIncentiveController, AccessControlUpgradeab
ISDUtilityPool sdUtilityPool = ISDUtilityPool(staderConfig.getSDUtilityPool());
uint256 currentBalance = sdUtilityPool.delegatorCTokenBalance(account) +
sdUtilityPool.delegatorWithdrawRequestedCTokenCount(account);
uint256 currentRewardPerToken = rewardPerToken();

return
((currentBalance * (currentRewardPerToken - userRewardPerTokenPaid[account])) / DECIMAL) + rewards[account];
return ((currentBalance * (rewardPerToken() - userRewardPerTokenPaid[account])) / DECIMAL) + rewards[account];
}

/// @dev Internal function to update the reward state for an account.
/// @param account The account to update the reward for.
function updateReward(address account) internal {
rewardPerTokenStored = rewardPerToken();
lastUpdateBlockNumber = block.number;
lastUpdateBlockNumber = lastRewardTime();

// If the account is not zero, update the reward for the account.
if (account != address(0)) {
Expand All @@ -148,4 +145,9 @@ contract SDIncentiveController is ISDIncentiveController, AccessControlUpgradeab

emit RewardUpdated(account, rewards[account]);
}

/// @dev Internal function to get the last possible reward block number.
function lastRewardTime() public view returns (uint256) {
return block.number >= rewardEndBlock ? rewardEndBlock : block.number;
}
}
4 changes: 3 additions & 1 deletion contracts/SDUtilityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,9 @@ contract SDUtilityPool is ISDUtilityPool, AccessControlUpgradeable, PausableUpgr
utilizerData[account].utilizeIndex = utilizeIndex;
totalUtilizedSD -= userData.totalInterestSD;

IERC20(staderConfig.getStaderToken()).transferFrom(msg.sender, address(this), userData.totalInterestSD);
if (!IERC20(staderConfig.getStaderToken()).transferFrom(msg.sender, address(this), userData.totalInterestSD)) {
revert SDTransferFailed();
}

uint256 totalInterestInEth = ISDCollateral(staderConfig.getSDCollateral()).convertSDToETH(
userData.totalInterestSD
Expand Down
9 changes: 5 additions & 4 deletions contracts/interfaces/ISDIncentiveController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ interface ISDIncentiveController {
error NoRewardsToClaim();
error InvalidEmissionRate();
error InvalidEndBlock();
error InvalidRewardAmount();
error ExistingRewardPeriod();
error SDTransferFailed();

// events
/// @dev Emitted when the Stader configuration contract is updated.
Expand All @@ -29,16 +32,14 @@ interface ISDIncentiveController {
event RewardEndBlockUpdated(uint256 newRewardEndBlock);

// functions
function start(uint256 rewardAmount, uint256 duration) external;

function claim(address account) external;

function updateRewardForAccount(address account) external;

function updateStaderConfig(address _staderConfig) external;

function updateEmissionRate(uint256 newEmissionRate) external;

function updateEndBlock(uint256 _newEndBlock) external;

function rewardPerToken() external view returns (uint256);

function earned(address account) external view returns (uint256);
Expand Down
183 changes: 183 additions & 0 deletions test/foundry_tests/SDIncentiveController.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: GPL-3.0-or-later

Check failure on line 1 in test/foundry_tests/SDIncentiveController.t.sol

View workflow job for this annotation

GitHub Actions / Prettier

test/foundry_tests/SDIncentiveController.t.sol#L1

There are issues with this file's formatting, please run Prettier to fix the errors
pragma solidity 0.8.16;

import '../../contracts/library/UtilLib.sol';

import '../../contracts/StaderConfig.sol';
import '../../contracts/SDUtilityPool.sol';
import '../../contracts/SDIncentiveController.sol';

import '../mocks/SDCollateralMock.sol';
import '../mocks/StaderTokenMock.sol';
import '../mocks/SDIncentiveControllerMock.sol';
import '../mocks/OperatorRewardsCollectorMock.sol';
import '../mocks/PoolUtilsMock.sol';

import 'forge-std/Test.sol';
import '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol';
import '@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol';

contract SDIncentiveControllerTest is Test {
address staderAdmin;
address staderManager;
address staderTreasury;

StaderConfig staderConfig;
SDUtilityPool sdUtilityPool;
SDIncentiveController sdIncentiveController;
StaderTokenMock staderToken;
SDCollateralMock sdCollateral;
OperatorRewardsCollectorMock operatorRewardsCollector;
PoolUtilsMock poolUtils;

function setUp() public {
staderAdmin = vm.addr(100);
staderManager = vm.addr(101);
address ethDepositAddr = vm.addr(102);
staderTreasury = vm.addr(105);

staderToken = new StaderTokenMock();
ProxyAdmin admin = new ProxyAdmin();

StaderConfig configImpl = new StaderConfig();
TransparentUpgradeableProxy configProxy = new TransparentUpgradeableProxy(
address(configImpl),
address(admin),
''
);
staderConfig = StaderConfig(address(configProxy));
staderConfig.initialize(staderAdmin, ethDepositAddr);

sdCollateral = new SDCollateralMock();
operatorRewardsCollector = new OperatorRewardsCollectorMock();
poolUtils = new PoolUtilsMock(address(staderConfig));

vm.startPrank(staderAdmin);
staderConfig.updateStaderToken(address(staderToken));
staderConfig.updateSDCollateral(address(sdCollateral));
staderConfig.updateOperatorRewardsCollector(address(operatorRewardsCollector));
staderConfig.updatePoolUtils(address(poolUtils));
staderConfig.grantRole(staderConfig.MANAGER(), staderManager);
vm.stopPrank();

vm.prank(staderManager);
staderConfig.updateStaderTreasury(staderTreasury);

SDUtilityPool sdUtilityPoolImpl = new SDUtilityPool();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(sdUtilityPoolImpl),
address(admin),
''
);
sdUtilityPool = SDUtilityPool(address(proxy));
sdUtilityPool.initialize(staderAdmin, address(staderConfig));

vm.prank(staderAdmin);
sdUtilityPool.updateRiskConfig(70, 30, 5, 50);

SDIncentiveController sdIncentiveControllerImpl = new SDIncentiveController();
TransparentUpgradeableProxy sdIncentiveControllerProxy = new TransparentUpgradeableProxy(
address(sdIncentiveControllerImpl),
address(admin),
''
);
sdIncentiveController = SDIncentiveController(address(sdIncentiveControllerProxy));
sdIncentiveController.initialize(staderAdmin, address(staderConfig));

vm.startPrank(staderAdmin);
staderConfig.updateSDIncentiveController(address(sdIncentiveController));
staderConfig.updateSDUtilityPool(address(sdUtilityPool));
vm.stopPrank();
}

function setupIncentive(uint256 incentiveAmount, uint256 duration) public returns(uint256, uint256) {
vm.assume(incentiveAmount > 0);
vm.assume(duration > 0);

incentiveAmount = ((incentiveAmount % 10000) + 1) * 1e18;
duration = ((duration%10) + 1) * 100;
incentiveAmount = (incentiveAmount / duration) * duration;
staderToken.transfer(staderManager, incentiveAmount);

vm.startPrank(staderManager);
staderToken.approve(address(sdIncentiveController), incentiveAmount);
sdIncentiveController.start(incentiveAmount, duration);
vm.stopPrank();

return (incentiveAmount, duration);
}

function test_Initialize() public {
ProxyAdmin admin = new ProxyAdmin();
SDIncentiveController sdIncentiveControllerImpl = new SDIncentiveController();
TransparentUpgradeableProxy sdIncentiveControllerProxy = new TransparentUpgradeableProxy(
address(sdIncentiveControllerImpl),
address(admin),
''
);
SDIncentiveController sdIncentiveController2 = SDIncentiveController(address(sdIncentiveControllerProxy));
sdIncentiveController2.initialize(staderAdmin, address(staderConfig));
}

function test_VerifyInitialize() public {
assertEq(address(sdIncentiveController.staderConfig()), address(staderConfig));
assertEq(sdIncentiveController.emissionPerBlock(), 0);
assertEq(sdIncentiveController.rewardEndBlock(), 0);
assertEq(sdIncentiveController.lastUpdateBlockNumber(), 0);
assertEq(sdIncentiveController.rewardPerTokenStored(), 0);
assertEq(sdIncentiveController.DECIMAL(), 1e18);
assertTrue(sdIncentiveController.hasRole(sdIncentiveController.DEFAULT_ADMIN_ROLE(), staderAdmin));
}

function test_Simple(uint128 sdAmount, uint16 randomSeed, uint256 incentiveAmount, uint256 duration) public {
vm.assume(randomSeed > 1);

(incentiveAmount, duration) = setupIncentive(incentiveAmount, duration);

uint256 deployerSDBalance = staderToken.balanceOf(address(this));
vm.assume(sdAmount <= deployerSDBalance / 4 && sdAmount > 1000e18);

address user = vm.addr(randomSeed);
address user2 = vm.addr(randomSeed - 1);

staderToken.transfer(user, sdAmount);
staderToken.transfer(user2, sdAmount);

assertEq(sdIncentiveController.earned(user), 0);
assertEq(sdIncentiveController.earned(user2), 0);

vm.startPrank(user2);
staderToken.approve(address(sdUtilityPool), sdAmount);
sdUtilityPool.delegate(sdAmount);
vm.stopPrank();

vm.roll(block.number + 10);

assertApproxEqAbs(sdIncentiveController.earned(user2), incentiveAmount/duration*10, 1e9);

vm.startPrank(user);
staderToken.approve(address(sdUtilityPool), sdAmount);
sdUtilityPool.delegate(sdAmount);
vm.stopPrank();

vm.roll(block.number + 20);

vm.startPrank(user2);
sdUtilityPool.requestWithdrawWithSDAmount(sdAmount/2);
vm.stopPrank();

vm.startPrank(staderAdmin);
sdUtilityPool.updateMinBlockDelayToFinalizeRequest(0);
sdUtilityPool.finalizeDelegatorWithdrawalRequest();
vm.stopPrank();

vm.startPrank(user2);
uint256 earned = sdIncentiveController.earned(user2);
sdUtilityPool.claim(1);
assertEq(staderToken.balanceOf(user2), earned + sdAmount/2);
vm.stopPrank();

vm.roll(block.number + duration);
assertApproxEqAbs(earned + sdIncentiveController.earned(user2) + sdIncentiveController.earned(user), incentiveAmount, 1e9);
}
}

0 comments on commit e9251e7

Please sign in to comment.