Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Challenge submission #13

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
620eaff
first commit
leanonchain Jun 8, 2022
5b4d913
first commit
leanonchain Jun 8, 2022
c7e6a8f
Main contracts
leanonchain Jun 8, 2022
f480897
RewardToken comments
leanonchain Jun 8, 2022
88d7124
Security checks to withdrawalFee and add gas reporter
leanonchain Jun 8, 2022
b9ac82b
Optimize withdraw variables packing
leanonchain Jun 8, 2022
fc846ea
typo
leanonchain Jun 8, 2022
c9f992c
RewardToken events
leanonchain Jun 8, 2022
2c94aa0
Staker comment requirements
leanonchain Jun 8, 2022
2a9da1f
ReawrdToken constructor parameters
leanonchain Jun 8, 2022
6852bda
Staker functions
leanonchain Jun 9, 2022
f7629cf
Staker withdrawal fee
leanonchain Jun 9, 2022
1b5b662
Staker totalStaked
leanonchain Jun 9, 2022
89faeff
Staker _getUserDebt
leanonchain Jun 9, 2022
34c69f3
rewardToken test deploy and mint
leanonchain Jun 9, 2022
0fd120d
typo
leanonchain Jun 9, 2022
3d64342
RewardToken test withdrawFee and rewardRate
leanonchain Jun 9, 2022
9969e74
rewardToken test transfers
leanonchain Jun 9, 2022
0950ba8
Staker removed Ownable
leanonchain Jun 9, 2022
1371485
Staker test
leanonchain Jun 9, 2022
c0f685d
Staker test deposit
leanonchain Jun 9, 2022
e75fb5f
Staker test updateStaking
leanonchain Jun 9, 2022
906ed66
Staker getPending function
leanonchain Jun 9, 2022
2e1869a
Staker test withdraw fees
leanonchain Jun 9, 2022
58f1ace
Staker test withdraw
leanonchain Jun 9, 2022
a679d08
Staker startBlock security check
leanonchain Jun 9, 2022
a3ce9d9
README
leanonchain Jun 9, 2022
9f39779
hardhat config
leanonchain Jun 9, 2022
aaa841f
Deploy script
leanonchain Jun 9, 2022
09fe1e1
README
leanonchain Jun 9, 2022
7a963cf
README instructions
leanonchain Jun 9, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
.env
coverage
coverage.json
typechain

#Hardhat files
cache
artifacts
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"staker"
]
}
67 changes: 49 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
# Challenge
Create and deploy (locally) an ERC20 token and a staking contract that will distribute rewards to stakers over time. No need for an app or UI. You can reuse published or open source code, but you must indicate the source and what you have modified.
# Contracts

## Deliverable
Create a PR from this repository and add all your codebase, tests, requirements and tool usage to your README.md
## RewardToken.sol

## User journey
An account with some balance of the tokens can deposit them into the staking contract (which also has the tokens and distributes them over time). As the time goes by and blocks are being produced, this user should accumulate more of the tokens and can claim the rewards and withdraw the deposit.
Mintable ERC20 token with properties of withdrawalFee and rewardRate for an Staking contract.

## RewardToken.sol
This contract defines an ERC20 token that will be used for staking/rewards. The owner should be able to mint the token, change reward rates and enable/disable withdraw fees (also modifiable)
### Features (owner)

- Mintable.
- Withdrawal fee modifiable and can be enabled/disabled.
- RewardRate modifiable. Can also be disabled setting to 0.

## Staker.sol
This contract will get deployed with some tokens minted for the distribution to the stakers. And then, according to a schedule, allocate the reward tokens to addresses that deposited those tokens into the contract. The schedule is up to you, but you could say that every block 100 tokens are being distributed; then you'd take the allocated tokens and divide by the total balance of the deposited tokens so each depositor get's proportional share of the rewards. Ultimately, a user will deposit some tokens and later will be able to withdraw the principal amount plus the earned rewards. The following functions must be implemented: deposit(), withdraw()

## Scoring criteria
- launch ERC20 token
- implement reward allocation logic
- safe deposit/withdraw functions (avoid common attack vectors)
- add test cases
Staking contract which receives and rewards with the same tokens.

### Features

- In deployment can set the startBlock.
- User can deposit multiple times.
- Withdraw send all the staked tokens and reward to the user.

## Deployed contracts

Contracts deployed to https://testnet.bscscan.com/

