The contract constructor

I'd like to make sure that this contract constructor is coded properly way.
Does this contract allow me to set a stake to any token as well as the reward could be any token?
for example

  • set staking token for myToken and the reward token is USDC
  • set staking token for LPs address and the reward is DAi
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/access/Ownable.sol';

interface IERC20Ext is IERC20 {
    function decimals() external returns (uint);
}

// The goal of this farm is to allow a stake dummyToken to earn anything model
// In a flip of a traditional farm, this contract only accepts dummyToken as the staking token
// Each new pool added is a new reward token, each with its own start times
// end times, and rewards per second.
contract StakeLab is Ownable {
    using SafeERC20 for IERC20;

    // Info of each user.
    struct UserInfo {
        uint256 amount;     // How many tokens the user has provided.
        uint256 rewardDebt; // Reward debt. See explanation below.
    }

    // Info of each pool.
    struct PoolInfo {
        IERC20 RewardToken;       // Address of reward token contract.
        uint256 RewardPerSecond;   // reward token per second for this pool
        uint256 TokenPrecision; // The precision factor used for calculations, dependent on a tokens decimals
        uint256 dummyTokenStakedAmount; // # of dummyToken allocated to this pool
        uint256 lastRewardTime;  // Last block time that reward distribution occurs.
        uint256 accRewardPerShare; // Accumulated reward per share, times the pools token precision. See below.
        uint256 endTime; // end time of pool
        uint256 startTime; // start time of pool
        uint256 userLimitEndTime;
        address protocolOwnerAddress; // this address is the owner of the protocol corresponding to the reward token, used for emergency withdraw to them only
    }

    IERC20 public immutable dummyToken;
    uint public baseUserLimitTime = 2 days;
    uint public baseUserLimit = 0;

    // Info of each pool.
    PoolInfo[] public poolInfo;
    // Info of each user that stakes tokens.
    mapping (uint256 => mapping (address => UserInfo)) public userInfo;

    event AdminTokenRecovery(address tokenRecovered, uint256 amount);
    event Deposit(address indexed user, uint256 indexed pid, uint256 amount);
    event Withdraw(address indexed user, uint256 indexed pid, uint256 amount);
    event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount);
    event SetRewardPerSecond(uint _pid, uint256 _gemsPerSecond);

    constructor(IERC20 _dummyToken) {
        dummyToken = _dummyToken;
    }


    function poolLength() external view returns (uint256) {
        return poolInfo.length;
    }

    // Return reward multiplier over the given _from to _to block.
    function getMultiplier(uint256 _from, uint256 _to, PoolInfo memory pool) internal pure returns (uint256) {
        _from = _from > pool.startTime ? _from : pool.startTime;
        if (_from > pool.endTime || _to < pool.startTime) {
            return 0;
        }
        if (_to > pool.endTime) {
            return pool.endTime - _from;
        }
        return _to - _from;
    }

    // View function to see pending DUMMYs on frontend.
    function pendingReward(uint256 _pid, address _user) external view returns (uint256) {
        PoolInfo memory pool = poolInfo[_pid];
        UserInfo memory user = userInfo[_pid][_user];
        uint256 accRewardPerShare = pool.accRewardPerShare;

        if (block.timestamp > pool.lastRewardTime && pool.dummyTokenStakedAmount != 0) {
            uint256 multiplier = getMultiplier(pool.lastRewardTime, block.timestamp, pool);
            uint256 reward = multiplier * pool.RewardPerSecond;
            accRewardPerShare += (reward * pool.TokenPrecision) / pool.dummyTokenStakedAmount;
        }
        return (user.amount * accRewardPerShare / pool.TokenPrecision) - user.rewardDebt;
    }

    // Update reward variables for all pools. Be careful of gas spending!
    function massUpdatePools() public {
        uint256 length = poolInfo.length;
        for (uint256 pid = 0; pid < length; ++pid) {
            updatePool(pid);
        }
    }

    // Update reward variables of the given pool to be up-to-date.
    function updatePool(uint256 _pid) internal {
        PoolInfo storage pool = poolInfo[_pid];
        if (block.timestamp <= pool.lastRewardTime) {
            return;
        }

        if (pool.dummyTokenStakedAmount == 0) {
            pool.lastRewardTime = block.timestamp;
            return;
        }
        uint256 multiplier = getMultiplier(pool.lastRewardTime, block.timestamp, pool);
        uint256 reward = multiplier * pool.RewardPerSecond;

        pool.accRewardPerShare += reward * pool.TokenPrecision / pool.dummyTokenStakedAmount;
        pool.lastRewardTime = block.timestamp;
    }

    // Deposit tokens.
    function deposit(uint256 _pid, uint256 _amount) external {

        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][msg.sender];

        if(baseUserLimit > 0 && block.timestamp < pool.userLimitEndTime) {
            require(user.amount + _amount <= baseUserLimit, "deposit: user has hit deposit cap");
        }

        updatePool(_pid);

        uint256 pending = (user.amount * pool.accRewardPerShare / pool.TokenPrecision) - user.rewardDebt;

        user.amount += _amount;
        pool.dummyTokenStakedAmount += _amount;
        user.rewardDebt = user.amount * pool.accRewardPerShare / pool.TokenPrecision;

        if(pending > 0) {
            safeTransfer(pool.RewardToken, msg.sender, pending);
        }
        dummyToken.safeTransferFrom(address(msg.sender), address(this), _amount);

        emit Deposit(msg.sender, _pid, _amount);
    }

    // Withdraw tokens.
    function withdraw(uint256 _pid, uint256 _amount) external {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][msg.sender];

        require(user.amount >= _amount, "withdraw: not good");

        updatePool(_pid);

        uint256 pending = (user.amount * pool.accRewardPerShare / pool.TokenPrecision) - user.rewardDebt;

        user.amount -= _amount;
        pool.dummyTokenStakedAmount -= _amount;
        user.rewardDebt = user.amount * pool.accRewardPerShare / pool.TokenPrecision;

        if(pending > 0) {
            safeTransfer(pool.RewardToken, msg.sender, pending);
        }

        safeTransfer(dummyToken, address(msg.sender), _amount);

        emit Withdraw(msg.sender, _pid, _amount);
    }

    // Withdraw without caring about rewards. EMERGENCY ONLY.
    function emergencyWithdraw(uint256 _pid) external {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][msg.sender];

        uint oldUserAmount = user.amount;
        pool.dummyTokenStakedAmount -= user.amount;
        user.amount = 0;
        user.rewardDebt = 0;

        dummyToken.safeTransfer(address(msg.sender), oldUserAmount);
        emit EmergencyWithdraw(msg.sender, _pid, oldUserAmount);

    }

    // Safe erc20 transfer function, just in case if rounding error causes pool to not have enough reward tokens.
    function safeTransfer(IERC20 token, address _to, uint256 _amount) internal {
        uint256 bal = token.balanceOf(address(this));
        if (_amount > bal) {
            token.safeTransfer(_to, bal);
        } else {
            token.safeTransfer(_to, _amount);
        }
    }

    // Admin functions

    function changeEndTime(uint _pid, uint32 addSeconds) external onlyOwner {
        poolInfo[_pid].endTime += addSeconds;
    }

    function stopReward(uint _pid) external onlyOwner {
        poolInfo[_pid].endTime = block.number;
    }

    function changePoolUserLimitEndTime(uint _pid, uint _time) external onlyOwner {
        poolInfo[_pid].userLimitEndTime = _time;
    }

    function changeUserLimit(uint _limit) external onlyOwner {
        baseUserLimit = _limit;
    }

    function changeBaseUserLimitTime(uint _time) external onlyOwner {
        baseUserLimitTime = _time;
    }

    function checkForToken(IERC20 _Token) private view {
        uint256 length = poolInfo.length;
        for (uint256 _pid = 0; _pid < length; _pid++) {
            require(poolInfo[_pid].RewardToken != _Token, "checkForToken: reward token provided");
        }
    }

    function recoverWrongTokens(address _tokenAddress) external onlyOwner {
        require(_tokenAddress != address(dummyToken), "recoverWrongTokens: Cannot be dummyToken");
        checkForToken(IERC20(_tokenAddress));

        uint bal = IERC20(_tokenAddress).balanceOf(address(this));
        IERC20(_tokenAddress).safeTransfer(address(msg.sender), bal);

        emit AdminTokenRecovery(_tokenAddress, bal);
    }

    function emergencyRewardWithdraw(uint _pid, uint256 _amount) external onlyOwner {
        poolInfo[_pid].RewardToken.safeTransfer(poolInfo[_pid].protocolOwnerAddress, _amount);
    }

    // Add a new token to the pool. Can only be called by the owner.
    function add(uint _rewardPerSecond, IERC20Ext _Token, uint _startTime, uint _endTime, address _protocolOwner) external onlyOwner {

        checkForToken(_Token); // ensure you cant add duplicate pools

        uint lastRewardTime = block.timestamp > _startTime ? block.timestamp : _startTime;
        uint decimalsRewardToken = _Token.decimals();
        require(decimalsRewardToken < 30, "Token has way too many decimals");
        uint precision = 10**(30 - decimalsRewardToken);

        poolInfo.push(PoolInfo({
            RewardToken: _Token,
            RewardPerSecond: _rewardPerSecond,
            TokenPrecision: precision,
            dummyTokenStakedAmount: 0,
            startTime: _startTime,
            endTime: _endTime,
            lastRewardTime: lastRewardTime,
            accRewardPerShare: 0,
            protocolOwnerAddress: _protocolOwner,
            userLimitEndTime: lastRewardTime + baseUserLimitTime
        }));
    }

    // Update the given pool's reward per second. Can only be called by the owner.
    function setRewardPerSecond(uint256 _pid, uint256 _rewardPerSecond) external onlyOwner {

        updatePool(_pid);

        poolInfo[_pid].RewardPerSecond = _rewardPerSecond;

        emit SetRewardPerSecond(_pid, _rewardPerSecond);
    }

}

