diff --git a/.github/workflows/mythx.yml b/.github/workflows/mythx.yml new file mode 100644 index 0000000000..36e5c98253 --- /dev/null +++ b/.github/workflows/mythx.yml @@ -0,0 +1,42 @@ +name: mythx +on: + push: + paths: + - 'contracts/**' +jobs: + build: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node: ["14.x"] + os: ["ubuntu-latest"] + env: + MYTHX_API_KEY: ${{ secrets.MYTHX }} + + steps: + - uses: actions/checkout@v2 + - name: Nodejs 14.x + uses: actions/setup-node@v2 + with: + node-version: "14.x" + check-latest: true + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install MythX CLI + run: python -m pip install mythx-cli + - name: Print version info + run: | + python3 -V + mythx version + + - name: Install dependencies + run: yarn install + + - name: Submit contracts + run: mythx analyze diff --git a/.github/workflows/sushiswap.yml b/.github/workflows/sushiswap.yml index 9fc114df0e..d1d615869d 100644 --- a/.github/workflows/sushiswap.yml +++ b/.github/workflows/sushiswap.yml @@ -1,70 +1,65 @@ # workflow pipeline name: sushiswap -on: ["push", "pull_request"] +on: + push: + paths: + - '**/**' + - '!**/*.md/**' + +env: + CI: true + FORCE_COLOR: 2 jobs: - build: - runs-on: ubuntu-latest + run: + name: Node ${{ matrix.node }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - node: ["14.x", "12.x"] + node: ["12.x", "14.x"] os: ["ubuntu-latest"] steps: - - uses: actions/checkout@v2 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - name: Nodejs ${{ matrix.node-version }} - uses: actions/setup-node@v2 + - uses: actions/checkout@v2.3.4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2.1.4 with: node-version: ${{ matrix.node-version }} - check-latest: true + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + +# Cache Optimization - - name: Install and Compile + - uses: actions/cache@v2.1.4 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: yarn install run: | yarn install - yarn build + id: hardhat_install - - name: Test and Coverage + - name: hardhat test run: | - yarn test - - mythx: - runs-on: ubuntu-latest - env: - MYTHX_API_KEY: ${{ secrets.MYTHX }} - - steps: - - name: Nodejs 14.x - uses: actions/setup-node@v2 - with: - node-version: "14.x" - check-latest: true + npx hardhat test --verbose + id: hh_test - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install MythX CLI - run: python -m pip install mythx-cli - - name: Print version info + - name: hardhat test_gas run: | - python3 -V - mythx version - - - uses: actions/checkout@v2 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Install dependencies - run: yarn install + yarn run test:gas + id: hh_gas - - name: Submit contracts - run: mythx analyze + - name: hardhat test_gas + run: | + yarn run test:coverage + id: hh_coverage diff --git a/.solhint.json b/.solhint.json index d7c3de9895..136746fc56 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,3 +1,15 @@ { - "extends": "solhint:default" + "extends": "solhint:recommended", + "plugins": ["prettier"], + "rules": { + "avoid-suicide": "error", + "avoid-sha3": "warn", + "code-complexity": ["warn", 7], + "compiler-version": "off", + "max-states-count": ["error", 18], + "max-line-length": ["warn", 145], + "not-rely-on-time": "warn", + "quotes": ["warn", "double"], + "prettier/prettier": "off" + } } diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 0000000000..06e7a461ae --- /dev/null +++ b/.solhintignore @@ -0,0 +1,3 @@ +contracts/uniswapv2 +node_modules/ + diff --git a/build-test/MasterChef.sol b/build-test/MasterChef.sol new file mode 100644 index 0000000000..407dc521c1 --- /dev/null +++ b/build-test/MasterChef.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppeltoken/ERC20/IERC20.sol"; +import "@openzeppeltoken/ERC20/SafeERC20.sol"; +import "@openzeppelutils/EnumerableSet.sol"; +import "@openzeppelmath/SafeMath.sol"; +import "@openzeppelaccess/Ownable.sol"; +import "./SushiToken.sol"; + +interface IMigratorChef { + // Perform LP token migration from legacy UniswapV2 to SushiSwap. + // Take the current LP token address and return the new LP token address. + // Migrator should have full access to the caller's LP token. + // Return the new LP token address. + // + // XXX Migrator must have allowance access to UniswapV2 LP tokens. + // SushiSwap must mint EXACTLY the same amount of SushiSwap LP tokens or + // else something bad will happen. Traditional UniswapV2 does not + // do that so be careful! + function migrate(IERC20 token) external returns (IERC20); +} + +// MasterChef is the master of Sushi. He can make Sushi and he is a fair guy. +// +// Note that it's ownable and the owner wields tremendous power. The ownership +// will be transferred to a governance smart contract once SUSHI is sufficiently +// distributed and the community can show to govern itself. +// +// Have fun reading it. Hopefully it's bug-free. God bless. +contract MasterChef is Ownable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + // Info of each user. + struct UserInfo { + 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 + // entitled to a user but is pending to be distributed is: + // + // pending reward = (user.amount * pool.accSushiPerShare) - user.rewardDebt + // + // Whenever a user deposits or withdraws LP tokens to a pool. Here's what happens: + // 1. The pool's `accSushiPerShare` (and `lastRewardBlock`) gets updated. + // 2. User receives the pending reward sent to his/her address. + // 3. User's `amount` gets updated. + // 4. User's `rewardDebt` gets updated. + } + // Info of each pool. + struct PoolInfo { + IERC20 lpToken; // Address of LP token contract. + uint256 allocPoint; // How many allocation points assigned to this pool. SUSHIs to distribute per block. + uint256 lastRewardBlock; // Last block number that SUSHIs distribution occurs. + uint256 accSushiPerShare; // Accumulated SUSHIs per share, times 1e12. See below. + } + // The SUSHI TOKEN! + SushiToken public sushi; + // Dev address. + address public devaddr; + // Block number when bonus SUSHI period ends. + uint256 public bonusEndBlock; + // SUSHI tokens created per block. + uint256 public sushiPerBlock; + // Bonus muliplier for early sushi makers. + uint256 public constant BONUS_MULTIPLIER = 10; + // The migrator contract. It has a lot of power. Can only be set through governance (owner). + IMigratorChef public migrator; + // Info of each pool. + PoolInfo[] public poolInfo; + // Info of each user that stakes LP tokens. + mapping(uint256 => mapping(address => UserInfo)) public userInfo; + // Total allocation poitns. Must be the sum of all allocation points in all pools. + uint256 public totalAllocPoint = 0; + // The block number when SUSHI mining starts. + uint256 public startBlock; + 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 + ); + + constructor( + SushiToken _sushi, + address _devaddr, + uint256 _sushiPerBlock, + uint256 _startBlock, + uint256 _bonusEndBlock + ) public { + sushi = _sushi; + devaddr = _devaddr; + sushiPerBlock = _sushiPerBlock; + bonusEndBlock = _bonusEndBlock; + startBlock = _startBlock; + } + + function poolLength() external view returns (uint256) { + return poolInfo.length; + } + + // Add a new lp to the pool. Can only be called by the owner. + // XXX DO NOT add the same LP token more than once. Rewards will be messed up if you do. + function add( + uint256 _allocPoint, + IERC20 _lpToken, + bool _withUpdate + ) public onlyOwner { + if (_withUpdate) { + massUpdatePools(); + } + uint256 lastRewardBlock = + block.number > startBlock ? block.number : startBlock; + totalAllocPoint = totalAllocPoint.add(_allocPoint); + poolInfo.push( + PoolInfo({ + lpToken: _lpToken, + allocPoint: _allocPoint, + lastRewardBlock: lastRewardBlock, + accSushiPerShare: 0 + }) + ); + } + + // Update the given pool's SUSHI allocation point. Can only be called by the owner. + function set( + uint256 _pid, + uint256 _allocPoint, + bool _withUpdate + ) public onlyOwner { + if (_withUpdate) { + massUpdatePools(); + } + totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add( + _allocPoint + ); + poolInfo[_pid].allocPoint = _allocPoint; + } + + // Set the migrator contract. Can only be called by the owner. + function setMigrator(IMigratorChef _migrator) public onlyOwner { + migrator = _migrator; + } + + // Migrate lp token to another lp contract. Can be called by anyone. We trust that migrator contract is good. + function migrate(uint256 _pid) public { + require(address(migrator) != address(0), "migrate: no migrator"); + PoolInfo storage pool = poolInfo[_pid]; + IERC20 lpToken = pool.lpToken; + uint256 bal = lpToken.balanceOf(address(this)); + lpToken.safeApprove(address(migrator), bal); + IERC20 newLpToken = migrator.migrate(lpToken); + require(bal == newLpToken.balanceOf(address(this)), "migrate: bad"); + pool.lpToken = newLpToken; + } + + // 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).mul(BONUS_MULTIPLIER); + } else if (_from >= bonusEndBlock) { + return _to.sub(_from); + } else { + return + bonusEndBlock.sub(_from).mul(BONUS_MULTIPLIER).add( + _to.sub(bonusEndBlock) + ); + } + } + + // View function to see pending SUSHIs on frontend. + function pendingSushi(uint256 _pid, address _user) + external + view + returns (uint256) + { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][_user]; + uint256 accSushiPerShare = pool.accSushiPerShare; + uint256 lpSupply = pool.lpToken.balanceOf(address(this)); + if (block.number > pool.lastRewardBlock && lpSupply != 0) { + uint256 multiplier = + getMultiplier(pool.lastRewardBlock, block.number); + uint256 sushiReward = + multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div( + totalAllocPoint + ); + accSushiPerShare = accSushiPerShare.add( + sushiReward.mul(1e12).div(lpSupply) + ); + } + return user.amount.mul(accSushiPerShare).div(1e12).sub(user.rewardDebt); + } + + // Update reward vairables 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) public { + PoolInfo storage pool = poolInfo[_pid]; + 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 sushiReward = + multiplier.mul(sushiPerBlock).mul(pool.allocPoint).div( + totalAllocPoint + ); + sushi.mint(devaddr, sushiReward.div(10)); + sushi.mint(address(this), sushiReward); + pool.accSushiPerShare = pool.accSushiPerShare.add( + sushiReward.mul(1e12).div(lpSupply) + ); + pool.lastRewardBlock = block.number; + } + + // Deposit LP tokens to MasterChef for SUSHI allocation. + function deposit(uint256 _pid, uint256 _amount) public { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][msg.sender]; + updatePool(_pid); + if (user.amount > 0) { + uint256 pending = + user.amount.mul(pool.accSushiPerShare).div(1e12).sub( + user.rewardDebt + ); + safeSushiTransfer(msg.sender, pending); + } + pool.lpToken.safeTransferFrom( + address(msg.sender), + address(this), + _amount + ); + user.amount = user.amount.add(_amount); + user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12); + emit Deposit(msg.sender, _pid, _amount); + } + + // Withdraw LP tokens from MasterChef. + function withdraw(uint256 _pid, uint256 _amount) public { + 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.mul(pool.accSushiPerShare).div(1e12).sub( + user.rewardDebt + ); + safeSushiTransfer(msg.sender, pending); + user.amount = user.amount.sub(_amount); + user.rewardDebt = user.amount.mul(pool.accSushiPerShare).div(1e12); + pool.lpToken.safeTransfer(address(msg.sender), _amount); + emit Withdraw(msg.sender, _pid, _amount); + } + + // Withdraw without caring about rewards. EMERGENCY ONLY. + function emergencyWithdraw(uint256 _pid) public { + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][msg.sender]; + pool.lpToken.safeTransfer(address(msg.sender), user.amount); + emit EmergencyWithdraw(msg.sender, _pid, user.amount); + user.amount = 0; + user.rewardDebt = 0; + } + + // 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?"); + devaddr = _devaddr; + } +} diff --git a/build-test/MasterChef.test.js b/build-test/MasterChef.test.js new file mode 100644 index 0000000000..e89d7ee1fd --- /dev/null +++ b/build-test/MasterChef.test.js @@ -0,0 +1,267 @@ +const { ethers } = require("hardhat") +const { expect } = require("chai") +const { time } = require("./utilities") + +describe("MasterChef", function () { + before(async function () { + this.signers = await ethers.getSigners() + this.alice = this.signers[0] + this.bob = this.signers[1] + this.carol = this.signers[2] + this.dev = this.signers[3] + this.minter = this.signers[4] + + this.MasterChef = await ethers.getContractFactory("MasterChef") + this.SushiToken = await ethers.getContractFactory("SushiToken") + this.ERC20Mock = await ethers.getContractFactory("ERC20Mock", this.minter) + }) + + beforeEach(async function () { + this.sushi = await this.SushiToken.deploy() + await this.sushi.deployed() + }) + + it("should set correct state variables", async function () { + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "1000", "0", "1000") + await this.chef.deployed() + + await this.sushi.transferOwnership(this.chef.address) + + const sushi = await this.chef.sushi() + const devaddr = await this.chef.devaddr() + const owner = await this.sushi.owner() + + expect(sushi).to.equal(this.sushi.address) + expect(devaddr).to.equal(this.dev.address) + expect(owner).to.equal(this.chef.address) + }) + + it("should allow dev and only dev to update dev", async function () { + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "1000", "0", "1000") + await this.chef.deployed() + + expect(await this.chef.devaddr()).to.equal(this.dev.address) + + await expect(this.chef.connect(this.bob).dev(this.bob.address, { from: this.bob.address })).to.be.revertedWith("dev: wut?") + + await this.chef.connect(this.dev).dev(this.bob.address, { from: this.dev.address }) + + expect(await this.chef.devaddr()).to.equal(this.bob.address) + + await this.chef.connect(this.bob).dev(this.alice.address, { from: this.bob.address }) + + expect(await this.chef.devaddr()).to.equal(this.alice.address) + }) + + context("With ERC/LP token added to the field", function () { + beforeEach(async function () { + this.lp = await this.ERC20Mock.deploy("LPToken", "LP", "10000000000") + + await this.lp.transfer(this.alice.address, "1000") + + await this.lp.transfer(this.bob.address, "1000") + + await this.lp.transfer(this.carol.address, "1000") + + this.lp2 = await this.ERC20Mock.deploy("LPToken2", "LP2", "10000000000") + + await this.lp2.transfer(this.alice.address, "1000") + + await this.lp2.transfer(this.bob.address, "1000") + + await this.lp2.transfer(this.carol.address, "1000") + }) + + it("should allow emergency withdraw", async function () { + // 100 per block farming rate starting at block 100 with bonus until block 1000 + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "100", "100", "1000") + await this.chef.deployed() + + await this.chef.add("100", this.lp.address, true) + + await this.lp.connect(this.bob).approve(this.chef.address, "1000") + + await this.chef.connect(this.bob).deposit(0, "100") + + expect(await this.lp.balanceOf(this.bob.address)).to.equal("900") + + await this.chef.connect(this.bob).emergencyWithdraw(0) + + expect(await this.lp.balanceOf(this.bob.address)).to.equal("1000") + }) + + it("should give out SUSHIs only after farming time", async function () { + // 100 per block farming rate starting at block 100 with bonus until block 1000 + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "100", "100", "1000") + await this.chef.deployed() + + await this.sushi.transferOwnership(this.chef.address) + + await this.chef.add("100", this.lp.address, true) + + await this.lp.connect(this.bob).approve(this.chef.address, "1000") + await this.chef.connect(this.bob).deposit(0, "100") + await time.advanceBlockTo("89") + + await this.chef.connect(this.bob).deposit(0, "0") // block 90 + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("0") + await time.advanceBlockTo("94") + + await this.chef.connect(this.bob).deposit(0, "0") // block 95 + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("0") + await time.advanceBlockTo("99") + + await this.chef.connect(this.bob).deposit(0, "0") // block 100 + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("0") + await time.advanceBlockTo("100") + + await this.chef.connect(this.bob).deposit(0, "0") // block 101 + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("1000") + + await time.advanceBlockTo("104") + await this.chef.connect(this.bob).deposit(0, "0") // block 105 + + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("5000") + expect(await this.sushi.balanceOf(this.dev.address)).to.equal("500") + expect(await this.sushi.totalSupply()).to.equal("5500") + }) + + it("should not distribute SUSHIs if no one deposit", async function () { + // 100 per block farming rate starting at block 200 with bonus until block 1000 + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "100", "200", "1000") + await this.chef.deployed() + await this.sushi.transferOwnership(this.chef.address) + await this.chef.add("100", this.lp.address, true) + await this.lp.connect(this.bob).approve(this.chef.address, "1000") + await time.advanceBlockTo("199") + expect(await this.sushi.totalSupply()).to.equal("0") + await time.advanceBlockTo("204") + expect(await this.sushi.totalSupply()).to.equal("0") + await time.advanceBlockTo("209") + await this.chef.connect(this.bob).deposit(0, "10") // block 210 + expect(await this.sushi.totalSupply()).to.equal("0") + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("0") + expect(await this.sushi.balanceOf(this.dev.address)).to.equal("0") + expect(await this.lp.balanceOf(this.bob.address)).to.equal("990") + await time.advanceBlockTo("219") + await this.chef.connect(this.bob).withdraw(0, "10") // block 220 + expect(await this.sushi.totalSupply()).to.equal("11000") + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("10000") + expect(await this.sushi.balanceOf(this.dev.address)).to.equal("1000") + expect(await this.lp.balanceOf(this.bob.address)).to.equal("1000") + }) + + it("should distribute SUSHIs properly for each staker", async function () { + // 100 per block farming rate starting at block 300 with bonus until block 1000 + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "100", "300", "1000") + await this.chef.deployed() + await this.sushi.transferOwnership(this.chef.address) + await this.chef.add("100", this.lp.address, true) + await this.lp.connect(this.alice).approve(this.chef.address, "1000", { + from: this.alice.address, + }) + await this.lp.connect(this.bob).approve(this.chef.address, "1000", { + from: this.bob.address, + }) + await this.lp.connect(this.carol).approve(this.chef.address, "1000", { + from: this.carol.address, + }) + // Alice deposits 10 LPs at block 310 + await time.advanceBlockTo("309") + await this.chef.connect(this.alice).deposit(0, "10", { from: this.alice.address }) + // Bob deposits 20 LPs at block 314 + await time.advanceBlockTo("313") + await this.chef.connect(this.bob).deposit(0, "20", { from: this.bob.address }) + // Carol deposits 30 LPs at block 318 + await time.advanceBlockTo("317") + await this.chef.connect(this.carol).deposit(0, "30", { from: this.carol.address }) + // 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.connect(this.alice).deposit(0, "10", { from: this.alice.address }) + expect(await this.sushi.totalSupply()).to.equal("11000") + expect(await this.sushi.balanceOf(this.alice.address)).to.equal("5666") + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("0") + expect(await this.sushi.balanceOf(this.carol.address)).to.equal("0") + expect(await this.sushi.balanceOf(this.chef.address)).to.equal("4334") + expect(await this.sushi.balanceOf(this.dev.address)).to.equal("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.connect(this.bob).withdraw(0, "5", { from: this.bob.address }) + expect(await this.sushi.totalSupply()).to.equal("22000") + expect(await this.sushi.balanceOf(this.alice.address)).to.equal("5666") + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("6190") + expect(await this.sushi.balanceOf(this.carol.address)).to.equal("0") + expect(await this.sushi.balanceOf(this.chef.address)).to.equal("8144") + expect(await this.sushi.balanceOf(this.dev.address)).to.equal("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.connect(this.alice).withdraw(0, "20", { from: this.alice.address }) + await time.advanceBlockTo("349") + await this.chef.connect(this.bob).withdraw(0, "15", { from: this.bob.address }) + await time.advanceBlockTo("359") + await this.chef.connect(this.carol).withdraw(0, "30", { from: this.carol.address }) + expect(await this.sushi.totalSupply()).to.equal("55000") + expect(await this.sushi.balanceOf(this.dev.address)).to.equal("5000") + // Alice should have: 5666 + 10*2/7*1000 + 10*2/6.5*1000 = 11600 + expect(await this.sushi.balanceOf(this.alice.address)).to.equal("11600") + // Bob should have: 6190 + 10*1.5/6.5 * 1000 + 10*1.5/4.5*1000 = 11831 + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("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 + expect(await this.sushi.balanceOf(this.carol.address)).to.equal("26568") + // All of them should have 1000 LPs back. + expect(await this.lp.balanceOf(this.alice.address)).to.equal("1000") + expect(await this.lp.balanceOf(this.bob.address)).to.equal("1000") + expect(await this.lp.balanceOf(this.carol.address)).to.equal("1000") + }) + + it("should give proper SUSHIs allocation to each pool", async function () { + // 100 per block farming rate starting at block 400 with bonus until block 1000 + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "100", "400", "1000") + await this.sushi.transferOwnership(this.chef.address) + await this.lp.connect(this.alice).approve(this.chef.address, "1000", { from: this.alice.address }) + await this.lp2.connect(this.bob).approve(this.chef.address, "1000", { from: this.bob.address }) + // Add first LP to the pool with allocation 1 + await this.chef.add("10", this.lp.address, true) + // Alice deposits 10 LPs at block 410 + await time.advanceBlockTo("409") + await this.chef.connect(this.alice).deposit(0, "10", { from: this.alice.address }) + // Add LP2 to the pool with allocation 2 at block 420 + await time.advanceBlockTo("419") + await this.chef.add("20", this.lp2.address, true) + // Alice should have 10*1000 pending reward + expect(await this.chef.pendingSushi(0, this.alice.address)).to.equal("10000") + // Bob deposits 10 LP2s at block 425 + await time.advanceBlockTo("424") + await this.chef.connect(this.bob).deposit(1, "5", { from: this.bob.address }) + // Alice should have 10000 + 5*1/3*1000 = 11666 pending reward + expect(await this.chef.pendingSushi(0, this.alice.address)).to.equal("11666") + await time.advanceBlockTo("430") + // At block 430. Bob should get 5*2/3*1000 = 3333. Alice should get ~1666 more. + expect(await this.chef.pendingSushi(0, this.alice.address)).to.equal("13333") + expect(await this.chef.pendingSushi(1, this.bob.address)).to.equal("3333") + }) + + it("should stop giving bonus SUSHIs after the bonus period ends", async function () { + // 100 per block farming rate starting at block 500 with bonus until block 600 + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "100", "500", "600") + await this.sushi.transferOwnership(this.chef.address) + await this.lp.connect(this.alice).approve(this.chef.address, "1000", { from: this.alice.address }) + await this.chef.add("1", this.lp.address, true) + // Alice deposits 10 LPs at block 590 + await time.advanceBlockTo("589") + await this.chef.connect(this.alice).deposit(0, "10", { from: this.alice.address }) + // At block 605, she should have 1000*10 + 100*5 = 10500 pending. + await time.advanceBlockTo("605") + expect(await this.chef.pendingSushi(0, this.alice.address)).to.equal("10500") + // At block 606, Alice withdraws all pending rewards and should get 10600. + await this.chef.connect(this.alice).deposit(0, "0", { from: this.alice.address }) + expect(await this.chef.pendingSushi(0, this.alice.address)).to.equal("0") + expect(await this.sushi.balanceOf(this.alice.address)).to.equal("10600") + }) + }) +}) diff --git a/build-test/MasterChefV2.sol b/build-test/MasterChefV2.sol new file mode 100644 index 0000000000..ef8c2cf082 --- /dev/null +++ b/build-test/MasterChefV2.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import "@boringcrypto/boring-solidilibraries/BoringMath.sol"; +import "@boringcrypto/boring-solidiBoringBatchable.sol"; +import "@boringcrypto/boring-solidiBoringOwnable.sol"; +import "./libraries/SignedSafeMath.sol"; +import "./interfaces/IRewarder.sol"; +import "./interfaces/IMasterChef.sol"; + +/// @notice The (older) MasterChef contract gives out a constant number of SUSHI per Block. +/// It is the only address with minting rights for the SUSHI token. +/// The idea for this MasterChef V2 (MCV2) contract is therefore to be the owner of a dummy token +/// that is deposited into the MasterChef V1 (MCV1) contract. +/// The allocation point for this pool on MCV1 is the total allocation point for all pools that receive double incentives. +contract MasterChefV2 is BoringOwnable, BoringBatchable { + using BoringMath for uint256; + using BoringMath128 for uint128; + using BoringERC20 for IERC20; + using SignedSafeMath for int256; + + /// @notice Info of each user. + /// `amount` of LP tokens the user has provided. + /// `rewardDebt` the amount of SUSHI entitled to the user. + struct UserInfo { + uint256 amount; + int256 rewardDebt; + } + + /// @notice Info of each pool. + /// `allocPoint` The amount of allocation points assigned to the pool. + /// Also known as the amount of SUSHIs to distribute per Block + struct PoolInfo { + uint128 accSushiPerShare; + uint64 lastRewardBlock; + uint64 allocPoint; + } + + /// @notice Address of MasterChef (v1). + IMasterChef public immutable MASTER_CHEF; + /// @notice The SUSHI ERC-20 contract. + IERC20 public immutable SUSHI; + /// @notice The index of the master pool. + uint256 public immutable MASTER_PID; + + /// @notice Info of each pool. + PoolInfo[] public poolInfo; + /// @notice Address of the ERC-20 for each Pool. + IERC20[] public lpToken; + /// @notice Address of each `IRewarder` contract. + IRewarder[] public rewarder; + + /// @notice Info of each user that stakes LP tokens. + mapping (uint256 => mapping (address => UserInfo)) public userInfo; + /// @dev Total allocation points. Must be the sum of all allocation points in all pools. + uint256 totalAllocPoint; + + uint256 private constant MASTERCHEF_SUSHI_PER_BLOCK = 1e20; + uint256 private constant ACC_SUSHI_PRECISION = 1e12; + bytes4 private constant SIG_ON_SUSHI_REWARD = 0xbb6cc2ef; // onSushiReward(uint256,address,uint256) + + event Deposit(address indexed user, uint256 indexed pid, uint256 amount, address indexed to); + event Withdraw(address indexed user, uint256 indexed pid, uint256 amount, address indexed to); + event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount, address indexed to); + event Harvest(address indexed user, uint256 indexed pid, uint256 amount); + event LogPoolAddition(uint256 indexed pid, uint256 allocPoint, IERC20 indexed lpToken, IRewarder indexed rewarder); + event LogSetPool(uint256 indexed pid, uint256 allocPoint, IRewarder rewarder, bool overwrite); + event LogUpdatePool(uint256 indexed pid, uint64 lastRewardBlock, uint256 lpSupply, uint256 accSushiPerShare); + event LogInit(); + + /// @param _MASTER_CHEF the SushiSwap MasterChef contract + /// @param _sushi the SUSHI Token + /// @param _MASTER_PID the pool ID of the dummy token on the base contract + constructor(IMasterChef _MASTER_CHEF, IERC20 _sushi, uint256 _MASTER_PID) public { + MASTER_CHEF = _MASTER_CHEF; + SUSHI = _sushi; + MASTER_PID = _MASTER_PID; + } + + /// @notice Deposits a dummy tokens to `MASTER_CHEF`. This is required because `MASTER_CHEF` holds the minting rights for SUSHI. + /// Any balance of transaction sender from `dummyToken` is transferred. + /// The allocation point for the pool on MCV1 is the total allocation point for all pools that receive double incentives. + function init(IERC20 dummyToken) external { + uint256 balance = dummyToken.balanceOf(msg.sender); + require(balance != 0, "Balance must exceed 0"); + dummyToken.safeTransferFrom(msg.sender, address(this), balance); + dummyToken.approve(address(MASTER_CHEF), balance); + MASTER_CHEF.deposit(MASTER_PID, balance); + emit LogInit(); + } + + /// @notice Returns the number of pools. + function poolLength() public view returns (uint256) { + return poolInfo.length; + } + + /// @notice Add a new lp to the pool. Can only be called by the owner. + /// DO NOT add the same LP token more than once. Rewards will be messed up if you do. + /// @param allocPoint AP of the new pool + /// @param _lpToken address of the LP token + /// @param _rewarder address of the reward Contract + function add(uint256 allocPoint, IERC20 _lpToken, IRewarder _rewarder) public onlyOwner { + uint256 lastRewardBlock = block.number; + totalAllocPoint = totalAllocPoint.add(allocPoint); + lpToken.push(_lpToken); + rewarder.push(_rewarder); + + poolInfo.push(PoolInfo({ + allocPoint: allocPoint.to64(), + lastRewardBlock: lastRewardBlock.to64(), + accSushiPerShare: 0 + })); + emit LogPoolAddition(lpToken.length.sub(1), allocPoint, _lpToken, _rewarder); + } + + + /// @notice Update the given pool's SUSHI allocation point and `IRewarder` contract. Can only be called by the owner. + /// @param _pid The index of the pool. See `poolInfo`. + /// @param _allocPoint new AP of the pool + /// @param _rewarder Address of the rewarder delegate. + /// @param overwrite True if _rewarder should be `set`. Otherwise `_rewarder` is ignored. + function set(uint256 _pid, uint256 _allocPoint, IRewarder _rewarder, bool overwrite) public onlyOwner { + totalAllocPoint = totalAllocPoint.sub(poolInfo[_pid].allocPoint).add(_allocPoint); + poolInfo[_pid].allocPoint = _allocPoint.to64(); + if (overwrite) { rewarder[_pid] = _rewarder; } + emit LogSetPool(_pid, _allocPoint, overwrite ? _rewarder : rewarder[_pid], overwrite); + } + + + /// @notice View function to see pending SUSHIs on frontend. + /// @param _pid The index of the pool. See `poolInfo`. + /// @param _user address of user + function pendingSushi(uint256 _pid, address _user) external view returns (uint256) { + PoolInfo memory pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][_user]; + uint256 accSushiPerShare = pool.accSushiPerShare; + uint256 lpSupply = lpToken[_pid].balanceOf(address(this)); + if (block.number > pool.lastRewardBlock && lpSupply != 0) { + uint256 blocks = block.number.sub(pool.lastRewardBlock); + uint256 sushiReward = blocks.mul(sushiPerBlock()).mul(pool.allocPoint) / totalAllocPoint; + accSushiPerShare = accSushiPerShare.add(sushiReward.mul(ACC_SUSHI_PRECISION) / lpSupply); + } + uint256 _pendingSushi = int256(user.amount.mul(accSushiPerShare) / ACC_SUSHI_PRECISION).sub(user.rewardDebt).toUInt256(); + return _pendingSushi; + } + + /// @notice Update reward variables for all pools. Be careful of gas spending! + /// @param pids pool IDs of all to be updated, make sure to update all active pools + function massUpdatePools(uint256[] calldata pids) external { + uint256 len = pids.length; + for (uint256 i = 0; i < len; ++i) { + updatePool(pids[i]); + } + } + + /// @notice calculates the `amount` of SUSHI per block + function sushiPerBlock() public view returns (uint256 amount) { + amount = uint256(MASTERCHEF_SUSHI_PER_BLOCK) + .mul(MASTER_CHEF.poolInfo(MASTER_PID).allocPoint) / MASTER_CHEF.totalAllocPoint(); + } + + + /// @notice Update reward variables of the given pool. + /// @param pid The index of the pool. See `poolInfo`. + /// @return pool returns the Pool that was updated + function updatePool(uint256 pid) public returns (PoolInfo memory pool) { + pool = poolInfo[pid]; + if (block.number > pool.lastRewardBlock) { + uint256 lpSupply = lpToken[pid].balanceOf(address(this)); + if (lpSupply > 0) { + uint256 blocks = block.number.sub(pool.lastRewardBlock); + uint256 sushiReward = blocks.mul(sushiPerBlock()).mul(pool.allocPoint) / totalAllocPoint; + pool.accSushiPerShare = pool.accSushiPerShare.add((sushiReward.mul(ACC_SUSHI_PRECISION) / lpSupply).to128()); + } + pool.lastRewardBlock = block.number.to64(); + poolInfo[pid] = pool; + emit LogUpdatePool(pid, pool.lastRewardBlock, lpSupply, pool.accSushiPerShare); + } + } + + /// @notice Deposit LP tokens to MasterChef for SUSHI allocation. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount to deposit. + /// @param to The receiver of `amount`. + function deposit(uint256 pid, uint256 amount, address to) public { + PoolInfo memory pool = updatePool(pid); + UserInfo storage user = userInfo[pid][to]; + + // Effects + user.amount = user.amount.add(amount); + user.rewardDebt = user.rewardDebt.add(int256(amount.mul(pool.accSushiPerShare) / ACC_SUSHI_PRECISION)); + + // Interactions + lpToken[pid].safeTransferFrom(address(msg.sender), address(this), amount); + + emit Deposit(msg.sender, pid, amount, to); + } + + /// @notice Withdraw LP tokens from MasterChef. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount of lp tokens to withdraw. + /// @param to Receiver of the lp tokens. + function withdraw(uint256 pid, uint256 amount, address to) public { + PoolInfo memory pool = updatePool(pid); + UserInfo storage user = userInfo[pid][msg.sender]; + + // Effects + user.rewardDebt = user.rewardDebt.sub(int256(amount.mul(pool.accSushiPerShare) / ACC_SUSHI_PRECISION)); + user.amount = user.amount.sub(amount); + + // Interactions + lpToken[pid].safeTransfer(to, amount); + + emit Withdraw(msg.sender, pid, amount, to); + } + + /// @notice Harvest proceeds for transaction sender to `to`. + /// @param pid The index of the pool. See `poolInfo`. + /// @param to Receiver of SUSHI rewards. + function harvest(uint256 pid, address to) public { + PoolInfo memory pool = updatePool(pid); + UserInfo storage user = userInfo[pid][msg.sender]; + int256 accumulatedSushi = int256(user.amount.mul(pool.accSushiPerShare) / ACC_SUSHI_PRECISION); + uint256 _pendingSushi = accumulatedSushi.sub(user.rewardDebt).toUInt256(); + if (_pendingSushi == 0) { return; } + + + // Effects + user.rewardDebt = accumulatedSushi; + + // Interactions + SUSHI.safeTransfer(to, _pendingSushi); + + + address _rewarder = address(rewarder[pid]); + if (_rewarder != address(0)){ + // Note: Do it this way because we don't want to fail harvest if only the delegate call fails. + // Additionally, forward less gas so that we have enough buffer to complete harvest if the call eats up too much gas. + // Forwarding: (63/64 of gasleft by evm convention) minus 5000 + // solhint-disable-next-line + _rewarder.call{ gas: gasleft() - 5000 }(abi.encodeWithSelector(SIG_ON_SUSHI_REWARD, pid, msg.sender, _pendingSushi)); + + } + + emit Harvest(msg.sender, pid, _pendingSushi); + } + + /// @notice Harvests SUSHI from `MASTER_CHEF` and pool `MASTER_PID` to this contract. + function harvestFromMasterChef () public { + MASTER_CHEF.deposit(MASTER_PID, 0); + } + + /// @notice Withdraw without caring about rewards. EMERGENCY ONLY. + /// @param pid The index of the pool. See `poolInfo`. + /// @param to Receiver of the lp tokens. + function emergencyWithdraw(uint256 pid, address to) public { + UserInfo storage user = userInfo[pid][msg.sender]; + uint256 amount = user.amount; + user.amount = 0; + user.rewardDebt = 0; + // Note: transfer can fail or succeed if `amount` is zero. + lpToken[pid].safeTransfer(to, amount); + emit EmergencyWithdraw(msg.sender, pid, amount, to); + } +} diff --git a/build-test/MasterChefV2.test.js b/build-test/MasterChefV2.test.js new file mode 100644 index 0000000000..5271a18550 --- /dev/null +++ b/build-test/MasterChefV2.test.js @@ -0,0 +1,234 @@ +const { ethers } = require("hardhat") +const { expect, assert } = require("chai") +const { time, prepare, deploy, getBigNumber, ADDRESS_ZERO} = require("./utilities") +const { advanceBlockTo, advanceBlock } = require("./utilities/time") + +describe("MasterChefV2", function () { + before(async function () { + await prepare(this, ['MasterChef', 'SushiToken', 'ERC20Mock', 'MasterChefV2', 'RewarderMock', 'RewarderBrokenMock']) + await deploy(this, [ + ["brokenRewarder", this.RewarderBrokenMock] + ]) + }) + + beforeEach(async function () { + await deploy(this, [ + ["sushi", this.SushiToken], + ]) + + await deploy(this, + [["lp", this.ERC20Mock, ["LP Token", "LPT", getBigNumber(10)]], + ["dummy", this.ERC20Mock, ["Dummy", "DummyT", getBigNumber(10)]], + ['chef', this.MasterChef, [this.sushi.address, this.bob.address, getBigNumber(100), "0", "0"]] + ]) + + await this.sushi.transferOwnership(this.chef.address) + await this.chef.add(100, this.lp.address, true) + await this.chef.add(100, this.dummy.address, true) + await this.lp.approve(this.chef.address, getBigNumber(10)) + await this.chef.deposit(0, getBigNumber(10)) + + await deploy(this, [ + ['chef2', this.MasterChefV2, [this.chef.address, this.sushi.address, 1]], + ["rlp", this.ERC20Mock, ["LP", "rLPT", getBigNumber(10)]], + ["r", this.ERC20Mock, ["Reward", "RewardT", getBigNumber(100000)]], + ]) + await deploy(this, [["rewarder", this.RewarderMock, [getBigNumber(1), this.r.address]]]) + await this.dummy.approve(this.chef2.address, getBigNumber(10)) + await this.chef2.init(this.dummy.address) + await this.rlp.transfer(this.bob.address, getBigNumber(1)) + }) + + describe("Init", function () { + it("Balance of dummyToken should be 0 after init(), repeated execution should fail", async function () { + await expect(this.chef2.init(this.dummy.address)) + .to.be.revertedWith("Balance must exceed 0") + }) + }) + + describe("PoolLength", function () { + it("PoolLength should execute", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + expect((await this.chef2.poolLength())).to.be.equal(1); + }) + }) + + describe("Set", function() { + it("Should emit event LogSetPool", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await expect(this.chef2.set(0, 10, this.dummy.address, false)) + .to.emit(this.chef2, "LogSetPool") + .withArgs(0, 10, this.rewarder.address, false) + await expect(this.chef2.set(0, 10, this.dummy.address, true)) + .to.emit(this.chef2, "LogSetPool") + .withArgs(0, 10, this.dummy.address, true) + }) + + it("Should revert if invalid pool", async function () { + let err; + try { + await this.chef2.set(0, 10, this.rewarder.address, false) + } catch (e) { + err = e; + } + + assert.equal(err.toString(), "Error: VM Exception while processing transaction: invalid opcode") + }) + }) + + describe("PendingSushi", function() { + it("PendingSushi should equal ExpectedSushi", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await this.rlp.approve(this.chef2.address, getBigNumber(10)) + let log = await this.chef2.deposit(0, getBigNumber(1), this.alice.address) + await advanceBlock() + let log2 = await this.chef2.updatePool(0) + await advanceBlock() + let expectedSushi = getBigNumber(100).mul(log2.blockNumber + 1 - log.blockNumber).div(2) + let pendingSushi = await this.chef2.pendingSushi(0, this.alice.address) + expect(pendingSushi).to.be.equal(expectedSushi) + }) + it("When block is lastRewardBlock", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await this.rlp.approve(this.chef2.address, getBigNumber(10)) + let log = await this.chef2.deposit(0, getBigNumber(1), this.alice.address) + await advanceBlockTo(3) + let log2 = await this.chef2.updatePool(0) + let expectedSushi = getBigNumber(100).mul(log2.blockNumber - log.blockNumber).div(2) + let pendingSushi = await this.chef2.pendingSushi(0, this.alice.address) + expect(pendingSushi).to.be.equal(expectedSushi) + }) + }) + + describe("MassUpdatePools", function () { + it("Should call updatePool", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await advanceBlockTo(1) + await this.chef2.massUpdatePools([0]) + //expect('updatePool').to.be.calledOnContract(); //not suported by heardhat + //expect('updatePool').to.be.calledOnContractWith(0); //not suported by heardhat + + }) + + it("Updating invalid pools should fail", async function () { + let err; + try { + await this.chef2.massUpdatePools([0, 10000, 100000]) + } catch (e) { + err = e; + } + + assert.equal(err.toString(), "Error: VM Exception while processing transaction: invalid opcode") + }) +}) + + describe("Add", function () { + it("Should add pool with reward token multiplier", async function () { + await expect(this.chef2.add(10, this.rlp.address, this.rewarder.address)) + .to.emit(this.chef2, "LogPoolAddition") + .withArgs(0, 10, this.rlp.address, this.rewarder.address) + }) + }) + + describe("UpdatePool", function () { + it("Should emit event LogUpdatePool", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await advanceBlockTo(1) + await expect(this.chef2.updatePool(0)) + .to.emit(this.chef2, "LogUpdatePool") + .withArgs(0, (await this.chef2.poolInfo(0)).lastRewardBlock, + (await this.rlp.balanceOf(this.chef2.address)), + (await this.chef2.poolInfo(0)).accSushiPerShare) + }) + + it("Should take else path", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await advanceBlockTo(1) + await this.chef2.batch( + [ + this.chef2.interface.encodeFunctionData("updatePool", [0]), + this.chef2.interface.encodeFunctionData("updatePool", [0]), + ], + true + ) + }) + }) + + describe("Deposit", function () { + it("Depositing 0 amount", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await this.rlp.approve(this.chef2.address, getBigNumber(10)) + await expect(this.chef2.deposit(0, getBigNumber(0), this.alice.address)) + .to.emit(this.chef2, "Deposit") + .withArgs(this.alice.address, 0, 0, this.alice.address) + }) + + it("Depositing into non-existent pool should fail", async function () { + let err; + try { + await this.chef2.deposit(1001, getBigNumber(0), this.alice.address) + } catch (e) { + err = e; + } + + assert.equal(err.toString(), "Error: VM Exception while processing transaction: invalid opcode") + }) + }) + + describe("Withdraw", function () { + it("Withdraw 0 amount", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await expect(this.chef2.withdraw(0, getBigNumber(0), this.alice.address)) + .to.emit(this.chef2, "Withdraw") + .withArgs(this.alice.address, 0, 0, this.alice.address) + }) + }) + + describe("Harvest", function () { + it("Should give back the correct amount of SUSHI and reward", async function () { + await this.r.transfer(this.rewarder.address, getBigNumber(100000)) + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await this.rlp.approve(this.chef2.address, getBigNumber(10)) + expect(await this.chef2.lpToken(0)).to.be.equal(this.rlp.address) + let log = await this.chef2.deposit(0, getBigNumber(1), this.alice.address) + await advanceBlockTo(20) + await this.chef2.harvestFromMasterChef() + let log2 = await this.chef2.withdraw(0, getBigNumber(1), this.alice.address) + let expectedSushi = getBigNumber(100).mul(log2.blockNumber - log.blockNumber).div(2) + expect((await this.chef2.userInfo(0, this.alice.address)).rewardDebt).to.be.equal("-"+expectedSushi) + await this.chef2.harvest(0, this.alice.address) + expect(await this.sushi.balanceOf(this.alice.address)).to.be.equal(await this.r.balanceOf(this.alice.address)).to.be.equal(expectedSushi) + }) + it("Harvest with empty user balance", async function () { + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await this.chef2.harvest(0, this.alice.address) + }) + + it("Harvest for SUSHI-only pool", async function () { + await this.chef2.add(10, this.rlp.address, ADDRESS_ZERO) + await this.rlp.approve(this.chef2.address, getBigNumber(10)) + expect(await this.chef2.lpToken(0)).to.be.equal(this.rlp.address) + let log = await this.chef2.deposit(0, getBigNumber(1), this.alice.address) + await advanceBlock() + await this.chef2.harvestFromMasterChef() + let log2 = await this.chef2.withdraw(0, getBigNumber(1), this.alice.address) + let expectedSushi = getBigNumber(100).mul(log2.blockNumber - log.blockNumber).div(2) + expect((await this.chef2.userInfo(0, this.alice.address)).rewardDebt).to.be.equal("-"+expectedSushi) + await this.chef2.harvest(0, this.alice.address) + expect(await this.sushi.balanceOf(this.alice.address)).to.be.equal(expectedSushi) + }) + }) + + describe("EmergencyWithdraw", function() { + it("Should emit event EmergencyWithdraw", async function () { + await this.r.transfer(this.rewarder.address, getBigNumber(100000)) + await this.chef2.add(10, this.rlp.address, this.rewarder.address) + await this.rlp.approve(this.chef2.address, getBigNumber(10)) + await this.chef2.deposit(0, getBigNumber(1), this.bob.address) + //await this.chef2.emergencyWithdraw(0, this.alice.address) + await expect(this.chef2.connect(this.bob).emergencyWithdraw(0, this.bob.address)) + .to.emit(this.chef2, "EmergencyWithdraw") + .withArgs(this.bob.address, 0, getBigNumber(1), this.bob.address) + }) + }) +}) diff --git a/build-test/Migrator.sol b/build-test/Migrator.sol new file mode 100644 index 0000000000..4fc535ca70 --- /dev/null +++ b/build-test/Migrator.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "./uniswapv2/interfaces/IUniswapV2Pair.sol"; +import "./uniswapv2/interfaces/IUniswapV2Factory.sol"; + + +contract Migrator { + address public chef; + address public oldFactory; + IUniswapV2Factory public factory; + uint256 public notBeforeBlock; + uint256 public desiredLiquidity = uint256(-1); + + constructor( + address _chef, + address _oldFactory, + IUniswapV2Factory _factory, + uint256 _notBeforeBlock + ) public { + chef = _chef; + oldFactory = _oldFactory; + factory = _factory; + notBeforeBlock = _notBeforeBlock; + } + + function migrate(IUniswapV2Pair orig) public returns (IUniswapV2Pair) { + require(msg.sender == chef, "not from master chef"); + require(block.number >= notBeforeBlock, "too early to migrate"); + require(orig.factory() == oldFactory, "not from old factory"); + address token0 = orig.token0(); + address token1 = orig.token1(); + IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(token0, token1)); + if (pair == IUniswapV2Pair(address(0))) { + pair = IUniswapV2Pair(factory.createPair(token0, token1)); + } + uint256 lp = orig.balanceOf(msg.sender); + if (lp == 0) return pair; + desiredLiquidity = lp; + orig.transferFrom(msg.sender, address(orig), lp); + orig.burn(address(pair)); + pair.mint(msg.sender); + desiredLiquidity = uint256(-1); + return pair; + } +} \ No newline at end of file diff --git a/build-test/Migrator.test.js b/build-test/Migrator.test.js new file mode 100644 index 0000000000..edda0d374b --- /dev/null +++ b/build-test/Migrator.test.js @@ -0,0 +1,99 @@ +const { ethers } = require("hardhat") +const { expect } = require("chai") + +describe("Migrator", function () { + before(async function () { + this.signers = await ethers.getSigners() + this.alice = this.signers[0] + this.bob = this.signers[1] + this.dev = this.signers[2] + this.minter = this.signers[3] + + this.UniswapV2Factory = await ethers.getContractFactory("UniswapV2Factory") + this.UniswapV2Pair = await ethers.getContractFactory("UniswapV2Pair") + this.ERC20Mock = await ethers.getContractFactory("ERC20Mock", this.minter) + this.SushiToken = await ethers.getContractFactory("SushiToken") + this.MasterChef = await ethers.getContractFactory("MasterChef") + this.Migrator = await ethers.getContractFactory("Migrator") + }) + + beforeEach(async function () { + this.factory1 = await this.UniswapV2Factory.deploy(this.alice.address) + await this.factory1.deployed() + + this.factory2 = await this.UniswapV2Factory.deploy(this.alice.address) + await this.factory2.deployed() + + this.sushi = await this.SushiToken.deploy() + await this.sushi.deployed() + + this.weth = await this.ERC20Mock.deploy("WETH", "WETH", "100000000") + await this.weth.deployed() + + this.token = await this.ERC20Mock.deploy("TOKEN", "TOKEN", "100000000") + await this.token.deployed() + + const pair1 = await this.factory1.createPair(this.weth.address, this.token.address) + + this.lp1 = await this.UniswapV2Pair.attach((await pair1.wait()).events[0].args.pair) + + const pair2 = await this.factory2.createPair(this.weth.address, this.token.address) + + this.lp2 = await this.UniswapV2Pair.attach((await pair2.wait()).events[0].args.pair) + + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "1000", "0", "100000") + await this.chef.deployed() + + this.migrator = await this.Migrator.deploy(this.chef.address, this.factory1.address, this.factory2.address, "0") + await this.migrator.deployed() + + await this.sushi.transferOwnership(this.chef.address) + + await this.chef.add("100", this.lp1.address, true) + }) + + it("should do the migration successfully", async function () { + await this.token.connect(this.minter).transfer(this.lp1.address, "10000000", { from: this.minter.address }) + await this.weth.connect(this.minter).transfer(this.lp1.address, "500000", { from: this.minter.address }) + await this.lp1.mint(this.minter.address) + expect(await this.lp1.balanceOf(this.minter.address)).to.equal("2235067") + + // Add some fake revenue + await this.token.connect(this.minter).transfer(this.lp1.address, "100000", { from: this.minter.address }) + await this.weth.connect(this.minter).transfer(this.lp1.address, "5000", { from: this.minter.address }) + await this.lp1.sync() + await this.lp1.connect(this.minter).approve(this.chef.address, "100000000000", { from: this.minter.address }) + await this.chef.connect(this.minter).deposit("0", "2000000", { from: this.minter.address }) + expect(await this.lp1.balanceOf(this.chef.address), "2000000") + await expect(this.chef.migrate(0)).to.be.revertedWith("migrate: no migrator") + + await this.chef.setMigrator(this.migrator.address) + await expect(this.chef.migrate(0)).to.be.revertedWith("migrate: bad") + + await this.factory2.setMigrator(this.migrator.address) + await this.chef.migrate(0) + expect(await this.lp1.balanceOf(this.chef.address)).to.equal("0") + expect(await this.lp2.balanceOf(this.chef.address)).to.equal("2000000") + + await this.chef.connect(this.minter).withdraw("0", "2000000", { from: this.minter.address }) + await this.lp2.connect(this.minter).transfer(this.lp2.address, "2000000", { from: this.minter.address }) + await this.lp2.burn(this.bob.address) + expect(await this.token.balanceOf(this.bob.address)).to.equal("9033718") + expect(await this.weth.balanceOf(this.bob.address)).to.equal("451685") + }) + + it("should allow first minting from public only after migrator is gone", async function () { + await this.factory2.setMigrator(this.migrator.address) + + this.tokenx = await this.ERC20Mock.deploy("TOKENX", "TOKENX", "100000000") + await this.tokenx.deployed() + + const pair = await this.factory2.createPair(this.weth.address, this.tokenx.address) + + this.lpx = await this.UniswapV2Pair.attach((await pair.wait()).events[0].args.pair) + + await this.weth.connect(this.minter).transfer(this.lpx.address, "10000000", { from: this.minter.address }) + await this.tokenx.connect(this.minter).transfer(this.lpx.address, "500000", { from: this.minter.address }) + await expect(this.lpx.mint(this.minter.address)).to.be.revertedWith("Must not have migrator") + }) +}) diff --git a/build-test/Ownable.sol b/build-test/Ownable.sol new file mode 100644 index 0000000000..6f76b63d29 --- /dev/null +++ b/build-test/Ownable.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// Audit on 5-Jan-2021 by Keno and BoringCrypto + +// P1 - P3: OK +pragma solidity 0.6.12; + +// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/mastaccess/Ownable.sol + Claimable.sol +// Edited by BoringCrypto + +// T1 - T4: OK +contract OwnableData { + // V1 - V5: OK + address public owner; + // V1 - V5: OK + address public pendingOwner; +} + +// T1 - T4: OK +contract Ownable is OwnableData { + // E1: OK + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + constructor () internal { + owner = msg.sender; + emit OwnershipTransferred(address(0), msg.sender); + } + + // F1 - F9: OK + // C1 - C21: OK + function transferOwnership(address newOwner, bool direct, bool renounce) public onlyOwner { + if (direct) { + // Checks + require(newOwner != address(0) || renounce, "Ownable: zero address"); + + // Effects + emit OwnershipTransferred(owner, newOwner); + owner = newOwner; + } else { + // Effects + pendingOwner = newOwner; + } + } + + // F1 - F9: OK + // C1 - C21: OK + function claimOwnership() public { + address _pendingOwner = pendingOwner; + + // Checks + require(msg.sender == _pendingOwner, "Ownable: caller != pending owner"); + + // Effects + emit OwnershipTransferred(owner, _pendingOwner); + owner = _pendingOwner; + pendingOwner = address(0); + } + + // M1 - M5: OK + // C1 - C21: OK + modifier onlyOwner() { + require(msg.sender == owner, "Ownable: caller is not the owner"); + _; + } +} diff --git a/build-test/SushiBar.sol b/build-test/SushiBar.sol new file mode 100644 index 0000000000..83c5225d7a --- /dev/null +++ b/build-test/SushiBar.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppeltoken/ERC20/IERC20.sol"; +import "@openzeppeltoken/ERC20/ERC20.sol"; +import "@openzeppelmath/SafeMath.sol"; + +// SushiBar is the coolest bar in town. You come in with some Sushi, and leave with more! The longer you stay, the more Sushi you get. +// +// This contract handles swapping to and from xSushi, SushiSwap's staking token. +contract SushiBar is ERC20("SushiBar", "xSUSHI"){ + using SafeMath for uint256; + IERC20 public sushi; + + // Define the Sushi token contract + constructor(IERC20 _sushi) public { + sushi = _sushi; + } + + // Enter the bar. Pay some SUSHIs. Earn some shares. + // Locks Sushi and mints xSushi + function enter(uint256 _amount) public { + // Gets the amount of Sushi locked in the contract + uint256 totalSushi = sushi.balanceOf(address(this)); + // Gets the amount of xSushi in existence + uint256 totalShares = totalSupply(); + // If no xSushi exists, mint it 1:1 to the amount put in + if (totalShares == 0 || totalSushi == 0) { + _mint(msg.sender, _amount); + } + // Calculate and mint the amount of xSushi the Sushi is worth. The ratio will change overtime, as xSushi is burned/minted and Sushi deposited + gained from fees / withdrawn. + else { + uint256 what = _amount.mul(totalShares).div(totalSushi); + _mint(msg.sender, what); + } + // Lock the Sushi in the contract + sushi.transferFrom(msg.sender, address(this), _amount); + } + + // Leave the bar. Claim back your SUSHIs. + // Unclocks the staked + gained Sushi and burns xSushi + function leave(uint256 _share) public { + // Gets the amount of xSushi in existence + uint256 totalShares = totalSupply(); + // Calculates the amount of Sushi the xSushi is worth + uint256 what = _share.mul(sushi.balanceOf(address(this))).div(totalShares); + _burn(msg.sender, _share); + sushi.transfer(msg.sender, what); + } +} \ No newline at end of file diff --git a/build-test/SushiBar.test.js b/build-test/SushiBar.test.js new file mode 100644 index 0000000000..a8d8d73920 --- /dev/null +++ b/build-test/SushiBar.test.js @@ -0,0 +1,61 @@ +const { ethers } = require("hardhat") +const { expect } = require("chai") + +describe("SushiBar", function () { + before(async function () { + this.SushiToken = await ethers.getContractFactory("SushiToken") + this.SushiBar = await ethers.getContractFactory("SushiBar") + + this.signers = await ethers.getSigners() + this.alice = this.signers[0] + this.bob = this.signers[1] + this.carol = this.signers[2] + }) + + beforeEach(async function () { + this.sushi = await this.SushiToken.deploy() + this.bar = await this.SushiBar.deploy(this.sushi.address) + this.sushi.mint(this.alice.address, "100") + this.sushi.mint(this.bob.address, "100") + this.sushi.mint(this.carol.address, "100") + }) + + it("should not allow enter if not enough approve", async function () { + await expect(this.bar.enter("100")).to.be.revertedWith("ERC20: transfer amount exceeds allowance") + await this.sushi.approve(this.bar.address, "50") + await expect(this.bar.enter("100")).to.be.revertedWith("ERC20: transfer amount exceeds allowance") + await this.sushi.approve(this.bar.address, "100") + await this.bar.enter("100") + expect(await this.bar.balanceOf(this.alice.address)).to.equal("100") + }) + + it("should not allow withraw more than what you have", async function () { + await this.sushi.approve(this.bar.address, "100") + await this.bar.enter("100") + await expect(this.bar.leave("200")).to.be.revertedWith("ERC20: burn amount exceeds balance") + }) + + it("should work with more than one participant", async function () { + await this.sushi.approve(this.bar.address, "100") + await this.sushi.connect(this.bob).approve(this.bar.address, "100", { from: this.bob.address }) + // Alice enters and gets 20 shares. Bob enters and gets 10 shares. + await this.bar.enter("20") + await this.bar.connect(this.bob).enter("10", { from: this.bob.address }) + expect(await this.bar.balanceOf(this.alice.address)).to.equal("20") + expect(await this.bar.balanceOf(this.bob.address)).to.equal("10") + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("30") + // SushiBar get 20 more SUSHIs from an external source. + await this.sushi.connect(this.carol).transfer(this.bar.address, "20", { from: this.carol.address }) + // Alice deposits 10 more SUSHIs. She should receive 10*30/50 = 6 shares. + await this.bar.enter("10") + expect(await this.bar.balanceOf(this.alice.address)).to.equal("26") + expect(await this.bar.balanceOf(this.bob.address)).to.equal("10") + // Bob withdraws 5 shares. He should receive 5*60/36 = 8 shares + await this.bar.connect(this.bob).leave("5", { from: this.bob.address }) + expect(await this.bar.balanceOf(this.alice.address)).to.equal("26") + expect(await this.bar.balanceOf(this.bob.address)).to.equal("5") + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("52") + expect(await this.sushi.balanceOf(this.alice.address)).to.equal("70") + expect(await this.sushi.balanceOf(this.bob.address)).to.equal("98") + }) +}) diff --git a/build-test/SushiMaker.sol b/build-test/SushiMaker.sol new file mode 100644 index 0000000000..28bbcdc932 --- /dev/null +++ b/build-test/SushiMaker.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: MIT + +// P1 - P3: OK +pragma solidity 0.6.12; +import "./libraries/SafeMath.sol"; +import "./libraries/SafeERC20.sol"; + +import "./uniswapv2/interfaces/IUniswapV2ERC20.sol"; +import "./uniswapv2/interfaces/IUniswapV2Pair.sol"; +import "./uniswapv2/interfaces/IUniswapV2Factory.sol"; + +import "./Ownable.sol"; + +// SushiMaker is MasterChef's left hand and kinda a wizard. He can cook up Sushi from pretty much anything! +// This contract handles "serving up" rewards for xSushi holders by trading tokens collected from fees for Sushi. + +// T1 - T4: OK +contract SushiMaker is Ownable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + // V1 - V5: OK + IUniswapV2Factory public immutable factory; + //0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac + // V1 - V5: OK + address public immutable bar; + //0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272 + // V1 - V5: OK + address private immutable sushi; + //0x6B3595068778DD592e39A122f4f5a5cF09C90fE2 + // V1 - V5: OK + address private immutable weth; + //0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + + // V1 - V5: OK + mapping(address => address) internal _bridges; + + // E1: OK + event LogBridgeSet(address indexed token, address indexed bridge); + // E1: OK + event LogConvert( + address indexed server, + address indexed token0, + address indexed token1, + uint256 amount0, + uint256 amount1, + uint256 amountSUSHI + ); + + constructor( + address _factory, + address _bar, + address _sushi, + address _weth + ) public { + factory = IUniswapV2Factory(_factory); + bar = _bar; + sushi = _sushi; + weth = _weth; + } + + // F1 - F10: OK + // C1 - C24: OK + function bridgeFor(address token) public view returns (address bridge) { + bridge = _bridges[token]; + if (bridge == address(0)) { + bridge = weth; + } + } + + // F1 - F10: OK + // C1 - C24: OK + function setBridge(address token, address bridge) external onlyOwner { + // Checks + require( + token != sushi && token != weth && token != bridge, + "SushiMaker: Invalid bridge" + ); + + // Effects + _bridges[token] = bridge; + emit LogBridgeSet(token, bridge); + } + + // M1 - M5: OK + // C1 - C24: OK + // C6: It's not a fool proof solution, but it prevents flash loans, so here it's ok to use tx.origin + modifier onlyEOA() { + // Try to make flash-loan exploit harder to do by only allowing externally owned addresses. + require(msg.sender == tx.origin, "SushiMaker: must use EOA"); + _; + } + + // F1 - F10: OK + // F3: _convert is separate to save gas by only checking the 'onlyEOA' modifier once in case of convertMultiple + // F6: There is an exploit to add lots of SUSHI to the bar, run convert, then remove the SUSHI again. + // As the size of the SushiBar has grown, this requires large amounts of funds and isn't super profitable anymore + // The onlyEOA modifier prevents this being done with a flash loan. + // C1 - C24: OK + function convert(address token0, address token1) external onlyEOA() { + _convert(token0, token1); + } + + // F1 - F10: OK, see convert + // C1 - C24: OK + // C3: Loop is under control of the caller + function convertMultiple( + address[] calldata token0, + address[] calldata token1 + ) external onlyEOA() { + // TODO: This can be optimized a fair bit, but this is safer and simpler for now + uint256 len = token0.length; + for (uint256 i = 0; i < len; i++) { + _convert(token0[i], token1[i]); + } + } + + // F1 - F10: OK + // C1- C24: OK + function _convert(address token0, address token1) internal { + // Interactions + // S1 - S4: OK + IUniswapV2Pair pair = IUniswapV2Pair(factory.getPair(token0, token1)); + require(address(pair) != address(0), "SushiMaker: Invalid pair"); + // balanceOf: S1 - S4: OK + // transfer: X1 - X5: OK + IERC20(address(pair)).safeTransfer( + address(pair), + pair.balanceOf(address(this)) + ); + // X1 - X5: OK + (uint256 amount0, uint256 amount1) = pair.burn(address(this)); + if (token0 != pair.token0()) { + (amount0, amount1) = (amount1, amount0); + } + emit LogConvert( + msg.sender, + token0, + token1, + amount0, + amount1, + _convertStep(token0, token1, amount0, amount1) + ); + } + + // F1 - F10: OK + // C1 - C24: OK + // All safeTransfer, _swap, _toSUSHI, _convertStep: X1 - X5: OK + function _convertStep( + address token0, + address token1, + uint256 amount0, + uint256 amount1 + ) internal returns (uint256 sushiOut) { + // Interactions + if (token0 == token1) { + uint256 amount = amount0.add(amount1); + if (token0 == sushi) { + IERC20(sushi).safeTransfer(bar, amount); + sushiOut = amount; + } else if (token0 == weth) { + sushiOut = _toSUSHI(weth, amount); + } else { + address bridge = bridgeFor(token0); + amount = _swap(token0, bridge, amount, address(this)); + sushiOut = _convertStep(bridge, bridge, amount, 0); + } + } else if (token0 == sushi) { + // eg. SUSHI - ETH + IERC20(sushi).safeTransfer(bar, amount0); + sushiOut = _toSUSHI(token1, amount1).add(amount0); + } else if (token1 == sushi) { + // eg. USDT - SUSHI + IERC20(sushi).safeTransfer(bar, amount1); + sushiOut = _toSUSHI(token0, amount0).add(amount1); + } else if (token0 == weth) { + // eg. ETH - USDC + sushiOut = _toSUSHI( + weth, + _swap(token1, weth, amount1, address(this)).add(amount0) + ); + } else if (token1 == weth) { + // eg. USDT - ETH + sushiOut = _toSUSHI( + weth, + _swap(token0, weth, amount0, address(this)).add(amount1) + ); + } else { + // eg. MIC - USDT + address bridge0 = bridgeFor(token0); + address bridge1 = bridgeFor(token1); + if (bridge0 == token1) { + // eg. MIC - USDT - and bridgeFor(MIC) = USDT + sushiOut = _convertStep( + bridge0, + token1, + _swap(token0, bridge0, amount0, address(this)), + amount1 + ); + } else if (bridge1 == token0) { + // eg. WBTC - DSD - and bridgeFor(DSD) = WBTC + sushiOut = _convertStep( + token0, + bridge1, + amount0, + _swap(token1, bridge1, amount1, address(this)) + ); + } else { + sushiOut = _convertStep( + bridge0, + bridge1, // eg. USDT - DSD - and bridgeFor(DSD) = WBTC + _swap(token0, bridge0, amount0, address(this)), + _swap(token1, bridge1, amount1, address(this)) + ); + } + } + } + + // F1 - F10: OK + // C1 - C24: OK + // All safeTransfer, swap: X1 - X5: OK + function _swap( + address fromToken, + address toToken, + uint256 amountIn, + address to + ) internal returns (uint256 amountOut) { + // Checks + // X1 - X5: OK + IUniswapV2Pair pair = + IUniswapV2Pair(factory.getPair(fromToken, toToken)); + require(address(pair) != address(0), "SushiMaker: Cannot convert"); + + // Interactions + // X1 - X5: OK + (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); + uint256 amountInWithFee = amountIn.mul(997); + if (fromToken == pair.token0()) { + amountOut = + amountIn.mul(997).mul(reserve1) / + reserve0.mul(1000).add(amountInWithFee); + IERC20(fromToken).safeTransfer(address(pair), amountIn); + pair.swap(0, amountOut, to, new bytes(0)); + // TODO: Add maximum slippage? + } else { + amountOut = + amountIn.mul(997).mul(reserve0) / + reserve1.mul(1000).add(amountInWithFee); + IERC20(fromToken).safeTransfer(address(pair), amountIn); + pair.swap(amountOut, 0, to, new bytes(0)); + // TODO: Add maximum slippage? + } + } + + // F1 - F10: OK + // C1 - C24: OK + function _toSUSHI(address token, uint256 amountIn) + internal + returns (uint256 amountOut) + { + // X1 - X5: OK + amountOut = _swap(token, sushi, amountIn, bar); + } +} diff --git a/build-test/SushiMaker.test.js b/build-test/SushiMaker.test.js new file mode 100644 index 0000000000..c4a5ebdcb2 --- /dev/null +++ b/build-test/SushiMaker.test.js @@ -0,0 +1,157 @@ +const { ethers } = require("hardhat") +const { prepare, deploy, getBigNumber, createSLP } = require("./utilities") +const { expect } = require("chai") + +describe("SushiMaker", function () { + before(async function () { + await prepare(this, ["SushiMaker", "SushiBar", "SushiMakerExploitMock", "ERC20Mock", "UniswapV2Factory", "UniswapV2Pair"]) + }) + + beforeEach(async function () { + await deploy(this, [ + ["sushi", this.ERC20Mock, ["SUSHI", "SUSHI", getBigNumber("10000000")]], + ["dai", this.ERC20Mock, ["DAI", "DAI", getBigNumber("10000000")]], + ["mic", this.ERC20Mock, ["MIC", "MIC", getBigNumber("10000000")]], + ["usdc", this.ERC20Mock, ["USDC", "USDC", getBigNumber("10000000")]], + ["weth", this.ERC20Mock, ["WETH", "ETH", getBigNumber("10000000")]], + ["strudel", this.ERC20Mock, ["$TRDL", "$TRDL", getBigNumber("10000000")]], + ["factory", this.UniswapV2Factory, [this.alice.address]], + ]) + await deploy(this, [["bar", this.SushiBar, [this.sushi.address]]]) + await deploy(this, [["sushiMaker", this.SushiMaker, [this.factory.address, this.bar.address, this.sushi.address, this.weth.address]]]) + await deploy(this, [["exploiter", this.SushiMakerExploitMock, [this.sushiMaker.address]]]) + await createSLP(this, "sushiEth", this.sushi, this.weth, getBigNumber(10)) + await createSLP(this, "strudelEth", this.strudel, this.weth, getBigNumber(10)) + await createSLP(this, "daiEth", this.dai, this.weth, getBigNumber(10)) + await createSLP(this, "usdcEth", this.usdc, this.weth, getBigNumber(10)) + await createSLP(this, "micUSDC", this.mic, this.usdc, getBigNumber(10)) + await createSLP(this, "sushiUSDC", this.sushi, this.usdc, getBigNumber(10)) + await createSLP(this, "daiUSDC", this.dai, this.usdc, getBigNumber(10)) + await createSLP(this, "daiMIC", this.dai, this.mic, getBigNumber(10)) + }) + describe("setBridge", function () { + it("does not allow to set bridge for Sushi", async function () { + await expect(this.sushiMaker.setBridge(this.sushi.address, this.weth.address)).to.be.revertedWith("SushiMaker: Invalid bridge") + }) + + it("does not allow to set bridge for WETH", async function () { + await expect(this.sushiMaker.setBridge(this.weth.address, this.sushi.address)).to.be.revertedWith("SushiMaker: Invalid bridge") + }) + + it("does not allow to set bridge to itself", async function () { + await expect(this.sushiMaker.setBridge(this.dai.address, this.dai.address)).to.be.revertedWith("SushiMaker: Invalid bridge") + }) + + it("emits correct event on bridge", async function () { + await expect(this.sushiMaker.setBridge(this.dai.address, this.sushi.address)) + .to.emit(this.sushiMaker, "LogBridgeSet") + .withArgs(this.dai.address, this.sushi.address) + }) + }) + describe("convert", function () { + it("should convert SUSHI - ETH", async function () { + await this.sushiEth.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.convert(this.sushi.address, this.weth.address) + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushiEth.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("1897569270781234370") + }) + + it("should convert USDC - ETH", async function () { + await this.usdcEth.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.convert(this.usdc.address, this.weth.address) + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.usdcEth.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("1590898251382934275") + }) + + it("should convert $TRDL - ETH", async function () { + await this.strudelEth.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.convert(this.strudel.address, this.weth.address) + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.strudelEth.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("1590898251382934275") + }) + + it("should convert USDC - SUSHI", async function () { + await this.sushiUSDC.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.convert(this.usdc.address, this.sushi.address) + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushiUSDC.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("1897569270781234370") + }) + + it("should convert using standard ETH path", async function () { + await this.daiEth.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.convert(this.dai.address, this.weth.address) + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.daiEth.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("1590898251382934275") + }) + + it("converts MIC/USDC using more complex path", async function () { + await this.micUSDC.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.setBridge(this.usdc.address, this.sushi.address) + await this.sushiMaker.setBridge(this.mic.address, this.usdc.address) + await this.sushiMaker.convert(this.mic.address, this.usdc.address) + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.micUSDC.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("1590898251382934275") + }) + + it("converts DAI/USDC using more complex path", async function () { + await this.daiUSDC.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.setBridge(this.usdc.address, this.sushi.address) + await this.sushiMaker.setBridge(this.dai.address, this.usdc.address) + await this.sushiMaker.convert(this.dai.address, this.usdc.address) + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.daiUSDC.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("1590898251382934275") + }) + + it("converts DAI/MIC using two step path", async function () { + await this.daiMIC.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.setBridge(this.dai.address, this.usdc.address) + await this.sushiMaker.setBridge(this.mic.address, this.dai.address) + await this.sushiMaker.convert(this.dai.address, this.mic.address) + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.daiMIC.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("1200963016721363748") + }) + + it("reverts if it loops back", async function () { + await this.daiMIC.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.setBridge(this.dai.address, this.mic.address) + await this.sushiMaker.setBridge(this.mic.address, this.dai.address) + await expect(this.sushiMaker.convert(this.dai.address, this.mic.address)).to.be.reverted + }) + + it("reverts if caller is not EOA", async function () { + await this.sushiEth.transfer(this.sushiMaker.address, getBigNumber(1)) + await expect(this.exploiter.convert(this.sushi.address, this.weth.address)).to.be.revertedWith("SushiMaker: must use EOA") + }) + + it("reverts if pair does not exist", async function () { + await expect(this.sushiMaker.convert(this.mic.address, this.micUSDC.address)).to.be.revertedWith("SushiMaker: Invalid pair") + }) + + it("reverts if no path is available", async function () { + await this.micUSDC.transfer(this.sushiMaker.address, getBigNumber(1)) + await expect(this.sushiMaker.convert(this.mic.address, this.usdc.address)).to.be.revertedWith("SushiMaker: Cannot convert") + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.micUSDC.balanceOf(this.sushiMaker.address)).to.equal(getBigNumber(1)) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal(0) + }) + }) + + describe("convertMultiple", function () { + it("should allow to convert multiple", async function () { + await this.daiEth.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiEth.transfer(this.sushiMaker.address, getBigNumber(1)) + await this.sushiMaker.convertMultiple([this.dai.address, this.sushi.address], [this.weth.address, this.weth.address]) + expect(await this.sushi.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.daiEth.balanceOf(this.sushiMaker.address)).to.equal(0) + expect(await this.sushi.balanceOf(this.bar.address)).to.equal("3186583558687783097") + }) + }) +}) diff --git a/build-test/SushiRoll.sol b/build-test/SushiRoll.sol new file mode 100644 index 0000000000..1ac611397a --- /dev/null +++ b/build-test/SushiRoll.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppeltoken/ERC20/IERC20.sol"; +import "@openzeppeltoken/ERC20/SafeERC20.sol"; +import "./uniswapv2/interfaces/IUniswapV2Pair.sol"; +import "./uniswapv2/interfaces/IUniswapV2Router01.sol"; +import "./uniswapv2/interfaces/IUniswapV2Factory.sol"; +import "./uniswapv2/libraries/UniswapV2Library.sol"; + +// SushiRoll helps your migrate your existing Uniswap LP tokens to SushiSwap LP ones +contract SushiRoll { + using SafeERC20 for IERC20; + + IUniswapV2Router01 public oldRouter; + IUniswapV2Router01 public router; + + constructor(IUniswapV2Router01 _oldRouter, IUniswapV2Router01 _router) public { + oldRouter = _oldRouter; + router = _router; + } + + function migrateWithPermit( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public { + IUniswapV2Pair pair = IUniswapV2Pair(pairForOldRouter(tokenA, tokenB)); + pair.permit(msg.sender, address(this), liquidity, deadline, v, r, s); + + migrate(tokenA, tokenB, liquidity, amountAMin, amountBMin, deadline); + } + + // msg.sender should have approved 'liquidity' amount of LP token of 'tokenA' and 'tokenB' + function migrate( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + uint256 deadline + ) public { + require(deadline >= block.timestamp, 'SushiSwap: EXPIRED'); + + // Remove liquidity from the old router with permit + (uint256 amountA, uint256 amountB) = removeLiquidity( + tokenA, + tokenB, + liquidity, + amountAMin, + amountBMin, + deadline + ); + + // Add liquidity to the new router + (uint256 pooledAmountA, uint256 pooledAmountB) = addLiquidity(tokenA, tokenB, amountA, amountB); + + // Send remaining tokens to msg.sender + if (amountA > pooledAmountA) { + IERC20(tokenA).safeTransfer(msg.sender, amountA - pooledAmountA); + } + if (amountB > pooledAmountB) { + IERC20(tokenB).safeTransfer(msg.sender, amountB - pooledAmountB); + } + } + + function removeLiquidity( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + uint256 deadline + ) internal returns (uint256 amountA, uint256 amountB) { + IUniswapV2Pair pair = IUniswapV2Pair(pairForOldRouter(tokenA, tokenB)); + pair.transferFrom(msg.sender, address(pair), liquidity); + (uint256 amount0, uint256 amount1) = pair.burn(address(this)); + (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); + (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); + require(amountA >= amountAMin, 'SushiRoll: INSUFFICIENT_A_AMOUNT'); + require(amountB >= amountBMin, 'SushiRoll: INSUFFICIENT_B_AMOUNT'); + } + + // calculates the CREATE2 address for a pair without making any external calls + function pairForOldRouter(address tokenA, address tokenB) internal view returns (address pair) { + (address token0, address token1) = UniswapV2Library.sortTokens(tokenA, tokenB); + pair = address(uint(keccak256(abi.encodePacked( + hex'ff', + oldRouter.factory(), + keccak256(abi.encodePacked(token0, token1)), + hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash + )))); + } + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired + ) internal returns (uint amountA, uint amountB) { + (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired); + address pair = UniswapV2Library.pairFor(router.factory(), tokenA, tokenB); + IERC20(tokenA).safeTransfer(pair, amountA); + IERC20(tokenB).safeTransfer(pair, amountB); + IUniswapV2Pair(pair).mint(msg.sender); + } + + function _addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired + ) internal returns (uint256 amountA, uint256 amountB) { + // create the pair if it doesn't exist yet + IUniswapV2Factory factory = IUniswapV2Factory(router.factory()); + if (factory.getPair(tokenA, tokenB) == address(0)) { + factory.createPair(tokenA, tokenB); + } + (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(address(factory), tokenA, tokenB); + if (reserveA == 0 && reserveB == 0) { + (amountA, amountB) = (amountADesired, amountBDesired); + } else { + uint256 amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); + if (amountBOptimal <= amountBDesired) { + (amountA, amountB) = (amountADesired, amountBOptimal); + } else { + uint256 amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); + assert(amountAOptimal <= amountADesired); + (amountA, amountB) = (amountAOptimal, amountBDesired); + } + } + } +} diff --git a/build-test/SushiRoll.test.js b/build-test/SushiRoll.test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/build-test/SushiToken.sol b/build-test/SushiToken.sol new file mode 100644 index 0000000000..cb0619a92f --- /dev/null +++ b/build-test/SushiToken.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppeltoken/ERC20/ERC20.sol"; +import "@openzeppelaccess/Ownable.sol"; + + +// SushiToken with Governance. +contract SushiToken is ERC20("SushiToken", "SUSHI"), Ownable { + /// @notice Creates `_amount` token to `_to`. Must only be called by the owner (MasterChef). + function mint(address _to, uint256 _amount) public onlyOwner { + _mint(_to, _amount); + _moveDelegates(address(0), _delegates[_to], _amount); + } + + // Copied and modified from YAM code: + // https://github.com/yam-finance/yam-protocol/blob/masttoken/YAMGovernanceStorage.sol + // https://github.com/yam-finance/yam-protocol/blob/masttoken/YAMGovernance.sol + // Which is copied and modified from COMPOUND: + // https://github.com/compound-finance/compound-protocol/blob/mastGovernance/Comp.sol + + /// @notice A record of each accounts delegate + mapping (address => address) internal _delegates; + + /// @notice A checkpoint for marking number of votes from a given block + struct Checkpoint { + uint32 fromBlock; + uint256 votes; + } + + /// @notice A record of votes checkpoints for each account, by index + mapping (address => mapping (uint32 => Checkpoint)) public checkpoints; + + /// @notice The number of checkpoints for each account + mapping (address => uint32) public numCheckpoints; + + /// @notice The EIP-712 typehash for the contract's domain + bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + + /// @notice The EIP-712 typehash for the delegation struct used by the contract + bytes32 public constant DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + /// @notice A record of states for signing / validating signatures + mapping (address => uint) public nonces; + + /// @notice An event thats emitted when an account changes its delegate + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /// @notice An event thats emitted when a delegate account's vote balance changes + event DelegateVotesChanged(address indexed delegate, uint previousBalance, uint newBalance); + + /** + * @notice Delegate votes from `msg.sender` to `delegatee` + * @param delegator The address to get delegatee for + */ + function delegates(address delegator) + external + view + returns (address) + { + return _delegates[delegator]; + } + + /** + * @notice Delegate votes from `msg.sender` to `delegatee` + * @param delegatee The address to delegate votes to + */ + function delegate(address delegatee) external { + return _delegate(msg.sender, delegatee); + } + + /** + * @notice Delegates votes from signatory to `delegatee` + * @param delegatee The address to delegate votes to + * @param nonce The contract state required to match the signature + * @param expiry The time at which to expire the signature + * @param v The recovery byte of the signature + * @param r Half of the ECDSA signature pair + * @param s Half of the ECDSA signature pair + */ + function delegateBySig( + address delegatee, + uint nonce, + uint expiry, + uint8 v, + bytes32 r, + bytes32 s + ) + external + { + bytes32 domainSeparator = keccak256( + abi.encode( + DOMAIN_TYPEHASH, + keccak256(bytes(name())), + getChainId(), + address(this) + ) + ); + + bytes32 structHash = keccak256( + abi.encode( + DELEGATION_TYPEHASH, + delegatee, + nonce, + expiry + ) + ); + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + structHash + ) + ); + + address signatory = ecrecover(digest, v, r, s); + require(signatory != address(0), "SUSHI::delegateBySig: invalid signature"); + require(nonce == nonces[signatory]++, "SUSHI::delegateBySig: invalid nonce"); + require(now <= expiry, "SUSHI::delegateBySig: signature expired"); + return _delegate(signatory, delegatee); + } + + /** + * @notice Gets the current votes balance for `account` + * @param account The address to get votes balance + * @return The number of current votes for `account` + */ + function getCurrentVotes(address account) + external + view + returns (uint256) + { + uint32 nCheckpoints = numCheckpoints[account]; + return nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; + } + + /** + * @notice Determine the prior number of votes for an account as of a block number + * @dev Block number must be a finalized block or else this function will revert to prevent misinformation. + * @param account The address of the account to check + * @param blockNumber The block number to get the vote balance at + * @return The number of votes the account had as of the given block + */ + function getPriorVotes(address account, uint blockNumber) + external + view + returns (uint256) + { + require(blockNumber < block.number, "SUSHI::getPriorVotes: not yet determined"); + + uint32 nCheckpoints = numCheckpoints[account]; + if (nCheckpoints == 0) { + return 0; + } + + // First check most recent balance + if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) { + return checkpoints[account][nCheckpoints - 1].votes; + } + + // Next check implicit zero balance + if (checkpoints[account][0].fromBlock > blockNumber) { + return 0; + } + + uint32 lower = 0; + uint32 upper = nCheckpoints - 1; + while (upper > lower) { + uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow + Checkpoint memory cp = checkpoints[account][center]; + if (cp.fromBlock == blockNumber) { + return cp.votes; + } else if (cp.fromBlock < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return checkpoints[account][lower].votes; + } + + function _delegate(address delegator, address delegatee) + internal + { + address currentDelegate = _delegates[delegator]; + uint256 delegatorBalance = balanceOf(delegator); // balance of underlying SUSHIs (not scaled); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveDelegates(currentDelegate, delegatee, delegatorBalance); + } + + function _moveDelegates(address srcRep, address dstRep, uint256 amount) internal { + if (srcRep != dstRep && amount > 0) { + if (srcRep != address(0)) { + // decrease old representative + uint32 srcRepNum = numCheckpoints[srcRep]; + uint256 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0; + uint256 srcRepNew = srcRepOld.sub(amount); + _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew); + } + + if (dstRep != address(0)) { + // increase new representative + uint32 dstRepNum = numCheckpoints[dstRep]; + uint256 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0; + uint256 dstRepNew = dstRepOld.add(amount); + _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew); + } + } + } + + function _writeCheckpoint( + address delegatee, + uint32 nCheckpoints, + uint256 oldVotes, + uint256 newVotes + ) + internal + { + uint32 blockNumber = safe32(block.number, "SUSHI::_writeCheckpoint: block number exceeds 32 bits"); + + if (nCheckpoints > 0 && checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber) { + checkpoints[delegatee][nCheckpoints - 1].votes = newVotes; + } else { + checkpoints[delegatee][nCheckpoints] = Checkpoint(blockNumber, newVotes); + numCheckpoints[delegatee] = nCheckpoints + 1; + } + + emit DelegateVotesChanged(delegatee, oldVotes, newVotes); + } + + function safe32(uint n, string memory errorMessage) internal pure returns (uint32) { + require(n < 2**32, errorMessage); + return uint32(n); + } + + function getChainId() internal pure returns (uint) { + uint256 chainId; + assembly { chainId := chainid() } + return chainId; + } +} \ No newline at end of file diff --git a/build-test/SushiToken.test.js b/build-test/SushiToken.test.js new file mode 100644 index 0000000000..8f7fdf7c55 --- /dev/null +++ b/build-test/SushiToken.test.js @@ -0,0 +1,66 @@ +const { ethers } = require("hardhat") +const { expect } = require("chai") +describe("SushiToken", function () { + before(async function () { + this.SushiToken = await ethers.getContractFactory("SushiToken") + this.signers = await ethers.getSigners() + this.alice = this.signers[0] + this.bob = this.signers[1] + this.carol = this.signers[2] + }) + + beforeEach(async function () { + this.sushi = await this.SushiToken.deploy() + await this.sushi.deployed() + }) + + it("should have correct name and symbol and decimal", async function () { + const name = await this.sushi.name() + const symbol = await this.sushi.symbol() + const decimals = await this.sushi.decimals() + expect(name, "SushiToken") + expect(symbol, "SUSHI") + expect(decimals, "18") + }) + + it("should only allow owner to mint token", async function () { + await this.sushi.mint(this.alice.address, "100") + await this.sushi.mint(this.bob.address, "1000") + await expect(this.sushi.connect(this.bob).mint(this.carol.address, "1000", { from: this.bob.address })).to.be.revertedWith( + "Ownable: caller is not the owner" + ) + const totalSupply = await this.sushi.totalSupply() + const aliceBal = await this.sushi.balanceOf(this.alice.address) + const bobBal = await this.sushi.balanceOf(this.bob.address) + const carolBal = await this.sushi.balanceOf(this.carol.address) + expect(totalSupply).to.equal("1100") + expect(aliceBal).to.equal("100") + expect(bobBal).to.equal("1000") + expect(carolBal).to.equal("0") + }) + + it("should supply token transfers properly", async function () { + await this.sushi.mint(this.alice.address, "100") + await this.sushi.mint(this.bob.address, "1000") + await this.sushi.transfer(this.carol.address, "10") + await this.sushi.connect(this.bob).transfer(this.carol.address, "100", { + from: this.bob.address, + }) + const totalSupply = await this.sushi.totalSupply() + const aliceBal = await this.sushi.balanceOf(this.alice.address) + const bobBal = await this.sushi.balanceOf(this.bob.address) + const carolBal = await this.sushi.balanceOf(this.carol.address) + expect(totalSupply, "1100") + expect(aliceBal, "90") + expect(bobBal, "900") + expect(carolBal, "110") + }) + + it("should fail if you try to do bad transfers", async function () { + await this.sushi.mint(this.alice.address, "100") + await expect(this.sushi.transfer(this.carol.address, "110")).to.be.revertedWith("ERC20: transfer amount exceeds balance") + await expect(this.sushi.connect(this.bob).transfer(this.carol.address, "1", { from: this.bob.address })).to.be.revertedWith( + "ERC20: transfer amount exceeds balance" + ) + }) +}) diff --git a/build-test/Timelock.test.js b/build-test/Timelock.test.js new file mode 100644 index 0000000000..b9d82b0efb --- /dev/null +++ b/build-test/Timelock.test.js @@ -0,0 +1,111 @@ +const { ethers } = require("hardhat") +const { expect } = require("chai") +const { encodeParameters, time } = require("./utilities") + +describe("Timelock", function () { + before(async function () { + this.signers = await ethers.getSigners() + this.alice = this.signers[0] + this.bob = this.signers[1] + this.carol = this.signers[2] + this.dev = this.signers[3] + this.minter = this.signers[4] + + this.SushiToken = await ethers.getContractFactory("SushiToken") + this.Timelock = await ethers.getContractFactory("Timelock") + this.ERC20Mock = await ethers.getContractFactory("ERC20Mock", this.minter) + this.MasterChef = await ethers.getContractFactory("MasterChef") + }) + + beforeEach(async function () { + this.sushi = await this.SushiToken.deploy() + this.timelock = await this.Timelock.deploy(this.bob.address, "259200") + }) + + it("should not allow non-owner to do operation", async function () { + await this.sushi.transferOwnership(this.timelock.address) + // await expectRevert(this.sushi.transferOwnership(carol, { from: alice }), "Ownable: caller is not the owner") + + await expect(this.sushi.transferOwnership(this.carol.address)).to.be.revertedWith("Ownable: caller is not the owner") + await expect(this.sushi.connect(this.bob).transferOwnership(this.carol.address)).to.be.revertedWith("Ownable: caller is not the owner") + + await expect( + this.timelock.queueTransaction( + this.sushi.address, + "0", + "transferOwnership(address)", + encodeParameters(["address"], [this.carol.address]), + (await time.latest()).add(time.duration.days(4)) + ) + ).to.be.revertedWith("Timelock::queueTransaction: Call must come from admin.") + }) + + it("should do the timelock thing", async function () { + await this.sushi.transferOwnership(this.timelock.address) + const eta = (await time.latest()).add(time.duration.days(4)) + await this.timelock + .connect(this.bob) + .queueTransaction(this.sushi.address, "0", "transferOwnership(address)", encodeParameters(["address"], [this.carol.address]), eta) + await time.increase(time.duration.days(1)) + await expect( + this.timelock + .connect(this.bob) + .executeTransaction(this.sushi.address, "0", "transferOwnership(address)", encodeParameters(["address"], [this.carol.address]), eta) + ).to.be.revertedWith("Timelock::executeTransaction: Transaction hasn't surpassed time lock.") + await time.increase(time.duration.days(4)) + await this.timelock + .connect(this.bob) + .executeTransaction(this.sushi.address, "0", "transferOwnership(address)", encodeParameters(["address"], [this.carol.address]), eta) + expect(await this.sushi.owner()).to.equal(this.carol.address) + }) + + it("should also work with MasterChef", async function () { + this.lp1 = await this.ERC20Mock.deploy("LPToken", "LP", "10000000000") + this.lp2 = await this.ERC20Mock.deploy("LPToken", "LP", "10000000000") + this.chef = await this.MasterChef.deploy(this.sushi.address, this.dev.address, "1000", "0", "1000") + await this.sushi.transferOwnership(this.chef.address) + await this.chef.add("100", this.lp1.address, true) + await this.chef.transferOwnership(this.timelock.address) + const eta = (await time.latest()).add(time.duration.days(4)) + await this.timelock + .connect(this.bob) + .queueTransaction( + this.chef.address, + "0", + "set(uint256,uint256,bool)", + encodeParameters(["uint256", "uint256", "bool"], ["0", "200", false]), + eta + ) + await this.timelock + .connect(this.bob) + .queueTransaction( + this.chef.address, + "0", + "add(uint256,address,bool)", + encodeParameters(["uint256", "address", "bool"], ["100", this.lp2.address, false]), + eta + ) + await time.increase(time.duration.days(4)) + await this.timelock + .connect(this.bob) + .executeTransaction( + this.chef.address, + "0", + "set(uint256,uint256,bool)", + encodeParameters(["uint256", "uint256", "bool"], ["0", "200", false]), + eta + ) + await this.timelock + .connect(this.bob) + .executeTransaction( + this.chef.address, + "0", + "add(uint256,address,bool)", + encodeParameters(["uint256", "address", "bool"], ["100", this.lp2.address, false]), + eta + ) + expect((await this.chef.poolInfo("0")).allocPoint).to.equal("200") + expect(await this.chef.totalAllocPoint()).to.equal("300") + expect(await this.chef.poolLength()).to.equal("2") + }) +}) diff --git a/hardhat.config.js b/hardhat.config.js index eb48fde6e9..fab52fcedc 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -13,6 +13,7 @@ require("hardhat-spdx-license-identifier") require("hardhat-typechain") require("hardhat-watcher") require("solidity-coverage") +require("hardhat-tracer"); require("./tasks") diff --git a/package.json b/package.json index b928b5941f..4d25f078db 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "arbitrum:deploy": "hardhat --network arbitrum deploy", "arbitrum:export": "hardhat --network arbitrum export --export exports/arbitrum.json", "export": "hardhat export --export-all exports/deployments.json", - "test": "hardhat test --verbose", + "test": "npx hardhat test --verbose", + "test:tracer": "npx hardhat test --logs", "test:coverage": "cross-env NODE_OPTIONS=\"--max-old-space-size=2048\" hardhat coverage", "test:gas": "cross-env REPORT_GAS=true yarn test", "prettier": "prettier --write test/**/*.{js,ts} && prettier --write contracts/**/*.sol", @@ -57,6 +58,7 @@ "prepublishOnly": "yarn run build && node scripts/prepublish.js" }, "devDependencies": { + "@boringcrypto/boring-solidity": "boringcrypto/BoringSolidity#b85f13c8f048136b3d3cb7e498ad6b76da05f98c", "@nomiclabs/hardhat-ethers": "^2.0.0", "@nomiclabs/hardhat-etherscan": "^2.1.0", "@nomiclabs/hardhat-solhint": "^2.0.0", @@ -78,13 +80,15 @@ "hardhat-gas-reporter": "^1.0.0", "hardhat-preprocessor": "^0.1.0", "hardhat-spdx-license-identifier": "^2.0.0", + "hardhat-tracer": "^1.0.0-alpha.1", "hardhat-typechain": "^0.3.4", "hardhat-watcher": "^2.0.0", - "prettier": "^2.2.0", + "prettier": "^2.2.1", "prettier-plugin-solidity": "^1.0.0-beta.5", - "ts-generator": "^0.1.1", - "typechain": "^4.0.1", + "solhint": "^3.3.2", + "solhint-plugin-prettier": "0.0.5", "solidity-coverage": "^0.7.13", - "@boringcrypto/boring-solidity": "boringcrypto/BoringSolidity#e06e943" + "ts-generator": "^0.1.1", + "typechain": "^4.0.1" } } diff --git a/solhint.json b/solhint.json deleted file mode 100644 index 136746fc56..0000000000 --- a/solhint.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "solhint:recommended", - "plugins": ["prettier"], - "rules": { - "avoid-suicide": "error", - "avoid-sha3": "warn", - "code-complexity": ["warn", 7], - "compiler-version": "off", - "max-states-count": ["error", 18], - "max-line-length": ["warn", 145], - "not-rely-on-time": "warn", - "quotes": ["warn", "double"], - "prettier/prettier": "off" - } -} diff --git a/yarn.lock b/yarn.lock index fdefa95a9d..6c4c00c630 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,17 +15,17 @@ integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== "@babel/highlight@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" - integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.8.tgz#10b2dac78526424dfc1f47650d0e415dfd9dc481" + integrity sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw== dependencies: "@babel/helper-validator-identifier" "^7.12.11" chalk "^2.0.0" js-tokens "^4.0.0" -"@boringcrypto/boring-solidity@boringcrypto/BoringSolidity#e06e943": - version "1.0.4" - resolved "https://codeload.github.com/boringcrypto/BoringSolidity/tar.gz/e06e943e7e8a168ed0395ef663a17fd71a6949a2" +"@boringcrypto/boring-solidity@boringcrypto/BoringSolidity#b85f13c8f048136b3d3cb7e498ad6b76da05f98c": + version "1.1.0" + resolved "https://codeload.github.com/boringcrypto/BoringSolidity/tar.gz/b85f13c8f048136b3d3cb7e498ad6b76da05f98c" "@ensdomains/ens@^0.4.4": version "0.4.5" @@ -43,32 +43,35 @@ resolved "https://registry.yarnpkg.com/@ensdomains/resolver/-/resolver-0.2.4.tgz#c10fe28bf5efbf49bff4666d909aed0265efbc89" integrity sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA== -"@ethereum-waffle/chai@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@ethereum-waffle/chai/-/chai-3.2.2.tgz#33a349688386c9a8fdc4da5baea329036b9fe75e" - integrity sha512-S2jKmCsCrrS35fw1C6rUwH9CRboytge37PDYBDqlGpIvQQws9v+IvBjv8tLRT2BWCZSS9dvwbvBYTJL31y5ytw== +"@ethereum-waffle/chai@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@ethereum-waffle/chai/-/chai-3.3.0.tgz#8aba94be27535cf12419e545e5f7027226ef732f" + integrity sha512-KqPH9DdTmfgM6dGa6M7/rUillYdRsUVkIiFLgVdLDvtaALITb6IseGNGRRerG/J6wUeIUQxOJY0ACZRYPCItaQ== dependencies: - "@ethereum-waffle/provider" "^3.2.2" + "@ethereum-waffle/provider" "^3.3.0" ethers "^5.0.0" -"@ethereum-waffle/compiler@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@ethereum-waffle/compiler/-/compiler-3.2.2.tgz#73d5bce44bcdc880d8630b9064591470c0123f57" - integrity sha512-6Y0TLIq26psgeoUSXCZIffeQHVqs6TOaJjHlQieJBx19defQIq5cYt8dRo1AZZGf+Eyjc2PZJERME/CfXiJgiQ== +"@ethereum-waffle/compiler@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@ethereum-waffle/compiler/-/compiler-3.3.0.tgz#26c2e9228f7118961625f3ace179b6c7004e9a6f" + integrity sha512-q5Nd0vlLeEYKszdJUNvIIuP2vj/tFkWt1LCvsIcFHIzxyIoLeaCFNzJI0UQ/s298svfPY59SyL7dKNcQWwbaWQ== dependencies: "@resolver-engine/imports" "^0.3.3" "@resolver-engine/imports-fs" "^0.3.3" + "@typechain/ethers-v5" "^2.0.0" "@types/mkdirp" "^0.5.2" "@types/node-fetch" "^2.5.5" ethers "^5.0.1" mkdirp "^0.5.1" node-fetch "^2.6.0" solc "^0.6.3" + ts-generator "^0.1.1" + typechain "^3.0.0" "@ethereum-waffle/ens@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@ethereum-waffle/ens/-/ens-3.2.2.tgz#2a1ea3270b8d64498324a80cd659db843b1ba4b3" - integrity sha512-bvoi/52dWEpLpvOBOm4fCkGEv7T88M7QI4StFAh7tRlCbp2oIZ0VcItQrIrz7Hek5BPMS/AJF2QtYoec4CtxBg== + version "3.2.3" + resolved "https://registry.yarnpkg.com/@ethereum-waffle/ens/-/ens-3.2.3.tgz#970f51e16a140e4e99c7b7831713d645be63aacb" + integrity sha512-OIfguJu4e+NYJHNnNVaFzvNG5WYPntWU1vnQuAFszBFytOeIkv2hAXv8RmRL+cledcvShtP3gmXU3Lvf0o4Sxw== dependencies: "@ensdomains/ens" "^0.4.4" "@ensdomains/resolver" "^0.2.4" @@ -82,10 +85,10 @@ "@ethersproject/abi" "^5.0.1" ethers "^5.0.1" -"@ethereum-waffle/provider@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@ethereum-waffle/provider/-/provider-3.2.2.tgz#6ab422015641f340ba71739d6ab85896277281e5" - integrity sha512-2UCNHsgr1fiI6JA7kmpSqt9AdOajGRK4Wyh24DeoAkCcZuaOdUY80fEmkSzhq8w3jIIvWRUQajBJPieEKw5NIw== +"@ethereum-waffle/provider@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@ethereum-waffle/provider/-/provider-3.3.0.tgz#3cacdb597ab04127c4c0b8a5b13e95ea33e932ab" + integrity sha512-JcHGwDz8ciqwDXcZXLzOif8AY2n4fUG5ju0ZQCGRkYiRHHTrbqzwWAtFsEHetWAxCi3VGlSgeN833DGulnQaZg== dependencies: "@ethereum-waffle/ens" "^3.2.2" ethers "^5.0.1" @@ -639,6 +642,11 @@ resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.11.1.tgz#fa840af64840c930f24a9c82c08d4a092a068add" integrity sha512-H8BSBoKE8EubJa0ONqecA2TviT3TnHeC4NpgnAHSUiuhZoQBfPB4L2P9bs8R6AoTW10Endvh3vc+fomVMIDIYQ== +"@solidity-parser/parser@^0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.8.2.tgz#a6a5e93ac8dca6884a99a532f133beba59b87b69" + integrity sha512-8LySx3qrNXPgB5JiULfG10O3V7QTxI/TLzSw5hFQhXWSkVxZBAv4rZQ0sYgLEbc8g3L2lmnujj1hKul38Eu5NQ== + "@sushiswap/sdk@^4.1.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@sushiswap/sdk/-/sdk-4.1.1.tgz#af975ff1900c7adb851776a0739976cb25c4701e" @@ -660,9 +668,9 @@ defer-to-connect "^1.0.1" "@tenderly/hardhat-tenderly@^1.0.0": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@tenderly/hardhat-tenderly/-/hardhat-tenderly-1.0.7.tgz#8745235623ee68fc4078b125b2e038caa1306fb1" - integrity sha512-QKHb/ZC5JKW031pFK8Rb81fzwF5TsRX8/bSi7ctnDzlMAzRvNqjH9gLTGfKhZwhi1CQbUSP+Ww5fPlqcMxGjIg== + version "1.0.10" + resolved "https://registry.yarnpkg.com/@tenderly/hardhat-tenderly/-/hardhat-tenderly-1.0.10.tgz#58c2d4d2b93efad6a9fbe54a3d21555bebdf58f2" + integrity sha512-r5n/fK3hmFzp0eLqoGzg90wJXJjVpAtXxdT5ygdFHkYML8hxNOe3lTFLfZsVwbvzlggbwEaxMUrFKGqJB1lkTA== dependencies: axios "^0.21.1" fs-extra "^9.0.1" @@ -692,6 +700,13 @@ "@truffle/interface-adapter" "^0.4.19" web3 "1.2.9" +"@typechain/ethers-v5@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-2.0.0.tgz#cd3ca1590240d587ca301f4c029b67bfccd08810" + integrity sha512-0xdCkyGOzdqh4h5JSf+zoWx85IusEjDcPIwNEHP8mrWSnCae4rvrqB+/gtpdNfX7zjlFlZiMeePn2r63EI3Lrw== + dependencies: + ethers "^5.0.2" + "@typechain/ethers-v5@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-5.0.0.tgz#6c91766b76c19886cf2c4833ded09611e117f92c" @@ -764,19 +779,19 @@ form-data "^3.0.0" "@types/node@*": - version "14.14.28" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.28.tgz#cade4b64f8438f588951a6b35843ce536853f25b" - integrity sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g== + version "14.14.31" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" + integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== "@types/node@^10.0.3", "@types/node@^10.12.18": - version "10.17.52" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.52.tgz#dc960d4e256331b3c697b7a573ee98b882febee5" - integrity sha512-bKnO8Rcj03i6JTzweabq96k29uVNcXGB0bkwjVQTFagDgxxNged18281AZ0nTMHl+aFpPPWyPrk4Z3+NtW/z5w== + version "10.17.54" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.54.tgz#a737488631aca3ec7bd9f6229d77f1079e444793" + integrity sha512-c8Lm7+hXdSPmWH4B9z/P/xIXhFK3mCQin4yCYMd2p1qpMG5AfgyJuYZ+3q2dT7qLiMMMGMd5dnkFpdqJARlvtQ== "@types/node@^12.12.6", "@types/node@^12.6.1": - version "12.20.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.1.tgz#63d36c10e162666f0107f247cdca76542c3c7472" - integrity sha512-tCkE96/ZTO+cWbln2xfyvd6ngHLanvVlJ3e5BeirJ3BYI5GbAyubIrmV4JjjugDly5D9fHjOL5MNsqsCnqwW6g== + version "12.20.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.4.tgz#73687043dd00fcb6962c60fbf499553a24d6bdf2" + integrity sha512-xRCgeE0Q4pT5UZ189TJ3SpYuX/QGl6QIAOAIeDSbAVAd2gX1NxSZup4jNVK7cxIeP8KDSbJgcckun495isP1jQ== "@types/node@^8.0.0": version "8.10.66" @@ -1128,6 +1143,11 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +ast-parents@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/ast-parents/-/ast-parents-0.0.1.tgz#508fd0f05d0c48775d9eccda2e174423261e8dd3" + integrity sha1-UI/Q8F0MSHddnszaLhdEIyYejdM= + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -1825,14 +1845,14 @@ bn.js@4.11.8: integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.4.0, bn.js@^4.8.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" - integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== + version "5.2.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" + integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== body-parser@1.19.0, body-parser@^1.16.0: version "1.19.0" @@ -2105,9 +2125,9 @@ camelcase@^5.0.0: integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== caniuse-lite@^1.0.30000844: - version "1.0.30001187" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001187.tgz#5706942631f83baa5a0218b7dfa6ced29f845438" - integrity sha512-w7/EP1JRZ9552CyrThUnay2RkZ1DXxKe/Q2swTC4+LElLh9RRYrL1Z+27LlakB8kzY0fSmHw9mc7XYDUKAKWMA== + version "1.0.30001192" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001192.tgz#b848ebc0ab230cf313d194a4775a30155d50ae40" + integrity sha512-63OrUnwJj5T1rUmoyqYTdRWBqFFxZFlyZnRRjDR8NSUQFB6A+j/uBORU/SyJ5WzDLg4SPiZH40hQCBNdZ/jmAw== caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" @@ -2457,9 +2477,9 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js-pure@^3.0.1: - version "3.8.3" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.8.3.tgz#10e9e3b2592ecaede4283e8f3ad7020811587c02" - integrity sha512-V5qQZVAr9K0xu7jXg1M7qTEwuxUgqr7dUOezGaNa7i+Xn9oXAU/d1fzqD9ObuwpVQOaorO5s70ckyi1woP9lVA== + version "3.9.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.9.1.tgz#677b322267172bd490e4464696f790cbc355bec5" + integrity sha512-laz3Zx0avrw9a4QEIdmIblnVuJz8W51leY9iLThatCsFawWxC3sE4guASC78JbCin+DkwMpCdp1AVAuzL/GN7A== core-js@^2.4.0, core-js@^2.5.0: version "2.6.12" @@ -2841,9 +2861,9 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.3.47: - version "1.3.667" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.667.tgz#18ca4f243ec163c3e354e506ba22ef46d31d925e" - integrity sha512-Ot1pPtAVb5nd7jeVF651zmfLFilRVFomlDzwXmdlWe5jyzOGa6mVsQ06XnAurT7wWfg5VEIY+LopbAdD/bpo5w== + version "1.3.675" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.675.tgz#7ad29f98d7b48da581554eb28bb9a71fd5fd4956" + integrity sha512-GEQw+6dNWjueXGkGfjgm7dAMtXfEqrfDG3uWcZdeaD4cZ3dKYdPRQVruVXQRXtPLtOr5GNVVlNLRMChOZ611pQ== elliptic@6.5.3: version "6.5.3" @@ -2947,7 +2967,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.18.0-next.1: +es-abstract@^1.18.0-next.2: version "1.18.0-next.2" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2" integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw== @@ -3353,14 +3373,14 @@ ethereum-cryptography@^0.1.2, ethereum-cryptography@^0.1.3: setimmediate "^1.0.5" ethereum-waffle@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/ethereum-waffle/-/ethereum-waffle-3.2.2.tgz#dbcdb96ebfa35d4deb6b749906ff7e12f593284f" - integrity sha512-Q8XrcFmQGDKKH0Lr867WA9Rl0oWQGMZcFrFPMV2KBIOkdeQnRlGEJq8RGFxj4MMWWxkoXIoxWgxg7U3qdgddEw== + version "3.3.0" + resolved "https://registry.yarnpkg.com/ethereum-waffle/-/ethereum-waffle-3.3.0.tgz#166a0cc1d3b2925f117b20ef0951b3fe72e38e79" + integrity sha512-4xm3RWAPCu5LlaVxYEg0tG3L7g5ovBw1GY/UebrzZ+OTx22vcPjI+bvelFlGBpkdnO5yOIFXjH2eK59tNAe9IA== dependencies: - "@ethereum-waffle/chai" "^3.2.2" - "@ethereum-waffle/compiler" "^3.2.2" + "@ethereum-waffle/chai" "^3.3.0" + "@ethereum-waffle/compiler" "^3.3.0" "@ethereum-waffle/mock-contract" "^3.2.2" - "@ethereum-waffle/provider" "^3.2.2" + "@ethereum-waffle/provider" "^3.3.0" ethers "^5.0.1" ethereumjs-abi@0.6.5: @@ -3598,7 +3618,7 @@ ethers@^4.0.32, ethers@^4.0.40: uuid "2.0.1" xmlhttprequest "1.8.0" -ethers@^5.0.0, ethers@^5.0.1: +ethers@^5.0.0, ethers@^5.0.1, ethers@^5.0.2, ethers@^5.0.24: version "5.0.31" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.0.31.tgz#60e3b1425864fe5d2babc147ede01be8382a7d2a" integrity sha512-zpq0YbNFLFn+t+ibS8UkVWFeK5w6rVMSvbSHrHAQslfazovLnQ/mc2gdN5+6P45/k8fPgHrfHrYvJ4XvyK/S1A== @@ -3671,9 +3691,9 @@ eventemitter3@^4.0.0: integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" @@ -3845,9 +3865,9 @@ fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.1.tgz#8b8f2ac8bf3632d67afcd65dac248d5fdc45385e" - integrity sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA== + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== dependencies: reusify "^1.0.4" @@ -3979,9 +3999,9 @@ fmix@^0.1.0: imul "^1.0.0" follow-redirects@^1.10.0, follow-redirects@^1.12.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147" - integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA== + version "1.13.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" + integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== for-each@^0.3.3, for-each@~0.3.3: version "0.3.3" @@ -4426,9 +4446,9 @@ hardhat-deploy-ethers@^0.3.0-beta.7: integrity sha512-JKMNte6vudu9LSNqgmBtNxc1gfxp3NUcPKVAf/FANHfl9pa/mBGg6hpQO7tD8CLkAbe6f4K5BjyYIPWX3p7MKw== hardhat-deploy@^0.7.0-beta.44: - version "0.7.0-beta.44" - resolved "https://registry.yarnpkg.com/hardhat-deploy/-/hardhat-deploy-0.7.0-beta.44.tgz#e03eb60765d4ecc3711dc7aadaba60c64b1d381c" - integrity sha512-eIM8RrLDgD12wINTdduYshxqNP/uz9qMCgao37jl6iiZ2LwHkNTL/X+RtTVfq/vS7bvLW7qpjymHJiak5rd1Pg== + version "0.7.0-beta.46" + resolved "https://registry.yarnpkg.com/hardhat-deploy/-/hardhat-deploy-0.7.0-beta.46.tgz#6caa34a534e7d40e0f2d98a26b90e154d5004e1b" + integrity sha512-VADqekd40MYHWVQxz3jn+C7Vtfv7qPboZZYH7N8wLtPU/ZcGvEQRgB4LBkOeuwQd/80MIXwSBOrnN4x6LjIauA== dependencies: "@ethersproject/abi" "^5.0.2" "@ethersproject/abstract-signer" "^5.0.2" @@ -4471,6 +4491,13 @@ hardhat-spdx-license-identifier@^2.0.0: resolved "https://registry.yarnpkg.com/hardhat-spdx-license-identifier/-/hardhat-spdx-license-identifier-2.0.3.tgz#6dd89a7e036ede4f7e86fb349eb23daaaad6890f" integrity sha512-G4u4I1md0tWaitX6Os7Nq9sYZ/CFdR+ibm7clCksGJ4yrtdHEZxgLjWpJ0WiALF9SoFKt03PwCe9lczDQ/5ADA== +hardhat-tracer@^1.0.0-alpha.1: + version "1.0.0-alpha.1" + resolved "https://registry.yarnpkg.com/hardhat-tracer/-/hardhat-tracer-1.0.0-alpha.1.tgz#9522edf8cff3ad9f85e81f27746d301ade57fd5c" + integrity sha512-X8Xaa+bXeJ7nZyPwrJFVNgmMeNlqFXkz6+hN3Gc6Y/qgzM1bHbNF0RlQCoUOiqKbrvk8wYlvOioN6TZuHUvRfg== + dependencies: + ethers "^5.0.24" + hardhat-typechain@^0.3.4: version "0.3.5" resolved "https://registry.yarnpkg.com/hardhat-typechain/-/hardhat-typechain-0.3.5.tgz#8e50616a9da348b33bd001168c8fda9c66b7b4af" @@ -4484,9 +4511,9 @@ hardhat-watcher@^2.0.0: chokidar "^3.4.3" hardhat@^2.0.7: - version "2.0.10" - resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.0.10.tgz#9b50da13b6915bb9b61b7f38f8f2b9b352447462" - integrity sha512-ZAcC+9Nb1AEb22/2hWj/zLPyIRLD9y1O3LW2KhbONpxn1bf0qWLW8QegB9J3KP9Bvt8LbW9pWuSyRQJU0vUWqA== + version "2.0.11" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.0.11.tgz#f7bc9d38045ecda58665dcf1ffae9ff940712f06" + integrity sha512-K3cyXV/F0reT0Lu7fHHBAgVOVenUaYa6uCNvzYbFnjKH+s8O4CEdrsFQ/yIbmLgMpC2EQeuBSwb+8ZhWVj8AGQ== dependencies: "@nomiclabs/ethereumjs-vm" "4.2.2" "@sentry/node" "^5.18.1" @@ -4561,9 +4588,9 @@ has-symbol-support-x@^1.4.1: integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== has-symbols@^1.0.0, has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== has-to-string-tag-x@^1.2.0: version "1.4.1" @@ -4990,6 +5017,11 @@ is-directory@^0.3.1: resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= +is-docker@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" + integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -5137,6 +5169,13 @@ is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is-wsl@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -5604,11 +5643,16 @@ lodash.toarray@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= -lodash@4.17.20, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4: +lodash@4.17.20: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + log-symbols@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" @@ -5837,17 +5881,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.45.0: - version "1.45.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== +mime-db@1.46.0: + version "1.46.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee" + integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ== mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.28" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" - integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== + version "2.1.29" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2" + integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ== dependencies: - mime-db "1.45.0" + mime-db "1.46.0" mime@1.6.0: version "1.6.0" @@ -6226,11 +6270,11 @@ object-inspect@~1.7.0: integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== object-is@^1.0.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" - integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: @@ -6271,13 +6315,13 @@ object.assign@^4.1.2: object-keys "^1.1.1" object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz#0dfda8d108074d9c563e80490c883b6661091544" - integrity sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng== + version "2.1.2" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" + integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" + es-abstract "^1.18.0-next.2" object.pick@^1.3.0: version "1.3.0" @@ -6314,6 +6358,14 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + optionator@^0.8.1, optionator@^0.8.2: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -6475,7 +6527,7 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= -patch-package@6.2.2, patch-package@^6.2.2: +patch-package@6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.2.2.tgz#71d170d650c65c26556f0d0fbbb48d92b6cc5f39" integrity sha512-YqScVYkVcClUY0v8fF0kWOjDYopzIM8e3bj/RU1DPeEF14+dCGm6UeOYm4jvCyxqIEQ5/eJzmbWfDWnUleFNMg== @@ -6493,6 +6545,25 @@ patch-package@6.2.2, patch-package@^6.2.2: slash "^2.0.0" tmp "^0.0.33" +patch-package@^6.2.2: + version "6.4.1" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.4.1.tgz#1724ddee6f62dafe6b35ccb8506b3b07f69fba53" + integrity sha512-6d8iHvKdGADAuAvZL+sjuvl39h0EqLWR2ZgA8TetEXiqtcsETfxP+YiOhCkWXZS/0HAJse05xLGN0S7GzIRPNQ== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^2.4.2" + cross-spawn "^6.0.5" + find-yarn-workspace-root "^1.2.1" + fs-extra "^7.0.1" + is-ci "^2.0.0" + klaw-sync "^6.0.0" + minimist "^1.2.0" + open "^7.4.2" + rimraf "^2.6.3" + semver "^5.6.0" + slash "^2.0.0" + tmp "^0.0.33" + path-browserify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" @@ -6632,6 +6703,13 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + prettier-plugin-solidity@^1.0.0-beta.5: version "1.0.0-beta.5" resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.5.tgz#04347bc3fb8deb5d097c9c823cbc01451a40da7a" @@ -6651,7 +6729,7 @@ prettier@^1.14.3: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== -prettier@^2.1.2, prettier@^2.2.0, prettier@^2.2.1: +prettier@^2.1.2, prettier@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== @@ -7231,9 +7309,9 @@ rustbn.js@~0.2.0: integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA== rxjs@^6.4.0: - version "6.6.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== + version "6.6.6" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70" + integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg== dependencies: tslib "^1.9.0" @@ -7595,6 +7673,13 @@ solc@^0.6.3: semver "^5.5.0" tmp "0.0.33" +solhint-plugin-prettier@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/solhint-plugin-prettier/-/solhint-plugin-prettier-0.0.5.tgz#e3b22800ba435cd640a9eca805a7f8bc3e3e6a6b" + integrity sha512-7jmWcnVshIrO2FFinIvDQmhQpfpS2rRRn3RejiYgnjIE68xO2bvrYvjqVNfrio4xH9ghOqn83tKuTzLjEbmGIA== + dependencies: + prettier-linter-helpers "^1.0.0" + solhint@^2.0.0: version "2.3.1" resolved "https://registry.yarnpkg.com/solhint/-/solhint-2.3.1.tgz#6fee8fc2635112bf5812f7cba8359c14e9d9a491" @@ -7615,6 +7700,28 @@ solhint@^2.0.0: optionalDependencies: prettier "^1.14.3" +solhint@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.3.2.tgz#ebd7270bb50fd378b427d7a6fc9f2a7fd00216c0" + integrity sha512-8tHCkIAk1axLLG6Qu2WIH3GgNABonj9eAWejJbov3o3ujkZQRNHeHU1cC4/Dmjsh3Om7UzFFeADUHu2i7ZJeiw== + dependencies: + "@solidity-parser/parser" "^0.8.2" + ajv "^6.6.1" + antlr4 "4.7.1" + ast-parents "0.0.1" + chalk "^2.4.2" + commander "2.18.0" + cosmiconfig "^5.0.7" + eslint "^5.6.0" + fast-diff "^1.1.2" + glob "^7.1.3" + ignore "^4.0.6" + js-yaml "^3.12.0" + lodash "^4.17.11" + semver "^6.3.0" + optionalDependencies: + prettier "^1.14.3" + solidity-comments-extractor@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.4.tgz#ce420aef23641ffd0131c7d80ba85b6e1e42147e" @@ -7819,37 +7926,37 @@ string-width@^3.0.0, string-width@^3.1.0: strip-ansi "^5.1.0" string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" string.prototype.trim@~1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.3.tgz#d23a22fde01c1e6571a7fadcb9be11decd8061a7" - integrity sha512-16IL9pIBA5asNOSukPfxX2W68BaBvxyiRK16H3RA/lWW9BDosh+w7f+LhomPHpXJ82QEe7w7/rY/S1CV97raLg== + version "1.2.4" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.4.tgz#6014689baf5efaf106ad031a5fa45157666ed1bd" + integrity sha512-hWCk/iqf7lp0/AgTF7/ddO1IWtSNPASjlzCicV5irAVdE1grjsneK26YG6xACMBEdCvO8fUST0UzDMh/2Qy+9Q== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" + es-abstract "^1.18.0-next.2" string.prototype.trimend@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b" - integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw== + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" string.prototype.trimstart@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" - integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg== + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" string_decoder@^1.1.1: @@ -8185,6 +8292,11 @@ ts-essentials@^1.0.0: resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-1.0.4.tgz#ce3b5dade5f5d97cf69889c11bf7d2da8555b15a" integrity sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ== +ts-essentials@^6.0.3: + version "6.0.7" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-6.0.7.tgz#5f4880911b7581a873783740ce8b94da163d18a6" + integrity sha512-2E4HIIj4tQJlIHuATRHayv0EfMGK3ris/GRk1E3CFnsZzeNV+hUmelbaTZHLtXaZppM5oLhHRtO04gINC4Jusw== + ts-essentials@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.1.tgz#d205508cae0cdadfb73c89503140cf2228389e2d" @@ -8277,10 +8389,23 @@ type@^2.0.0: resolved "https://registry.yarnpkg.com/type/-/type-2.3.0.tgz#ada7c045f07ead08abf9e2edd29be1a0c0661132" integrity sha512-rgPIqOdfK/4J9FhiVrZ3cveAjRRo5rsQBAIhnylX874y1DX/kEKSVdLsnuHB6l1KTjHyU01VjiMBHgU2adejyg== +typechain@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/typechain/-/typechain-3.0.0.tgz#d5a47700831f238e43f7429b987b4bb54849b92e" + integrity sha512-ft4KVmiN3zH4JUFu2WJBrwfHeDf772Tt2d8bssDTo/YcckKW2D+OwFrHXRC6hJvO3mHjFQTihoMV6fJOi0Hngg== + dependencies: + command-line-args "^4.0.7" + debug "^4.1.1" + fs-extra "^7.0.0" + js-sha3 "^0.8.0" + lodash "^4.17.15" + ts-essentials "^6.0.3" + ts-generator "^0.1.1" + typechain@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/typechain/-/typechain-4.0.1.tgz#b40eaf5ede15588d97a4b9a5f85120f7ea1cf262" - integrity sha512-H/1VpRmplp1qhCTVLU9PCgzyVCQ7Lth7YvaaI1hTvT31IpWnLLNpDpQD4vXJGr26T9BsZ0ZIceOwieAbcoywXw== + version "4.0.2" + resolved "https://registry.yarnpkg.com/typechain/-/typechain-4.0.2.tgz#31a961bf1fc43b8cde39193247439715e43ce5d3" + integrity sha512-SopnfdQrS5ek6sTbvymsnBACA+70FstX/ZLWY8lQWNdLUXGyoGFoa73Y+1hKlbz2DfCnO39bQ551qUMhk5GYSw== dependencies: command-line-args "^4.0.7" debug "^4.1.1"