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

👷 KSX - Remove escrow on inflationary rewards and remove staking cooldown #252

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
62462f1
👷 remove staking cooldown
Flocqst Jul 2, 2024
6006911
👷 update interface
Flocqst Jul 2, 2024
1052a6d
✅ Adapt test suite to staking cooldown removal
Flocqst Jul 2, 2024
5142e90
📚 update doc
Flocqst Jul 2, 2024
1735867
👷 Remove escrow on inflationary rewards
Flocqst Jul 5, 2024
43300af
📚 @custom:todo adjust tests to non escrowed rewards
Flocqst Jul 8, 2024
371f847
👷 Adjust compound rewards to newly non escrowed rewards
Flocqst Jul 8, 2024
b4b69a7
👷 adjust for compounding on behalf
Flocqst Jul 16, 2024
d33ee53
✅ Adjust tests to non escrowed rewards
Flocqst Jul 16, 2024
9dee0eb
✅ fix fork test
Flocqst Jul 16, 2024
fb03e81
✅ Add unstakeEscrowAdmin tests
Flocqst Jul 17, 2024
6336698
✅ rename function helper
Flocqst Jul 17, 2024
7ff1a0a
📚 Add author handle
Flocqst Jul 17, 2024
15eea33
👷 fix updateReward modifier for compoundOnBehalf
Flocqst Jul 22, 2024
9cb9509
🔀 Resolved conflicts after merging main into staking-cooldown
Flocqst Aug 27, 2024
427134b
🔀 Resolved conflicts after merging main into remove-inflation-escrow
Flocqst Aug 27, 2024
827e85c
✅ Adjust stake Escrow on behalf tests
Flocqst Aug 27, 2024
e4d6166
Merge branch 'staking-cooldown' into remove-inflation-escrow
Flocqst Aug 27, 2024
48a2b16
✅ Adjust precision delta usdc reward calculation
Flocqst Aug 27, 2024
0d173e7
👷 Remove uses of userLastStakeTime
Flocqst Sep 11, 2024
0f1db85
👷 Refactor processReward
Flocqst Sep 11, 2024
c9e48f6
👷 add updateReward(_account) modifier to _processReward()
cmontecoding Oct 28, 2024
bf210e9
✅ test_compound add assert
cmontecoding Oct 28, 2024
c6d26b6
✅ add asserts to test_compoundOnBehalf
cmontecoding Oct 28, 2024
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
11 changes: 1 addition & 10 deletions contracts/EscrowMigrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {IStakingRewardsIntegrator} from "./interfaces/IStakingRewardsIntegrator.
/// @title KWENTA Escrow Migrator
/// Used to migrate escrow entries from RewardEscrowV1 to RewardEscrowV2
/// @author tommyrharper ([email protected])
/// @author Flocqst ([email protected])
contract EscrowMigrator is
IEscrowMigrator,
Ownable2StepUpgradeable,
Expand Down Expand Up @@ -410,7 +411,6 @@ contract EscrowMigrator is
_payForMigration(_account);

uint256 migratedEscrow;
uint256 cooldown = stakingRewardsV2.cooldownPeriod();
mapping(uint256 => VestingEntry) storage userEntries = registeredVestingSchedules[_account];

