From 11974baa6f2def59e43e667ccf59dc2b6c1a0963 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Tue, 7 Sep 2021 00:25:15 +0200 Subject: [PATCH 01/21] started new model --- contracts/pool/IncentivizedPool.sol | 19 +++++++++ contracts/pool/TridentERC20.sol | 28 ++++++++----- contracts/rewards/RewardsManager.sol | 59 ++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 contracts/pool/IncentivizedPool.sol create mode 100644 contracts/rewards/RewardsManager.sol diff --git a/contracts/pool/IncentivizedPool.sol b/contracts/pool/IncentivizedPool.sol new file mode 100644 index 00000000..d60c6c2d --- /dev/null +++ b/contracts/pool/IncentivizedPool.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity >=0.8.0; + +import "./IndexPool.sol"; +import "../rewards/RewardsManager.sol"; + +/// @notice A pool that simply is an incentivized version of the index pool. +contract IncentivizedPool is IndexPool { + RewardsManager public immutable rewards; + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal override { + rewards.claimRewards(from); + } +} diff --git a/contracts/pool/TridentERC20.sol b/contracts/pool/TridentERC20.sol index 6da08569..36d49588 100644 --- a/contracts/pool/TridentERC20.sol +++ b/contracts/pool/TridentERC20.sol @@ -20,8 +20,7 @@ contract TridentERC20 { mapping(address => mapping(address => uint256)) public allowance; /// @notice The EIP-712 typehash for the permit struct used by this contract. - bytes32 public constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); /// @notice The EIP-712 typehash for this contract's domain. bytes32 public immutable DOMAIN_SEPARATOR; /// @notice owner -> nonce mapping used in {permit}. @@ -58,6 +57,8 @@ contract TridentERC20 { /// @param amount The token `amount` to move. /// @return (bool) Returns 'true' if succeeded. function transfer(address recipient, uint256 amount) external returns (bool) { + _beforeTokenTransfer(msg.sender, recipient, amount); + balanceOf[msg.sender] -= amount; // @dev This is safe from overflow - the sum of all user // balances can't exceed 'type(uint256).max'. @@ -82,6 +83,9 @@ contract TridentERC20 { allowance[sender][msg.sender] -= amount; } balanceOf[sender] -= amount; + + _beforeTokenTransfer(sender, recipient, amount); + // @dev This is safe from overflow - the sum of all user // balances can't exceed 'type(uint256).max'. unchecked { @@ -113,20 +117,17 @@ contract TridentERC20 { // beyond 'type(uint256).max' is exceedingly unlikely. unchecked { bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR, - keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, amount, nonces[owner]++, deadline)) - ) + abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, amount, nonces[owner]++, deadline))) ); - address recoveredAddress = ecrecover(digest, v, r, s); - require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_PERMIT_SIGNATURE"); - allowance[recoveredAddress][spender] = amount; + address recoveredAddress = ecrecover(digest, v, r, s); + require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_PERMIT_SIGNATURE"); + allowance[recoveredAddress][spender] = amount; } emit Approval(owner, spender, amount); } function _mint(address recipient, uint256 amount) internal { + _beforeTokenTransfer(address(0x0), recipient, amount); totalSupply += amount; // @dev This is safe from overflow - the sum of all user // balances can't exceed 'type(uint256).max'. @@ -137,6 +138,7 @@ contract TridentERC20 { } function _burn(address sender, uint256 amount) internal { + _beforeTokenTransfer(sender, address(0x0), amount); balanceOf[sender] -= amount; // @dev This is safe from underflow - users won't ever // have a balance larger than `totalSupply`. @@ -145,4 +147,10 @@ contract TridentERC20 { } emit Transfer(sender, address(0), amount); } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} } diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol new file mode 100644 index 00000000..c4097d17 --- /dev/null +++ b/contracts/rewards/RewardsManager.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity >=0.8.0; + +import "../interfaces/IPool.sol"; + +/// @notice Manages the rewards for various pools without requiring users to stake LP tokens. +contract RewardsManager { + /// @notice Info of each Incentivized 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 Info of each pool. + mapping(address => PoolInfo) public poolInfo; + + mapping(address => mapping(address => uint256)) public rewardDebt; + + // @TODO CHANGE SO ANYONE CAN CALL, BUT ALSO LET THE POOL CALL + function claimRewardsFor(address account) external { + IPool pool = IPool(msg.sender); + PoolInfo memory info = poolInfo[msg.sender]; + + if (block.number <= info.lastRewardBlock) { + return; + } + + if (pool.totalSupply == 0) { + info.lastRewardBlock = block.number; + return; + } + + // @TODO UPDATE TO V2 MATH + uint256 multiplier = getMultiplier(info.lastRewardBlock, block.number); + uint256 reward = (multiplier * sushiPerBlock() * info.allocPoint) / info.totalAllocPoint; + + info.accPerShare = info.accPerShare + ((reward * 1e12) / pool.totalSupply()); + + info.lastRewardBlock = block.number; + + if (pool.balanceOf(account) > 0) { + rewardToken.transfer(account, unclaimedRewardsFor(account)); + } + + rewardDebt[msg.sender][account] = ((pool.balanceOf(account) * info.accPerShare) / 1e12); + } + + function unclaimedRewardsFor(address account) public view returns (uint256) { + return ((balanceOf[account] * accPerShare) / 1e12) - rewardDebt[account]; + } + + function sushiPerBlock() public view returns (uint256 amount) { + amount = uint256(MASTERCHEF_SUSHI_PER_BLOCK).mul(MASTER_CHEF.poolInfo(MASTER_PID).allocPoint) / MASTER_CHEF.totalAllocPoint(); + } +} From d31a898826536b473b159123a460229a6c057df3 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Tue, 7 Sep 2021 12:19:21 +0200 Subject: [PATCH 02/21] fixes, moving to MCv2 --- contracts/pool/IncentivizedPool.sol | 2 +- contracts/rewards/RewardsManager.sol | 68 +++++++++++++++++----------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/contracts/pool/IncentivizedPool.sol b/contracts/pool/IncentivizedPool.sol index d60c6c2d..b30b8cfe 100644 --- a/contracts/pool/IncentivizedPool.sol +++ b/contracts/pool/IncentivizedPool.sol @@ -14,6 +14,6 @@ contract IncentivizedPool is IndexPool { address to, uint256 amount ) internal override { - rewards.claimRewards(from); + rewards.claimRewardsFor(this, from); } } diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index c4097d17..6a0b1820 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -6,7 +6,7 @@ import "../interfaces/IPool.sol"; /// @notice Manages the rewards for various pools without requiring users to stake LP tokens. contract RewardsManager { - /// @notice Info of each Incentivized pool. + /// @notice Info of each pool. /// `allocPoint` The amount of allocation points assigned to the pool. /// Also known as the amount of SUSHI to distribute per block. struct PoolInfo { @@ -15,45 +15,59 @@ contract RewardsManager { uint64 allocPoint; } - /// @notice Info of each pool. + /// `rewardDebt` The amount of SUSHI entitled to the user for a specific pool. + mapping(address => mapping(address => int256)) public rewardDebt; + mapping(address => PoolInfo) public poolInfo; - mapping(address => mapping(address => uint256)) public rewardDebt; + /// @dev Total allocation points. Must be the sum of all allocation points in all pools. + uint256 public totalAllocPoint; - // @TODO CHANGE SO ANYONE CAN CALL, BUT ALSO LET THE POOL CALL - function claimRewardsFor(address account) external { - IPool pool = IPool(msg.sender); - PoolInfo memory info = poolInfo[msg.sender]; + uint256 private constant MASTERCHEF_SUSHI_PER_BLOCK = 1e20; + uint256 private constant ACC_SUSHI_PRECISION = 1e12; - if (block.number <= info.lastRewardBlock) { - return; - } + event Harvest(address indexed user, uint256 indexed pid, uint256 amount); - if (pool.totalSupply == 0) { - info.lastRewardBlock = block.number; - return; - } + function claimRewardsFor(IPool pool, address account) external { + PoolInfo memory info = updatePool(pool); - // @TODO UPDATE TO V2 MATH - uint256 multiplier = getMultiplier(info.lastRewardBlock, block.number); - uint256 reward = (multiplier * sushiPerBlock() * info.allocPoint) / info.totalAllocPoint; + uint256 debt = rewardDebt[pool][account]; + uint256 amount = pool.balanceOf(account); - info.accPerShare = info.accPerShare + ((reward * 1e12) / pool.totalSupply()); + int256 accumulatedSushi = int256(amount.mul(info.accSushiPerShare) / ACC_SUSHI_PRECISION); + uint256 _pendingSushi = accumulatedSushi.sub(debt).toUInt256(); - info.lastRewardBlock = block.number; + // Effects + rewardDebt[pool][account] = accumulatedSushi; - if (pool.balanceOf(account) > 0) { - rewardToken.transfer(account, unclaimedRewardsFor(account)); + // Interactions + if (_pendingSushi != 0) { + SUSHI.safeTransfer(to, _pendingSushi); } - rewardDebt[msg.sender][account] = ((pool.balanceOf(account) * info.accPerShare) / 1e12); - } + IRewarder _rewarder = rewarder[pid]; + if (address(_rewarder) != address(0)) { + _rewarder.onSushiReward(pid, account, account, _pendingSushi, amount); + } - function unclaimedRewardsFor(address account) public view returns (uint256) { - return ((balanceOf[account] * accPerShare) / 1e12) - rewardDebt[account]; + emit Harvest(account, pid, _pendingSushi); } - 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 pool The address of the pool. See `poolInfo`. + /// @return info Returns the pool that was updated. + function updatePool(IPool pool) public returns (PoolInfo memory info) { + info = poolInfo[pool]; + if (block.number > info.lastRewardBlock) { + uint256 lpSupply = pool.totalSupply(); + if (lpSupply > 0) { + uint256 blocks = block.number.sub(info.lastRewardBlock); + uint256 sushiReward = blocks.mul(sushiPerBlock()).mul(info.allocPoint) / totalAllocPoint; + info.accSushiPerShare = info.accSushiPerShare.add((sushiReward.mul(ACC_SUSHI_PRECISION) / lpSupply).to128()); + } + info.lastRewardBlock = block.number.to64(); + poolInfo[pool] = info; + emit LogUpdatePool(pool, info.lastRewardBlock, lpSupply, info.accSushiPerShare); + } } } From 8d5c4b0f4e116c09bcc132b1261201852c49157e Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Tue, 7 Sep 2021 12:23:39 +0200 Subject: [PATCH 03/21] fixes --- contracts/interfaces/IERC20.sol | 5 +++++ contracts/rewards/RewardsManager.sol | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 contracts/interfaces/IERC20.sol diff --git a/contracts/interfaces/IERC20.sol b/contracts/interfaces/IERC20.sol new file mode 100644 index 00000000..c38309c4 --- /dev/null +++ b/contracts/interfaces/IERC20.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity >=0.8.0; + +interface IERC20 {} diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 6a0b1820..b9335263 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.0; import "../interfaces/IPool.sol"; +import "../interfaces/IRewarder.sol"; /// @notice Manages the rewards for various pools without requiring users to stake LP tokens. contract RewardsManager { @@ -20,6 +21,8 @@ contract RewardsManager { mapping(address => PoolInfo) public poolInfo; + mapping(address => IRewarder) public rewarder; + /// @dev Total allocation points. Must be the sum of all allocation points in all pools. uint256 public totalAllocPoint; @@ -34,18 +37,18 @@ contract RewardsManager { uint256 debt = rewardDebt[pool][account]; uint256 amount = pool.balanceOf(account); - int256 accumulatedSushi = int256(amount.mul(info.accSushiPerShare) / ACC_SUSHI_PRECISION); - uint256 _pendingSushi = accumulatedSushi.sub(debt).toUInt256(); + int256 accumulatedSushi = int256((amount * info.accSushiPerShare) / ACC_SUSHI_PRECISION); + uint256 _pendingSushi = uint256(accumulatedSushi - debt); // Effects rewardDebt[pool][account] = accumulatedSushi; // Interactions if (_pendingSushi != 0) { - SUSHI.safeTransfer(to, _pendingSushi); + SUSHI.safeTransfer(account, _pendingSushi); } - IRewarder _rewarder = rewarder[pid]; + IRewarder _rewarder = rewarder[address(pool)]; if (address(_rewarder) != address(0)) { _rewarder.onSushiReward(pid, account, account, _pendingSushi, amount); } @@ -70,4 +73,9 @@ contract RewardsManager { emit LogUpdatePool(pool, info.lastRewardBlock, lpSupply, info.accSushiPerShare); } } + + /// @notice Calculates and returns the `amount` of SUSHI per block. + function sushiPerBlock() public view returns (uint256 amount) { + amount = (uint256(MASTERCHEF_SUSHI_PER_BLOCK) * MASTER_CHEF.poolInfo(MASTER_PID).allocPoint) / MASTER_CHEF.totalAllocPoint(); + } } From 296b5f5f6210a7d04c97cba2ce5922efdd1c6a86 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Tue, 7 Sep 2021 13:58:32 +0200 Subject: [PATCH 04/21] rewarder --- contracts/interfaces/IRewarder.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 contracts/interfaces/IRewarder.sol diff --git a/contracts/interfaces/IRewarder.sol b/contracts/interfaces/IRewarder.sol new file mode 100644 index 00000000..73647e19 --- /dev/null +++ b/contracts/interfaces/IRewarder.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity >=0.8.0; + +import "./IERC20.sol"; + +interface IRewarder { + 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); +} From 4386df3da98074af7992be3c9ca730854dd7a4ad Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Tue, 7 Sep 2021 14:38:43 +0200 Subject: [PATCH 05/21] fix --- contracts/interfaces/IERC20.sol | 16 +++++++- contracts/interfaces/IMasterChef.sol | 5 +++ contracts/interfaces/IRewarder.sol | 4 +- contracts/pool/IncentivizedPool.sol | 12 +++++- contracts/rewards/RewardsManager.sol | 55 ++++++++++++++++------------ 5 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 contracts/interfaces/IMasterChef.sol diff --git a/contracts/interfaces/IERC20.sol b/contracts/interfaces/IERC20.sol index c38309c4..7e326859 100644 --- a/contracts/interfaces/IERC20.sol +++ b/contracts/interfaces/IERC20.sol @@ -2,4 +2,18 @@ pragma solidity >=0.8.0; -interface IERC20 {} +interface IERC20 { + function approve(address, uint256) external returns (bool); + + function transfer(address, uint256) external returns (bool); + + function transferFrom( + address, + address, + uint256 + ) external returns (bool); + + function balanceOf(address) external view returns (uint256); + + function totalSupply() external view returns (uint256); +} diff --git a/contracts/interfaces/IMasterChef.sol b/contracts/interfaces/IMasterChef.sol new file mode 100644 index 00000000..32fb2b12 --- /dev/null +++ b/contracts/interfaces/IMasterChef.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.4; + +interface IMasterChef { + function sushiPerBlock() external view returns (uint256); +} diff --git a/contracts/interfaces/IRewarder.sol b/contracts/interfaces/IRewarder.sol index 73647e19..ab8c31de 100644 --- a/contracts/interfaces/IRewarder.sol +++ b/contracts/interfaces/IRewarder.sol @@ -6,7 +6,7 @@ import "./IERC20.sol"; interface IRewarder { function onSushiReward( - uint256 pid, + address pool, address user, address recipient, uint256 sushiAmount, @@ -14,7 +14,7 @@ interface IRewarder { ) external; function pendingTokens( - uint256 pid, + address pool, address user, uint256 sushiAmount ) external view returns (IERC20[] memory, uint256[] memory); diff --git a/contracts/pool/IncentivizedPool.sol b/contracts/pool/IncentivizedPool.sol index b30b8cfe..057eaa9c 100644 --- a/contracts/pool/IncentivizedPool.sol +++ b/contracts/pool/IncentivizedPool.sol @@ -7,13 +7,23 @@ import "../rewards/RewardsManager.sol"; /// @notice A pool that simply is an incentivized version of the index pool. contract IncentivizedPool is IndexPool { - RewardsManager public immutable rewards; + RewardsManager public rewards; + + constructor(bytes memory _deployData, address _masterDeployer) IndexPool(_deployData, _masterDeployer) { + (, , , address _rewards) = abi.decode(_deployData, (address[], uint256[], uint256, address)); + + rewards = RewardsManager(_rewards); + } function _beforeTokenTransfer( address from, address to, uint256 amount ) internal override { + if (address(rewards) == address(0)) { + return; + } + rewards.claimRewardsFor(this, from); } } diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index b9335263..9b81c9fe 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -4,6 +4,11 @@ pragma solidity >=0.8.0; import "../interfaces/IPool.sol"; import "../interfaces/IRewarder.sol"; +import "../interfaces/IMasterChef.sol"; + +interface Sushi { + function safeTransfer(address, uint256) external returns (bool); +} /// @notice Manages the rewards for various pools without requiring users to stake LP tokens. contract RewardsManager { @@ -16,12 +21,11 @@ contract RewardsManager { uint64 allocPoint; } - /// `rewardDebt` The amount of SUSHI entitled to the user for a specific pool. - mapping(address => mapping(address => int256)) public rewardDebt; - - mapping(address => PoolInfo) public poolInfo; + /// @notice Address of MCV1 contract. + IMasterChef public immutable MASTER_CHEF; - mapping(address => IRewarder) public rewarder; + /// @notice Address of SUSHI contract. + Sushi public immutable SUSHI; /// @dev Total allocation points. Must be the sum of all allocation points in all pools. uint256 public totalAllocPoint; @@ -29,19 +33,27 @@ contract RewardsManager { uint256 private constant MASTERCHEF_SUSHI_PER_BLOCK = 1e20; uint256 private constant ACC_SUSHI_PRECISION = 1e12; - event Harvest(address indexed user, uint256 indexed pid, uint256 amount); + /// `rewardDebt` The amount of SUSHI entitled to the user for a specific pool. + mapping(address => mapping(address => int256)) public rewardDebt; + + mapping(address => PoolInfo) public poolInfo; + + mapping(address => IRewarder) public rewarder; + + event Harvest(address indexed user, address indexed pid, uint256 amount); + event LogUpdatePool(address indexed pid, uint64 lastRewardBlock, uint256 lpSupply, uint256 accSushiPerShare); function claimRewardsFor(IPool pool, address account) external { PoolInfo memory info = updatePool(pool); - uint256 debt = rewardDebt[pool][account]; - uint256 amount = pool.balanceOf(account); + int256 debt = rewardDebt[address(pool)][account]; + uint256 amount = IERC20(address(pool)).balanceOf(account); int256 accumulatedSushi = int256((amount * info.accSushiPerShare) / ACC_SUSHI_PRECISION); uint256 _pendingSushi = uint256(accumulatedSushi - debt); // Effects - rewardDebt[pool][account] = accumulatedSushi; + rewardDebt[address(pool)][account] = accumulatedSushi; // Interactions if (_pendingSushi != 0) { @@ -50,32 +62,27 @@ contract RewardsManager { IRewarder _rewarder = rewarder[address(pool)]; if (address(_rewarder) != address(0)) { - _rewarder.onSushiReward(pid, account, account, _pendingSushi, amount); + _rewarder.onSushiReward(address(pool), account, account, _pendingSushi, amount); } - emit Harvest(account, pid, _pendingSushi); + emit Harvest(account, address(pool), _pendingSushi); } /// @notice Update reward variables of the given pool. /// @param pool The address of the pool. See `poolInfo`. /// @return info Returns the pool that was updated. function updatePool(IPool pool) public returns (PoolInfo memory info) { - info = poolInfo[pool]; + info = poolInfo[address(pool)]; if (block.number > info.lastRewardBlock) { - uint256 lpSupply = pool.totalSupply(); + uint256 lpSupply = IERC20(address(pool)).totalSupply(); if (lpSupply > 0) { - uint256 blocks = block.number.sub(info.lastRewardBlock); - uint256 sushiReward = blocks.mul(sushiPerBlock()).mul(info.allocPoint) / totalAllocPoint; - info.accSushiPerShare = info.accSushiPerShare.add((sushiReward.mul(ACC_SUSHI_PRECISION) / lpSupply).to128()); + uint256 blocks = block.number - info.lastRewardBlock; + uint256 sushiReward = (blocks * MASTER_CHEF.sushiPerBlock() * info.allocPoint) / totalAllocPoint; + info.accSushiPerShare = info.accSushiPerShare + uint128((sushiReward * ACC_SUSHI_PRECISION) / lpSupply); } - info.lastRewardBlock = block.number.to64(); - poolInfo[pool] = info; - emit LogUpdatePool(pool, info.lastRewardBlock, lpSupply, info.accSushiPerShare); + info.lastRewardBlock = uint64(block.number); + poolInfo[address(pool)] = info; + emit LogUpdatePool(address(pool), info.lastRewardBlock, lpSupply, info.accSushiPerShare); } } - - /// @notice Calculates and returns the `amount` of SUSHI per block. - function sushiPerBlock() public view returns (uint256 amount) { - amount = (uint256(MASTERCHEF_SUSHI_PER_BLOCK) * MASTER_CHEF.poolInfo(MASTER_PID).allocPoint) / MASTER_CHEF.totalAllocPoint(); - } } From 6d35172f3d9783575227da61273130a1edc690d1 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Tue, 7 Sep 2021 14:43:54 +0200 Subject: [PATCH 06/21] fix constructor --- contracts/rewards/RewardsManager.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 9b81c9fe..08cda376 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -43,6 +43,11 @@ contract RewardsManager { event Harvest(address indexed user, address indexed pid, uint256 amount); event LogUpdatePool(address indexed pid, uint64 lastRewardBlock, uint256 lpSupply, uint256 accSushiPerShare); + constructor(IMasterChef _MASTER_CHEF, Sushi _SUSHI) { + MASTER_CHEF = _MASTER_CHEF; + SUSHI = _SUSHI; + } + function claimRewardsFor(IPool pool, address account) external { PoolInfo memory info = updatePool(pool); From 2dd57d814225a0996c9a6d88aef7f91d719bdfa0 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Tue, 7 Sep 2021 18:02:12 +0200 Subject: [PATCH 07/21] added from masterchef --- contracts/rewards/RewardsManager.sol | 32 +++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 08cda376..fed10ac7 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -5,13 +5,14 @@ pragma solidity >=0.8.0; import "../interfaces/IPool.sol"; import "../interfaces/IRewarder.sol"; import "../interfaces/IMasterChef.sol"; +import "../utils/TridentOwnable.sol"; interface Sushi { function safeTransfer(address, uint256) external returns (bool); } /// @notice Manages the rewards for various pools without requiring users to stake LP tokens. -contract RewardsManager { +contract RewardsManager is TridentOwnable { /// @notice Info of each pool. /// `allocPoint` The amount of allocation points assigned to the pool. /// Also known as the amount of SUSHI to distribute per block. @@ -42,12 +43,41 @@ contract RewardsManager { event Harvest(address indexed user, address indexed pid, uint256 amount); event LogUpdatePool(address indexed pid, uint64 lastRewardBlock, uint256 lpSupply, uint256 accSushiPerShare); + event LogSetPool(address indexed pid, uint256 allocPoint, IRewarder indexed rewarder, bool overwrite); constructor(IMasterChef _MASTER_CHEF, Sushi _SUSHI) { MASTER_CHEF = _MASTER_CHEF; SUSHI = _SUSHI; } + /// @notice Update the given pool's SUSHI allocation point and `IRewarder` contract. Can only be called by the owner. + /// @param _pool The address 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( + address _pool, + uint256 _allocPoint, + IRewarder _rewarder, + bool overwrite + ) public onlyOwner { + totalAllocPoint = (totalAllocPoint - poolInfo[_pool].allocPoint) + _allocPoint; + poolInfo[_pool].allocPoint = uint64(_allocPoint); + if (overwrite) { + rewarder[_pool] = _rewarder; + } + emit LogSetPool(_pool, _allocPoint, overwrite ? _rewarder : rewarder[_pool], overwrite); + } + + /// @notice Update reward variables for all pools. Be careful of gas spending! + /// @param pools Pool addresses of all to be updated. Make sure to update all active pools. + function massUpdatePools(IPool[] calldata pools) external { + uint256 len = pools.length; + for (uint256 i = 0; i < len; ++i) { + updatePool(pools[i]); + } + } + function claimRewardsFor(IPool pool, address account) external { PoolInfo memory info = updatePool(pool); From f6ed58d257e43174b32fc0c0189cc6ec387d79db Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Tue, 7 Sep 2021 18:07:46 +0200 Subject: [PATCH 08/21] notes --- contracts/rewards/RewardsManager.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index fed10ac7..dbb9c90a 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -78,6 +78,9 @@ contract RewardsManager is TridentOwnable { } } + /// @notice Harvest rewards for a specific account for a given pool. + /// @param pool The address of the pool. See `poolInfo`. + /// @param account The account to claim for. function claimRewardsFor(IPool pool, address account) external { PoolInfo memory info = updatePool(pool); From ba1a5ea197ea3fb3434526eaaa4c3bef76a95dcd Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Tue, 7 Sep 2021 19:58:21 +0200 Subject: [PATCH 09/21] fix --- contracts/pool/IncentivizedPool.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/pool/IncentivizedPool.sol b/contracts/pool/IncentivizedPool.sol index 057eaa9c..3dac09c4 100644 --- a/contracts/pool/IncentivizedPool.sol +++ b/contracts/pool/IncentivizedPool.sol @@ -24,6 +24,12 @@ contract IncentivizedPool is IndexPool { return; } - rewards.claimRewardsFor(this, from); + if (from != address(0)) { + rewards.claimRewardsFor(this, from); + } + + if (to != address(0)) { + rewards.claimRewardsFor(this, to); + } } } From bd4699b0a5fd989fec900619c1df60d9d585c9a0 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Sat, 11 Sep 2021 18:04:41 +0200 Subject: [PATCH 10/21] setting block --- contracts/pool/IncentivizedPoolFactory.sol | 21 +++ contracts/rewards/RewardsManager.sol | 5 + test/IncentivizedPool.test.ts | 197 +++++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 contracts/pool/IncentivizedPoolFactory.sol create mode 100644 test/IncentivizedPool.test.ts diff --git a/contracts/pool/IncentivizedPoolFactory.sol b/contracts/pool/IncentivizedPoolFactory.sol new file mode 100644 index 00000000..2ac6e615 --- /dev/null +++ b/contracts/pool/IncentivizedPoolFactory.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity >=0.8.0; + +import "./IncentivizedPool.sol"; +import "./PoolDeployer.sol"; + +/// @notice Contract for deploying Trident exchange Incentivized Pool with configurations. +/// @author Dean Eigenmann +contract IncentivizedPoolFactory is PoolDeployer { + constructor(address _masterDeployer) PoolDeployer(_masterDeployer) {} + + function deployPool(bytes memory _deployData) external returns (address pool) { + (address[] memory tokens, , , ) = abi.decode(_deployData, (address[], uint256[], uint256, address)); + + // @dev Salt is not actually needed since `_deployData` is part of creationCode and already contains the salt. + bytes32 salt = keccak256(_deployData); + pool = address(new IncentivizedPool{salt: salt}(_deployData, masterDeployer)); + _registerPool(pool, tokens, salt); + } +} diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index dbb9c90a..adc754ac 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -66,6 +66,11 @@ contract RewardsManager is TridentOwnable { if (overwrite) { rewarder[_pool] = _rewarder; } + + if (poolInfo[_pool].lastRewardBlock == 0) { + poolInfo[_pool].lastRewardBlock = block.number; + } + emit LogSetPool(_pool, _allocPoint, overwrite ? _rewarder : rewarder[_pool], overwrite); } diff --git a/test/IncentivizedPool.test.ts b/test/IncentivizedPool.test.ts new file mode 100644 index 00000000..a70b094a --- /dev/null +++ b/test/IncentivizedPool.test.ts @@ -0,0 +1,197 @@ +//@ts-nocheck + +import { BigNumber } from "@ethersproject/bignumber"; +import { ethers } from "hardhat"; +import { getBigNumber } from "./utilities"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; +import { Contract, ContractFactory } from "ethers"; +import { expect } from "chai"; +import { calcOutByIn } from "@sushiswap/sdk"; + +// ------------- PARAMETERS ------------- + +// alice's usdt/usdc balance +const aliceUSDTBalance: BigNumber = getBigNumber("100000000000000000"); +const aliceUSDCBalance: BigNumber = getBigNumber("100000000000000000"); + +// what each ERC20 is deployed with +const ERCDeployAmount: BigNumber = getBigNumber("1000000000000000000"); + +// what gets minted for alice on the pool +const poolMintAmount: BigNumber = getBigNumber("1", 16); + +// token weights passed into the pool +const tokenWeights: BigNumber[] = [getBigNumber("10"), getBigNumber("10")]; + +// pool swap fee +const poolSwapFee: number | BigNumber = getBigNumber("1", 13); + +// ------------- ------------- + +function encodeSwapData( + tokenIn: string, + tokenOut: string, + recipient: string, + unwrapBento: boolean, + amountIn: BigNumber | number +): string { + return ethers.utils.defaultAbiCoder.encode( + ["address", "address", "address", "bool", "uint256"], + [tokenIn, tokenOut, recipient, unwrapBento, amountIn] + ); +} + +describe("IncentivizedPool test", function () { + let alice: SignerWithAddress, + feeTo: SignerWithAddress, + usdt: Contract, + usdc: Contract, + weth: Contract, + bento: Contract, + masterDeployer: Contract, + tridentPoolFactory: Contract, + router: Contract, + Pool: ContractFactory; + + async function deployPool(): Promise { + const ERC20 = await ethers.getContractFactory("ERC20Mock"); + const Bento = await ethers.getContractFactory("BentoBoxV1"); + const Deployer = await ethers.getContractFactory("MasterDeployer"); + const PoolFactory = await ethers.getContractFactory( + "IncentivizedPoolFactory" + ); + const SwapRouter = await ethers.getContractFactory("TridentRouter"); + Pool = await ethers.getContractFactory("IncentivizedPool"); + [alice, feeTo] = await ethers.getSigners(); + // deploy erc20's + weth = await ERC20.deploy("WETH", "WETH", ERCDeployAmount); + await weth.deployed(); + usdt = await ERC20.deploy("USDT", "USDT", ERCDeployAmount); + await usdt.deployed(); + usdc = await ERC20.deploy("USDC", "USDC", ERCDeployAmount); + await usdc.deployed(); + + bento = await Bento.deploy(weth.address); + await bento.deployed(); + + masterDeployer = await Deployer.deploy(17, feeTo.address, bento.address); + await masterDeployer.deployed(); + + tridentPoolFactory = await PoolFactory.deploy(masterDeployer.address); + await tridentPoolFactory.deployed(); + router = await SwapRouter.deploy( + bento.address, + masterDeployer.address, + weth.address + ); + await router.deployed(); + + // Whitelist pool factory in master deployer + await masterDeployer.addToWhitelist(tridentPoolFactory.address); + + // Whitelist Router on BentoBox + await bento.whitelistMasterContract(router.address, true); + // Approve BentoBox token deposits + await usdc.approve(bento.address, ERCDeployAmount); + await usdt.approve(bento.address, ERCDeployAmount); + // Make BentoBox token deposits + await bento.deposit( + usdc.address, + alice.address, + alice.address, + ERCDeployAmount, + 0 + ); + await bento.deposit( + usdt.address, + alice.address, + alice.address, + ERCDeployAmount, + 0 + ); + // Approve Router to spend 'alice' BentoBox tokens + await bento.setMasterContractApproval( + alice.address, + router.address, + true, + "0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + + const tokens: string[] = + usdt.address.toUpperCase() < usdc.address.toUpperCase() + ? [usdt.address, usdc.address] + : [usdc.address, usdt.address]; + + // address[], uint256[], uint256 + const deployData = ethers.utils.defaultAbiCoder.encode( + ["address[]", "uint256[]", "uint256", "address"], + [tokens, tokenWeights, poolSwapFee, alice.address] // @TODO + ); + + let tx = await ( + await masterDeployer.deployPool(tridentPoolFactory.address, deployData) + ).wait(); + const pool: Contract = await Pool.attach(tx.events[1].args.pool); + + await bento.transfer( + usdt.address, + alice.address, + pool.address, + aliceUSDTBalance + ); + await bento.transfer( + usdc.address, + alice.address, + pool.address, + aliceUSDCBalance + ); + + await pool.mint( + ethers.utils.defaultAbiCoder.encode( + ["address", "uint256"], + [alice.address, poolMintAmount] + ) + ); + + return pool; + } + + it.skip("pool balance should be equal to transferred value", async function () { + const pool: Contract = await deployPool(); + + interface PoolInfo { + type: string; + reserve0: BigNumber; + reserve1: BigNumber; + fee: number; + } + + const poolInfo: PoolInfo = { + type: "Weighted", + reserve0: aliceUSDTBalance, + reserve1: aliceUSDCBalance, + fee: poolSwapFee, + }; + + const transferredToPoolLiquidity = poolMintAmount; + const poolUSDTBalance = (await pool.records(usdt.address)).balance; + const poolUSDCBalance = (await pool.records(usdc.address)).balance; + + expect(transferredToPoolLiquidity.eq(poolUSDTBalance)).to.be.true; + expect(transferredToPoolLiquidity.eq(poolUSDCBalance)).to.be.true; + + // let tx = await pool + // .connect(alice) + // .swap( + // encodeSwapData(usdt.address, usdc.address, alice.address, false, 1) + // ); + + // const out = calcOutByIn(poolInfo, 1, false); + // console.log(out); + // await expect(tx).to.eventually.be.rejectedWith( + // "VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)" + // ); + }); +}); From 182517b8fe206b4009f092ddbe6e26ea66641c98 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Sat, 11 Sep 2021 18:09:53 +0200 Subject: [PATCH 11/21] whoops --- contracts/rewards/RewardsManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index adc754ac..14faecfc 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -68,7 +68,7 @@ contract RewardsManager is TridentOwnable { } if (poolInfo[_pool].lastRewardBlock == 0) { - poolInfo[_pool].lastRewardBlock = block.number; + poolInfo[_pool].lastRewardBlock = uint64(block.number); } emit LogSetPool(_pool, _allocPoint, overwrite ? _rewarder : rewarder[_pool], overwrite); From 9123a7e0b430999a6f61544b5161f6a7030374d8 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Sat, 11 Sep 2021 18:28:06 +0200 Subject: [PATCH 12/21] fix --- contracts/rewards/RewardsManager.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 14faecfc..3127f4d1 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -12,6 +12,7 @@ interface Sushi { } /// @notice Manages the rewards for various pools without requiring users to stake LP tokens. +/// Based on MasterChefV2. contract RewardsManager is TridentOwnable { /// @notice Info of each pool. /// `allocPoint` The amount of allocation points assigned to the pool. From 5a3fad2720ca8290d2f4ac6983b7dde2dda840b2 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Sat, 11 Sep 2021 18:29:54 +0200 Subject: [PATCH 13/21] unused --- contracts/rewards/RewardsManager.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 3127f4d1..09147ea2 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -32,7 +32,6 @@ contract RewardsManager is TridentOwnable { /// @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; /// `rewardDebt` The amount of SUSHI entitled to the user for a specific pool. From 51c052d1f26310a9c1f29478dc04dd4ce142889a Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Sat, 11 Sep 2021 18:50:14 +0200 Subject: [PATCH 14/21] using zeppelin interface --- contracts/interfaces/IERC20.sol | 19 ------------------- contracts/interfaces/IMasterChef.sol | 2 ++ contracts/interfaces/IRewarder.sol | 2 +- contracts/pool/IncentivizedPool.sol | 2 +- 4 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 contracts/interfaces/IERC20.sol diff --git a/contracts/interfaces/IERC20.sol b/contracts/interfaces/IERC20.sol deleted file mode 100644 index 7e326859..00000000 --- a/contracts/interfaces/IERC20.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity >=0.8.0; - -interface IERC20 { - function approve(address, uint256) external returns (bool); - - function transfer(address, uint256) external returns (bool); - - function transferFrom( - address, - address, - uint256 - ) external returns (bool); - - function balanceOf(address) external view returns (uint256); - - function totalSupply() external view returns (uint256); -} diff --git a/contracts/interfaces/IMasterChef.sol b/contracts/interfaces/IMasterChef.sol index 32fb2b12..f5425899 100644 --- a/contracts/interfaces/IMasterChef.sol +++ b/contracts/interfaces/IMasterChef.sol @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + pragma solidity ^0.8.4; interface IMasterChef { diff --git a/contracts/interfaces/IRewarder.sol b/contracts/interfaces/IRewarder.sol index ab8c31de..dd4fd10b 100644 --- a/contracts/interfaces/IRewarder.sol +++ b/contracts/interfaces/IRewarder.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.0; -import "./IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IRewarder { function onSushiReward( diff --git a/contracts/pool/IncentivizedPool.sol b/contracts/pool/IncentivizedPool.sol index 3dac09c4..fa7dc009 100644 --- a/contracts/pool/IncentivizedPool.sol +++ b/contracts/pool/IncentivizedPool.sol @@ -18,7 +18,7 @@ contract IncentivizedPool is IndexPool { function _beforeTokenTransfer( address from, address to, - uint256 amount + uint256 ) internal override { if (address(rewards) == address(0)) { return; From 37b251f5c06c03e6f990b9be164b57e7440c68f9 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Mon, 13 Sep 2021 11:47:58 +0200 Subject: [PATCH 15/21] fixes & tests --- contracts/mocks/ERC20Mock.sol | 5 +++ contracts/rewards/RewardsManager.sol | 48 +++++++++++++++++++++------- test/IncentivizedPool.test.ts | 32 ++++++++++++++++++- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/contracts/mocks/ERC20Mock.sol b/contracts/mocks/ERC20Mock.sol index 80393a96..6fc77a82 100644 --- a/contracts/mocks/ERC20Mock.sol +++ b/contracts/mocks/ERC20Mock.sol @@ -12,4 +12,9 @@ contract ERC20Mock is ERC20 { ) ERC20(name, symbol) { _mint(msg.sender, supply); } + + function safeTransfer(address recipient, uint256 amount) external returns (bool) { + _transfer(msg.sender, recipient, amount); + return true; + } } diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 09147ea2..5ce8bc53 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -23,15 +23,14 @@ contract RewardsManager is TridentOwnable { uint64 allocPoint; } - /// @notice Address of MCV1 contract. - IMasterChef public immutable MASTER_CHEF; - /// @notice Address of SUSHI contract. Sushi public immutable SUSHI; /// @dev Total allocation points. Must be the sum of all allocation points in all pools. uint256 public totalAllocPoint; + address private constant MASTER_PID = address(0); + uint256 private constant MASTERCHEF_SUSHI_PER_BLOCK = 1e20; uint256 private constant ACC_SUSHI_PRECISION = 1e12; /// `rewardDebt` The amount of SUSHI entitled to the user for a specific pool. @@ -45,11 +44,29 @@ contract RewardsManager is TridentOwnable { event LogUpdatePool(address indexed pid, uint64 lastRewardBlock, uint256 lpSupply, uint256 accSushiPerShare); event LogSetPool(address indexed pid, uint256 allocPoint, IRewarder indexed rewarder, bool overwrite); - constructor(IMasterChef _MASTER_CHEF, Sushi _SUSHI) { - MASTER_CHEF = _MASTER_CHEF; + constructor(Sushi _SUSHI) { SUSHI = _SUSHI; } + /// @notice View function to see pending SUSHI on frontend. + /// @param _pid The address of the pool. See `poolInfo`. + /// @param _user Address of user. + /// @return pending SUSHI reward for a given user. + function pendingSushi(address _pid, address _user) external view returns (uint256 pending) { + PoolInfo memory pool = poolInfo[_pid]; + uint256 accSushiPerShare = pool.accSushiPerShare; + + uint256 lpSupply = IERC20(address(_pid)).totalSupply(); + if (block.number > pool.lastRewardBlock && lpSupply != 0) { + uint256 blocks = block.number - pool.lastRewardBlock; + uint256 sushiReward = (blocks * sushiPerBlock() * pool.allocPoint) / totalAllocPoint; + accSushiPerShare = accSushiPerShare + ((sushiReward * ACC_SUSHI_PRECISION) / lpSupply); + } + + uint256 amount = IERC20(address(_pid)).balanceOf(_user); + pending = uint256(int256((amount * (accSushiPerShare)) / ACC_SUSHI_PRECISION) - (rewardDebt[_pid][_user])); + } + /// @notice Update the given pool's SUSHI allocation point and `IRewarder` contract. Can only be called by the owner. /// @param _pool The address of the pool. See `poolInfo`. /// @param _allocPoint New AP of the pool. @@ -61,16 +78,20 @@ contract RewardsManager is TridentOwnable { IRewarder _rewarder, bool overwrite ) public onlyOwner { - totalAllocPoint = (totalAllocPoint - poolInfo[_pool].allocPoint) + _allocPoint; + PoolInfo memory info = poolInfo[_pool]; + if (info.lastRewardBlock == 0 && info.allocPoint == 0) { + totalAllocPoint = totalAllocPoint + _allocPoint; + poolInfo[_pool].lastRewardBlock = uint64(block.number); + } else { + totalAllocPoint = (totalAllocPoint - poolInfo[_pool].allocPoint) + _allocPoint; + } + poolInfo[_pool].allocPoint = uint64(_allocPoint); + if (overwrite) { rewarder[_pool] = _rewarder; } - if (poolInfo[_pool].lastRewardBlock == 0) { - poolInfo[_pool].lastRewardBlock = uint64(block.number); - } - emit LogSetPool(_pool, _allocPoint, overwrite ? _rewarder : rewarder[_pool], overwrite); } @@ -120,7 +141,7 @@ contract RewardsManager is TridentOwnable { uint256 lpSupply = IERC20(address(pool)).totalSupply(); if (lpSupply > 0) { uint256 blocks = block.number - info.lastRewardBlock; - uint256 sushiReward = (blocks * MASTER_CHEF.sushiPerBlock() * info.allocPoint) / totalAllocPoint; + uint256 sushiReward = (blocks * sushiPerBlock() * info.allocPoint) / totalAllocPoint; info.accSushiPerShare = info.accSushiPerShare + uint128((sushiReward * ACC_SUSHI_PRECISION) / lpSupply); } info.lastRewardBlock = uint64(block.number); @@ -128,4 +149,9 @@ contract RewardsManager is TridentOwnable { emit LogUpdatePool(address(pool), info.lastRewardBlock, lpSupply, info.accSushiPerShare); } } + + /// @notice Calculates and returns the `amount` of SUSHI per block. + function sushiPerBlock() public view returns (uint256 amount) { + amount = uint256(MASTERCHEF_SUSHI_PER_BLOCK * poolInfo[MASTER_PID].allocPoint) / totalAllocPoint; + } } diff --git a/test/IncentivizedPool.test.ts b/test/IncentivizedPool.test.ts index a70b094a..a865e988 100644 --- a/test/IncentivizedPool.test.ts +++ b/test/IncentivizedPool.test.ts @@ -26,6 +26,8 @@ const tokenWeights: BigNumber[] = [getBigNumber("10"), getBigNumber("10")]; // pool swap fee const poolSwapFee: number | BigNumber = getBigNumber("1", 13); +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + // ------------- ------------- function encodeSwapData( @@ -51,12 +53,15 @@ describe("IncentivizedPool test", function () { masterDeployer: Contract, tridentPoolFactory: Contract, router: Contract, + rewardToken: Contract, + rewardsManager: Contract, Pool: ContractFactory; async function deployPool(): Promise { const ERC20 = await ethers.getContractFactory("ERC20Mock"); const Bento = await ethers.getContractFactory("BentoBoxV1"); const Deployer = await ethers.getContractFactory("MasterDeployer"); + const RewardsManager = await ethers.getContractFactory("RewardsManager"); const PoolFactory = await ethers.getContractFactory( "IncentivizedPoolFactory" ); @@ -70,6 +75,11 @@ describe("IncentivizedPool test", function () { await usdt.deployed(); usdc = await ERC20.deploy("USDC", "USDC", ERCDeployAmount); await usdc.deployed(); + rewardToken = await ERC20.deploy("SUSHI", "SUSHI", ERCDeployAmount); + await rewardToken.deployed(); + + rewardsManager = await RewardsManager.deploy(rewardToken.address); + await rewardsManager.deployed(); bento = await Bento.deploy(weth.address); await bento.deployed(); @@ -127,7 +137,7 @@ describe("IncentivizedPool test", function () { // address[], uint256[], uint256 const deployData = ethers.utils.defaultAbiCoder.encode( ["address[]", "uint256[]", "uint256", "address"], - [tokens, tokenWeights, poolSwapFee, alice.address] // @TODO + [tokens, tokenWeights, poolSwapFee, rewardsManager.address] ); let tx = await ( @@ -135,6 +145,10 @@ describe("IncentivizedPool test", function () { ).wait(); const pool: Contract = await Pool.attach(tx.events[1].args.pool); + await rewardsManager.set(ZERO_ADDRESS, 10, ZERO_ADDRESS, true); + await rewardsManager.set(pool.address, 10, ZERO_ADDRESS, true); + await rewardToken.transfer(rewardsManager.address, ERCDeployAmount); + await bento.transfer( usdt.address, alice.address, @@ -194,4 +208,20 @@ describe("IncentivizedPool test", function () { // "VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)" // ); }); + + it("should pay out rewards", async () => { + const pool: Contract = await deployPool(); + + expect( + ( + await rewardsManager.pendingSushi(pool.address, alice.address) + ).toString() + ).to.not.be.eq("0"); + await pool.transfer(ZERO_ADDRESS, 0); + expect( + ( + await rewardsManager.pendingSushi(pool.address, alice.address) + ).toNumber() + ).to.eq(0); + }); }); From 2cfd08489953f248f28f22d3a6d5978293aa3458 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Mon, 13 Sep 2021 12:21:34 +0200 Subject: [PATCH 16/21] ierc20 --- contracts/mocks/ERC20Mock.sol | 5 ----- contracts/rewards/RewardsManager.sol | 11 +++++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/contracts/mocks/ERC20Mock.sol b/contracts/mocks/ERC20Mock.sol index 6fc77a82..80393a96 100644 --- a/contracts/mocks/ERC20Mock.sol +++ b/contracts/mocks/ERC20Mock.sol @@ -12,9 +12,4 @@ contract ERC20Mock is ERC20 { ) ERC20(name, symbol) { _mint(msg.sender, supply); } - - function safeTransfer(address recipient, uint256 amount) external returns (bool) { - _transfer(msg.sender, recipient, amount); - return true; - } } diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 5ce8bc53..17dc0193 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -7,9 +7,8 @@ import "../interfaces/IRewarder.sol"; import "../interfaces/IMasterChef.sol"; import "../utils/TridentOwnable.sol"; -interface Sushi { - function safeTransfer(address, uint256) external returns (bool); -} +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../flat/BentoBoxV1Flat.sol"; /// @notice Manages the rewards for various pools without requiring users to stake LP tokens. /// Based on MasterChefV2. @@ -24,7 +23,7 @@ contract RewardsManager is TridentOwnable { } /// @notice Address of SUSHI contract. - Sushi public immutable SUSHI; + IERC20 public immutable SUSHI; /// @dev Total allocation points. Must be the sum of all allocation points in all pools. uint256 public totalAllocPoint; @@ -44,7 +43,7 @@ contract RewardsManager is TridentOwnable { event LogUpdatePool(address indexed pid, uint64 lastRewardBlock, uint256 lpSupply, uint256 accSushiPerShare); event LogSetPool(address indexed pid, uint256 allocPoint, IRewarder indexed rewarder, bool overwrite); - constructor(Sushi _SUSHI) { + constructor(IERC20 _SUSHI) { SUSHI = _SUSHI; } @@ -121,7 +120,7 @@ contract RewardsManager is TridentOwnable { // Interactions if (_pendingSushi != 0) { - SUSHI.safeTransfer(account, _pendingSushi); + SUSHI.transfer(account, _pendingSushi); } IRewarder _rewarder = rewarder[address(pool)]; From 5f93b2200cbdaca6c0083efba77f2670b6a13c7e Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Mon, 13 Sep 2021 12:26:30 +0200 Subject: [PATCH 17/21] using low level call to avoid reverts --- contracts/rewards/RewardsManager.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 17dc0193..2aa18f56 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -8,7 +8,6 @@ import "../interfaces/IMasterChef.sol"; import "../utils/TridentOwnable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "../flat/BentoBoxV1Flat.sol"; /// @notice Manages the rewards for various pools without requiring users to stake LP tokens. /// Based on MasterChefV2. @@ -125,7 +124,7 @@ contract RewardsManager is TridentOwnable { IRewarder _rewarder = rewarder[address(pool)]; if (address(_rewarder) != address(0)) { - _rewarder.onSushiReward(address(pool), account, account, _pendingSushi, amount); + address(_rewarder).call(abi.encodePacked(_rewarder.onSushiReward.selector, address(pool), account, account, _pendingSushi, amount)); } emit Harvest(account, address(pool), _pendingSushi); From fdc3da25cf8424bb2b833296604dafe80106c1b7 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Mon, 13 Sep 2021 12:29:44 +0200 Subject: [PATCH 18/21] revert --- contracts/rewards/RewardsManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 2aa18f56..5bd700c6 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -124,7 +124,7 @@ contract RewardsManager is TridentOwnable { IRewarder _rewarder = rewarder[address(pool)]; if (address(_rewarder) != address(0)) { - address(_rewarder).call(abi.encodePacked(_rewarder.onSushiReward.selector, address(pool), account, account, _pendingSushi, amount)); + _rewarder.onSushiReward(address(pool), account, account, _pendingSushi, amount); } emit Harvest(account, address(pool), _pendingSushi); From aa1085187ce2b60a4cea95d90dfe59326ad780d7 Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Mon, 13 Sep 2021 12:49:08 +0200 Subject: [PATCH 19/21] no longer need masterchef --- contracts/interfaces/IMasterChef.sol | 7 ------- contracts/rewards/RewardsManager.sol | 1 - 2 files changed, 8 deletions(-) delete mode 100644 contracts/interfaces/IMasterChef.sol diff --git a/contracts/interfaces/IMasterChef.sol b/contracts/interfaces/IMasterChef.sol deleted file mode 100644 index f5425899..00000000 --- a/contracts/interfaces/IMasterChef.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -pragma solidity ^0.8.4; - -interface IMasterChef { - function sushiPerBlock() external view returns (uint256); -} diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 5bd700c6..10b87b01 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -4,7 +4,6 @@ pragma solidity >=0.8.0; import "../interfaces/IPool.sol"; import "../interfaces/IRewarder.sol"; -import "../interfaces/IMasterChef.sol"; import "../utils/TridentOwnable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; From e21bb0af5f49854073b1b00a29d85bbb462d8492 Mon Sep 17 00:00:00 2001 From: Dean Eigenmann <7621705+decanus@users.noreply.github.com> Date: Tue, 14 Sep 2021 17:33:09 +0200 Subject: [PATCH 20/21] Update TridentERC20.sol --- contracts/pool/TridentERC20.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/pool/TridentERC20.sol b/contracts/pool/TridentERC20.sol index 7dcc5be9..b8c2eecc 100644 --- a/contracts/pool/TridentERC20.sol +++ b/contracts/pool/TridentERC20.sol @@ -74,13 +74,12 @@ abstract contract TridentERC20 { address recipient, uint256 amount ) external returns (bool) { + _beforeTokenTransfer(sender, recipient, amount); if (allowance[sender][msg.sender] != type(uint256).max) { allowance[sender][msg.sender] -= amount; } balanceOf[sender] -= amount; - _beforeTokenTransfer(sender, recipient, amount); - // @dev This is safe from overflow - the sum of all user // balances can't exceed 'type(uint256).max'. unchecked { From 7ddc657955d72a2cf5ba468610c4ac0805f94fee Mon Sep 17 00:00:00 2001 From: decanus <7621705+decanus@users.noreply.github.com> Date: Thu, 16 Sep 2021 15:08:44 +0200 Subject: [PATCH 21/21] experimental --- contracts/rewards/RewardsManager.sol | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 10b87b01..2af9561d 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -20,6 +20,12 @@ contract RewardsManager is TridentOwnable { uint64 allocPoint; } + /// @notice Info of rewards that failed to be claimed from IRewarder. + struct RewardAmount { + uint256 pendingSushi; + uint256 amount; + } + /// @notice Address of SUSHI contract. IERC20 public immutable SUSHI; @@ -33,6 +39,9 @@ contract RewardsManager is TridentOwnable { /// `rewardDebt` The amount of SUSHI entitled to the user for a specific pool. mapping(address => mapping(address => int256)) public rewardDebt; + /// `unclaimedRewards` The amount of rewards that failed to be claimed from `IRewarder`s + mapping(address => mapping(address => RewardAmount)) public unclaimedRewards; + mapping(address => PoolInfo) public poolInfo; mapping(address => IRewarder) public rewarder; @@ -123,12 +132,27 @@ contract RewardsManager is TridentOwnable { IRewarder _rewarder = rewarder[address(pool)]; if (address(_rewarder) != address(0)) { - _rewarder.onSushiReward(address(pool), account, account, _pendingSushi, amount); + RewardAmount memory reward = unclaimedRewards[address(pool)][account]; + try _rewarder.onSushiReward(address(pool), account, account, _pendingSushi + reward.pendingSushi, amount + reward.amount) { + unclaimedRewards[address(pool)][account] = RewardAmount({pendingSushi: 0, amount: 0}); + } catch { + unclaimedRewards[address(pool)][account] = RewardAmount({pendingSushi: reward.pendingSushi + _pendingSushi, amount: reward.amount + amount}); + } } emit Harvest(account, address(pool), _pendingSushi); } + function claimFailedRewarderRewards(IPool pool, address account) external { + IRewarder _rewarder = rewarder[address(pool)]; + if (address(_rewarder) != address(0)) { + return; + } + + RewardAmount memory reward = unclaimedRewards[address(pool)][account]; + _rewarder.onSushiReward(address(pool), account, account, reward.pendingSushi, reward.amount); + } + /// @notice Update reward variables of the given pool. /// @param pool The address of the pool. See `poolInfo`. /// @return info Returns the pool that was updated.