my hardhat config

// hardhat.config.ts

import "dotenv/config"
import "@nomiclabs/hardhat-etherscan"
import "@nomiclabs/hardhat-solhint"
import "@nomiclabs/hardhat-waffle"
import "hardhat-abi-exporter"
import "hardhat-deploy"
import "hardhat-deploy-ethers"
import "hardhat-gas-reporter"
import "hardhat-spdx-license-identifier"
import "hardhat-typechain"
import "hardhat-watcher"
import "solidity-coverage"
import "@nomiclabs/hardhat-web3"
import "hardhat-etherscan-abi";

import { HardhatUserConfig } from "hardhat/types"
import { removeConsoleLog } from "hardhat-preprocessor"

const accounts = {
  mnemonic: process.env.MNEMONIC || "test test test test test test test test test test test junk",
  // accountsBalance: "990000000000000000000",
}

// Change private keys accordingly - ONLY FOR DEMOSTRATION PURPOSES - PLEASE STORE PRIVATE KEYS IN A SAFE PLACE
// Export your private key as
//       export PRIVKEY=0x.....
const privateKey =
    '0x99b3c12287537e38c90a9219d4cb074a89a16e9cdb20bf85728ebd97c343e342';
const privateKeyDev =
   '0x99b3c12287537e38c90a9219d4cb074a89a16e9cdb20bf85728ebd97c343e342';