for (uint256 i = 0; i < _entryIDs.length; i++) {
Expand All @@ -432,15 +432,6 @@ contract EscrowMigrator is
registeredEntry.migrated = true;
migratedEscrow += originalEscrowAmount;

/// @dev it essential for security that the duration is not less than the cooldown period,
/// otherwise the user could do a governance attack by bypassing the unstaking cooldown lock
/// by migrating their escrow then staking, voting, and vesting immediately
if (duration < cooldown) {
uint256 timeCreated = endTime - duration;
duration = cooldown;
endTime = timeCreated + cooldown;
}

IRewardEscrowV2.VestingEntry memory entry = IRewardEscrowV2.VestingEntry({
escrowAmount: originalEscrowAmount,
duration: duration,
Expand Down
7 changes: 3 additions & 4 deletions contracts/RewardEscrowV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {IEscrowMigrator} from "./interfaces/IEscrowMigrator.sol";
/// @title KWENTA Reward Escrow V2
/// @author Originally inspired by SYNTHETIX RewardEscrow
/// @author Kwenta's RewardEscrow V1 by JaredBorders ([email protected]), JChiaramonte7 ([email protected])
/// @author RewardEscrowV2 by tommyrharper ([email protected])
/// @author RewardEscrowV2 by tommyrharper ([email protected]), Flocqst ([email protected])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would break each author up on separate lines

/// @notice Updated version of Synthetix's RewardEscrow with new features specific to Kwenta
contract RewardEscrowV2 is
IRewardEscrowV2,
Expand Down Expand Up @@ -379,7 +379,7 @@ contract RewardEscrowV2 is
unchecked {
amountToUnstake = totalWithFee - unstakedEscrow;
}
stakingRewards.unstakeEscrowSkipCooldown(msg.sender, amountToUnstake);
stakingRewards.unstakeEscrowAdmin(msg.sender, amountToUnstake);
jcmonte marked this conversation as resolved.
Show resolved Hide resolved
}

// update balances
Expand Down Expand Up @@ -432,8 +432,7 @@ contract RewardEscrowV2 is
if (_earlyVestingFee > MAXIMUM_EARLY_VESTING_FEE) revert EarlyVestingFeeTooHigh();
if (_earlyVestingFee < MINIMUM_EARLY_VESTING_FEE) revert EarlyVestingFeeTooLow();
if (_deposit == 0) revert ZeroAmount();
uint256 minimumDuration = stakingRewards.cooldownPeriod();
if (_duration < minimumDuration || _duration > MAX_DURATION) revert InvalidDuration();
if (_duration == 0 || _duration > MAX_DURATION) revert InvalidDuration();

/// @dev this will revert if the kwenta token transfer fails
kwenta.transferFrom(msg.sender, address(this), _deposit);
Expand Down
114 changes: 46 additions & 68 deletions contracts/StakingRewardsV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {IRewardEscrowV2} from "./interfaces/IRewardEscrowV2.sol";
/// @title KWENTA Staking Rewards V2
/// @author Originally inspired by SYNTHETIX StakingRewards
/// @author Kwenta's StakingRewards V1 by JaredBorders ([email protected]), JChiaramonte7 ([email protected])
/// @author StakingRewardsV2 (this) by tommyrharper ([email protected])
/// @author StakingRewardsV2 (this) by tommyrharper ([email protected]), Flocqst ([email protected])
/// @notice Updated version of Synthetix's StakingRewards with new features specific to Kwenta
contract StakingRewardsV2 is
IStakingRewardsV2,
Expand All @@ -27,12 +27,6 @@ contract StakingRewardsV2 is
CONSTANTS/IMMUTABLES
///////////////////////////////////////////////////////////////*/

/// @notice minimum time length of the unstaking cooldown period
uint256 public constant MIN_COOLDOWN_PERIOD = 1 weeks;

/// @notice maximum time length of the unstaking cooldown period
uint256 public constant MAX_COOLDOWN_PERIOD = 52 weeks;

/// @notice Contract for KWENTA ERC20 token - used for BOTH staking and rewards
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IKwenta public immutable kwenta;
Expand Down Expand Up @@ -81,9 +75,6 @@ contract StakingRewardsV2 is
/// @notice summation of rewardRate divided by total staked tokens
uint256 public rewardPerTokenStored;

/// @inheritdoc IStakingRewardsV2
uint256 public cooldownPeriod;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we redeploying staking v2 or upgrading? because im pretty sure if we upgrade this removal will brick everything


/// @notice represents the rewardPerToken
/// value the last time the staker calculated earned() rewards
mapping(address => uint256) public userRewardPerTokenPaid;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it wont let me comment a couple lines below for some reason but mapping(address => uint256) public userLastStakeTime; should be removed because we remove the use of it everywhere in the code

Expand Down Expand Up @@ -136,17 +127,6 @@ contract StakingRewardsV2 is
if (msg.sender != address(rewardsNotifier)) revert OnlyRewardsNotifier();
}

/// @notice only allow execution after the unstaking cooldown period has elapsed
modifier afterCooldown(address _account) {
_afterCooldown(_account);
_;
}

function _afterCooldown(address _account) internal view {
uint256 canUnstakeAt = userLastStakeTime[_account] + cooldownPeriod;
if (canUnstakeAt > block.timestamp) revert MustWaitForUnlock(canUnstakeAt);
}

/*///////////////////////////////////////////////////////////////
CONSTRUCTOR / INITIALIZER
///////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -191,7 +171,6 @@ contract StakingRewardsV2 is

// define values
rewardsDuration = 1 weeks;
cooldownPeriod = 2 weeks;
}

/*///////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -239,28 +218,30 @@ contract StakingRewardsV2 is
///////////////////////////////////////////////////////////////*/

/// @inheritdoc IStakingRewardsV2
function stake(uint256 _amount) external whenNotPaused updateReward(msg.sender) {
function stake(uint256 _amount) external {
_stake(msg.sender, _amount);

// transfer token to this contract from the caller
kwenta.transferFrom(msg.sender, address(this), _amount);
}

function _stake(address _account, uint256 _amount)
internal
whenNotPaused
updateReward(_account)
{
if (_amount == 0) revert AmountZero();

// update state
userLastStakeTime[msg.sender] = block.timestamp;
_addTotalSupplyCheckpoint(totalSupply() + _amount);
_addBalancesCheckpoint(msg.sender, balanceOf(msg.sender) + _amount);

// emit staking event and index msg.sender
emit Staked(msg.sender, _amount);
_addBalancesCheckpoint(_account, balanceOf(_account) + _amount);

// transfer token to this contract from the caller
kwenta.transferFrom(msg.sender, address(this), _amount);
// emit staking event and index _account
emit Staked(_account, _amount);
}

/// @inheritdoc IStakingRewardsV2
function unstake(uint256 _amount)
public
whenNotPaused
updateReward(msg.sender)
afterCooldown(msg.sender)
{
function unstake(uint256 _amount) public whenNotPaused updateReward(msg.sender) {
if (_amount == 0) revert AmountZero();
uint256 nonEscrowedBalance = nonEscrowedBalanceOf(msg.sender);
if (_amount > nonEscrowedBalance) revert InsufficientBalance(nonEscrowedBalance);
Expand Down Expand Up @@ -291,7 +272,6 @@ contract StakingRewardsV2 is
if (_amount > unstakedEscrow) revert InsufficientUnstakedEscrow(unstakedEscrow);

// update state
userLastStakeTime[_account] = block.timestamp;
_addBalancesCheckpoint(_account, balanceOf(_account) + _amount);
_addEscrowedBalancesCheckpoint(_account, escrowedBalanceOf(_account) + _amount);

Expand All @@ -304,15 +284,12 @@ contract StakingRewardsV2 is
}

/// @inheritdoc IStakingRewardsV2
function unstakeEscrow(uint256 _amount) external afterCooldown(msg.sender) {
function unstakeEscrow(uint256 _amount) external {
_unstakeEscrow(msg.sender, _amount);
}

/// @inheritdoc IStakingRewardsV2
function unstakeEscrowSkipCooldown(address _account, uint256 _amount)
external
onlyRewardEscrow
{
function unstakeEscrowAdmin(address _account, uint256 _amount) external onlyRewardEscrow {
_unstakeEscrow(_account, _amount);
}

Expand Down Expand Up @@ -361,31 +338,43 @@ contract StakingRewardsV2 is
whenNotPaused
updateReward(_account)
{
uint256 reward = rewards[_account];
if (reward > 0) {
_processReward(_account, _to, true);
}

/// @notice Process KWENTA and USDC rewards
/// @dev transferKwenta is set to false when compounding KWENTA rewards
/// @param _account The address of the account to process rewards for
/// @param _to The address to transfer rewards to
/// @param transferKwenta Boolean flag to determine if Kwenta should be transferred
/// @return kwentaReward The amount of Kwenta reward processed
function _processReward(address _account, address _to, bool transferKwenta)
internal
returns (uint256 kwentaReward)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem you're using kwentaReward outside of _processReward, but for consistency sake you should either return both kwenta and USDC reward as a tuple or return neither.

{
// Process Kwenta reward
kwentaReward = rewards[_account];
if (kwentaReward > 0) {
// update state (first)
rewards[_account] = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if transferKwenta is set to false meaning they want to compound their rewards then doesnt this line ruin that?

their rewards are set to 0 but they dont get transferred their kwenta, so next time this is called again (im assuming after they waited for it to compound in this contract), then their rewards will be 0 or close to 0 and therefore they lost their kwenta entirely?

if im understanding this correctly then this rewards[_account] = 0; line shouldnt apply if transferKwenta == false

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh upon further look, kwentaReward is extrapolating this value? and it hopefully gets relogged when calling _stake in _compound?


// emit reward claimed event and index account
emit RewardPaid(_account, reward);
emit RewardPaid(_account, kwentaReward);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other event is called RewardPaidUSDC

this should be called RewardPaidKwenta to match


// transfer token from this contract to the rewardEscrow
// and create a vesting entry at the _to address
kwenta.transfer(address(rewardEscrow), reward);
rewardEscrow.appendVestingEntry(_to, reward);
if (transferKwenta) {
kwenta.transfer(_to, kwentaReward);
}
}

uint256 rewardUSDC = rewardsUSDC[_account] / PRECISION;
if (rewardUSDC > 0) {
// Process USDC reward
uint256 usdcReward = rewardsUSDC[_account] / PRECISION;
if (usdcReward > 0) {
// update state (first)
rewardsUSDC[_account] = 0;

// emit reward claimed event and index account
emit RewardPaidUSDC(_account, rewardUSDC);
emit RewardPaidUSDC(_account, usdcReward);

// transfer token from this contract to the account
// as newly issued rewards from inflation are now issued as non-escrowed
usdc.transfer(_to, rewardUSDC);
usdc.transfer(_to, usdcReward);
}
}

Expand All @@ -397,8 +386,8 @@ contract StakingRewardsV2 is
/// @dev internal helper to compound for a given account
/// @param _account the account to compound for
function _compound(address _account) internal {
_getReward(_account);
_stakeEscrow(_account, unstakedEscrowedBalanceOf(_account));
uint256 reward = _processReward(_account, _account, false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will reward be 0 here?

_stake(_account, reward);
}

/*///////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -688,17 +677,6 @@ contract StakingRewardsV2 is
emit RewardsDurationUpdated(rewardsDuration);
}

/// @inheritdoc IStakingRewardsV2
function setCooldownPeriod(uint256 _cooldownPeriod) external onlyOwner {
if (_cooldownPeriod < MIN_COOLDOWN_PERIOD) revert CooldownPeriodTooLow(MIN_COOLDOWN_PERIOD);
if (_cooldownPeriod > MAX_COOLDOWN_PERIOD) {
revert CooldownPeriodTooHigh(MAX_COOLDOWN_PERIOD);
}

cooldownPeriod = _cooldownPeriod;
emit CooldownPeriodUpdated(cooldownPeriod);
}

/*///////////////////////////////////////////////////////////////
PAUSABLE
///////////////////////////////////////////////////////////////*/
Expand Down
29 changes: 3 additions & 26 deletions contracts/interfaces/IStakingRewardsV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ interface IStakingRewardsV2 {
/// @param _account: address to check
/// @return amount of tokens escrowed but not staked
function unstakedEscrowedBalanceOf(address _account) external view returns (uint256);

/// @notice the period of time a user has to wait after staking to unstake
function cooldownPeriod() external view returns (uint256);


// rewards

/// @notice calculate the total rewards for one duration based on the current rate
Expand Down Expand Up @@ -158,11 +155,11 @@ interface IStakingRewardsV2 {
/// @dev updateReward() called prior to function logic
function unstakeEscrow(uint256 _amount) external;

/// @notice unstake escrowed token skipping the cooldown wait period
/// @notice unstake escrowed token on behalf of another account
/// @param _account: address of account to unstake from
/// @param _amount: amount to unstake
/// @dev this function is used to allow tokens to be vested at any time by RewardEscrowV2
function unstakeEscrowSkipCooldown(address _account, uint256 _amount) external;
function unstakeEscrowAdmin(address _account, uint256 _amount) external;

/// @notice unstake all available staked non-escrowed tokens and
/// claim any rewards
Expand Down Expand Up @@ -211,10 +208,6 @@ interface IStakingRewardsV2 {
/// @param _rewardsDuration: denoted in seconds
function setRewardsDuration(uint256 _rewardsDuration) external;

/// @notice set unstaking cooldown period
/// @param _cooldownPeriod: denoted in seconds
function setCooldownPeriod(uint256 _cooldownPeriod) external;

// pausable

/// @dev Triggers stopped state
Expand Down Expand Up @@ -279,10 +272,6 @@ interface IStakingRewardsV2 {
/// @param amount: amount of token recovered
event Recovered(address token, uint256 amount);

/// @notice emitted when the unstaking cooldown period is updated
/// @param cooldownPeriod: the new unstaking cooldown period
event CooldownPeriodUpdated(uint256 cooldownPeriod);

/// @notice emitted when an operator is approved
/// @param owner: owner of tokens
/// @param operator: address of operator
Expand Down Expand Up @@ -322,21 +311,9 @@ interface IStakingRewardsV2 {
/// @notice recovering the usdc reward token is not allowed
error CannotRecoverRewardToken();

/// @notice error when user tries unstake during the cooldown period
/// @param canUnstakeAt timestamp when user can unstake
error MustWaitForUnlock(uint256 canUnstakeAt);

/// @notice error when trying to set a rewards duration that is too short
error RewardsDurationCannotBeZero();

/// @notice error when trying to set a cooldown period below the minimum
/// @param minCooldownPeriod minimum cooldown period
error CooldownPeriodTooLow(uint256 minCooldownPeriod);

/// @notice error when trying to set a cooldown period above the maximum
/// @param maxCooldownPeriod maximum cooldown period
error CooldownPeriodTooHigh(uint256 maxCooldownPeriod);

/// @notice the caller is not approved to take this action
error NotApproved();

Expand Down
21 changes: 1 addition & 20 deletions test/foundry/integration/escrow.migrator.fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -794,26 +794,7 @@ contract StakingV2MigrationForkTests is EscrowMigratorTestHelpers {
// check final state - user2 didn't manage to migrate any entries
checkStateAfterStepThree(user1, 0, 0);
}

function test_Cannot_Bypass_Unstaking_Cooldown_Lock() public {
// this is the malicious entry - the duration is set to 1
createRewardEscrowEntryV1(user1, 50 ether, 1);

(uint256[] memory _entryIDs, uint256 numVestingEntries,) = claimAndFullyMigrate(user1);
checkStateAfterStepThree(user1, _entryIDs);

// specifically
uint256[] memory migratedEntryIDs =
rewardEscrowV2.getAccountVestingEntryIDs(user1, numVestingEntries - 2, 1);
uint256 maliciousEntryID = migratedEntryIDs[0];
(uint256 endTime, uint256 escrowAmount, uint256 duration, uint256 earlyVestingFee) =
rewardEscrowV2.getVestingEntry(maliciousEntryID);
assertEq(endTime, block.timestamp + stakingRewardsV2.cooldownPeriod());
assertEq(escrowAmount, 50 ether);
assertEq(duration, stakingRewardsV2.cooldownPeriod());
assertEq(earlyVestingFee, 90);
}


function test_Cannot_Migrate_In_Non_Initiated_State() public {
(uint256[] memory _entryIDs,) = claimAndCheckInitialState(user1);

Expand Down
7 changes: 4 additions & 3 deletions test/foundry/integration/stakingV2.migration.fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,11 @@ contract StakingV2MigrationForkTests is StakingTestHelpers {
// get rewards
getStakingRewardsV2(user1);

// assert v2 rewards have been earned
assertGt(kwenta.balanceOf(user1), 0);

// stake the rewards
stakeAllUnstakedEscrowV2(user1);
stakeFundsV2(user1, kwenta.balanceOf(user1));

// check StakingRewardsV1 balance unchanged
assertEq(stakingRewardsV1.nonEscrowedBalanceOf(user1), 0);
Expand All @@ -117,8 +120,6 @@ contract StakingV2MigrationForkTests is StakingTestHelpers {
uint256 user1EscrowStakedV2 = stakingRewardsV2.escrowedBalanceOf(user1);
uint256 user1NonEscrowedStakeV2 = stakingRewardsV2.nonEscrowedBalanceOf(user1);

// assert v2 rewards have been earned
assertGt(rewardEscrowV2.escrowedBalanceOf(user1), 0);
// v2 staked balance is equal to escrowed + non-escrowed balance
assertEq(stakingRewardsV2.balanceOf(user1), user1EscrowStakedV2 + user1NonEscrowedStakeV2);
// v2 reward escrow balance is equal to escrow staked balance
Expand Down
Loading
Loading