Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/reward-master-pool #95

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions contracts/interfaces/IRewarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity >=0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IRewarder {
function onSushiReward(
address pool,
address user,
address recipient,
uint256 sushiAmount,
uint256 newLpAmount
) external;

function pendingTokens(
address pool,
address user,
uint256 sushiAmount
) external view returns (IERC20[] memory, uint256[] memory);
}
35 changes: 35 additions & 0 deletions contracts/pool/IncentivizedPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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 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
) internal override {
if (address(rewards) == address(0)) {
return;
}

if (from != address(0)) {
rewards.claimRewardsFor(this, from);
}

if (to != address(0)) {
rewards.claimRewardsFor(this, to);
}
}
}
21 changes: 21 additions & 0 deletions contracts/pool/IncentivizedPoolFactory.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
13 changes: 12 additions & 1 deletion contracts/pool/TridentERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ abstract contract TridentERC20 {
mapping(address => uint256) public balanceOf;
/// @notice owner -> spender -> allowance mapping.
mapping(address => mapping(address => uint256)) public allowance;

/// @notice The EIP-712 typehash for this contract's {permit} struct.
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.
Expand Down Expand Up @@ -53,6 +52,8 @@ abstract 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'.
Expand All @@ -73,10 +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;

// @dev This is safe from overflow - the sum of all user
// balances can't exceed 'type(uint256).max'.
unchecked {
Expand Down Expand Up @@ -114,6 +117,7 @@ abstract contract TridentERC20 {
}

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'.
Expand All @@ -124,6 +128,7 @@ abstract 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`.
Expand All @@ -132,4 +137,10 @@ abstract contract TridentERC20 {
}
emit Transfer(sender, address(0), amount);
}

function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -395,4 +395,4 @@ contract FranchisedConstantProductPool is IPool, TridentFranchisedERC20 {
{
return _getReserves();
}
}
}
2 changes: 1 addition & 1 deletion contracts/pool/franchised/FranchisedHybridPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -467,4 +467,4 @@ contract FranchisedHybridPool is IPool, TridentFranchisedERC20 {
function getReserves() public view returns (uint256 _reserve0, uint256 _reserve1) {
(_reserve0, _reserve1) = _getReserves();
}
}
}
2 changes: 1 addition & 1 deletion contracts/pool/franchised/FranchisedIndexPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,4 @@ contract FranchisedIndexPool is IPool, TridentFranchisedERC20 {
}
}
}
}
}
178 changes: 178 additions & 0 deletions contracts/rewards/RewardsManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity >=0.8.0;

import "../interfaces/IPool.sol";
import "../interfaces/IRewarder.sol";
import "../utils/TridentOwnable.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @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.
/// Also known as the amount of SUSHI to distribute per block.
struct PoolInfo {
uint128 accSushiPerShare;
uint64 lastRewardBlock;
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;

/// @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.
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;

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(IERC20 _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.
/// @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 {
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;
}

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]);
}
}

/// @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);

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[address(pool)][account] = accumulatedSushi;

// Interactions
if (_pendingSushi != 0) {
SUSHI.transfer(account, _pendingSushi);
}

IRewarder _rewarder = rewarder[address(pool)];
if (address(_rewarder) != address(0)) {
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});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imagine if a user has 10 LP tokens. If they call it once while the rewarder fails, on the next call the rewarder is called with a new LP amount of 20, even though the user still only has 10 LP tokens. If the event is e.g. a transfer it is also problematic because the rewards continue to run for two accounts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we need to think about how best to do this. Maybe subtract the rewards.amount from pendingRewards if its greater than 0?

}
}

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.
function updatePool(IPool pool) public returns (PoolInfo memory info) {
info = poolInfo[address(pool)];
if (block.number > info.lastRewardBlock) {
uint256 lpSupply = IERC20(address(pool)).totalSupply();
if (lpSupply > 0) {
uint256 blocks = block.number - info.lastRewardBlock;
uint256 sushiReward = (blocks * sushiPerBlock() * info.allocPoint) / totalAllocPoint;
info.accSushiPerShare = info.accSushiPerShare + uint128((sushiReward * ACC_SUSHI_PRECISION) / lpSupply);
}
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 * poolInfo[MASTER_PID].allocPoint) / totalAllocPoint;
}
}
Loading