const config: HardhatUserConfig = {
  abiExporter: {
    path: "./abi",
    clear: false,
    flat: true,
    // only: [],
    // except: []
  },


   defaultNetwork: "hardhat",
   etherscan: {
     apiKey: ' ',
   },
   gasReporter: {
     coinmarketcap: process.env.COINMARKETCAP_API_KEY,
     currency: "USD",
     enabled: process.env.REPORT_GAS === "true",
     excludeContracts: ["contracts/mocks/", "contracts/libraries/"],
   },
   mocha: {
     timeout: 20000,
   },
   namedAccounts: {
     deployer: {
       default: 0,
     },
     dev: {
       // Default to 1
       default: 1,
       // dev address mainnet
       // 1: "",
     },
   },
   networks: {
     mainnet: {
       url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`,
       accounts: [privateKey],
       gasPrice: 120 * 1000000000,
       chainId: 1,
     },
     localhost: {
       live: false,
       saveDeployments: true,
       tags: ["local"],
     },
     hardhat: {
       forking: {
         enabled: process.env.FORKING === "true",
         url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_API_KEY}`,
       },
       live: false,
       saveDeployments: true,
       tags: ["test", "local"],
     },
     ropsten: {
       url: `https://eth-ropsten.alchemyapi.io/v2/${process.env.ALCHEMY_API_KEY}`,
       accounts: [privateKey],
       chainId: 3,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasPrice: 5000000000,
       gasMultiplier: 2,
     },
     rinkeby: {
       url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`,
       accounts: [privateKey],
       chainId: 4,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasPrice: 5000000000,
       gasMultiplier: 2,
     },
     goerli: {
       url: `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`,
       accounts: [privateKey],
       chainId: 5,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasPrice: 5000000000,
       gasMultiplier: 2,
     },
     kovan: {
       url: `https://kovan.infura.io/v3/${process.env.INFURA_API_KEY}`,
       accounts: [privateKey],
       chainId: 42,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasPrice: 20000000000,
       gasMultiplier: 2,
     },
     moonbase: {
       url: "https://rpc.testnet.moonbeam.network",
       accounts: [privateKey],
       chainId: 1287,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gas: 5198000,
       gasMultiplier: 2,
     },
     fantom: {
       url: "https://rpcapi.fantom.network",
       accounts: [privateKey],
       chainId: 250,
       live: true,
       saveDeployments: true,
       gasPrice: 22000000000,
     },
     fantomtest: {
       url: "https://rpc.testnet.fantom.network",
       accounts: [privateKey],
       chainId: 4002,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasMultiplier: 2,
     },
     matic: {
       url: "https://rpc-mainnet.matic.quiknode.pro",
       accounts: [privateKey],
       chainId: 137,
       live: true,
       saveDeployments: true,
     },
     mumbai: {
       url: "https://rpc-mumbai.maticvigil.com/",
       accounts: [privateKey],
       chainId: 80001,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasMultiplier: 2,
     },
     xdai: {
       url: "https://rpc.xdaichain.com",
       accounts: [privateKey],
       chainId: 100,
       live: true,
       saveDeployments: true,
     },
     bsc: {
       url: "https://bsc-dataseed.binance.org",
       accounts: [privateKey],
       chainId: 56,
       live: true,
       saveDeployments: true,
     },
     "bsc-testnet": {
       url: "https://data-seed-prebsc-2-s3.binance.org:8545",
       accounts: [privateKey],
       chainId: 97,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasMultiplier: 2,
     },
     heco: {
       url: "https://http-mainnet.hecochain.com",
       accounts: [privateKey],
       chainId: 128,
       live: true,
       saveDeployments: true,
     },
     "heco-testnet": {
       url: "https://http-testnet.hecochain.com",
       accounts: [privateKey],
       chainId: 256,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasMultiplier: 2,
     },
     avalanche: {
       url: "https://api.avax.network/ext/bc/C/rpc",
       accounts: [privateKey],
       chainId: 43114,
       live: true,
       saveDeployments: true,
       gasPrice: 470000000000,
     },
     fuji: {
       url: "https://api.avax-test.network/ext/bc/C/rpc",
       accounts: [privateKey],
       chainId: 43113,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasMultiplier: 2,
     },
     harmony: {
       url: "https://api.s0.t.hmny.io",
       accounts: [privateKey],
       chainId: 1666600000,
       live: true,
       saveDeployments: true,
     },
     "harmony-testnet": {
       url: "https://api.s0.b.hmny.io",
       accounts: [privateKey],
       chainId: 1666700000,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasMultiplier: 2,
     },
     okex: {
       url: "https://exchainrpc.okex.org",
       accounts: [privateKey],
       chainId: 66,
       live: true,
       saveDeployments: true,
     },
     "okex-testnet": {
       url: "https://exchaintestrpc.okex.org",
       accounts: [privateKey],
       chainId: 65,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasMultiplier: 2,
     },
     arbitrum: {
       url: "https://arb1.arbitrum.io/rpc",
       accounts: [privateKey],
       chainId: 42161,
       live: true,
       saveDeployments: true,
       blockGasLimit: 700000,
     },
     "arbitrum-testnet": {
       url: "https://kovan3.arbitrum.io/rpc",
       accounts: [privateKey],
       chainId: 79377087078960,
       live: true,
       saveDeployments: true,
       tags: ["staging"],
       gasMultiplier: 2,
     },
     celo: {
       url: "https://forno.celo.org",
       accounts: [privateKey],
       chainId: 42220,
       live: true,
       saveDeployments: true,
     },
   },
   paths: {
     artifacts: "artifacts",
     cache: "cache",
     deploy: "deploy",
     deployments: "deployments",
     imports: "imports",
     sources: "contracts",
     tests: "test",
   },
   preprocess: {
     eachLine: removeConsoleLog((bre) => bre.network.name !== "hardhat" && bre.network.name !== "localhost"),
   },
   solidity: {
     compilers: [
       {
            version: '0.5.16',
            settings: {
               optimizer: {
                  enabled: true,
                  runs: 200,
               },
            },
         },
       {
         version: "0.6.12",
         settings: {
           optimizer: {
             enabled: true,
             runs: 200,
           },
         },
       },
     ],
   },
   spdxLicenseIdentifier: {
     overwrite: false,
     runOnCompile: true,
   },
   typechain: {
     outDir: "types",
     target: "ethers-v5",
   },
   watcher: {
     compile: {
       tasks: ["compile"],
       files: ["./contracts"],
       verbose: true,
     },
   },
   }

   export default config

