Skip to content
This repository has been archived by the owner on Oct 14, 2021. It is now read-only.

feat: Add smartchef v2 #14

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
179 changes: 179 additions & 0 deletions contracts/SmartChef.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
pragma solidity 0.6.12;

import '@pancakeswap/pancake-swap-lib/contracts/math/SafeMath.sol';
import '@pancakeswap/pancake-swap-lib/contracts/token/BEP20/IBEP20.sol';
import '@pancakeswap/pancake-swap-lib/contracts/token/BEP20/SafeBEP20.sol';
import '@pancakeswap/pancake-swap-lib/contracts/access/Ownable.sol';
import '@pancakeswap/pancake-swap-lib/contracts/utils/ReentrancyGuard.sol';

contract SmartChef is Ownable, ReentrancyGuard {
using SafeMath for uint256;
using SafeBEP20 for IBEP20;

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

// Info of each pool.
struct PoolInfo {
IBEP20 lpToken; // Address of LP token contract.
uint256 lastRewardBlock; // Last block number that CAKEs distribution occurs.
uint256 accBlzdPerShare; // Accumulated CAKEs per share, times 1e12. See below.
}

// The CAKE TOKEN!
IBEP20 public blzd;
IBEP20 public rewardToken;

bool public withMaxAmount; //default false
uint256 public maxStaking;

// CAKE tokens created per block.
uint256 public rewardPerBlock;

uint256 public addressLength;

// Info of pool.
PoolInfo public pool;
// Info of each user that stakes LP tokens.
mapping (address => UserInfo) public userInfo;
// The block number when CAKE mining starts.
uint256 public startBlock;
// The block number when CAKE mining ends.
uint256 public bonusEndBlock;

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

constructor(
IBEP20 _blzd,
IBEP20 _rewardToken,
uint256 _rewardPerBlock,
uint256 _startBlock,
uint256 _bonusEndBlock
) public {
blzd = _blzd;
rewardToken = _rewardToken;
rewardPerBlock = _rewardPerBlock;
startBlock = _startBlock;
bonusEndBlock = _bonusEndBlock;

// staking pool
pool = PoolInfo({
lpToken: _blzd,
lastRewardBlock: startBlock,
accBlzdPerShare: 0
});
}

function stopReward() public onlyOwner {
bonusEndBlock = block.number;
}

function setMaxAmount(uint256 _amount) public onlyOwner {
withMaxAmount = true;
maxStaking = _amount;
}

// Return reward multiplier over the given _from to _to block.
function getMultiplier(uint256 _from, uint256 _to) public view returns (uint256) {
if (_to <= bonusEndBlock) {
return _to.sub(_from);
} else if (_from >= bonusEndBlock) {
return 0;
} else {
return bonusEndBlock.sub(_from);
}
}

// View function to see pending Reward on frontend.
function pendingReward(address _user) external view returns (uint256) {
UserInfo memory user = userInfo[_user];
uint256 accBlzdPerShare = pool.accBlzdPerShare;
uint256 lpSupply = pool.lpToken.balanceOf(address(this));
if (block.number > pool.lastRewardBlock && lpSupply != 0) {
uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number);
uint256 blzdReward = multiplier.mul(rewardPerBlock);
accBlzdPerShare = accBlzdPerShare.add(blzdReward.mul(1e12).div(lpSupply));
}
return user.amount.mul(accBlzdPerShare).div(1e12).sub(user.rewardDebt);
}

// Update reward variables of the given pool to be up-to-date.
function updatePool() public {
if (block.number <= pool.lastRewardBlock) {
return;
}
uint256 lpSupply = pool.lpToken.balanceOf(address(this));
if (lpSupply == 0) {
pool.lastRewardBlock = block.number;
return;
}
uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number);
uint256 blzdReward = multiplier.mul(rewardPerBlock);
pool.accBlzdPerShare = pool.accBlzdPerShare.add(blzdReward.mul(1e12).div(lpSupply));
pool.lastRewardBlock = block.number;
}

// // Stake Blzd tokens to SmartChef
function deposit(uint256 _amount) public nonReentrant {
UserInfo storage user = userInfo[msg.sender];
if (withMaxAmount) {
require (user.amount.add(_amount) <= maxStaking, 'exceed max staking amount');
}
updatePool();
if (user.amount == 0 && _amount != 0) {
addressLength = addressLength + 1;
}
if (user.amount > 0) {
uint256 pending = user.amount.mul(pool.accBlzdPerShare).div(1e12).sub(user.rewardDebt);
if(pending > 0) {
rewardToken.safeTransfer(address(msg.sender), pending);
}
}
if(_amount > 0) {
pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount);
user.amount = user.amount.add(_amount);
}
user.rewardDebt = user.amount.mul(pool.accBlzdPerShare).div(1e12);
emit Deposit(msg.sender, _amount);
}

