Dynamic Staking

Hey guys,
Today another tutorial where I will show you how to setup a dynamic staking contract.

what is dynamic staking?
Dynamic Staking means that users can deposit and withdraw crypto A at any time without a time lock. Also rewards are not distributed by the means of a stable APY or token distribution. This means that tokens (rewards) can be deposited at any time and any amount.

using EnumerableSet for EnumerableSet.AddressSet;
using SafeMath for uint;

What do we need to get started?
For this tutorial I will use safemath and the EnumerableSet.AddressSet from Openzeppelin.

Variables
We will first define two ERC20 variables that represent our deposit and our withdraw token. Also just because I like it, I will keep track of all depositor addresses and the total amount of the depositors with the addressSet.

IERC20 private depToken; // deposit token (LP)
IERC20 private rewToken; // reward token
EnumerableSet.AddressSet private stakeholders; // list of depositor addresses

We will also need to define a Struct for the deposits. We will call this Struct "Stake". We will need a variable staked which is the amount of deposited tokens and a variable shares which tells us how much of the staking pool and thus rewards a user owns.

struct Stake {
        uint staked;
        uint shares;
    }

Now we define some other basic variables as the owner and also other variables to keep track of some information such ass the total amount of deposited tokens and total amount of shares and if the contract is initialized.

    address public owner; // owner of the contract
    uint private totalStakes; // total amount of tokens deposited
    uint private totalShares; // total amount of shares issued
    bool private initialized; // if contract is initialized

In order to utilize our struct we need to create a mapping from a corresponding user address to our struct

mapping(address => Stake) private stakeholderToStake; // mapping from the depositor address to his information (tokens deposited etc.)

We will now define a onlyOwner modifier as we have a important function that should only be called by the owner.

    /*\
    function with this modifier can only be called by the owner
    \*/
    modifier onlyOwner() {
        require(msg.sender == owner, "caller not owner");
        _;
    }

To give easier access to UI's and track historical information later in development we will also add some events.

    event StakeAdded(address indexed stakeholder, uint amount, uint shares, uint timestamp); // this event emits on every deposit
    event StakeRemoved(address indexed stakeholder, uint amount, uint shares, uint reward, uint timestamp); // this event emits on every withdraw

Functions
Initialize
After the deployment of our contract we will first have to call the initialize function with a small amount of our deposit tokens. This is because otherwise we will encounter a 0 division. We could counter this by checking that the values in the deposit function are non zero but this would in the long term cost a lot more gas for the users. Please note that the small amount of deposit tokens in the initilize functions are lost forever, thus are the rewards that these token accumulate. For this exact reason you should use a very small amount of tokens (<1) in order to initialize the contract.
The initialize function will also renounce ownership as there is no reason for ownership after deployment.

/*\
    initialize all values
    amount will be locked forever
    \*/
    function initialize(uint _amount) external onlyOwner {
        require(!initialized, "already initialized!");
        uint balBef = depToken.balanceOf(address(this));
        require(depToken.transferFrom(msg.sender, address(this), _amount), "transfer failed!");
        _amount = depToken.balanceOf(address(this)).sub(balBef);

        stakeholderToStake[address(0)] = Stake({
            staked: _amount,
            shares: _amount
        });
        totalStakes = _amount;
        totalShares = _amount;
        initialized = true;
        owner = address(0);
        emit StakeAdded(address(0), _amount, _amount, block.timestamp);
    }

Deposit
We make sure the contract is initialized and that the users deposits more than 0 tokens.
We add the amount of deposit and reward tokens in our contract and note it as tbal
The shares that the users receives is the deposited amount * total shares / tbal

    /*\
    stake tokens
    \*/
    function _deposit(address _account, uint _amount) private {
        require(initialized, "not initialized!");
        require(_amount > 0, "amount too small!");

        uint tbal = depToken.balanceOf(address(this)).add(rewToken.balanceOf(address(this)));
        uint shares = _amount.mul(totalShares).div(tbal);
        uint balBef = depToken.balanceOf(address(this));
        require(depToken.transferFrom(_account, address(this), _amount), "transfer failed!");
        _amount = depToken.balanceOf(address(this)).sub(balBef);

        stakeholders.add(_account);
        stakeholderToStake[_account] = Stake({
            staked: _amount,
            shares: shares
        });
        totalStakes = totalStakes.add(_amount);
        totalShares += shares;
        emit StakeAdded(_account, _amount, shares, block.timestamp);
    }

Withdraw
For the Withdraw we first call our rewardOf() function and then update all values, after that we will send him his reward and deposited tokens. The function will always withdraw 100% of tokens. As a dev you should however be able to easily change this.

/*\
    remove staked tokens
    \*/
   function _withdraw(address _account) internal {
        require(stakeholderToStake[_account].staked > 0, "not staked!");
        uint rewards = rewardOf(_account);
        uint stake = stakeholderToStake[_account].staked;
        uint shares = stakeholderToStake[_account].shares;

        stakeholderToStake[_account] = Stake({
            staked: 0,
            shares: 0
        });
        totalShares = totalShares.sub(shares);
        totalStakes = totalStakes.sub(stake);

        require(depToken.transfer(_account, stake), "initial transfer failed!");
        require(rewToken.transfer(_account, rewards), "reward transfer failed!");

        stakeholders.remove(_account);

        emit StakeRemoved(_account, stake, shares, rewards, block.timestamp);
    }

