From 710c2a739b2806ade49dd5a3b805d6cba8d92366 Mon Sep 17 00:00:00 2001 From: Clearwood Date: Sat, 4 Sep 2021 13:21:20 -0600 Subject: [PATCH] Clone rewarder (#103) * added ComplexRewarderTime * renaming * changed to interface * log change * Added Triple Rewarder * Reverted back BoringSolidity version * Added reward rates function * Remove return doc to appease compiler * Migrated to latest BoringSolidity version * Downgraded for Certora rules * Updated Naming * Added better comments + caching * Removed unused events * Reset interface --- contracts/mocks/CloneRewarderTime.sol | 149 ++++++++++++++++++ contracts/mocks/CloneRewarderTimeDual.sol | 175 ++++++++++++++++++++++ hardhat.config.ts | 2 + tasks/index.js | 101 +++++++++++++ 4 files changed, 427 insertions(+) create mode 100644 contracts/mocks/CloneRewarderTime.sol create mode 100644 contracts/mocks/CloneRewarderTimeDual.sol diff --git a/contracts/mocks/CloneRewarderTime.sol b/contracts/mocks/CloneRewarderTime.sol new file mode 100644 index 0000000000..ba65e91045 --- /dev/null +++ b/contracts/mocks/CloneRewarderTime.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "../interfaces/IRewarder.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; +import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; + +interface IMasterChefV2 { + function lpToken(uint256 pid) external view returns (IERC20 _lpToken); +} + +/// @author @0xKeno +contract CloneRewarderTime is IRewarder, BoringOwnable{ + using BoringMath for uint256; + using BoringMath128 for uint128; + using BoringERC20 for IERC20; + + IERC20 public rewardToken; + + /// @notice Info of each Rewarder user. + /// `amount` LP token amount the user has provided. + /// `rewardDebt` The amount of Reward Token entitled to the user. + struct UserInfo { + uint256 amount; + uint256 rewardDebt; + } + + /// @notice Info of the rewarder pool + struct PoolInfo { + uint128 accToken1PerShare; + uint64 lastRewardTime; + } + + /// @notice Mapping to track the rewarder pool. + mapping (uint256 => PoolInfo) public poolInfo; + + + /// @notice Info of each user that stakes LP tokens. + mapping (uint256 => mapping (address => UserInfo)) public userInfo; + + uint256 public rewardPerSecond; + IERC20 public masterLpToken; + uint256 private constant ACC_TOKEN_PRECISION = 1e12; + + address public immutable MASTERCHEF_V2; + + event LogOnReward(address indexed user, uint256 indexed pid, uint256 amount, address indexed to); + event LogUpdatePool(uint256 indexed pid, uint64 lastRewardTime, uint256 lpSupply, uint256 accToken1PerShare); + event LogRewardPerSecond(uint256 rewardPerSecond); + event LogInit(IERC20 indexed rewardToken, address owner, uint256 rewardPerSecond, IERC20 indexed masterLpToken); + + constructor (address _MASTERCHEF_V2) public { + MASTERCHEF_V2 = _MASTERCHEF_V2; + } + + /// @notice Serves as the constructor for clones, as clones can't have a regular constructor + /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData) + function init(bytes calldata data) public payable { + require(rewardToken == IERC20(0), "Rewarder: already initialized"); + (rewardToken, owner, rewardPerSecond, masterLpToken) = abi.decode(data, (IERC20, address, uint256, IERC20)); + require(rewardToken != IERC20(0), "Rewarder: bad token"); + emit LogInit(rewardToken, owner, rewardPerSecond, masterLpToken); + } + + function onSushiReward (uint256 pid, address _user, address to, uint256, uint256 lpTokenAmount) onlyMCV2 override external { + require(IMasterChefV2(MASTERCHEF_V2).lpToken(pid) == masterLpToken); + + PoolInfo memory pool = updatePool(pid); + UserInfo storage user = userInfo[pid][_user]; + uint256 pending; + if (user.amount > 0) { + pending = + (user.amount.mul(pool.accToken1PerShare) / ACC_TOKEN_PRECISION).sub( + user.rewardDebt + ); + rewardToken.safeTransfer(to, pending); + } + user.amount = lpTokenAmount; + user.rewardDebt = lpTokenAmount.mul(pool.accToken1PerShare) / ACC_TOKEN_PRECISION; + emit LogOnReward(_user, pid, pending, to); + } + + function pendingTokens(uint256 pid, address user, uint256) override external view returns (IERC20[] memory rewardTokens, uint256[] memory rewardAmounts) { + IERC20[] memory _rewardTokens = new IERC20[](1); + _rewardTokens[0] = (rewardToken); + uint256[] memory _rewardAmounts = new uint256[](1); + _rewardAmounts[0] = pendingToken(pid, user); + return (_rewardTokens, _rewardAmounts); + } + + function rewardRates() external view returns (uint256[] memory) { + uint256[] memory _rewardRates = new uint256[](1); + _rewardRates[0] = rewardPerSecond; + return (_rewardRates); + } + + /// @notice Sets the sushi per second to be distributed. Can only be called by the owner. + /// @param _rewardPerSecond The amount of Sushi to be distributed per second. + function setRewardPerSecond(uint256 _rewardPerSecond) public onlyOwner { + rewardPerSecond = _rewardPerSecond; + emit LogRewardPerSecond(_rewardPerSecond); + } + + modifier onlyMCV2 { + require( + msg.sender == MASTERCHEF_V2, + "Only MCV2 can call this function." + ); + _; + } + + /// @notice View function to see pending Token + /// @param _pid The index of the pool. See `poolInfo`. + /// @param _user Address of user. + /// @return pending SUSHI reward for a given user. + function pendingToken(uint256 _pid, address _user) public view returns (uint256 pending) { + PoolInfo memory pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][_user]; + uint256 accToken1PerShare = pool.accToken1PerShare; + uint256 lpSupply = IMasterChefV2(MASTERCHEF_V2).lpToken(_pid).balanceOf(MASTERCHEF_V2); + if (block.timestamp > pool.lastRewardTime && lpSupply != 0) { + uint256 time = block.timestamp.sub(pool.lastRewardTime); + uint256 sushiReward = time.mul(rewardPerSecond); + accToken1PerShare = accToken1PerShare.add(sushiReward.mul(ACC_TOKEN_PRECISION) / lpSupply); + } + pending = (user.amount.mul(accToken1PerShare) / ACC_TOKEN_PRECISION).sub(user.rewardDebt); + } + + /// @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.timestamp > pool.lastRewardTime) { + uint256 lpSupply = IMasterChefV2(MASTERCHEF_V2).lpToken(pid).balanceOf(MASTERCHEF_V2); + + if (lpSupply > 0) { + uint256 time = block.timestamp.sub(pool.lastRewardTime); + uint256 sushiReward = time.mul(rewardPerSecond); + pool.accToken1PerShare = pool.accToken1PerShare.add((sushiReward.mul(ACC_TOKEN_PRECISION) / lpSupply).to128()); + } + pool.lastRewardTime = block.timestamp.to64(); + poolInfo[pid] = pool; + emit LogUpdatePool(pid, pool.lastRewardTime, lpSupply, pool.accToken1PerShare); + } + } +} diff --git a/contracts/mocks/CloneRewarderTimeDual.sol b/contracts/mocks/CloneRewarderTimeDual.sol new file mode 100644 index 0000000000..02ed9a4b86 --- /dev/null +++ b/contracts/mocks/CloneRewarderTimeDual.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "../interfaces/IRewarder.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; +import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; + +interface IMasterChefV2 { + function lpToken(uint256 pid) external view returns (IERC20 _lpToken); +} + +/// @author @0xKeno +contract CloneRewarderTimeDual is IRewarder, BoringOwnable{ + using BoringMath for uint256; + using BoringMath128 for uint128; + using BoringERC20 for IERC20; + + IERC20 public rewardToken1; + IERC20 public rewardToken2; + + /// @notice Info of each Rewarder user. + /// `amount` LP token amount the user has provided. + /// `rewardDebt1` The amount of reward token 1 entitled to the user. + /// `rewardDebt2` The amount of reward token 2 entitled to the user. + struct UserInfo { + uint256 amount; + uint256 rewardDebt1; + uint256 rewardDebt2; + } + + /// @notice Info of the rewarder pool. + struct PoolInfo { + uint128 accToken1PerShare; + uint128 accToken2PerShare; + uint64 lastRewardTime; + } + + /// @notice Info of each pool. + mapping (uint256 => PoolInfo) public poolInfo; + + + /// @notice Info of each user that stakes LP tokens. + mapping (uint256 => mapping (address => UserInfo)) public userInfo; + + uint128 public rewardPerSecond1; + uint128 public rewardPerSecond2; + IERC20 public masterLpToken; + uint256 private constant ACC_TOKEN_PRECISION = 1e12; + + address public immutable MASTERCHEF_V2; + + event LogOnReward(address indexed user, uint256 indexed pid, uint256 amount1, uint256 amount2, address indexed to); + event LogUpdatePool(uint256 indexed pid, uint64 lastRewardTime, uint256 lpSupply, uint256 accToken1PerShare, uint256 accToken2PerShare); + event LogRewardPerSecond(uint256 rewardPerSecond1, uint256 rewardPerSecond2); + event LogInit(IERC20 rewardToken1, IERC20 rewardToken2, address owner, uint256 rewardPerSecond1, uint256 rewardPerSecond2, IERC20 indexed masterLpToken); + + constructor (address _MASTERCHEF_V2) public { + MASTERCHEF_V2 = _MASTERCHEF_V2; + } + + /// @notice Serves as the constructor for clones, as clones can't have a regular constructor + /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData) + function init(bytes calldata data) public payable { + require(rewardToken1 == IERC20(0), "Rewarder: already initialized"); + (rewardToken1, rewardToken2, owner, rewardPerSecond1, rewardPerSecond2, masterLpToken) = abi.decode(data, (IERC20, IERC20, address, uint128, uint128, IERC20)); + require(rewardToken1 != IERC20(0), "Rewarder: bad token"); + emit LogInit(rewardToken1, rewardToken2, owner, rewardPerSecond1, rewardPerSecond2, masterLpToken); + } + + function onSushiReward (uint256 pid, address _user, address to, uint256, uint256 lpTokenAmount) onlyMCV2 override external { + require(IMasterChefV2(MASTERCHEF_V2).lpToken(pid) == masterLpToken); + + PoolInfo memory pool = updatePool(pid); + UserInfo memory _userInfo = userInfo[pid][_user]; + uint256 pending1; + uint256 pending2; + if (_userInfo.amount > 0) { + pending1 = + (_userInfo.amount.mul(pool.accToken1PerShare) / ACC_TOKEN_PRECISION).sub( + _userInfo.rewardDebt1 + ); + pending2 = + (_userInfo.amount.mul(pool.accToken2PerShare) / ACC_TOKEN_PRECISION).sub( + _userInfo.rewardDebt2 + ); + rewardToken1.safeTransfer(to, pending1); + rewardToken2.safeTransfer(to, pending2); + } + _userInfo.amount = lpTokenAmount; + _userInfo.rewardDebt1 = lpTokenAmount.mul(pool.accToken1PerShare) / ACC_TOKEN_PRECISION; + _userInfo.rewardDebt2 = lpTokenAmount.mul(pool.accToken2PerShare) / ACC_TOKEN_PRECISION; + + userInfo[pid][_user] = _userInfo; + + emit LogOnReward(_user, pid, pending1, pending2, to); + } + + function pendingTokens(uint256 pid, address user, uint256) override external view returns (IERC20[] memory rewardTokens, uint256[] memory rewardAmounts) { + IERC20[] memory _rewardTokens = new IERC20[](2); + _rewardTokens[0] = rewardToken1; + _rewardTokens[1] = rewardToken2; + uint256[] memory _rewardAmounts = new uint256[](2); + (uint256 reward1, uint256 reward2) = pendingToken(pid, user); + _rewardAmounts[0] = reward1; + _rewardAmounts[1] = reward2; + return (_rewardTokens, _rewardAmounts); + } + + function rewardRates() external view returns (uint256[] memory) { + uint256[] memory _rewardRates = new uint256[](2); + _rewardRates[0] = rewardPerSecond1; + _rewardRates[1] = rewardPerSecond2; + return (_rewardRates); + } + + /// @notice Sets the sushi per second to be distributed. Can only be called by the owner. + /// @param _rewardPerSecond1 The amount of reward token 1 to be distributed per second. + /// @param _rewardPerSecond2 The amount of reward token 2 to be distributed per second. + function setRewardPerSecond(uint128 _rewardPerSecond1, uint128 _rewardPerSecond2) public onlyOwner { + rewardPerSecond1 = _rewardPerSecond1; + rewardPerSecond2 = _rewardPerSecond2; + emit LogRewardPerSecond(_rewardPerSecond1, _rewardPerSecond2); + } + + modifier onlyMCV2 { + require( + msg.sender == MASTERCHEF_V2, + "Only MCV2 can call this function." + ); + _; + } + + /// @notice View function to see pending Token + /// @param _pid The index of the pool. See `poolInfo`. + /// @param _user Address of user. + function pendingToken(uint256 _pid, address _user) public view returns (uint256 reward1, uint256 reward2) { + PoolInfo memory pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][_user]; + uint256 accToken1PerShare = pool.accToken1PerShare; + uint256 accToken2PerShare = pool.accToken2PerShare; + uint256 lpSupply = IMasterChefV2(MASTERCHEF_V2).lpToken(_pid).balanceOf(MASTERCHEF_V2); + if (block.timestamp > pool.lastRewardTime && lpSupply != 0) { + uint256 time = block.timestamp.sub(pool.lastRewardTime); + uint256 pending1 = time.mul(rewardPerSecond1); + uint256 pending2 = time.mul(rewardPerSecond2); + accToken1PerShare = accToken1PerShare.add(pending1.mul(ACC_TOKEN_PRECISION) / lpSupply); + accToken2PerShare = accToken2PerShare.add(pending2.mul(ACC_TOKEN_PRECISION) / lpSupply); + } + reward1 = (user.amount.mul(accToken1PerShare) / ACC_TOKEN_PRECISION).sub(user.rewardDebt1); + reward2 = (user.amount.mul(accToken2PerShare) / ACC_TOKEN_PRECISION).sub(user.rewardDebt2); + } + + /// @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.timestamp > pool.lastRewardTime) { + uint256 lpSupply = IMasterChefV2(MASTERCHEF_V2).lpToken(pid).balanceOf(MASTERCHEF_V2); + + if (lpSupply > 0) { + uint256 time = block.timestamp.sub(pool.lastRewardTime); + uint256 pending1 = time.mul(rewardPerSecond1); + uint256 pending2 = time.mul(rewardPerSecond2); + pool.accToken1PerShare = pool.accToken1PerShare.add((pending1.mul(ACC_TOKEN_PRECISION) / lpSupply).to128()); + pool.accToken2PerShare = pool.accToken2PerShare.add((pending2.mul(ACC_TOKEN_PRECISION) / lpSupply).to128()); + } + pool.lastRewardTime = block.timestamp.to64(); + poolInfo[pid] = pool; + emit LogUpdatePool(pid, pool.lastRewardTime, lpSupply, pool.accToken1PerShare, pool.accToken2PerShare); + } + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index b71146f4be..7e86927bcc 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -311,4 +311,6 @@ const config: HardhatUserConfig = { }, } + + export default config diff --git a/tasks/index.js b/tasks/index.js index 730e33abf9..2815dde4c4 100644 --- a/tasks/index.js +++ b/tasks/index.js @@ -2,6 +2,107 @@ const { task } = require("hardhat/config") const { ethers: { constants: { MaxUint256 }}} = require("ethers") +const fs = require("fs") + +function getSortedFiles(dependenciesGraph) { + const tsort = require("tsort") + const graph = tsort() + + const filesMap = {} + const resolvedFiles = dependenciesGraph.getResolvedFiles() + resolvedFiles.forEach((f) => (filesMap[f.sourceName] = f)) + + for (const [from, deps] of dependenciesGraph.entries()) { + for (const to of deps) { + graph.add(to.sourceName, from.sourceName) + } + } + + const topologicalSortedNames = graph.sort() + + // If an entry has no dependency it won't be included in the graph, so we + // add them and then dedup the array + const withEntries = topologicalSortedNames.concat(resolvedFiles.map((f) => f.sourceName)) + + const sortedNames = [...new Set(withEntries)] + return sortedNames.map((n) => filesMap[n]) +} + +function getFileWithoutImports(resolvedFile) { + const IMPORT_SOLIDITY_REGEX = /^\s*import(\s+)[\s\S]*?;\s*$/gm + + return resolvedFile.content.rawContent.replace(IMPORT_SOLIDITY_REGEX, "").trim() +} + +subtask("flat:get-flattened-sources", "Returns all contracts and their dependencies flattened") + .addOptionalParam("files", undefined, undefined, types.any) + .addOptionalParam("output", undefined, undefined, types.string) + .setAction(async ({ files, output }, { run }) => { + const dependencyGraph = await run("flat:get-dependency-graph", { files }) + console.log(dependencyGraph) + + let flattened = "" + + if (dependencyGraph.getResolvedFiles().length === 0) { + return flattened + } + + const sortedFiles = getSortedFiles(dependencyGraph) + + let isFirst = true + for (const file of sortedFiles) { + if (!isFirst) { + flattened += "\n" + } + flattened += `// File ${file.getVersionedName()}\n` + flattened += `${getFileWithoutImports(file)}\n` + + isFirst = false + } + + // Remove every line started with "// SPDX-License-Identifier:" + flattened = flattened.replace(/SPDX-License-Identifier:/gm, "License-Identifier:") + + flattened = `// SPDX-License-Identifier: MIXED\n\n${flattened}` + + // Remove every line started with "pragma experimental ABIEncoderV2;" except the first one + flattened = flattened.replace(/pragma experimental ABIEncoderV2;\n/gm, ((i) => (m) => (!i++ ? m : ""))(0)) + + flattened = flattened.trim() + if (output) { + console.log("Writing to", output) + fs.writeFileSync(output, flattened) + return "" + } + return flattened + }) + +subtask("flat:get-dependency-graph") + .addOptionalParam("files", undefined, undefined, types.any) + .setAction(async ({ files }, { run }) => { + const sourcePaths = files === undefined ? await run("compile:solidity:get-source-paths") : files.map((f) => fs.realpathSync(f)) + + const sourceNames = await run("compile:solidity:get-source-names", { + sourcePaths, + }) + + const dependencyGraph = await run("compile:solidity:get-dependency-graph", { sourceNames }) + + return dependencyGraph + }) + +task("flat", "Flattens and prints contracts and their dependencies") + .addOptionalVariadicPositionalParam("files", "The files to flatten", undefined, types.inputFile) + .addOptionalParam("output", "Specify the output file", undefined, types.string) + .setAction(async ({ files, output }, { run }) => { + console.log( + await run("flat:get-flattened-sources", { + files, + output, + }) + ) + }) + task("accounts", "Prints the list of accounts", require("./accounts")) task("gas-price", "Prints gas price").setAction(async function({ address }, { ethers }) { console.log("Gas price", (await ethers.provider.getGasPrice()).toString())