Skip to content

Commit

Permalink
Draft Abstract Staking logic (#83)
Browse files Browse the repository at this point in the history
* Draft Abstract Staking logic

* draft implementation

* Modified ValueDistributor and Staking contracts, added natspec comments

* Refactored ValueDistributor and Staking contracts.
Fixed some logic

* Modified ValueDistributor and Staking contracts

* Implemented tests for AbstractStaking and ValueDistributor contracts

* Refactored AbstractStaking and ValueDistributor contracts, added more tests

* Refactored AbstractStaking and ValueDistributor contracts

* Refactored tests and moved events to the AbstractValueDistributor contract

* Added 2 tests for AbstractStaking to reach the full coverage

* Updated the patch version

* Refactored AbstractStaking, AbstractValueDistributor and AbstractValueDistributorMock contracts

* Refactored AbstractStaking and AbstractValueDistributor contracts

---------

Co-authored-by: Artem Chystiakov <[email protected]>
  • Loading branch information
mllwchrry and Arvolear authored Jan 23, 2024
1 parent 5f7b2b6 commit c76706a
Show file tree
Hide file tree
Showing 9 changed files with 1,548 additions and 4 deletions.
76 changes: 76 additions & 0 deletions contracts/mock/staking/AbstractStakingMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {AbstractStaking} from "../../staking/AbstractStaking.sol";

contract AbstractStakingMock is AbstractStaking, Multicall {
function __AbstractStakingMock_init(
address sharesToken_,
address rewardsToken_,
uint256 rate_,
uint256 stakingStartTime_
) external initializer {
__AbstractStaking_init(sharesToken_, rewardsToken_, rate_, stakingStartTime_);
}

function mockInit(
address sharesToken_,
address rewardsToken_,
uint256 rate_,
uint256 stakingStartTime_
) external {
__AbstractStaking_init(sharesToken_, rewardsToken_, rate_, stakingStartTime_);
}

function setStakingStartTime(uint256 stakingStartTime_) external {
_setStakingStartTime(stakingStartTime_);
}

function setRate(uint256 newRate_) external {
_setRate(newRate_);
}

function userShares(address user_) external view returns (uint256) {
return userDistribution(user_).shares;
}

function userOwedValue(address user_) external view returns (uint256) {
return userDistribution(user_).owedValue;
}
}

contract StakersFactory is Multicall {
Staker[] public stakers;

function createStaker() public {
Staker staker_ = new Staker();
stakers.push(staker_);
}

function stake(
address stakingContract_,
address staker_,
address token_,
uint256 amount_
) external {
Staker(staker_).stake(stakingContract_, token_, amount_);
}

function unstake(address stakingContract_, address staker_, uint256 amount_) external {
Staker(staker_).unstake(stakingContract_, amount_);
}
}

contract Staker {
function stake(address stakingContract_, address token_, uint256 amount_) external {
IERC20(token_).approve(stakingContract_, amount_);
AbstractStakingMock(stakingContract_).stake(amount_);
}

function unstake(address stakingContract_, uint256 amount_) external {
AbstractStakingMock(stakingContract_).unstake(amount_);
}
}
36 changes: 36 additions & 0 deletions contracts/mock/staking/AbstractValueDistributorMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol";

import {AbstractValueDistributor} from "../../staking/AbstractValueDistributor.sol";
import {DECIMAL} from "../../utils/Globals.sol";

contract AbstractValueDistributorMock is AbstractValueDistributor, Multicall {
function addShares(address user_, uint256 amount_) external {
_addShares(user_, amount_);
}

function removeShares(address user_, uint256 amount_) external {
_removeShares(user_, amount_);
}

function distributeValue(address user_, uint256 amount_) external {
_distributeValue(user_, amount_);
}

function userShares(address user_) external view returns (uint256) {
return userDistribution(user_).shares;
}

function userOwedValue(address user_) external view returns (uint256) {
return userDistribution(user_).owedValue;
}

function _getValueToDistribute(
uint256 timeUpTo_,
uint256 timeLastUpdate_
) internal view virtual override returns (uint256) {
return DECIMAL * (timeUpTo_ - timeLastUpdate_);
}
}
2 changes: 1 addition & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solarity/solidity-lib",
"version": "2.6.13",
"version": "2.6.14",
"license": "MIT",
"author": "Distributed Lab",
"readme": "README.md",
Expand Down
204 changes: 204 additions & 0 deletions contracts/staking/AbstractStaking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

import {AbstractValueDistributor} from "./AbstractValueDistributor.sol";

/**
* @notice The AbstractStaking module
*
* Contract module for staking tokens and earning rewards based on shares.
*/
abstract contract AbstractStaking is AbstractValueDistributor, Initializable {
using SafeERC20 for IERC20;

address private _sharesToken;
address private _rewardsToken;

/**
* @dev The rate of rewards distribution per second.
*
* It determines the rate at which rewards are earned and distributed
* to stakers based on their shares.
*
* Note: Ensure that the `_rate` value is set correctly to match
* the decimal precision of the `_rewardsToken` to ensure accurate rewards distribution.
*/
uint256 private _rate;

uint256 private _stakingStartTime;

/**
* @dev Throws if the staking has not started yet.
*/
modifier stakingStarted() {
_checkStakingStarted();
_;
}

/**
* @notice Initializes the contract setting the values provided as shares token, rewards token, reward rate and staking start time.
*
* Warning: when shares and rewards tokens are the same, users may accidentally withdraw
* other users' shares as a reward if the rewards token balance is improperly handled.
*
* @param sharesToken_ The address of the shares token.
* @param rewardsToken_ The address of the rewards token.
* @param rate_ The reward rate.
* @param stakingStartTime_ The staking start time
*/
function __AbstractStaking_init(
address sharesToken_,
address rewardsToken_,
uint256 rate_,
uint256 stakingStartTime_
) internal onlyInitializing {
require(sharesToken_ != address(0), "Staking: zero address cannot be the Shares Token");
require(rewardsToken_ != address(0), "Staking: zero address cannot be the Rewards Token");

_sharesToken = sharesToken_;
_rewardsToken = rewardsToken_;
_setRate(rate_);
_setStakingStartTime(stakingStartTime_);
}

/**
* @notice Stakes the specified amount of tokens.
* @param amount_ The amount of tokens to stake.
*/
function stake(uint256 amount_) public stakingStarted {
_addShares(msg.sender, amount_);
}

/**
* @notice Unstakes the specified amount of tokens.
* @param amount_ The amount of tokens to unstake.
*/
function unstake(uint256 amount_) public stakingStarted {
_removeShares(msg.sender, amount_);
}

/**
* @notice Claims the specified amount of rewards.
* @param amount_ The amount of rewards to claim.
*/
function claim(uint256 amount_) public stakingStarted {
_distributeValue(msg.sender, amount_);
}

/**
* @notice Withdraws all the staked tokens together with rewards.
*
* Note: All the rewards are claimed after the shares are removed.
*
* @return shares_ The amount of shares being withdrawn.
* @return owedValue_ The total value of the rewards owed to the user.
*/
function withdraw() public stakingStarted returns (uint256 shares_, uint256 owedValue_) {
shares_ = userDistribution(msg.sender).shares;
owedValue_ = getOwedValue(msg.sender);

unstake(shares_);
claim(owedValue_);
}

/**
* @notice Returns the shares token.
* @return The address of the shares token contract.
*/
function sharesToken() public view returns (address) {
return _sharesToken;
}

/**
* @notice Returns the rewards token.
* @return The address of the rewards token contract.
*/
function rewardsToken() public view returns (address) {
return _rewardsToken;
}

/**
* @notice Returns the staking start time.
* @return The timestamp when staking starts.
*/
function stakingStartTime() public view returns (uint256) {
return _stakingStartTime;
}

/**
* @notice Returns the rate of rewards distribution.
* @return The rate of rewards distribution per second.
*/
function rate() public view returns (uint256) {
return _rate;
}

/**
* @notice Sets the staking start time.
* @param stakingStartTime_ The timestamp when staking will start.
*/
function _setStakingStartTime(uint256 stakingStartTime_) internal {
_stakingStartTime = stakingStartTime_;
}

/**
* @notice Sets the rate of rewards distribution per second.
* @param newRate_ The new rate of rewards distribution.
*/
function _setRate(uint256 newRate_) internal {
_update(address(0));

_rate = newRate_;
}

/**
* @notice Hook function that is called after shares have been added to a user's distribution.
* @param user_ The address of the user.
* @param amount_ The amount of shares added.
*/
function _afterAddShares(address user_, uint256 amount_) internal virtual override {
IERC20(_sharesToken).safeTransferFrom(user_, address(this), amount_);
}

/**
* @notice Hook function that is called after shares have been removed from a user's distribution.
* @param user_ The address of the user.
* @param amount_ The amount of shares removed.
*/
function _afterRemoveShares(address user_, uint256 amount_) internal virtual override {
IERC20(_sharesToken).safeTransfer(user_, amount_);
}

/**
* @notice Hook function that is called after value has been distributed to a user.
* @param user_ The address of the user.
* @param amount_ The amount of value distributed.
*/
function _afterDistributeValue(address user_, uint256 amount_) internal virtual override {
IERC20(_rewardsToken).safeTransfer(user_, amount_);
}

/**
* @dev Throws if the staking has not started yet.
*/
function _checkStakingStarted() internal view {
require(block.timestamp >= _stakingStartTime, "Staking: staking has not started yet");
}

/**
* @notice Gets the value to be distributed for a given time period.
* @param timeUpTo_ The end timestamp of the period.
* @param timeLastUpdate_ The start timestamp of the period.
* @return The value to be distributed for the period.
*/
function _getValueToDistribute(
uint256 timeUpTo_,
uint256 timeLastUpdate_
) internal view virtual override returns (uint256) {
return _rate * (timeUpTo_ - timeLastUpdate_);
}
}
Loading

0 comments on commit c76706a

Please sign in to comment.