Skip to content

Commit

Permalink
Merge pull request #13 from Azuro-protocol/rewardpool-v2
Browse files Browse the repository at this point in the history
RewardPoolV2
  • Loading branch information
Shchepetov authored Aug 6, 2024
2 parents 6c63f77 + a1755a0 commit 97ae4fe
Show file tree
Hide file tree
Showing 15 changed files with 9,580 additions and 1,063 deletions.
6 changes: 5 additions & 1 deletion .env.mock
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ AZUR=0x765...456
UNSTAKEPERIOD=2592000
REWARD_POOL_ADDRESS=

NAME="Staked $AZUR"
SYMBOL="stAZUR"
REWARD_POOL_V2_ADDRESS=0xa..d3

POLYGONSCAN_API_KEY=1N..XX
ETHERSCAN_API_KEY=1N..XX
MUMBAI_PRIVATE_KEY=db0e..82c2
POLYGON_PRIVATE_KEY=db0e..82c2
MAINNET_PRIVATE_KEY=db0e..82c2
INFURA_API_KEY=
INFURA_API_KEY=
65 changes: 55 additions & 10 deletions contracts/hardhat/contracts/RewardPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

pragma solidity 0.8.24;

import "./interface/IRewardPool.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "./interface/IRewardPool.sol";

/*
This contract is used for distributing rewards for rewardPool to various stakers.
Expand Down Expand Up @@ -61,6 +61,9 @@ contract RewardPool is OwnableUpgradeable, IRewardPool {
uint256 public rewardPerPower;
uint256 public unstakePeriod;

IRewardPoolV2 public rewardPoolV2;
bool public isStakingProhibited;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
Expand All @@ -80,6 +83,26 @@ contract RewardPool is OwnableUpgradeable, IRewardPool {
distributions[0].time = block.timestamp.toUint64();
}

/**
* @notice Owner's function that is used to change the address of liquid staking contract to migrate.
*/
function changeRewardPoolV2(
IRewardPoolV2 newRewardPoolV2
) external onlyOwner {
rewardPoolV2 = newRewardPoolV2;
emit RewardPoolV2Changed(newRewardPoolV2);
}

/**
* @dev Changes the staking status.
* @param isStakingProhibited_ The new staking status (true for prohibited, false for allowed).
*/
function changeStakingStatus(bool isStakingProhibited_) external onlyOwner {
if (isStakingProhibited_ == isStakingProhibited) revert NoChanges();
isStakingProhibited = isStakingProhibited_;
emit StakingStatusChanged(isStakingProhibited_);
}

/**
* @notice Owner's function that is used to change unstake period
* @param newUnstakePeriod new unstake period, will be actual for new unstake requests
Expand Down Expand Up @@ -167,6 +190,8 @@ contract RewardPool is OwnableUpgradeable, IRewardPool {
address account,
uint96 amount
) public returns (uint256 stakeId) {
if (isStakingProhibited) revert StakingIsProhibited();

token.safeTransferFrom(msg.sender, address(this), amount);

// This stake's first distribution will be next distribution
Expand Down Expand Up @@ -203,6 +228,22 @@ contract RewardPool is OwnableUpgradeable, IRewardPool {
_withdrawReward(stakeId);
}

/**
* @dev Migrates a stake to the RewardPoolV2 contract.
* @param stakeId The ID of the stake to migrate.
*/
function migrateToV2(uint256 stakeId) external {
if (address(rewardPoolV2) == address(0)) revert RewardPoolV2NotSet();

_withdrawReward(stakeId);

uint96 amount = _removeStake(stakeId);
token.approve(address(rewardPoolV2), amount);
rewardPoolV2.depositFor(msg.sender, amount);

emit StakeMigrated(stakeId, address(rewardPoolV2));
}

