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