- RewardToken deployed to: [0xe3ee3acce613E5fab3a9225619A792b796aA9A37](https://testnet.bscscan.com/address/0x1afa492ba972a12b4e5492c6d7c20df1547831ce#code)
- Staker deployed to: [0x28bcB704BB6D70562c1D61B48A858C46a1c9a204](https://testnet.bscscan.com/address/0x8ea1c67abe52ecfda43fc4913308b6c0d42f048a#code)
- Start block: [20156079](https://testnet.bscscan.com/block/countdown/20156079)

## Tools
Recommended tools:

- Hardhat
- Truffle/Ganache
- Remix
- web3.js/ethers.js
- ethers.js
- [@openzeppelin/contracts](https://docs.openzeppelin.com/contracts/4.x/)
- [hardhat-gas-reporter](https://www.npmjs.com/package/hardhat-gas-reporter)

## Instructions
### Compile and test

1. Clone repo: `git clone https://github.com/leanonchain/solidity-challenge.git`
2. Install dependencies: `yarn`
3. Compile: `npx hardhat compile`
4. Test: `npx hardhat test`

### Deploy
1. Create .env file with:
```
BSC_TESTNET_RPC = ""
BSC_TESTNET_API_KEY = ""
PRIVATE_KEY = ""
```
2. `npx hardhat run --network bsc_testnet scripts/deploy.js`

## To-do real case / with more time

- Testing refactor with typescript and remove redundant code.
- Harvest function and partial withdraw. This could be easily implemented but as I understood the requirements was for a withdraw function of all the staked tokens.
82 changes: 82 additions & 0 deletions contracts/RewardToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/// @title ERC20 token used for staking/rewards in Staker contract
/** @dev The owner is able to mint the token, change reward rates and
enable/disable withdraw fees (also modifiable) */
contract RewardToken is ERC20, Ownable {
// These variables are used by the Staker contract.
// Just here for the challenge requirements, but normally they would be
// in the Staker contract

uint public rewardRate;
uint16 public withdrawalFee;
// maxWithdrawalFee for user security reasons. If we don't have one
// we should ensure that withdrawalFee can't be greater than 10000 = 100%
uint16 public constant maxWithdrawalFee = 1000; // 10%
// We could use withdrawalFee = 0 as a way to disabled the fees
// saving gas storage in RewardToken and gas costs in Staker
// without checking if isWithdrawalFeeEnabled is true or false
// But as I understood, this approach to enabled/disabled fees
// is a requirement.
bool public isWithdrawalFeeEnabled;

event RewardRateUpdated(uint256);
event WithdrawalFeeToggled(bool);
event WithdrawalFeeUpdated(uint16);

constructor(
string memory _name,
string memory _symbol,
bool _isWithdrawalFeeEnabled,
uint16 _withdrawalFee,
uint _rewardRate
) ERC20(_name, _symbol) {
require(_withdrawalFee > 0, "withdrawalFee can't be 0");
require(
_withdrawalFee <= maxWithdrawalFee,
"withdrawalFee can't be greater than 1000"
);
withdrawalFee = _withdrawalFee;
isWithdrawalFeeEnabled = _isWithdrawalFeeEnabled;
rewardRate = _rewardRate;
}

function mint(address _to, uint _amount) external onlyOwner {
_mint(_to, _amount);
}

function setIsWithdrawalFeeEnabled(bool _isWithdrawalFeeEnabled)
external
onlyOwner
{
// Avoid calls with no changes
require(
_isWithdrawalFeeEnabled != isWithdrawalFeeEnabled,
"isWithdrawalFeeEnabled already has the valor sent"
);
isWithdrawalFeeEnabled = _isWithdrawalFeeEnabled;
emit WithdrawalFeeToggled(_isWithdrawalFeeEnabled);
}

function setWithdrawalFee(uint16 _withdrawalFee) external onlyOwner {
require(
_withdrawalFee > 0,
"withdrawalFee can't be 0. Maybe you want to call setIsWithdrawalFeeEnabled with false"
);
require(
_withdrawalFee <= maxWithdrawalFee,
"withdrawalFee can't be greater than 1000"
);
withdrawalFee = _withdrawalFee;
emit WithdrawalFeeUpdated(_withdrawalFee);
}

function setRewardRate(uint _rewardRate) external onlyOwner {
rewardRate = _rewardRate;
emit RewardRateUpdated(_rewardRate);
}
}
99 changes: 99 additions & 0 deletions contracts/Staker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "./RewardToken.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/// @title ERC20 token used for staking/rewards in Staker contract
/** @dev This contract will get deployed with some tokens minted for the distribution to the stakers. And then, according to a schedule, allocate the reward tokens to addresses that deposited those tokens into the contract. Then the allocated tokens are and divide by the total balance of the deposited tokens so each depositor get's proportional share of the rewards. Ultimately, a user will deposit some tokens and later will be able to withdraw the principal amount plus the earned rewards. The following functions must be implemented: deposit(), withdraw()
*/

/// Reward manage system based on:
/// https://github.com/sushiswap/sushiswap/blob/archieve/canary/contracts/MasterChefV2.sol
contract Staker is ReentrancyGuard {
using SafeERC20 for RewardToken;

struct UserInfo {
uint256 amount; // How many tokens the user has provided
uint256 rewardDebt; // The amount of rewardToken entitled to the user
}

RewardToken public immutable rewardToken;
// A big number to perform mul and div operations
uint256 private constant STAKER_SHARE_PRECISION = 1e18;
uint256 public lastRewardBlock;
uint256 public accRewardTokenPerShare;
uint256 public totalStaked;
// Info of each user that stakes tokens
mapping(address => UserInfo) public userInfo;

event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);

constructor(address _rewardToken, uint256 _startBlock) {
require(
_startBlock > block.number,
"startBlock can't be lower than current block."
);
rewardToken = RewardToken(_rewardToken);
lastRewardBlock = _startBlock;
}

function deposit(uint256 _amount) external nonReentrant {
require(_amount > 0, "Can't deposit 0 tokens");
UserInfo storage user = userInfo[msg.sender];
updateStaking();
totalStaked += _amount;
user.amount += _amount;
user.rewardDebt += getUserDebt(msg.sender);
rewardToken.safeTransferFrom(
address(msg.sender),
address(this),
_amount
);
emit Deposit(msg.sender, _amount);
}

function withdraw() external nonReentrant {
UserInfo storage user = userInfo[msg.sender];
require(user.amount > 0, "Nothing to withdraw");
updateStaking();
uint256 pending = getPending(msg.sender);
uint256 userTotalTokens = user.amount + pending;
totalStaked -= user.amount;
user.amount = 0;
user.rewardDebt = 0;
if (rewardToken.isWithdrawalFeeEnabled()) {
uint256 withdrawalFee = (userTotalTokens *
rewardToken.withdrawalFee()) / 10000;
userTotalTokens -= withdrawalFee;
rewardToken.safeTransfer(rewardToken.owner(), withdrawalFee);
}
rewardToken.safeTransfer(msg.sender, userTotalTokens);
emit Withdraw(msg.sender, userTotalTokens);
}

function updateStaking() public {
if (block.number <= lastRewardBlock) {
return;
}
if (totalStaked != 0) {
uint256 multiplier = block.number - lastRewardBlock;
uint256 tokenReward = multiplier * rewardToken.rewardRate();
accRewardTokenPerShare +=
(tokenReward * STAKER_SHARE_PRECISION) /
totalStaked;
}
lastRewardBlock = block.number;
}

function getPending(address user) public view returns (uint) {
return getUserDebt(user) - userInfo[user].rewardDebt;
}

function getUserDebt(address user) public view returns (uint) {
return ((userInfo[user].amount * accRewardTokenPerShare) /
STAKER_SHARE_PRECISION);
}
}
16 changes: 16 additions & 0 deletions hardhat.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config();

module.exports = {
networks: {
bsc_testnet: {
url: process.env.BSC_TESTNET_RPC,
accounts: [process.env.PRIVATE_KEY],
},
},
etherscan: {
apiKey: process.env.BSC_TESTNET_API_KEY,
},
solidity: "0.8.14",
};
33 changes: 33 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "solidity-challenge",
"version": "1.0.0",
"description": "Create and deploy (locally) an ERC20 token and a staking contract that will distribute rewards to stakers over time. No need for an app or UI. You can reuse published or open source code, but you must indicate the source and what you have modified.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://leanonchain:[email protected]/leanonchain/solidity-challenge.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/leanonchain/solidity-challenge/issues"
},
"homepage": "https://github.com/leanonchain/solidity-challenge#readme",
"dependencies": {
"@nomiclabs/hardhat-etherscan": "^3.1.0",
"@openzeppelin/contracts": "^4.6.0",
"dotenv": "^16.0.1",
"hardhat": "^2.9.7",
"hardhat-gas-reporter": "^1.0.8"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.0",
"@nomiclabs/hardhat-waffle": "^2.0.0",
"chai": "^4.2.0",
"ethereum-waffle": "^3.0.0",
"ethers": "^5.0.0"
}
}
3 changes: 3 additions & 0 deletions rewardTokenArguments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const rewardTokenParams = ["RewardToken", "RTK", true, 400, "1000000000000000"];

module.exports = {rewardTokenParams};
24 changes: 24 additions & 0 deletions scripts/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const hre = require("hardhat");
const { ethers } = require("hardhat");
const provider = ethers.provider;
const { rewardTokenParams } = require("../rewardTokenArguments.js");

async function main() {
const RewardToken = await hre.ethers.getContractFactory("RewardToken");
const rewardToken = await RewardToken.deploy(...rewardTokenParams);
await rewardToken.deployed();
console.log("RewardToken deployed to:", rewardToken.address);

const Staker = await ethers.getContractFactory("Staker");
const startBlock = (await provider.getBlock("latest")).number + 100000;
const staker = await Staker.deploy(rewardToken.address, startBlock);
console.log("Staker deployed to: ", staker.address);
console.log("Start block: ", startBlock);
}

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