the deploy script

// Deploy function
async function deploy() {
   [account] = await ethers.getSigners();
   deployerAddress = account.address;
   console.log(`Deploying contracts using ${deployerAddress}`);

   //Deploy StakeLab
   const stakeLab = await ethers.getContractFactory('StakeLab');
   const stakeLabInstance = await stakeLab.deploy(
   StakeToken.address'0x1C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6a',
   RewardToken.address'0x2C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6b',
   RewardPerSecond('10'),
   startTime=1000000,
   endTime=1100000
   Last block time that reward distribution occurs=99999999
   );
   await stakeLabInstance.deployed();

   console.log(`StakeLab deployed to : ${stakeLabInstance.address}`);

}

deploy()
   .then(() => process.exit(0))
   .catch((error) => {
      console.error(error);
      process.exit(1);
   });

I do appreciate your help in advance

Hi, it seems like you set the staking token in the constructor: _dummyToken, and set reward token when call the function add().
You can write some test cases to test your code.
And maybe not any token, I think you should have a test for deflationary token.

Thanks for your reply, so this contract does not allow me to set the staking token to any token address. but allow me to set the reward to any token address.

does the deploy script above is accurate ?

The deploy script doesn't look correct. Your constructor only takes one parameter _dummyToken. And the add() function takes the other parameters you pass into the deploy function.