// Withdraw BLZD tokens from STAKING.
function withdraw(uint256 _amount) public nonReentrant {
UserInfo storage user = userInfo[msg.sender];
require(user.amount >= _amount, "withdraw: not enough");
updatePool();
uint256 pending = user.amount.mul(pool.accBlzdPerShare).div(1e12).sub(user.rewardDebt);
if(pending > 0) {
rewardToken.safeTransfer(address(msg.sender), pending);
}
if(_amount > 0) {
user.amount = user.amount.sub(_amount);
pool.lpToken.safeTransfer(address(msg.sender), _amount);
}
user.rewardDebt = user.amount.mul(pool.accBlzdPerShare).div(1e12);

emit Withdraw(msg.sender, _amount);
}

// Withdraw without caring about rewards. EMERGENCY ONLY.
function emergencyWithdraw() public nonReentrant {
UserInfo storage user = userInfo[msg.sender];
uint256 amount = user.amount;
user.amount = 0;
user.rewardDebt = 0;
pool.lpToken.safeTransfer(address(msg.sender), amount);
emit EmergencyWithdraw(msg.sender, amount);
}

// Withdraw reward. EMERGENCY ONLY.
function emergencyRewardWithdraw(uint256 _amount) public onlyOwner {
require(_amount <= rewardToken.balanceOf(address(this)), 'not enough token');
rewardToken.safeTransfer(address(msg.sender), _amount);
}

}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@openzeppelin/test-helpers": "^0.5.6",
"@pancakeswap/pancake-swap-lib": "^0.0.1",
"@pancakeswap/pancake-swap-lib": "^0.0.4",
"truffle": "^5.1.58",
"truffle-flattener": "^1.4.4"
},
Expand Down
104 changes: 104 additions & 0 deletions test/SmartChef.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
const { expectRevert, time } = require('@openzeppelin/test-helpers');
const CakeToken = artifacts.require('CakeToken');
const SmartChef = artifacts.require('SmartChef');
const MockBEP20 = artifacts.require('libs/MockBEP20');

contract('SmartChef', ([alice, bob, carol, dev, minter]) => {
beforeEach(async () => {
this.syrup = await MockBEP20.new('LPToken1', 'LP1', '1000000000000', {
from: minter,
});
this.reward = await MockBEP20.new('LPToken2', 'LP2', '100000000000', {
from: minter,
});
this.chef = await SmartChef.new(this.syrup.address, this.reward.address, '40', '310', '400', {
from: minter,
});
});

it('smart chef now', async () => {
await this.reward.transfer(this.chef.address, '100000', { from: minter });
await this.syrup.transfer(bob, '1000', { from: minter });
await this.syrup.transfer(carol, '1000', { from: minter });
await this.syrup.transfer(alice, '1000', { from: minter });
assert.equal((await this.syrup.balanceOf(bob)).toString(), '1000');

await this.syrup.approve(this.chef.address, '1000', { from: bob });
await this.syrup.approve(this.chef.address, '1000', { from: alice });
await this.syrup.approve(this.chef.address, '1000', { from: carol });

await this.chef.deposit('10', { from: bob });
await this.chef.deposit('30', { from: alice });
assert.equal(
(await this.syrup.balanceOf(this.chef.address)).toString(),
'40'
);

await time.advanceBlockTo('310');
assert.equal(
(await this.chef.pendingReward(bob, { from: bob })).toString(),
'0'
);

await time.advanceBlockTo('311');
assert.equal(
(await this.chef.pendingReward(bob, { from: bob })).toString(),
'10'
);
assert.equal(
(await this.chef.pendingReward(alice, { from: alice })).toString(),
'30'
);

await this.chef.deposit('40', { from: carol });
assert.equal(
(await this.syrup.balanceOf(this.chef.address)).toString(),
'80'
);
await time.advanceBlockTo('313');
// bob 10, alice 30, carol 40
assert.equal(
(await this.chef.pendingReward(bob, { from: bob })).toString(),
'25'
);
assert.equal(
(await this.chef.pendingReward(alice, { from: alice })).toString(),
'75'
);
assert.equal(
(await this.chef.pendingReward(carol, { from: carol })).toString(),
'20'
);

await this.chef.withdraw('10', { from: bob });
await this.chef.withdraw('30', { from: alice });
await expectRevert(this.chef.withdraw('50', { from: carol }), 'not enough');
await this.chef.deposit('30', { from: carol });
await time.advanceBlockTo('450');
assert.equal(
(await this.chef.pendingReward(bob, { from: bob })).toString(),
'0'
);
assert.equal(
(await this.chef.pendingReward(alice, { from: alice })).toString(),
'0'
);
await this.chef.withdraw('70', { from: carol });
assert.equal((await this.chef.addressLength()).toString(), '3');
});

it('emergencyWithdraw', async () => {
await this.syrup.transfer(alice, '1000', { from: minter });
assert.equal((await this.syrup.balanceOf(alice)).toString(), '1000');

await this.syrup.approve(this.chef.address, '1000', { from: alice });
await this.chef.deposit('10', { from: alice });
assert.equal((await this.syrup.balanceOf(alice)).toString(), '990');
await this.chef.emergencyWithdraw({ from: alice });
assert.equal((await this.syrup.balanceOf(alice)).toString(), '1000');
assert.equal(
(await this.chef.pendingReward(alice, { from: alice })).toString(),
'0'
);
});
});
Loading