rewardOf
We divide the deposited tokens of the user by the shares. This is the ratio at which the user deposited his tokens. Then we get the current ratio which is defined as tbal / total shares, where tbal stands for the combined amount of deposited and reward tokens. If the current ratio is smaller than the ratio at which a user has deposited then there are no rewards.
The reward is calculated as follows: the user shares * (current ratio - user ratio)

/*\
    get rewards that user received
    \*/
    function rewardOf(address stakeholder) public view returns (uint) {
        uint stakeholderStake = stakeOf(stakeholder);
        uint stakeholderShares = sharesOf(stakeholder);

        if (stakeholderShares == 0) {
            return 0;
        }

        uint stakedRatio = stakeholderStake.mul(1e18);
        uint currentRatio = stakeholderShares.mul(getRatio());
        
        if (currentRatio <= stakedRatio) {
            return 0;
        }
        
        uint rewards = currentRatio.sub(stakedRatio).div(1e18);
        return rewards;
    }

Emergency withdraw
Now even tho there should be no errors or edge cases, there is always a non-zero chance of contract failure, this might also apply to reward tokens. For this exact reason there is a emergy withdraw feature that let's user withdraw all their deposit without receiving any rewards. The lost rewards are automaticlly distributed to the other users.

    /*\
    withdraw function if in emergency state (no rewards)
    \*/
   function emergencyWithdraw() external returns(bool) {
        uint stake = stakeholderToStake[msg.sender].staked;
        uint shares = stakeholderToStake[msg.sender].shares;

        delete stakeholderToStake[msg.sender];
        totalShares = totalShares.sub(shares);
        totalStakes = totalStakes.sub(stake);

        require(depToken.transfer(msg.sender, stake), "initial transfer failed!");

        stakeholders.remove(msg.sender);
        return true;
    }

Misc
There are some for less fundamental functions of this contract. There are more things do to such as maybe ass time locks and custom withdraw amounts. The whole code is up on github
And if you need a professional smart contract dev for your next project then contact me on telegram: @solidityX

You don't really need that as of solc v0.8.0 (and your code will look much cleaner without it).


Semantic: that might lead the reader to think that you don't fully conceive hexadecimal representation for what it actually is (i.e., that you ascribe some sort of "magical characteristic" to it).
In short, address(0) is good enough.


Your code does not take into account the possibility of depToken implementing an internal tax mechanism (i.e., tax-on-transfer), which will lead it to entitle the caller with more than what their de-facto deposit is worth.

You probably want to record depToken.balanceOf(address(this)) before and after the call to transferFrom, and then determine exactly how much was transferred into your contract (rather than assuming that the input _amount indicates it correctly).


Or simply delete stakeholderToStake[_account];.


Here is a cleaner, cheaper and more accurate way to do this:

uint stakedRatio = stakeholderStake.mul(1e18);
uint currentRatio = stakeholderShares.mul(getRatio());

if (currentRatio <= stakedRatio) {
    return 0;
}

uint rewards = currentRatio.sub(stakedRatio).div(1e18);

And as previously mentioned, if you're on solc 0.8.0 or higher, then you may as well replace all of these SafeMath library function calls with native arithmetic operations.

1 Like

thanks for your optimizations, this code is older and not really up to date but it's a little project that I like and wanted to share with others. Also I just like to use safemath, I know it's not necessary but I like it. Thanks you for the other optimizations I will adjust the code accordingly.

NP, I was trying to help you with your statement from the original question:

Since using SafeMarh just because "you like it" doesn't sound too professional.

And it doesn't sound too professional for several good reasons:

  • It makes the contract's code a lot less readable
  • It makes the contract's byte-code larger, getting it closer to the maximum limit of 24KB
  • It makes the contract's byte-code larger, consuming more gas during contract deployment
  • It makes the involved functions more expensive, consuming more gas whenever they're executed

The only place where it makes sense to use it is when you have two unchecked blocks with a little bit of checked code in between them, and you'd like to merge them into a single unchecked block, where you'll need to replace arithmetic operations in the previously-checked code with SafeMath functions.

For example, if you have something like this:

unchecked {
    ...
}
uint256 z = x * y;
unchecked {
    ...
}

Then there is some sense in doing this instead:

unchecked {
    ...
    uint256 z = x.mul(y);
    ...
}

And again, this is useful only for readability.

In short - if you want to sound professional, then try to avoid saying out loud stuff like:

1 Like

Thanks mate, that's true and I normally don't use safemath but I enjoy using it in projects that I for whatever reason started it. Otherwise professional is I suppose more of a marketing term, professional is also subjective and you are probably a better solidity dev and actually professional compared to me. Thanks for all your feedback and I will definitly incoperate it into my coding style from now on.

1 Like