diff --git a/packages/contracts/contracts/LPRewards/Dependencies/BoringERC20.sol b/packages/contracts/contracts/LPRewards/Dependencies/BoringERC20.sol new file mode 100644 index 000000000..972882d30 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/Dependencies/BoringERC20.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; + +import "../../Dependencies/IERC20.sol"; + +// solhint-disable avoid-low-level-calls + +library BoringERC20 { + bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol() + bytes4 private constant SIG_NAME = 0x06fdde03; // name() + bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals() + bytes4 private constant SIG_BALANCE_OF = 0x70a08231; // balanceOf(address) + bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256) + bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256) + + function returnDataToString(bytes memory data) internal pure returns (string memory) { + if (data.length >= 64) { + return abi.decode(data, (string)); + } else if (data.length == 32) { + uint8 i = 0; + while(i < 32 && data[i] != 0) { + i++; + } + bytes memory bytesArray = new bytes(i); + for (i = 0; i < 32 && data[i] != 0; i++) { + bytesArray[i] = data[i]; + } + return string(bytesArray); + } else { + return "???"; + } + } + + /// @notice Provides a safe ERC20.symbol version which returns '???' as fallback string. + /// @param token The address of the ERC-20 token contract. + /// @return (string) Token symbol. + function safeSymbol(IERC20 token) internal view returns (string memory) { + (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_SYMBOL)); + return success ? returnDataToString(data) : "???"; + } + + /// @notice Provides a safe ERC20.name version which returns '???' as fallback string. + /// @param token The address of the ERC-20 token contract. + /// @return (string) Token name. + function safeName(IERC20 token) internal view returns (string memory) { + (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_NAME)); + return success ? returnDataToString(data) : "???"; + } + + /// @notice Provides a safe ERC20.decimals version which returns '18' as fallback value. + /// @param token The address of the ERC-20 token contract. + /// @return (uint8) Token decimals. + function safeDecimals(IERC20 token) internal view returns (uint8) { + (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_DECIMALS)); + return success && data.length == 32 ? abi.decode(data, (uint8)) : 18; + } + + /// @notice Provides a gas-optimized balance check to avoid a redundant extcodesize check in addition to the returndatasize check. + /// @param token The address of the ERC-20 token. + /// @param to The address of the user to check. + /// @return amount The token amount. + function safeBalanceOf(IERC20 token, address to) internal view returns (uint256 amount) { + (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_BALANCE_OF, to)); + require(success && data.length >= 32, "BoringERC20: BalanceOf failed"); + amount = abi.decode(data, (uint256)); + } + + /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations. + /// Reverts on a failed transfer. + /// @param token The address of the ERC-20 token. + /// @param to Transfer tokens to. + /// @param amount The token amount. + function safeTransfer( + IERC20 token, + address to, + uint256 amount + ) internal { + (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount)); + require(success && (data.length == 0 || abi.decode(data, (bool))), "BoringERC20: Transfer failed"); + } + + /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations. + /// Reverts on a failed transfer. + /// @param token The address of the ERC-20 token. + /// @param from Transfer tokens from. + /// @param to Transfer tokens to. + /// @param amount The token amount. + function safeTransferFrom( + IERC20 token, + address from, + address to, + uint256 amount + ) internal { + (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount)); + require(success && (data.length == 0 || abi.decode(data, (bool))), "BoringERC20: TransferFrom failed"); + } +} diff --git a/packages/contracts/contracts/LPRewards/Dependencies/BoringMath.sol b/packages/contracts/contracts/LPRewards/Dependencies/BoringMath.sol new file mode 100644 index 000000000..2573f6078 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/Dependencies/BoringMath.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; + +/// @notice A library for performing overflow-/underflow-safe math, +/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math). +library BoringMath { + function add(uint256 a, uint256 b) internal pure returns (uint256 c) { + require((c = a + b) >= b, "BoringMath: Add Overflow"); + } + + function sub(uint256 a, uint256 b) internal pure returns (uint256 c) { + require((c = a - b) <= a, "BoringMath: Underflow"); + } + + function mul(uint256 a, uint256 b) internal pure returns (uint256 c) { + require(b == 0 || (c = a * b) / b == a, "BoringMath: Mul Overflow"); + } + + function to128(uint256 a) internal pure returns (uint128 c) { + require(a <= uint128(-1), "BoringMath: uint128 Overflow"); + c = uint128(a); + } + + function to64(uint256 a) internal pure returns (uint64 c) { + require(a <= uint64(-1), "BoringMath: uint64 Overflow"); + c = uint64(a); + } + + function to32(uint256 a) internal pure returns (uint32 c) { + require(a <= uint32(-1), "BoringMath: uint32 Overflow"); + c = uint32(a); + } +} + +/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128. +library BoringMath128 { + function add(uint128 a, uint128 b) internal pure returns (uint128 c) { + require((c = a + b) >= b, "BoringMath: Add Overflow"); + } + + function sub(uint128 a, uint128 b) internal pure returns (uint128 c) { + require((c = a - b) <= a, "BoringMath: Underflow"); + } +} + +/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64. +library BoringMath64 { + function add(uint64 a, uint64 b) internal pure returns (uint64 c) { + require((c = a + b) >= b, "BoringMath: Add Overflow"); + } + + function sub(uint64 a, uint64 b) internal pure returns (uint64 c) { + require((c = a - b) <= a, "BoringMath: Underflow"); + } +} + +/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32. +library BoringMath32 { + function add(uint32 a, uint32 b) internal pure returns (uint32 c) { + require((c = a + b) >= b, "BoringMath: Add Overflow"); + } + + function sub(uint32 a, uint32 b) internal pure returns (uint32 c) { + require((c = a - b) <= a, "BoringMath: Underflow"); + } +} diff --git a/packages/contracts/contracts/LPRewards/Dependencies/SignedSafeMath.sol b/packages/contracts/contracts/LPRewards/Dependencies/SignedSafeMath.sol new file mode 100644 index 000000000..cd098ff25 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/Dependencies/SignedSafeMath.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.11; + + +library SignedSafeMath { + int256 constant private _INT256_MIN = -2**255; + + /** + * @dev Returns the multiplication of two signed integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * + * - Multiplication cannot overflow. + */ + function mul(int256 a, int256 b) internal pure returns (int256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; + } + + require(!(a == -1 && b == _INT256_MIN), "SignedSafeMath: multiplication overflow"); + + int256 c = a * b; + require(c / a == b, "SignedSafeMath: multiplication overflow"); + + return c; + } + + /** + * @dev Returns the integer division of two signed integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * + * - The divisor cannot be zero. + */ + function div(int256 a, int256 b) internal pure returns (int256) { + require(b != 0, "SignedSafeMath: division by zero"); + require(!(b == -1 && a == _INT256_MIN), "SignedSafeMath: division overflow"); + + int256 c = a / b; + + return c; + } + + /** + * @dev Returns the subtraction of two signed integers, reverting on + * overflow. + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * + * - Subtraction cannot overflow. + */ + function sub(int256 a, int256 b) internal pure returns (int256) { + int256 c = a - b; + require((b >= 0 && c <= a) || (b < 0 && c > a), "SignedSafeMath: subtraction overflow"); + + return c; + } + + /** + * @dev Returns the addition of two signed integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * + * - Addition cannot overflow. + */ + function add(int256 a, int256 b) internal pure returns (int256) { + int256 c = a + b; + require((b >= 0 && c >= a) || (b < 0 && c < a), "SignedSafeMath: addition overflow"); + + return c; + } + + function toUInt256(int256 a) internal pure returns (uint256) { + require(a >= 0, "Integer < 0"); + return uint256(a); + } +} diff --git a/packages/contracts/contracts/LPRewards/Interfaces/IRewarder.sol b/packages/contracts/contracts/LPRewards/Interfaces/IRewarder.sol new file mode 100644 index 000000000..51fa99542 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/Interfaces/IRewarder.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; + +import "../Dependencies/BoringERC20.sol"; + + +interface IRewarder { + using BoringERC20 for IERC20; + function onSushiReward(uint256 pid, address user, address recipient, uint256 sushiAmount, uint256 newLpAmount) external; + function pendingTokens(uint256 pid, address user, uint256 sushiAmount) external view returns (IERC20[] memory, uint256[] memory); +} diff --git a/packages/contracts/contracts/LPRewards/SushiSwapOhmLqtyRewarder.sol b/packages/contracts/contracts/LPRewards/SushiSwapOhmLqtyRewarder.sol new file mode 100644 index 000000000..845a0d4d7 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/SushiSwapOhmLqtyRewarder.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; + +import "./Interfaces/IRewarder.sol"; +import "./Dependencies/BoringERC20.sol"; +import "./Dependencies/BoringMath.sol"; + + +// Based on: +// https://github.com/sushiswap/sushiswap/blob/master/contracts/mocks/RewarderMock.sol +contract SushiSwapOhmLqtyRewarder is IRewarder { + using BoringMath for uint256; + using BoringERC20 for IERC20; + + uint256 private immutable ohmRewardMultiplier; + IERC20 private immutable ohmToken; + uint256 private immutable lqtyRewardMultiplier; + IERC20 private immutable lqtyToken; + uint256 private constant REWARD_TOKEN_DIVISOR = 1e18; + address private immutable MASTERCHEF_V2; + + // Make sure to pass correct multipliers at deployment !! + // See mainnetDeployment/test/SushiSwapOhmLqtyRewarderTest_mainnet.js for a real example + constructor ( + uint256 _ohmRewardMultiplier, + IERC20 _ohmToken, + uint256 _lqtyRewardMultiplier, + IERC20 _lqtyToken, + address _MASTERCHEF_V2 + ) public { + ohmRewardMultiplier = _ohmRewardMultiplier; + ohmToken = _ohmToken; + lqtyRewardMultiplier = _lqtyRewardMultiplier; + lqtyToken = _lqtyToken; + MASTERCHEF_V2 = _MASTERCHEF_V2; + } + + function onSushiReward (uint256, address, address to, uint256 sushiAmount, uint256) onlyMCV2 override external { + // OHM rewards + uint256 ohmPendingReward = sushiAmount.mul(ohmRewardMultiplier) / REWARD_TOKEN_DIVISOR; + uint256 ohmBal = ohmToken.balanceOf(address(this)); + uint256 ohmReward = ohmPendingReward > ohmBal ? ohmBal : ohmPendingReward; + if (ohmReward > 0) { + ohmToken.safeTransfer(to, ohmReward); + } + + // LQTY rewards + uint256 lqtyPendingReward = sushiAmount.mul(lqtyRewardMultiplier) / REWARD_TOKEN_DIVISOR; + uint256 lqtyBal = lqtyToken.balanceOf(address(this)); + uint256 lqtyReward = lqtyPendingReward > lqtyBal ? lqtyBal : lqtyPendingReward; + if (lqtyReward > 0) { + lqtyToken.safeTransfer(to, lqtyReward); + } + } + + function pendingTokens(uint256, address, uint256 sushiAmount) override external view returns (IERC20[] memory rewardTokens, uint256[] memory rewardAmounts) { + IERC20[] memory _rewardTokens = new IERC20[](2); + _rewardTokens[0] = ohmToken; + _rewardTokens[1] = lqtyToken; + uint256[] memory _rewardAmounts = new uint256[](2); + _rewardAmounts[0] = sushiAmount.mul(ohmRewardMultiplier) / REWARD_TOKEN_DIVISOR; + _rewardAmounts[1] = sushiAmount.mul(lqtyRewardMultiplier) / REWARD_TOKEN_DIVISOR; + return (_rewardTokens, _rewardAmounts); + } + + modifier onlyMCV2 { + require( + msg.sender == MASTERCHEF_V2, + "Only MCV2 can call this function." + ); + _; + } +} diff --git a/packages/contracts/contracts/LPRewards/TestContracts/BoringBatchable.sol b/packages/contracts/contracts/LPRewards/TestContracts/BoringBatchable.sol new file mode 100644 index 000000000..d379c20a8 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/TestContracts/BoringBatchable.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; +pragma experimental ABIEncoderV2; + +// solhint-disable avoid-low-level-calls +// solhint-disable no-inline-assembly + +// Audit on 5-Jan-2021 by Keno and BoringCrypto +// WARNING!!! +// Combining BoringBatchable with msg.value can cause double spending issues +// https://www.paradigm.xyz/2021/08/two-rights-might-make-a-wrong/ + +import "./IERC20Permit.sol"; + + +contract BaseBoringBatchable { + /// @dev Helper function to extract a useful revert message from a failed call. + /// If the returned data is malformed or not correctly abi encoded then this call can fail itself. + function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) { + // If the _res length is less than 68, then the transaction failed silently (without a revert message) + if (_returnData.length < 68) return "Transaction reverted silently"; + + assembly { + // Slice the sighash. + _returnData := add(_returnData, 0x04) + } + return abi.decode(_returnData, (string)); // All that remains is the revert string + } + + /// @notice Allows batched call to self (this contract). + /// @param calls An array of inputs for each call. + /// @param revertOnFail If True then reverts after a failed call and stops doing further calls. + // F1: External is ok here because this is the batch function, adding it to a batch makes no sense + // F2: Calls in the batch may be payable, delegatecall operates in the same context, so each call in the batch has access to msg.value + // C3: The length of the loop is fully under user control, so can't be exploited + // C7: Delegatecall is only used on the same contract, so it's safe + function batch(bytes[] calldata calls, bool revertOnFail) external payable { + for (uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory result) = address(this).delegatecall(calls[i]); + if (!success && revertOnFail) { + revert(_getRevertMsg(result)); + } + } + } +} + +contract BoringBatchable is BaseBoringBatchable { + /// @notice Call wrapper that performs `ERC20.permit` on `token`. + /// Lookup `IERC20.permit`. + // F6: Parameters can be used front-run the permit and the user's permit will fail (due to nonce or other revert) + // if part of a batch this could be used to grief once as the second call would not need the permit + function permitToken( + IERC20Permit token, + address from, + address to, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public { + token.permit(from, to, amount, deadline, v, r, s); + } +} diff --git a/packages/contracts/contracts/LPRewards/TestContracts/BoringOwnable.sol b/packages/contracts/contracts/LPRewards/TestContracts/BoringOwnable.sol new file mode 100644 index 000000000..d5ce61594 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/TestContracts/BoringOwnable.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; + +// Audit on 5-Jan-2021 by Keno and BoringCrypto +// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol +// Edited by BoringCrypto + +contract BoringOwnableData { + address public owner; + address public pendingOwner; +} + +contract BoringOwnable is BoringOwnableData { + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @notice `owner` defaults to msg.sender on construction. + constructor() public { + owner = msg.sender; + emit OwnershipTransferred(address(0), msg.sender); + } + + /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. + /// Can only be invoked by the current `owner`. + /// @param newOwner Address of the new owner. + /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`. + /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise. + 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; + pendingOwner = address(0); + } else { + // Effects + pendingOwner = newOwner; + } + } + + /// @notice Needs to be called by `pendingOwner` to claim ownership. + 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); + } + + /// @notice Only allows the `owner` to execute the function. + modifier onlyOwner() { + require(msg.sender == owner, "Ownable: caller is not the owner"); + _; + } +} diff --git a/packages/contracts/contracts/LPRewards/TestContracts/ERC20Mock.sol b/packages/contracts/contracts/LPRewards/TestContracts/ERC20Mock.sol index c26051c8c..73854071e 100644 --- a/packages/contracts/contracts/LPRewards/TestContracts/ERC20Mock.sol +++ b/packages/contracts/contracts/LPRewards/TestContracts/ERC20Mock.sol @@ -32,4 +32,8 @@ contract ERC20Mock is ERC20 { function approveInternal(address owner, address spender, uint256 value) public { _approve(owner, spender, value); } + + function setupDecimals(uint8 decimals) external virtual { + _setupDecimals(decimals); + } } diff --git a/packages/contracts/contracts/LPRewards/TestContracts/IERC20Permit.sol b/packages/contracts/contracts/LPRewards/TestContracts/IERC20Permit.sol new file mode 100644 index 000000000..678e9c521 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/TestContracts/IERC20Permit.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; + +interface IERC20Permit { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + // EIP 2612 + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; +} diff --git a/packages/contracts/contracts/LPRewards/TestContracts/IMasterChef.sol b/packages/contracts/contracts/LPRewards/TestContracts/IMasterChef.sol new file mode 100644 index 000000000..600638f70 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/TestContracts/IMasterChef.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; +pragma experimental ABIEncoderV2; +import "../Dependencies/BoringERC20.sol"; + + +interface IMasterChef { + using BoringERC20 for IERC20; + struct UserInfo { + uint256 amount; // How many LP tokens the user has provided. + uint256 rewardDebt; // Reward debt. See explanation below. + } + + struct PoolInfo { + IERC20 lpToken; // Address of LP token contract. + uint256 allocPoint; // How many allocation points assigned to this pool. SUSHI to distribute per block. + uint256 lastRewardBlock; // Last block number that SUSHI distribution occurs. + uint256 accSushiPerShare; // Accumulated SUSHI per share, times 1e12. See below. + } + + function poolInfo(uint256 pid) external view returns (IMasterChef.PoolInfo memory); + function totalAllocPoint() external view returns (uint256); + function deposit(uint256 _pid, uint256 _amount) external; +} diff --git a/packages/contracts/contracts/LPRewards/TestContracts/MasterChefV2.sol b/packages/contracts/contracts/LPRewards/TestContracts/MasterChefV2.sol new file mode 100644 index 000000000..f8a5726c1 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/TestContracts/MasterChefV2.sol @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.11; +pragma experimental ABIEncoderV2; + +import "../Dependencies/BoringMath.sol"; +import "../Dependencies/BoringERC20.sol"; +import "../Dependencies/SignedSafeMath.sol"; +import "../Interfaces/IRewarder.sol"; + +import "./BoringBatchable.sol"; +import "./BoringOwnable.sol"; +import "./IMasterChef.sol"; + +interface IMigratorChef { + // Take the current LP token address and return the new LP token address. + // Migrator should have full access to the caller's LP token. + function migrate(IERC20 token) external returns (IERC20); +} + +/// @notice The (older) MasterChef contract gives out a constant number of SUSHI tokens per block. +/// It is the only address with minting rights for SUSHI. +/// 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 MCV2 user. + /// `amount` LP token amount the user has provided. + /// `rewardDebt` The amount of SUSHI entitled to the user. + struct UserInfo { + uint256 amount; + int256 rewardDebt; + } + + /// @notice Info of each MCV2 pool. + /// `allocPoint` The amount of allocation points assigned to the pool. + /// Also known as the amount of SUSHI to distribute per block. + struct PoolInfo { + uint128 accSushiPerShare; + uint64 lastRewardBlock; + uint64 allocPoint; + } + + /// @notice Address of MCV1 contract. + IMasterChef public immutable MASTER_CHEF; + /// @notice Address of SUSHI contract. + IERC20 public immutable SUSHI; + /// @notice The index of MCV2 master pool in MCV1. + uint256 public immutable MASTER_PID; + // @notice The migrator contract. It has a lot of power. Can only be set through governance (owner). + IMigratorChef public migrator; + + /// @notice Info of each MCV2 pool. + PoolInfo[] public poolInfo; + /// @notice Address of the LP token for each MCV2 pool. + IERC20[] public lpToken; + /// @notice Address of each `IRewarder` contract in MCV2. + 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 public totalAllocPoint; + + uint256 private constant MASTERCHEF_SUSHI_PER_BLOCK = 1e20; + uint256 private constant ACC_SUSHI_PRECISION = 1e12; + + 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 indexed rewarder, bool overwrite); + event LogUpdatePool(uint256 indexed pid, uint64 lastRewardBlock, uint256 lpSupply, uint256 accSushiPerShare); + event LogInit(); + + /// @param _MASTER_CHEF The SushiSwap MCV1 contract address. + /// @param _sushi The SUSHI token contract address. + /// @param _MASTER_PID The pool ID of the dummy token on the base MCV1 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 token to `MASTER_CHEF` MCV1. This is required because MCV1 holds the minting rights for SUSHI. + /// Any balance of transaction sender in `dummyToken` is transferred. + /// The allocation point for the pool on MCV1 is the total allocation point for all pools that receive double incentives. + /// @param dummyToken The address of the ERC-20 token to deposit into MCV1. + function init(IERC20 dummyToken) external { + uint256 balance = dummyToken.balanceOf(msg.sender); + require(balance != 0, "MasterChefV2: 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 MCV2 pools. + function poolLength() public view returns (uint256 pools) { + pools = 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 ERC-20 token. + /// @param _rewarder Address of the rewarder delegate. + 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 Set the `migrator` contract. Can only be called by the owner. + /// @param _migrator The contract address to set. + function setMigrator(IMigratorChef _migrator) public onlyOwner { + migrator = _migrator; + } + + /// @notice Migrate LP token to another LP contract through the `migrator` contract. + /// @param _pid The index of the pool. See `poolInfo`. + function migrate(uint256 _pid) public { + require(address(migrator) != address(0), "MasterChefV2: no migrator set"); + IERC20 _lpToken = lpToken[_pid]; + uint256 bal = _lpToken.balanceOf(address(this)); + _lpToken.approve(address(migrator), bal); + IERC20 newLpToken = migrator.migrate(_lpToken); + require(bal == newLpToken.balanceOf(address(this)), "MasterChefV2: migrated balance must match"); + lpToken[_pid] = newLpToken; + } + + /// @notice View function to see pending SUSHI on frontend. + /// @param _pid The index of the pool. See `poolInfo`. + /// @param _user Address of user. + /// @return pending SUSHI reward for a given user. + function pendingSushi(uint256 _pid, address _user) external view returns (uint256 pending) { + 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); + } + pending = int256(user.amount.mul(accSushiPerShare) / ACC_SUSHI_PRECISION).sub(user.rewardDebt).toUInt256(); + } + + /// @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 and returns 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 MCV2 for SUSHI allocation. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount LP token amount to deposit. + /// @param to The receiver of `amount` deposit benefit. + 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 + IRewarder _rewarder = rewarder[pid]; + if (address(_rewarder) != address(0)) { + _rewarder.onSushiReward(pid, to, to, 0, user.amount); + } + + lpToken[pid].safeTransferFrom(msg.sender, address(this), amount); + + emit Deposit(msg.sender, pid, amount, to); + } + + /// @notice Withdraw LP tokens from MCV2. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount LP token amount 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 + IRewarder _rewarder = rewarder[pid]; + if (address(_rewarder) != address(0)) { + _rewarder.onSushiReward(pid, msg.sender, to, 0, user.amount); + } + + 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(); + + // Effects + user.rewardDebt = accumulatedSushi; + + // Interactions + if (_pendingSushi != 0) { + SUSHI.safeTransfer(to, _pendingSushi); + } + + IRewarder _rewarder = rewarder[pid]; + if (address(_rewarder) != address(0)) { + _rewarder.onSushiReward( pid, msg.sender, to, _pendingSushi, user.amount); + } + + emit Harvest(msg.sender, pid, _pendingSushi); + } + + /// @notice Withdraw LP tokens from MCV2 and harvest proceeds for transaction sender to `to`. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount LP token amount to withdraw. + /// @param to Receiver of the LP tokens and SUSHI rewards. + function withdrawAndHarvest(uint256 pid, uint256 amount, 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(); + + // Effects + user.rewardDebt = accumulatedSushi.sub(int256(amount.mul(pool.accSushiPerShare) / ACC_SUSHI_PRECISION)); + user.amount = user.amount.sub(amount); + + // Interactions + SUSHI.safeTransfer(to, _pendingSushi); + + IRewarder _rewarder = rewarder[pid]; + if (address(_rewarder) != address(0)) { + _rewarder.onSushiReward(pid, msg.sender, to, _pendingSushi, user.amount); + } + + lpToken[pid].safeTransfer(to, amount); + + emit Withdraw(msg.sender, pid, amount, to); + emit Harvest(msg.sender, pid, _pendingSushi); + } + + /// @notice Harvests SUSHI from `MASTER_CHEF` MCV1 and pool `MASTER_PID` to this MCV2 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; + + IRewarder _rewarder = rewarder[pid]; + if (address(_rewarder) != address(0)) { + _rewarder.onSushiReward(pid, msg.sender, to, 0, 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/packages/contracts/contracts/LPRewards/TestContracts/MasterChefV2Mock.sol b/packages/contracts/contracts/LPRewards/TestContracts/MasterChefV2Mock.sol new file mode 100644 index 000000000..7e76a4183 --- /dev/null +++ b/packages/contracts/contracts/LPRewards/TestContracts/MasterChefV2Mock.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.11; +pragma experimental ABIEncoderV2; + +import "../../Dependencies/IERC20.sol"; +import "../Dependencies/BoringMath.sol"; +import "../Dependencies/BoringERC20.sol"; +import "../Dependencies/SignedSafeMath.sol"; +import "../Interfaces/IRewarder.sol"; + + +contract MasterChefV2Mock { + using BoringMath for uint256; + using BoringMath128 for uint128; + using SignedSafeMath for int256; + using BoringERC20 for IERC20; + + uint256 private constant MASTERCHEF_SUSHI_PER_BLOCK = 227821185682704153; + uint256 private constant ACC_SUSHI_PRECISION = 1e12; + + /// @notice Info of each MCV2 user. + /// `amount` LP token amount the user has provided. + /// `rewardDebt` The amount of SUSHI entitled to the user. + struct UserInfo { + uint256 amount; + int256 rewardDebt; + } + + /// @notice Info of each MCV2 pool. + /// Also known as the amount of SUSHI to distribute per block. + struct PoolInfo { + uint128 accSushiPerShare; + uint64 lastRewardBlock; + } + + PoolInfo public poolInfo; + IERC20 public lpToken; + IRewarder public rewarder; + + /// @notice Info of each user that stakes LP tokens. + mapping (address => UserInfo) public userInfo; + + function init(IERC20 _lpToken, IRewarder _rewarder) public { + lpToken = _lpToken; + rewarder = _rewarder; + poolInfo.lastRewardBlock = block.number.to64(); + poolInfo.accSushiPerShare = 0; + + } + + /// @notice View function to see pending SUSHI on frontend. + /// @param _user Address of user. + /// @return pending SUSHI reward for a given user. + function pendingSushi(address _user) external view returns (uint256 pending) { + PoolInfo memory pool = poolInfo; + UserInfo storage user = userInfo[_user]; + uint256 accSushiPerShare = pool.accSushiPerShare; + uint256 lpSupply = lpToken.balanceOf(address(this)); + if (block.number > pool.lastRewardBlock && lpSupply != 0) { + uint256 blocks = block.number.sub(pool.lastRewardBlock); + uint256 sushiReward = blocks.mul(sushiPerBlock()); + accSushiPerShare = accSushiPerShare.add(sushiReward.mul(ACC_SUSHI_PRECISION) / lpSupply); + } + pending = int256(user.amount.mul(accSushiPerShare) / ACC_SUSHI_PRECISION).sub(user.rewardDebt).toUInt256(); + } + + /// @notice Calculates and returns the `amount` of SUSHI per block. + function sushiPerBlock() public pure returns (uint256 amount) { + amount = MASTERCHEF_SUSHI_PER_BLOCK; + } + + /// @notice Update reward variables + /// @return pool Returns the pool that was updated. + function updatePool() public returns (PoolInfo memory pool) { + pool = poolInfo; + if (block.number > pool.lastRewardBlock) { + uint256 lpSupply = lpToken.balanceOf(address(this)); + if (lpSupply > 0) { + uint256 blocks = block.number.sub(pool.lastRewardBlock); + uint256 sushiReward = blocks.mul(sushiPerBlock()); + pool.accSushiPerShare = pool.accSushiPerShare.add((sushiReward.mul(ACC_SUSHI_PRECISION) / lpSupply).to128()); + } + pool.lastRewardBlock = block.number.to64(); + poolInfo = pool; + } + } + + /// @notice Deposit LP tokens to MCV2 for SUSHI allocation. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount LP token amount to deposit. + /// @param to The receiver of `amount` deposit benefit. + function deposit(uint256 pid, uint256 amount, address to) public { + PoolInfo memory pool = updatePool(); + UserInfo storage user = userInfo[to]; + + // Effects + user.amount = user.amount.add(amount); + user.rewardDebt = user.rewardDebt.add(int256(amount.mul(pool.accSushiPerShare) / ACC_SUSHI_PRECISION)); + + // Interactions + rewarder.onSushiReward(pid, to, to, 0, user.amount); + + lpToken.safeTransferFrom(msg.sender, address(this), amount); + } + + /// @notice Withdraw LP tokens from MCV2. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount LP token amount to withdraw. + /// @param to Receiver of the LP tokens. + function withdraw(uint256 pid, uint256 amount, address to) public { + PoolInfo memory pool = updatePool(); + UserInfo storage user = userInfo[msg.sender]; + + // Effects + user.rewardDebt = user.rewardDebt.sub(int256(amount.mul(pool.accSushiPerShare) / ACC_SUSHI_PRECISION)); + user.amount = user.amount.sub(amount); + + rewarder.onSushiReward(pid, msg.sender, to, 0, user.amount); + + lpToken.safeTransfer(to, amount); + } + + /// @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(); + UserInfo storage user = userInfo[msg.sender]; + int256 accumulatedSushi = int256(user.amount.mul(pool.accSushiPerShare) / ACC_SUSHI_PRECISION); + uint256 _pendingSushi = accumulatedSushi.sub(user.rewardDebt).toUInt256(); + + // Effects + user.rewardDebt = accumulatedSushi; + + rewarder.onSushiReward( pid, msg.sender, to, _pendingSushi, user.amount); + } + + /// @notice Withdraw LP tokens from MCV2 and harvest proceeds for transaction sender to `to`. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount LP token amount to withdraw. + /// @param to Receiver of the LP tokens and SUSHI rewards. + function withdrawAndHarvest(uint256 pid, uint256 amount, address to) public { + PoolInfo memory pool = updatePool(); + UserInfo storage user = userInfo[msg.sender]; + int256 accumulatedSushi = int256(user.amount.mul(pool.accSushiPerShare) / ACC_SUSHI_PRECISION); + uint256 _pendingSushi = accumulatedSushi.sub(user.rewardDebt).toUInt256(); + + // Effects + user.rewardDebt = accumulatedSushi.sub(int256(amount.mul(pool.accSushiPerShare) / ACC_SUSHI_PRECISION)); + user.amount = user.amount.sub(amount); + + // Interactions + rewarder.onSushiReward(pid, msg.sender, to, _pendingSushi, user.amount); + + lpToken.safeTransfer(to, amount); + } + + /// @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[msg.sender]; + uint256 amount = user.amount; + user.amount = 0; + user.rewardDebt = 0; + + rewarder.onSushiReward(pid, msg.sender, to, 0, 0); + + // Note: transfer can fail or succeed if `amount` is zero. + lpToken.safeTransfer(to, amount); + } +} diff --git a/packages/contracts/mainnetDeployment/test/SushiSwapOhmLqtyRewarderTest_mainnet.js b/packages/contracts/mainnetDeployment/test/SushiSwapOhmLqtyRewarderTest_mainnet.js new file mode 100644 index 000000000..dec91e80e --- /dev/null +++ b/packages/contracts/mainnetDeployment/test/SushiSwapOhmLqtyRewarderTest_mainnet.js @@ -0,0 +1,177 @@ +// Test with: +// GAS_PRICE=200000000000 BLOCK_NUMBER=13276436 npx hardhat run mainnetDeployment/test/SushiSwapOhmLqtyRewarderTest_mainnet.js --config hardhat.config.mainnet-fork.js + +const { TestHelper: th } = require("../../utils/testHelpers.js"); +const { dec } = th; +const toBN = ethers.BigNumber.from; + +const SushiSwapOhmLqtyRewarder = require('../../artifacts/contracts/LPRewards/SushiSwapOhmLqtyRewarder.sol/SushiSwapOhmLqtyRewarder.json'); +const MasterChefV2 = require('../../artifacts/contracts/LPRewards/TestContracts/MasterChefV2.sol/MasterChefV2.json'); +const ERC20 = require('../../artifacts/contracts/LPRewards/TestContracts/ERC20Mock.sol/ERC20Mock.json'); + +const MASTERCHEF_V2_ADDRESS = '0xEF0881eC094552b2e128Cf945EF17a6752B4Ec5d'; +const MASTERCHEF_V2_OWNER = '0x19B3Eb3Af5D93b77a5619b047De0EED7115A19e7'; +const UNIT = toBN(dec(1, 18)); +const MASTERCHEF_SUSHI_PER_BLOCK = toBN('227821185682704153'); // ~0.23 +const MASTERCHEF_TOTAL_ALLOC_POINT = toBN(540); +const ALLOC_POINT = toBN(10); +// OHM has 9 decimals! +// See: https://github.com/liquity/dev/pull/704#discussion_r715745203 +// 1 OHM = $625, $5,000 per day = 8 OHM per day, ~6,400 blocks per day => 0.00125 LQTY per block +const OHM_MULTIPLIER = toBN(dec(125, 4)) + .mul(UNIT).div(MASTERCHEF_SUSHI_PER_BLOCK) + .mul(MASTERCHEF_TOTAL_ALLOC_POINT.add(ALLOC_POINT)).div(ALLOC_POINT); +// 960 LQTY per day, ~6,400 blocks per day => 0.15 LQTY per block +const LQTY_MULTIPLIER = toBN(dec(15, 16)) + .mul(UNIT).div(MASTERCHEF_SUSHI_PER_BLOCK) + .mul(MASTERCHEF_TOTAL_ALLOC_POINT.add(ALLOC_POINT)).div(ALLOC_POINT); +const ACC_SUSHI_PRECISION = toBN(dec(1, 12)); + +async function main() { + // MasterChef owner + const impersonateAddress = MASTERCHEF_V2_OWNER; + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [ impersonateAddress ] + }); + const deployerWallet = await ethers.provider.getSigner(impersonateAddress); + const deployerWalletAddress = impersonateAddress; + console.log('Deployer: ', deployerWalletAddress); + + // regular user + const userWallet = (await ethers.getSigners())[0]; + const userWalletAddress = userWallet.address; + console.log('User: ', userWalletAddress); + + + const masterChef = new ethers.Contract( + MASTERCHEF_V2_ADDRESS, + MasterChefV2.abi, + deployerWallet + ); + const totalAllocPoint = await masterChef.totalAllocPoint(); + const sushiPerBlock = await masterChef.sushiPerBlock(); + th.logBN('Total alloc point', totalAllocPoint); + th.logBN('Sushi per block ', sushiPerBlock); + th.logBN('OHM multiplier ', OHM_MULTIPLIER); + th.logBN('LQTY multiplier ', LQTY_MULTIPLIER); + assert.equal(totalAllocPoint.toString(), MASTERCHEF_TOTAL_ALLOC_POINT.toString()); + assert.equal(sushiPerBlock.toString(), MASTERCHEF_SUSHI_PER_BLOCK.toString()); + + const tokenFactory = new ethers.ContractFactory(ERC20.abi, ERC20.bytecode, deployerWallet); + const lpToken = await tokenFactory.deploy('LP Token', 'LPT', deployerWalletAddress, 0); + const ohmToken = await tokenFactory.deploy('OHM Token', 'OHM', deployerWalletAddress, 0); + await ohmToken.setupDecimals(9); + const lqtyToken = await tokenFactory.deploy('LQTY Token', 'LQTY', deployerWalletAddress, 0); + console.log('LP token: ', lpToken.address); + console.log('OHM: ', ohmToken.address); + console.log('LQTY: ', lqtyToken.address); + + const rewarderFactory = new ethers.ContractFactory( + SushiSwapOhmLqtyRewarder.abi, + SushiSwapOhmLqtyRewarder.bytecode, + deployerWallet + ); + const rewarder = await rewarderFactory.deploy( + OHM_MULTIPLIER, + ohmToken.address, + LQTY_MULTIPLIER, + lqtyToken.address, + masterChef.address + ); + console.log('Rewarder: ', rewarder.address); + + // add rewarder + const poolAddTx = await masterChef.add(ALLOC_POINT, lpToken.address, rewarder.address); + const poolAddReceipt = await poolAddTx.wait(); + // We need to get the new pool address out of the PoolCreated event + const events = poolAddReceipt.events.filter((e) => e.event === 'LogPoolAddition'); + const poolId = events[0].args.pid; + console.log('poolId: ', poolId.toString()); + + // mint + const initialAmount = dec(1, 24); // 1m + const lpMintTx = await lpToken.mint(userWalletAddress, initialAmount); + await lpMintTx.wait(); + th.logBN('User LP balance ', await lpToken.balanceOf(userWalletAddress)); + + // fund rewarder + const ohmMintTx = await ohmToken.mint(rewarder.address, initialAmount); + await ohmMintTx.wait(); + const lqtyMintTx = await lqtyToken.mint(rewarder.address, initialAmount); + await lqtyMintTx.wait(); + th.logBN('Rewarder OHM bal ', await ohmToken.balanceOf(rewarder.address)); + th.logBN('Rewarder LQTY bal', await lqtyToken.balanceOf(rewarder.address)); + + // approve + const depositAmount = toBN(dec(10, 18)); + const userLpToken = new ethers.Contract( + lpToken.address, + ERC20.abi, + userWallet + ); + const approveTx = await userLpToken.approve(masterChef.address, depositAmount); + await approveTx.wait(); + // deposit + const userMasterChef = new ethers.Contract( + MASTERCHEF_V2_ADDRESS, + MasterChefV2.abi, + userWallet + ); + const ohmInitialDeposit = await ohmToken.balanceOf(userWalletAddress); + const lqtyInitialDeposit = await lqtyToken.balanceOf(userWalletAddress); + th.logBN('User OHM balance ', await ohmToken.balanceOf(userWalletAddress)); + th.logBN('User LQTY balance', await lqtyToken.balanceOf(userWalletAddress)); + const depositTx = await userMasterChef.deposit(poolId, depositAmount, userWalletAddress); + const depositReceipt = await depositTx.wait(); + const depositBlockNumber = depositReceipt.blockNumber; + assert.equal((await ohmToken.balanceOf(userWalletAddress)).toString(), 0); + assert.equal((await lqtyToken.balanceOf(userWalletAddress)).toString(), 0); + + // fast-forward + const blocks = 5; + for (let i = 0; i < blocks; i++) { await ethers.provider.send('evm_mine'); } + + // harvest + const harvestTx = await userMasterChef.harvest(poolId, userWalletAddress); + const harvestReceipt = await harvestTx.wait(); + const harvestBlockNumber = harvestReceipt.blockNumber; + const poolInfo = await masterChef.poolInfo(poolId); + th.logBN('Accumulated sushi per share: ', poolInfo.accSushiPerShare); + const ohmBalance = await ohmToken.balanceOf(userWalletAddress); + const lqtyBalance = await lqtyToken.balanceOf(userWalletAddress); + th.logBN('User OHM balance ', ohmBalance, 9); + th.logBN('User LQTY balance', lqtyBalance); + const baseAmount = poolInfo.accSushiPerShare.mul(UNIT).div(ACC_SUSHI_PRECISION) + .mul(depositAmount).div(UNIT); + //th.logBN('base', baseAmount); + assert.equal( + ohmBalance.toString(), + baseAmount.mul(OHM_MULTIPLIER).div(UNIT).toString(), + 'OHM rewards don’t match' + ); + assert.equal( + lqtyBalance.toString(), + baseAmount.mul(LQTY_MULTIPLIER).div(UNIT).toString(), + 'LQTY rewards don’t match' + ); + th.assertIsApproximatelyEqual( + ohmBalance.toString(), + dec(75, 5), // 0.00125 OHM per block x 6 blocks (and / 1e9) + 1 + ); + th.assertIsApproximatelyEqual( + lqtyBalance.toString(), + dec(90, 16), // 0.15 LQTY per block x 6 blocks + 1e10 + ); + + console.log('\n -- Mainnet deployment test finished successfully! -- \n'); +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 26ebb684e..3cf6d35bc 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -37,7 +37,7 @@ "hardhat": "^2.6.1", "hardhat-gas-reporter": "^1.0.1", "npm-run-all": "^4.1.5", - "solidity-coverage": "^0.7.16", + "solidity-coverage": "^0.7.17", "web3": "^1.3.4" } } diff --git a/packages/contracts/test/SushiSwapOhmLqtyRewarderTest.js b/packages/contracts/test/SushiSwapOhmLqtyRewarderTest.js new file mode 100644 index 000000000..d9ea09e8a --- /dev/null +++ b/packages/contracts/test/SushiSwapOhmLqtyRewarderTest.js @@ -0,0 +1,436 @@ +const testHelpers = require("../utils/testHelpers.js") + +const MasterChefV2 = artifacts.require('MasterChefV2Mock') +const SushiSwapOhmLqtyRewarder = artifacts.require('SushiSwapOhmLqtyRewarder') +const ERC20 = artifacts.require('ERC20Mock') + +const th = testHelpers.TestHelper +const { toBN, assertRevert, dec, ZERO_ADDRESS } = testHelpers.TestHelper + +const UNIT = toBN(dec(1, 18)) +const MASTERCHEF_SUSHI_PER_BLOCK = toBN('227821185682704153') // ~0.23 +// OHM has 9 decimals! +// See: https://github.com/liquity/dev/pull/704#discussion_r715745203 +// 1 OHM = $625, $5,000 per day = 8 OHM per day, ~6,400 blocks per day => 0.00125 LQTY per block +const OHM_MULTIPLIER = toBN(dec(125, 4)).mul(UNIT).div(MASTERCHEF_SUSHI_PER_BLOCK) +// 960 LQTY per day, ~6,400 blocks per day => 0.15 LQTY per block +const LQTY_MULTIPLIER = toBN(dec(15, 16)).mul(UNIT).div(MASTERCHEF_SUSHI_PER_BLOCK) + +contract('SushiSwapOhmLqtyRewarder', async accounts => { + const [owner, alice, bob, carol, dennis] = accounts + + let rewarder, masterChef + let lpToken, ohmToken, lqtyToken + + beforeEach(async () => { + const initialAmount = dec(1, 24) // 1m + + lpToken = await ERC20.new('LP Token', 'LPT', owner, 0) + ohmToken = await ERC20.new('OHM Token', 'OHM', owner, initialAmount) + await ohmToken.setupDecimals(9) + lqtyToken = await ERC20.new('LQTY Token', 'LQTY', owner, initialAmount) + + masterChef = await MasterChefV2.new() + rewarder = await SushiSwapOhmLqtyRewarder.new( + OHM_MULTIPLIER, + ohmToken.address, + LQTY_MULTIPLIER, + lqtyToken.address, + masterChef.address + ) + await masterChef.init(lpToken.address, rewarder.address) + + // mint some LP tokens + await lpToken.mint(alice, initialAmount) + await lpToken.mint(bob, initialAmount) + await lpToken.mint(carol, initialAmount) + await lpToken.mint(dennis, initialAmount) + }) + + const blocksToSushi = blocks => toBN(blocks).mul(MASTERCHEF_SUSHI_PER_BLOCK) + const sushiToOhm = sushiRewards => sushiRewards.mul(OHM_MULTIPLIER).div(UNIT) + const sushiToLqty = sushiRewards => sushiRewards.mul(LQTY_MULTIPLIER).div(UNIT) + const blocksToRewards = blocks => { + const sushi = blocksToSushi(blocks) + return [sushiToOhm(sushi), sushiToLqty(sushi)] + } + const rewardsToUser = (rewards, multiplier) => rewards.map(multiplier) + + const deposit = async (user, amount) => { + await lpToken.approve(masterChef.address, amount, { from: user }) + await masterChef.deposit(0, amount, user, { from: user }) + } + + const checkRewards = async (blocks) => { + const sushiRewards = await masterChef.pendingSushi(alice) + const pendingRewards = await rewarder.pendingTokens(0, ZERO_ADDRESS, sushiRewards) + th.assertIsApproximatelyEqual(sushiRewards.toString(), blocksToSushi(blocks), 1e8) + assert.equal(pendingRewards.rewardTokens[0], ohmToken.address) + assert.equal(pendingRewards.rewardTokens[1], lqtyToken.address) + assert.equal(pendingRewards.rewardAmounts[0].toString(), sushiToOhm(sushiRewards)) + assert.equal(pendingRewards.rewardAmounts[1].toString(), sushiToLqty(sushiRewards)) + } + + const checkBalances = async (user, ohmBalance, lqtyBalance, error = 0) => { + th.assertIsApproximatelyEqual(await ohmToken.balanceOf(user), toBN(ohmBalance), error) + th.assertIsApproximatelyEqual( + await lqtyToken.balanceOf(user), + toBN(lqtyBalance), + error * LQTY_MULTIPLIER.div(OHM_MULTIPLIER).toNumber() + ) + } + + const logPoolInfo = async () => { + const poolInfo = await masterChef.poolInfo() + th.logBN('Acc. sushi per share', poolInfo.accSushiPerShare.mul(toBN(dec(1, 6)))) + console.log('last reward block :', poolInfo.lastRewardBlock.toString()) + } + + const logUserInfo = async (user) => { + const userInfo = await masterChef.userInfo(user) + th.logBN('amount ', userInfo.amount) + th.logBN('rewardDebt', userInfo.rewardDebt) + } + + context('On MasterChef with funds', async () => { + beforeEach(async () => { + const initialAmount = dec(1, 24) // 1m + // fund rewarder + await ohmToken.transfer(rewarder.address, initialAmount, { from: owner }) + await lqtyToken.transfer(rewarder.address, initialAmount, { from: owner }) + }) + + it('Alice deposits once and harvest', async () => { + const amount = toBN(dec(10, 18)) + const blocks = 5 + + await checkRewards(0) + + await deposit(alice, amount) + + await checkRewards(0) + + await th.fastForwardBlocks(blocks, web3.currentProvider) + + await checkRewards(blocks) + + // harvest + await checkBalances(alice, 0, 0) + await masterChef.harvest(0, alice, { from: alice }) + await checkBalances(alice, ...blocksToRewards(blocks + 1), 1e7) + }) + + // This actually belongs to MasterChefV2, as the rewarder ignores the user, it only uses the recipient + it('Alice deposits, Bob can’t harvest for her', async () => { + const amount = toBN(dec(10, 18)) + const blocks = 5 + + await checkRewards(0) + + await deposit(alice, amount) + + await checkRewards(0) + + await th.fastForwardBlocks(blocks, web3.currentProvider) + + await checkRewards(blocks) + + // harvest + await checkBalances(alice, 0, 0) + await masterChef.harvest(0, alice, { from: bob }) + await checkBalances(alice, 0, 0) + await checkBalances(bob, 0, 0) + await masterChef.harvest(0, bob, { from: bob }) + await checkBalances(alice, 0, 0) + await checkBalances(bob, 0, 0) + }) + + it('Alice deposits twice', async () => { + const amount = toBN(dec(10, 18)) + const blocks = 5 + + await checkRewards(0) + + // deposit + await lpToken.approve(masterChef.address, amount, { from: alice }) + const blockBeforeDeposit = await web3.eth.getBlockNumber() + await masterChef.deposit(0, amount, alice, { from: alice }) + // await logUserInfo(alice) + // await logPoolInfo() + + await th.fastForwardBlocks(blocks, web3.currentProvider) + + await checkRewards(blocks) + + // deposit + await deposit(alice, amount) + // await logUserInfo(alice) + // await logPoolInfo() + + await checkRewards(blocks + 2) + + // harvest + await checkBalances(alice, 0, 0) + const blockBeforeHarvest = await web3.eth.getBlockNumber() + await masterChef.harvest(0, alice, { from: alice }) + await checkBalances(alice, ...blocksToRewards(blockBeforeHarvest - blockBeforeDeposit), 1e9) + // await logUserInfo(alice) + // await logPoolInfo() + }) + + it('Alice deposits once, withdraws and harvest', async () => { + const amount = toBN(dec(10, 18)) + const blocks = 5 + + await checkRewards(0) + + // deposit + await lpToken.approve(masterChef.address, amount, { from: alice }) + const blockBeforeDeposit = await web3.eth.getBlockNumber() + await masterChef.deposit(0, amount, alice, { from: alice }) + // await logUserInfo(alice) + // await logPoolInfo() + + await checkRewards(0) + + await th.fastForwardBlocks(blocks, web3.currentProvider) + + await checkRewards(blocks) + + // withdraw + const blockBeforeWithdraw = await web3.eth.getBlockNumber() + await masterChef.withdraw(0, amount, alice, { from: alice }) + await checkBalances(alice, 0, 0) + // await logUserInfo(alice) + // await logPoolInfo() + + // harvest + await masterChef.harvest(0, alice, { from: alice }) + await checkBalances(alice, ...blocksToRewards(blockBeforeWithdraw - blockBeforeDeposit), 1e8) + // await logUserInfo(alice) + // await logPoolInfo() + }) + + it('Alice deposits once, then withdraws and harvest in 1 tx', async () => { + const amount = toBN(dec(10, 18)) + const blocks = 5 + + await checkRewards(0) + + await lpToken.approve(masterChef.address, amount, { from: alice }) + const blockBeforeDeposit = await web3.eth.getBlockNumber() + await masterChef.deposit(0, amount, alice, { from: alice }) + // await logUserInfo(alice) + // await logPoolInfo() + + await checkRewards(0) + + await th.fastForwardBlocks(blocks, web3.currentProvider) + + await checkRewards(blocks) + + // withdraw & harvest + await checkBalances(alice, 0, 0) + const blockBeforeWithdraw = await web3.eth.getBlockNumber() + await masterChef.withdrawAndHarvest(0, amount, alice, { from: alice }) + await checkBalances(alice, ...blocksToRewards(blockBeforeWithdraw - blockBeforeDeposit), 1e8) + // await logUserInfo(alice) + // await logPoolInfo() + }) + + it('Alice deposits once, withdraws and harvest to Bob', async () => { + const amount = toBN(dec(10, 18)) + const blocks = 5 + + await checkRewards(0) + + await lpToken.approve(masterChef.address, amount, { from: alice }) + const blockBeforeDeposit = await web3.eth.getBlockNumber() + await masterChef.deposit(0, amount, alice, { from: alice }) + + await checkRewards(0) + + await th.fastForwardBlocks(blocks, web3.currentProvider) + + await checkRewards(blocks) + + // withdraw + const blockBeforeWithdraw = await web3.eth.getBlockNumber() + await masterChef.withdraw(0, amount, alice, { from: alice }) + await checkBalances(alice, 0, 0) + + // harvest + await masterChef.harvest(0, bob, { from: alice }) + await checkBalances(alice, 0, 0) + await checkBalances(bob, ...blocksToRewards(blockBeforeWithdraw - blockBeforeDeposit), 1e8) + }) + + /* + | Action | A d | B d | A w | C d | B w&h | A h | C h | D d | D h | A w | D w | A h | D h | + | Blocks | 14 | 4 | 2 | 2 | 4 | 1 | 2 | 3 | 4 | 5 | 6 | 1 | 1 | + | Balance | 10 | 210 | 206 | 256 | 56 | 56 | 56 | 156 | 156 | 150 | 50 | 50 | 50 | + */ + it('several users, mixed deposits and withdrawals', async () => { + const periods = [14, 4, 2, 2, 4, 1, 2, 3, 4, 5, 6, 1, 1] + const totals = [10, 210, 206, 256, 56, 56, 56, 156, 156, 150, 50, 50, 50] + const multiplier = (balances) => + x => { + const combinedArray = balances.map((bal, i) => [periods[i], totals[i], bal]) + const totalTime = toBN(combinedArray.reduce((total, next) => total + next[0], 0)) + return combinedArray.reduce( + (total, next) => total.add(x.mul(toBN(next[0])).mul(toBN(next[2])).div(totalTime).div(toBN(next[1]))), + toBN(0) + ) + } + + // alice deposits + const initialBlock = await web3.eth.getBlockNumber() + 2 // approve and deposit tx blocks + await deposit(alice, toBN(dec(10, 18))) + await checkBalances(alice, 0, 0) + await th.fastForwardBlocks(12, web3.currentProvider) + + // bob deposits + await deposit(bob, toBN(dec(200, 18))) + await checkBalances(bob, 0, 0) + await th.fastForwardBlocks(3, web3.currentProvider) + + // alice partially withdraws + await masterChef.withdraw(0, toBN(dec(4, 18)), alice, { from: alice }) + await checkBalances(alice, 0, 0) + + // carol deposits + await deposit(carol, toBN(dec(50, 18))) + await checkBalances(carol, 0, 0) + await th.fastForwardBlocks(1, web3.currentProvider) + + // bob withdraws and harvests + const block1 = await web3.eth.getBlockNumber() + await masterChef.withdrawAndHarvest(0, toBN(dec(200, 18)), bob, { from: bob }) + await checkBalances( + bob, + ...rewardsToUser( + blocksToRewards(await web3.eth.getBlockNumber() - initialBlock), + multiplier([0, 200, 200, 200]) + ), + 1e8 // error + ) + await th.fastForwardBlocks(3, web3.currentProvider) + + // alice and carol harvest + await masterChef.harvest(0, alice, { from: alice }) + await checkBalances( + alice, + ...rewardsToUser( + blocksToRewards(await web3.eth.getBlockNumber() - initialBlock), + multiplier([10, 10, 6, 6, 6]) + ), + 1e8 // error + ) + + await masterChef.harvest(0, carol, { from: carol }) + await checkBalances( + carol, + ...rewardsToUser( + blocksToRewards(await web3.eth.getBlockNumber() - initialBlock), + multiplier([0, 0, 0, 50, 50, 50]) + ), + 1e8 // error + ) + + // dennis deposits + await deposit(dennis, toBN(dec(100, 18))) + await th.fastForwardBlocks(2, web3.currentProvider) + + // dennis harvests + await masterChef.harvest(0, dennis, { from: dennis }) + await checkBalances( + dennis, + ...rewardsToUser( + blocksToRewards(await web3.eth.getBlockNumber() - initialBlock), + multiplier([0, 0, 0, 0, 0, 0, 0, 100]) + ), + 1e8 // error + ) + await th.fastForwardBlocks(3, web3.currentProvider) + + // alice fully withdraws + await masterChef.withdraw(0, toBN(dec(6, 18)), alice, { from: alice }) + await th.fastForwardBlocks(4, web3.currentProvider) + + // dennis withdraws + await masterChef.withdraw(0, toBN(dec(100, 18)), dennis, { from: dennis }) + await th.fastForwardBlocks(5, web3.currentProvider) + + // alice and dennis harvest + await masterChef.harvest(0, alice, { from: alice }) + await checkBalances( + alice, + ...rewardsToUser( + blocksToRewards(await web3.eth.getBlockNumber() - initialBlock), + multiplier([10, 10, 6, 6, 6, 6, 6, 6, 6, 0, 0]) + ), + 1e8 // error + ) + + await masterChef.harvest(0, dennis, { from: dennis }) + await checkBalances( + dennis, + ...rewardsToUser( + blocksToRewards(await web3.eth.getBlockNumber() - initialBlock), + multiplier([0, 0, 0, 0, 0, 0, 0, 100, 100, 100, 0, 0]) + ), + 1e8 // error + ) + }) + + it('emergency withdrawal', async () => { + const amount = toBN(dec(10, 18)) + const blocks = 5 + + // deposit + await deposit(alice, amount) + + await th.fastForwardBlocks(blocks, web3.currentProvider) + + await checkRewards(blocks) + + // emergency withdraw + await checkBalances(alice, 0, 0) + await masterChef.emergencyWithdraw(0, alice, { from: alice }) + await checkBalances(alice, 0, 0) + }) + + }) + + context('On MasterChef without funds', async () => { + it('Alice deposits once and harvest', async () => { + const amount = toBN(dec(10, 18)) + const blocks = 5 + + await checkRewards(0) + + await deposit(alice, amount) + + await checkRewards(0) + + await th.fastForwardBlocks(blocks, web3.currentProvider) + + await checkRewards(blocks) + + // harvest + await checkBalances(alice, 0, 0) + await masterChef.harvest(0, alice, { from: alice }) + // As there are no funds, no rewards are obtained + await checkBalances(alice, 0, 0) + }) + }) + + context('Check access modifier', () => { + it('Regular accounts can’t call onSushiReward', async () => { + await assertRevert( + rewarder.onSushiReward(0, ZERO_ADDRESS, alice, toBN(dec(1, 18)), 0, { from: owner }), + 'Only MCV2 can call this function.' + ) + }) + }) +}) diff --git a/packages/contracts/utils/testHelpers.js b/packages/contracts/utils/testHelpers.js index 4011cc73f..6eafd469d 100644 --- a/packages/contracts/utils/testHelpers.js +++ b/packages/contracts/utils/testHelpers.js @@ -266,11 +266,11 @@ class TestHelper { } } - static logBN(label, x) { - x = x.toString().padStart(18, '0') + static logBN(label, x, decimals=18) { + x = x.toString().padStart(decimals, '0') // TODO: thousand separators - const integerPart = x.slice(0, x.length-18) ? x.slice(0, x.length-18) : '0' - console.log(`${label}:`, integerPart + '.' + x.slice(-18)) + const integerPart = x.slice(0, x.length-decimals) ? x.slice(0, x.length-decimals) : '0' + console.log(`${label}:`, integerPart + '.' + x.slice(-decimals)) } // --- TCR and Recovery Mode functions --- @@ -1114,6 +1114,19 @@ class TestHelper { (err) => { if (err) console.log(err) }) } + static async fastForwardBlocks(blocks, currentWeb3Provider) { + for (let i = 0; i < blocks; i++) { + await currentWeb3Provider.send( + { + id: 0, + jsonrpc: '2.0', + method: 'evm_mine' + }, + (err) => { if (err) console.log(err) } + ) + } + } + static async getLatestBlockTimestamp(web3Instance) { const blockNumber = await web3Instance.eth.getBlockNumber() const block = await web3Instance.eth.getBlock(blockNumber) diff --git a/yarn.lock b/yarn.lock index 1747e8ce7..46b53cf4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3290,6 +3290,13 @@ resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.12.0.tgz#18a0fb2a9d2484b23176f63b16093c64794fc323" integrity sha512-DT3f/Aa4tQysZwUsuqBwvr8YRJzKkvPUKV/9o2/o5EVw3xqlbzmtx4O60lTUcZdCawL+N8bBLNUyOGpHjGlJVQ== +"@solidity-parser/parser@^0.13.2": + version "0.13.2" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.13.2.tgz#b6c71d8ca0b382d90a7bbed241f9bc110af65cbe" + integrity sha512-RwHnpRnfrnD2MSPveYoPh8nhofEvX7fgjHk1Oq+NNvCcLx4r1js91CO9o+F/F3fBzOCyvm8kKRTriFICX/odWw== + dependencies: + antlr4ts "^0.5.0-alpha.4" + "@styled-system/background@^5.1.2": version "5.1.2" resolved "https://registry.yarnpkg.com/@styled-system/background/-/background-5.1.2.tgz#75c63d06b497ab372b70186c0bf608d62847a2ba" @@ -4965,6 +4972,11 @@ ansistyles@~0.1.3: resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk= +antlr4ts@^0.5.0-alpha.4: + version "0.5.0-alpha.4" + resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a" + integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ== + any-observable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" @@ -10052,7 +10064,7 @@ functional-red-black-tree@^1.0.1, functional-red-black-tree@~1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -ganache-cli@^6.11.0: +ganache-cli@^6.12.2: version "6.12.2" resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.12.2.tgz#c0920f7db0d4ac062ffe2375cb004089806f627a" integrity sha512-bnmwnJDBDsOWBUP8E/BExWf85TsdDEFelQSzihSJm9VChVO1SHp94YXLP5BlA4j/OTxp0wR4R1Tje9OHOuAJVw== @@ -18864,18 +18876,18 @@ solc@^0.6.11: semver "^5.5.0" tmp "0.0.33" -solidity-coverage@^0.7.16: - version "0.7.16" - resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.16.tgz#c8c8c46baa361e2817bbf275116ddd2ec90a55fb" - integrity sha512-ttBOStywE6ZOTJmmABSg4b8pwwZfYKG8zxu40Nz+sRF5bQX7JULXWj/XbX0KXps3Fsp8CJXg8P29rH3W54ipxw== +solidity-coverage@^0.7.17: + version "0.7.17" + resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.17.tgz#5139de8f6666d4755d88f453d8e35632a7bb3444" + integrity sha512-Erw2hd2xdACAvDX8jUdYkmgJlIIazGznwDJA5dhRaw4def2SisXN9jUjneeyOZnl/E7j6D3XJYug4Zg9iwodsg== dependencies: - "@solidity-parser/parser" "^0.12.0" + "@solidity-parser/parser" "^0.13.2" "@truffle/provider" "^0.2.24" chalk "^2.4.2" death "^1.1.0" detect-port "^1.3.0" fs-extra "^8.1.0" - ganache-cli "^6.11.0" + ganache-cli "^6.12.2" ghost-testrpc "^0.0.2" global-modules "^2.0.0" globby "^10.0.1"