/**
* @notice Returns current reward of given stake
* @param stakeId ID of the stake to get reward for
Expand Down Expand Up @@ -278,6 +319,18 @@ contract RewardPool is OwnableUpgradeable, IRewardPool {
function _requestUnstake(uint256 stakeId) private {
_withdrawReward(stakeId);

uint96 amount = _removeStake(stakeId);
uint256 unstakeTime = block.timestamp + unstakePeriod;
unstakes[stakeId] = Unstake({
owner: msg.sender,
amount: amount,
time: unstakeTime.toUint64()
});

emit UnstakeRequested(stakeId, msg.sender, amount, unstakeTime);
}

function _removeStake(uint256 stakeId) internal returns (uint96) {
uint32 distributionId = lastDistributionId + 1;
uint96 amount = stakes[stakeId].amount;

Expand All @@ -290,17 +343,9 @@ contract RewardPool is OwnableUpgradeable, IRewardPool {
distributions[distributionId].powerXTimeDelta -= timeDelta * amount;
}

uint256 unstakeTime = block.timestamp + unstakePeriod;

unstakes[stakeId] = Unstake({
owner: msg.sender,
amount: amount,
time: unstakeTime.toUint64()
});

delete stakes[stakeId];

emit UnstakeRequested(stakeId, msg.sender, amount, unstakeTime);
return amount;
}

/**
Expand Down
171 changes: 171 additions & 0 deletions contracts/hardhat/contracts/RewardPoolV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20WrapperUpgradeable.sol";

contract RewardPoolV2 is ERC20WrapperUpgradeable, OwnableUpgradeable {
struct WithdrawalRequest {
uint256 value;
address requester;
uint64 withdrawAfter;
}

// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC20Wrapper")) - 1)) & ~bytes32(uint256(0xff))
bytes32 internal constant ERC20WrapperStorageLocation =
0x3b5a617e0d4c238430871a64fe18212794b0c8d05a4eac064a8c9039fb5e0700;

mapping(uint256 => WithdrawalRequest) public withdrawalRequests;

uint256 public nextWithdrawalRequestId;
uint256 public withdrawalDelay;

event WithdrawalDelayChanged(uint256 newWithdrawalDelay);
event WithdrawalRequested(
address indexed requester,
uint256 indexed requestId,
uint256 value,
uint64 withdrawAfter
);
event WithdrawalRequestProcessed(uint256 indexed requestId, address indexed to);

error OnlyRequesterCanWithdrawToAnotherAddress(address requester);
error RequestDoesNotExist(uint256 requestId);
error WithdrawalLocked(uint256 withdrawAfter);
error ZeroValue();

function initialize(
IERC20 underlyingToken_,
string calldata name_,
string calldata symbol_,
uint256 unwrapDelay_
) external initializer {
__Ownable_init_unchained(msg.sender);
__ERC20_init_unchained(name_, symbol_);
__ERC20Wrapper_init_unchained(underlyingToken_);
withdrawalDelay = unwrapDelay_;
}

/**
* @dev Updates the withdrawal delay period.
* @param newWithdrawalDelay The new delay in seconds.
*/
function changeWithdrawalDelay(
uint256 newWithdrawalDelay
) external onlyOwner {
withdrawalDelay = newWithdrawalDelay;
emit WithdrawalDelayChanged(newWithdrawalDelay);
}

/**
* @dev Mint wrapped token to cover any underlyingTokens that would have been transferred by mistake.
* @param account The address to receive the tokens.
*/
function recover(address account) external onlyOwner returns (uint256) {
return _recover(account);
}

/**
* @dev Initiates a withdrawal request by burning tokens.
* @param value The amount of tokens to withdraw.
*/
function requestWithdrawal(uint256 value) external returns (uint256) {
if (value == 0) revert ZeroValue();

_burn(_msgSender(), value);

uint256 requestId = nextWithdrawalRequestId++;
uint64 withdrawAfter = uint64(block.timestamp + withdrawalDelay);
withdrawalRequests[requestId] = WithdrawalRequest({
value: value,
requester: msg.sender,
withdrawAfter: withdrawAfter
});

emit WithdrawalRequested(msg.sender, requestId, value, withdrawAfter);

return requestId;
}