You want the script to look something like this:

const stakeLabInstance = await stakeLab.deploy('0x1C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6a');
await stakeLabInstance.deployed();

Emmmm maybe not, if the reward token is a deflationary token, maybe it will also have some issue, such as, maybe the front-end shows the user can get 100 token as rewards, but actually, the user only get 99 token. And I am not sure what do you mean for any token, contains not only ERC20 token, but ERC777 token, right?

If you want to write a very compatible contract, I think that's a real challenge.

Thanks for your reply, but if I put the stake token address only. how I do set the other parameters?
Reward token address
Reward per second
Start time
End time
Last block time that reward distribution occurs

I do mean for any token, that is ERC20 tokens, deflationary tokens, and LPs address. and yes the reward token is a deflationary token.

Your contract specifies these parameters in a different function than the constructor. You should call add() with the reward parameters.

Sorry I don't have time to test it out, but after the contract is deployed try adding this (but pass in the actual values):

await stakeLabInstance.add(rewardPerSecond, rewardTokenAddress, startTime, endTime, protocol owner)

You don't have to pass in last block time because it is calculated in the function.

I believe the function should be called by the same address that deployed the contract (the owner).

1 Like
// Deploy function
async function deploy() {
   [account] = await ethers.getSigners();
   deployerAddress = account.address;
   console.log(`Deploying contracts using ${deployerAddress}`);

   //Deploy StakeLab
   const stakeLab = await ethers.getContractFactory('StakeLab');
   const stakeLabInstance = await stakeLab.deploy(
   '0x1C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6a',
   );
   await stakeLabInstance.deployed();
await stakeLabInstance.add('100', '0x2C69bEe701ef814a2B6a3EDD4B1652CB9', '11000', '18000', account.address);

   console.log(`StakeLab deployed to : ${stakeLabInstance.address}`);

}

deploy()
   .then(() => process.exit(0))
   .catch((error) => {
      console.error(error);
      process.exit(1);
   });

Like that, I do appreciate your help