From eee6ead30a627efa1775a60d5e72f4c757ceb5ec Mon Sep 17 00:00:00 2001 From: Chef Nomi Date: Mon, 24 Aug 2020 19:50:03 +0700 Subject: [PATCH] Add some MasterChef tests --- contracts/MasterChef.sol | 22 ++++-- test/MasterChef.test.js | 140 +++++++++++++++++++++++++++++++++++++++ test/SushiToken.test.js | 71 ++++++++++---------- 3 files changed, 191 insertions(+), 42 deletions(-) create mode 100644 test/MasterChef.test.js diff --git a/contracts/MasterChef.sol b/contracts/MasterChef.sol index 89ea28cedc..f2c5513c6a 100644 --- a/contracts/MasterChef.sol +++ b/contracts/MasterChef.sol @@ -12,7 +12,7 @@ import "./SushiToken.sol"; interface Migrator { // Perform LP token migration from legacy UniswapV2 to SushiSwap. // Take ERC20 token address and return the new LP token address. - // Migrator should have full access to the caller's LP token. + // Migrator should have full access to the caller's LP token. // Return the new LP token address and new `howmany`. // // XXX Migrator must have allowance access to UniswapV2 LP tokens. @@ -46,7 +46,7 @@ contract MasterChef is Ownable { uint256 amount; // How many LP tokens the user has provided. uint256 rewardDebt; // Reward debt. See explanation below. // - // We do some fancy math here. Basically, any point in time, the amount of SUSHIs + // We do some fancy math here. Basically, any point in time, the amount of SUSHIs // entitled to a user but is pending to be distributed is: // // pending reward = (user.amount * pool.accSushiPerShare) - user.rewardDebt @@ -121,7 +121,7 @@ contract MasterChef is Ownable { _pool.lastRewardBlock = block.number; return; } - uint256 blockCount = _pool.lastRewardBlock.sub(block.number); + uint256 blockCount = block.number.sub(_pool.lastRewardBlock); uint256 sushiReward = blockCount.mul(_pool.howmany); sushi.mint(devaddr, sushiReward.mul(devshare).div(1e9)); sushi.mint(address(this), sushiReward); @@ -137,7 +137,7 @@ contract MasterChef is Ownable { updatePool(pool); if (user.amount > 0) { uint256 pending = user.amount.mul(pool.accSushiPerShare).div(1e9).sub(user.rewardDebt); - sushi.transfer(msg.sender, pending); + safeSushiTransfer(msg.sender, pending); } pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount); user.amount = user.amount.add(_amount); @@ -149,10 +149,10 @@ contract MasterChef is Ownable { PoolInfo storage pool = poolInfo[_token]; UserInfo storage user = userInfo[_token][msg.sender]; require(pool.lpToken != IERC20(address(0)), "withdraw: wut?"); - require(user.amount > _amount, "withdraw: not good"); + require(user.amount >= _amount, "withdraw: not good"); updatePool(pool); uint256 pending = user.amount.mul(pool.accSushiPerShare).div(1e9).sub(user.rewardDebt); - sushi.transfer(msg.sender, pending); + safeSushiTransfer(msg.sender, pending); user.amount = user.amount.sub(_amount); user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e9); pool.lpToken.safeTransfer(address(msg.sender), _amount); @@ -168,6 +168,16 @@ contract MasterChef is Ownable { pool.lpToken.safeTransfer(address(msg.sender), user.amount); } + // Safe sushi transfer function, just in case if rounding error causes pool to not have enough SUSHIs. + function safeSushiTransfer(address _to, uint256 _amount) internal { + uint256 sushiBal = sushi.balanceOf(address(this)); + if (_amount > sushiBal) { + sushi.transfer(_to, sushiBal); + } else { + sushi.transfer(_to, _amount); + } + } + // Update dev address by the previous dev. function dev(address _devaddr) public { require(msg.sender == devaddr, "dev: wut?"); diff --git a/test/MasterChef.test.js b/test/MasterChef.test.js new file mode 100644 index 0000000000..85204fbc2a --- /dev/null +++ b/test/MasterChef.test.js @@ -0,0 +1,140 @@ +const { expectRevert, time } = require('@openzeppelin/test-helpers'); +const SushiToken = artifacts.require('SushiToken'); +const MasterChef = artifacts.require('MasterChef'); +const MockERC20 = artifacts.require('MockERC20'); + +contract('SushiToken', ([alice, bob, carol, dev, minter]) => { + beforeEach(async () => { + this.sushi = await SushiToken.new({ from: alice }); + this.chef = await MasterChef.new(this.sushi.address, dev, { from: alice }); + await this.sushi.transferOwnership(this.chef.address, { from: alice }); + }); + + it('should set correct state variables', async () => { + const sushi = await this.chef.sushi(); + const devaddr = await this.chef.devaddr(); + const owner = await this.sushi.owner(); + assert.equal(sushi.valueOf(), this.sushi.address); + assert.equal(devaddr.valueOf(), dev); + assert.equal(owner.valueOf(), this.chef.address); + }); + + context('With ERC/LP token added to the field', () => { + beforeEach(async () => { + this.token = await MockERC20.new('Token', 'TOKEN', '10000000000', { + from: minter, + }); + this.lp = await MockERC20.new('LPToken', 'LP', '10000000000', { + from: minter, + }); + await this.lp.transfer(alice, '1000', { from: minter }); + await this.lp.transfer(bob, '1000', { from: minter }); + await this.lp.transfer(carol, '1000', { from: minter }); + }); + + it('should give out SUSHIs only after farming time', async () => { + // 1000 per block farming rate starting at block 100 + await this.chef.add(this.token.address, '1000', this.lp.address, '100'); + await this.lp.approve(this.chef.address, '1000', { from: bob }); + await this.chef.deposit(this.token.address, '100', { from: bob }); + await time.advanceBlockTo('89'); + await this.chef.deposit(this.token.address, '0', { from: bob }); // block 90 + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '0'); + await time.advanceBlockTo('94'); + await this.chef.deposit(this.token.address, '0', { from: bob }); // block 95 + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '0'); + await time.advanceBlockTo('99'); + await this.chef.deposit(this.token.address, '0', { from: bob }); // block 100 + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '0'); + await time.advanceBlockTo('100'); + await this.chef.deposit(this.token.address, '0', { from: bob }); // block 101 + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '1000'); + await time.advanceBlockTo('104'); + await this.chef.deposit(this.token.address, '0', { from: bob }); // block 105 + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '5000'); + assert.equal((await this.sushi.balanceOf(dev)).valueOf(), '500'); + assert.equal((await this.sushi.totalSupply()).valueOf(), '5500'); + }); + + it('should not distribute SUSHIs if no one deposit', async () => { + // 1000 per block farming rate starting at block 200 + await this.chef.add(this.token.address, '1000', this.lp.address, '100'); + await this.lp.approve(this.chef.address, '1000', { from: bob }); + await time.advanceBlockTo('199'); + assert.equal((await this.sushi.totalSupply()).valueOf(), '0'); + await time.advanceBlockTo('204'); + assert.equal((await this.sushi.totalSupply()).valueOf(), '0'); + await time.advanceBlockTo('209'); + await this.chef.deposit(this.token.address, '10', { from: bob }); // block 210 + assert.equal((await this.sushi.totalSupply()).valueOf(), '0'); + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '0'); + assert.equal((await this.sushi.balanceOf(dev)).valueOf(), '0'); + assert.equal((await this.lp.balanceOf(bob)).valueOf(), '990'); + await time.advanceBlockTo('219'); + await this.chef.withdraw(this.token.address, '10', { from: bob }); // block 220 + assert.equal((await this.sushi.totalSupply()).valueOf(), '11000'); + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '10000'); + assert.equal((await this.sushi.balanceOf(dev)).valueOf(), '1000'); + assert.equal((await this.lp.balanceOf(bob)).valueOf(), '1000'); + }); + + it('should distribute SUSHIs properly', async () => { + // 1000 per block farming rate starting at block 300 + await this.chef.add(this.token.address, '1000', this.lp.address, '300'); + await this.lp.approve(this.chef.address, '1000', { from: alice }); + await this.lp.approve(this.chef.address, '1000', { from: bob }); + await this.lp.approve(this.chef.address, '1000', { from: carol }); + // Alice deposits 10 LPs at block 310 + await time.advanceBlockTo('309'); + await this.chef.deposit(this.token.address, '10', { from: alice }); + // Bob deposits 20 LPs at block 314 + await time.advanceBlockTo('313'); + await this.chef.deposit(this.token.address, '20', { from: bob }); + // Carol deposits 30 LPs at block 318 + await time.advanceBlockTo('317'); + await this.chef.deposit(this.token.address, '30', { from: carol }); + // Alice deposits 10 more LPs at block 320. At this point: + // Alice should have: 4*1000 + 4*1/3*1000 + 2*1/6*1000 = 5666 + // MasterChef should have the remaining: 10000 - 5666 = 4334 + await time.advanceBlockTo('319') + await this.chef.deposit(this.token.address, '10', { from: alice }); + assert.equal((await this.sushi.totalSupply()).valueOf(), '11000'); + assert.equal((await this.sushi.balanceOf(alice)).valueOf(), '5666'); + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '0'); + assert.equal((await this.sushi.balanceOf(carol)).valueOf(), '0'); + assert.equal((await this.sushi.balanceOf(this.chef.address)).valueOf(), '4334'); + assert.equal((await this.sushi.balanceOf(dev)).valueOf(), '1000'); + // Bob withdraws 5 LPs at block 330. At this point: + // Bob should have: 4*2/3*1000 + 2*2/6*1000 + 10*2/7*1000 = 6190 + await time.advanceBlockTo('329') + await this.chef.withdraw(this.token.address, '5', { from: bob }); + assert.equal((await this.sushi.totalSupply()).valueOf(), '22000'); + assert.equal((await this.sushi.balanceOf(alice)).valueOf(), '5666'); + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '6190'); + assert.equal((await this.sushi.balanceOf(carol)).valueOf(), '0'); + assert.equal((await this.sushi.balanceOf(this.chef.address)).valueOf(), '8144'); + assert.equal((await this.sushi.balanceOf(dev)).valueOf(), '2000'); + // Alice withdraws 20 LPs at block 340. + // Bob withdraws 15 LPs at block 350. + // Carol withdraws 30 LPs at block 360. + await time.advanceBlockTo('339') + await this.chef.withdraw(this.token.address, '20', { from: alice }); + await time.advanceBlockTo('349') + await this.chef.withdraw(this.token.address, '15', { from: bob }); + await time.advanceBlockTo('359') + await this.chef.withdraw(this.token.address, '30', { from: carol }); + assert.equal((await this.sushi.totalSupply()).valueOf(), '55000'); + assert.equal((await this.sushi.balanceOf(dev)).valueOf(), '5000'); + // Alice should have: 5666 + 10*2/7*1000 + 10*2/6.5*1000 = 11600 + assert.equal((await this.sushi.balanceOf(alice)).valueOf(), '11600'); + // Bob should have: 6190 + 10*1.5/6.5 * 1000 + 10*1.5/4.5*1000 = 11831 + assert.equal((await this.sushi.balanceOf(bob)).valueOf(), '11831'); + // Carol should have: 2*3/6*1000 + 10*3/7*1000 + 10*3/6.5*1000 + 10*3/4.5*1000 + 10*1000 = 26568 + assert.equal((await this.sushi.balanceOf(carol)).valueOf(), '26568'); + // All of them should have 1000 LPs back. + assert.equal((await this.lp.balanceOf(alice)).valueOf(), '1000'); + assert.equal((await this.lp.balanceOf(bob)).valueOf(), '1000'); + assert.equal((await this.lp.balanceOf(carol)).valueOf(), '1000'); + }); + }); +}); diff --git a/test/SushiToken.test.js b/test/SushiToken.test.js index d43f3b068e..c7177e9920 100644 --- a/test/SushiToken.test.js +++ b/test/SushiToken.test.js @@ -1,5 +1,5 @@ -const { expectRevert } = require("@openzeppelin/test-helpers"); -const SushiToken = artifacts.require("SushiToken"); +const { expectRevert } = require('@openzeppelin/test-helpers'); +const SushiToken = artifacts.require('SushiToken'); contract('SushiToken', ([alice, bob, carol]) => { beforeEach(async () => { @@ -7,56 +7,55 @@ contract('SushiToken', ([alice, bob, carol]) => { }); it('should have correct name and symbol and decimal', async () => { - const name = await this.sushi.name.call(); - const symbol = await this.sushi.symbol.call(); - const decimals = await this.sushi.decimals.call(); - assert.equal(name.valueOf(), "SushiToken"); - assert.equal(symbol.valueOf(), "SUSHI"); - assert.equal(decimals.valueOf(), "18"); + const name = await this.sushi.name(); + const symbol = await this.sushi.symbol(); + const decimals = await this.sushi.decimals(); + assert.equal(name.valueOf(), 'SushiToken'); + assert.equal(symbol.valueOf(), 'SUSHI'); + assert.equal(decimals.valueOf(), '18'); }); it('should only allow owner to mint token', async () => { - await this.sushi.mint(alice, "100", { from: alice }); - await this.sushi.mint(bob, "1000", { from: alice }); + await this.sushi.mint(alice, '100', { from: alice }); + await this.sushi.mint(bob, '1000', { from: alice }); await expectRevert( - this.sushi.mint(carol, "1000", { from: bob }), - "Ownable: caller is not the owner", + this.sushi.mint(carol, '1000', { from: bob }), + 'Ownable: caller is not the owner', ); const totalSupply = await this.sushi.totalSupply(); - const aliceBal = await this.sushi.balanceOf.call(alice); - const bobBal = await this.sushi.balanceOf.call(bob); - const carolBal = await this.sushi.balanceOf.call(carol); - assert.equal(totalSupply.valueOf(), "1100"); - assert.equal(aliceBal.valueOf(), "100"); - assert.equal(bobBal.valueOf(), "1000"); - assert.equal(carolBal.valueOf(), "0"); + const aliceBal = await this.sushi.balanceOf(alice); + const bobBal = await this.sushi.balanceOf(bob); + const carolBal = await this.sushi.balanceOf(carol); + assert.equal(totalSupply.valueOf(), '1100'); + assert.equal(aliceBal.valueOf(), '100'); + assert.equal(bobBal.valueOf(), '1000'); + assert.equal(carolBal.valueOf(), '0'); }); it('should supply token transfers properly', async () => { - await this.sushi.mint(alice, "100", { from: alice }); - await this.sushi.mint(bob, "1000", { from: alice }); - await this.sushi.transfer(carol, "10", { from: alice }); - await this.sushi.transfer(carol, "100", { from: bob }); + await this.sushi.mint(alice, '100', { from: alice }); + await this.sushi.mint(bob, '1000', { from: alice }); + await this.sushi.transfer(carol, '10', { from: alice }); + await this.sushi.transfer(carol, '100', { from: bob }); const totalSupply = await this.sushi.totalSupply(); - const aliceBal = await this.sushi.balanceOf.call(alice); - const bobBal = await this.sushi.balanceOf.call(bob); - const carolBal = await this.sushi.balanceOf.call(carol); - assert.equal(totalSupply.valueOf(), "1100"); - assert.equal(aliceBal.valueOf(), "90"); - assert.equal(bobBal.valueOf(), "900"); - assert.equal(carolBal.valueOf(), "110"); + const aliceBal = await this.sushi.balanceOf(alice); + const bobBal = await this.sushi.balanceOf(bob); + const carolBal = await this.sushi.balanceOf(carol); + assert.equal(totalSupply.valueOf(), '1100'); + assert.equal(aliceBal.valueOf(), '90'); + assert.equal(bobBal.valueOf(), '900'); + assert.equal(carolBal.valueOf(), '110'); }); it('should fail if you try to do bad transfers', async () => { - await this.sushi.mint(alice, "100", { from: alice }); + await this.sushi.mint(alice, '100', { from: alice }); await expectRevert( - this.sushi.transfer(carol, "110", { from: alice }), - "ERC20: transfer amount exceeds balance", + this.sushi.transfer(carol, '110', { from: alice }), + 'ERC20: transfer amount exceeds balance', ); await expectRevert( - this.sushi.transfer(carol, "1", { from: bob }), - "ERC20: transfer amount exceeds balance", + this.sushi.transfer(carol, '1', { from: bob }), + 'ERC20: transfer amount exceeds balance', ); }); }); - \ No newline at end of file