/**
* @dev Processes multiple withdrawal requests and transfers tokens.
* @param account The address to receive the tokens.
* @param requestIds The IDs of the withdrawal requests.
*/
function batchWithdrawTo(
address account,
uint256[] calldata requestIds
) external {
uint256 numRequests = requestIds.length;
uint256 totalValue;
for (uint256 i; i < numRequests; ++i) {
totalValue += _processWithdrawalRequest(account, requestIds[i]);
}

SafeERC20.safeTransfer(
__getERC20WrapperStorage()._underlying,
account,
totalValue
);
}

/**
* @dev Processes a single withdrawal request and transfers tokens.
* @param account The address to receive the tokens.
* @param requestId The ID of the withdrawal request.
*/
function withdrawTo(
address account,
uint256 requestId
) public override returns (bool) {
uint256 value = _processWithdrawalRequest(account, requestId);
SafeERC20.safeTransfer(
__getERC20WrapperStorage()._underlying,
account,
value
);

return true;
}

/**
* @dev Handle a withdrawal request.
* @param account The address to transfer tokens to.
* @param requestId The ID of the withdrawal request.
* @return value The amount of tokens to withdraw.
*/
function _processWithdrawalRequest(
address account,
uint256 requestId
) internal returns (uint256) {
WithdrawalRequest storage request = withdrawalRequests[requestId];
uint256 value = request.value;
uint256 withdrawAfter = request.withdrawAfter;
address requester = request.requester;

if (value == 0) revert RequestDoesNotExist(requestId);
if (block.timestamp < withdrawAfter)
revert WithdrawalLocked(withdrawAfter);
if (account != requester && requester != msg.sender)
revert OnlyRequesterCanWithdrawToAnotherAddress(requester);

delete withdrawalRequests[requestId];
emit WithdrawalRequestProcessed(requestId, account);

return value;
}

/**
* @dev Returns the storage reference for the ERC20 wrapped token.
*/
function __getERC20WrapperStorage()
internal
pure
returns (ERC20WrapperStorage storage $)
{
assembly {
$.slot := ERC20WrapperStorageLocation
}
}
}
11 changes: 11 additions & 0 deletions contracts/hardhat/contracts/interface/IRewardPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

pragma solidity 0.8.24;

import "./IRewardPoolV2.sol";

interface IRewardPool {
/** @notice Structure describing one reward distribution for node */
struct Distribution {
Expand Down Expand Up @@ -29,6 +31,10 @@ interface IRewardPool {
uint64 time;
}

event RewardPoolV2Changed(IRewardPoolV2 newRewardPoolV2);

event StakingStatusChanged(bool isStakingProhibited);

/** @notice Event emitted when new stake is created */
event Staked(
uint256 indexed stakeId,
Expand All @@ -39,6 +45,8 @@ interface IRewardPool {
/** @notice Event emitted when reward is wihdrawn for some stake */
event RewardWithdrawn(uint256 indexed stakeId, uint256 reward);

event StakeMigrated(uint256 stakeId, address rewardPoolV2);

/** @notice Event emitted when some unstake is requested */
event UnstakeRequested(
uint256 indexed stakeId,
Expand All @@ -60,9 +68,12 @@ interface IRewardPool {
/** @notice Event emitted when reward is distributed */
event RewardDistributed(uint256 reward);

error NoChanges();
error NoStakes();
error NotStakeOwner();
error IncorrectUnstake();
error IncorrectUnstakeTime();
error MaxUnstakePeriodExceeded();
error RewardPoolV2NotSet();
error StakingIsProhibited();
}
20 changes: 20 additions & 0 deletions contracts/hardhat/contracts/interface/IRewardPoolV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

interface IRewardPoolV2 {
function changeWithdrawalDelay(uint256 newWithdrawalDelay) external;

function depositFor(address account, uint256 value) external returns (bool);

function recover(address account) external returns (uint256);

function requestWithdrawal(uint256 value) external returns (uint256);

function withdrawTo(address account, uint256 requestId) external;

function batchWithdrawTo(
address account,
uint256[] calldata requestIds
) external;
}
Loading

0 comments on commit 97ae4fe

Please sign in to comment.