-
Notifications
You must be signed in to change notification settings - Fork 4
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
Add OGNRewardsSource contract #408
Merged
Merged
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
4f205b0
Add OGNRewardsSource contract
shahthepro 8e45acc
Make collectRewards only callable by RewardsTarget
shahthepro ef72063
Draft xOGN staking contract
DanielVF 1126cf2
Correct maxStakeDuration
DanielVF cd1bfda
Add penalty event
DanielVF ff30762
Change names
shahthepro 7997625
Merge branch 'DanielVF/xogn2' into shah/ogn-rewards-source
shahthepro 3f51c2d
Fix lockup ID
shahthepro c02d6b5
Revert change and cast properly
shahthepro f3db15a
Merge branch 'DanielVF/xogn2' into shah/ogn-rewards-source
shahthepro 7e0daae
Gas opts
shahthepro db52be3
Remove casting
shahthepro 820fccf
Add `getLockupsCount` method (#411)
shahthepro 3784333
Allow non-duration change amount increase staking extends
DanielVF 1864b68
Add tests, add move lockupid code
DanielVF 50a0021
Merge branch 'DanielVF/xogn2' into shah/ogn-rewards-source
shahthepro 3c3b799
Add Migrator (#410)
shahthepro 51035a9
Return 0 if uninitialized (#415)
shahthepro 2a51241
Check available balance in `previewRewards` (#413)
shahthepro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.10; | ||
|
||
import {ERC20Votes} from "OpenZeppelin/[email protected]/contracts/token/ERC20/extensions/ERC20Votes.sol"; | ||
import {ERC20Permit} from | ||
"OpenZeppelin/[email protected]/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; | ||
import {ERC20} from "OpenZeppelin/[email protected]/contracts/token/ERC20/ERC20.sol"; | ||
import {PRBMathUD60x18} from "paulrberg/[email protected]/contracts/PRBMathUD60x18.sol"; | ||
import {RewardsSource} from "./RewardsSource.sol"; | ||
|
||
/// @title ExponentialStaking | ||
/// @author Daniel Von Fange | ||
/// @notice Provides staking, vote power history, vote delegation, and rewards | ||
/// distribution. | ||
/// | ||
/// The balance received for staking (and thus the voting power and rewards | ||
/// distribution) goes up exponentially by the end of the staked period. | ||
contract ExponentialStaking is ERC20Votes { | ||
uint256 public immutable epoch; // Start of staking program - timestamp | ||
ERC20 public immutable asset; // Must not allow reentrancy | ||
RewardsSource public immutable rewardsSource; | ||
uint256 public immutable minStakeDuration; // in seconds | ||
uint256 public constant maxStakeDuration = 365 days; | ||
uint256 constant YEAR_BASE = 14e17; | ||
int256 constant NEW_STAKE = -1; | ||
|
||
// 2. Staking and Lockup Storage | ||
struct Lockup { | ||
uint128 amount; | ||
uint128 end; | ||
uint256 points; | ||
} | ||
|
||
mapping(address => Lockup[]) public lockups; | ||
|
||
// 3. Reward Storage | ||
mapping(address => uint256) public rewardDebtPerShare; | ||
uint256 public accRewardPerShare; | ||
|
||
// Events | ||
event Stake(address indexed user, uint256 lockupId, uint256 amount, uint256 end, uint256 points); | ||
event Unstake(address indexed user, uint256 lockupId, uint256 amount, uint256 end, uint256 points); | ||
event Reward(address indexed user, uint256 amount); | ||
event Penalty(address indexed user, uint256 amount); | ||
|
||
// Core ERC20 Functions | ||
|
||
constructor(address asset_, uint256 epoch_, uint256 minStakeDuration_, address rewardsSource_) | ||
ERC20("", "") | ||
ERC20Permit("xOGN") | ||
{ | ||
asset = ERC20(asset_); | ||
epoch = epoch_; | ||
minStakeDuration = minStakeDuration_; | ||
rewardsSource = RewardsSource(rewardsSource_); | ||
} | ||
|
||
function name() public pure override returns (string memory) { | ||
return "Staked OGN"; | ||
} | ||
|
||
function symbol() public pure override returns (string memory) { | ||
return "xOGN"; | ||
} | ||
|
||
function transfer(address, uint256) public override returns (bool) { | ||
revert("Staking: Transfers disabled"); | ||
} | ||
|
||
function transferFrom(address, address, uint256) public override returns (bool) { | ||
revert("Staking: Transfers disabled"); | ||
} | ||
|
||
// Staking Functions | ||
|
||
/// @notice Stake asset to an address that may not be the same as the | ||
/// sender of the funds. This can be used to give staked funds to someone | ||
/// else. | ||
/// | ||
/// If staking before the start of staking (epoch), then the lockup start | ||
/// and end dates are shifted forward so that the lockup starts at the | ||
/// epoch. | ||
/// | ||
/// Any rewards previously earned will be paid out or rolled into the stake. | ||
/// | ||
/// @param amountIn asset to lockup in the stake | ||
/// @param duration in seconds for the stake | ||
/// @param to address to receive ownership of the stake | ||
/// @param stakeRewards should pending user rewards be added to the stake | ||
/// @param lockupId previous stake to extend / add funds to. -1 to create a new stake. | ||
function stake(uint256 amountIn, uint256 duration, address to, bool stakeRewards, int256 lockupId) external { | ||
require(to != address(0), "Staking: To the zero address"); | ||
require(duration >= minStakeDuration, "Staking: Too short"); | ||
// Too long checked in preview points | ||
|
||
uint256 newAmount = amountIn; | ||
uint256 oldPoints = 0; | ||
uint256 oldEnd = 0; | ||
Comment on lines
+97
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. gas: no need for initialising with default values. |
||
Lockup memory lockup; | ||
|
||
// Allow gifts, but not control of other's accounts | ||
if (to != msg.sender) { | ||
require(stakeRewards == false, "Staking: Self only"); | ||
require(lockupId == NEW_STAKE, "Staking: Self only"); | ||
} | ||
|
||
// Collect funds from user | ||
if (amountIn > 0) { | ||
// Important that `msg.sender` aways pays, not the `to` address. | ||
asset.transferFrom(msg.sender, address(this), amountIn); | ||
// amountIn already added into newAmount during initialization | ||
} | ||
|
||
// Collect funds from old stake (optional) | ||
if (lockupId != NEW_STAKE) { | ||
lockup = lockups[to][uint256(lockupId)]; | ||
uint256 oldAmount = lockup.amount; | ||
oldEnd = lockup.end; | ||
oldPoints = lockup.points; | ||
require(oldAmount > 1, "Staking: Already closed stake"); | ||
emit Unstake(to, uint256(lockupId), oldAmount, oldEnd, oldPoints); | ||
newAmount += oldAmount; | ||
} | ||
|
||
// Collect funds from rewards (optional) | ||
newAmount += _collectRewards(to, stakeRewards); | ||
|
||
// Caculate Points and lockup | ||
require(newAmount > 0, "Staking: Not enough"); | ||
require(newAmount <= type(uint128).max, "Staking: Too much"); | ||
(uint256 newPoints, uint256 newEnd) = previewPoints(newAmount, duration); | ||
require(newPoints + totalSupply() <= type(uint192).max, "Staking: Max points exceeded"); | ||
lockup.end = uint128(newEnd); | ||
lockup.amount = uint128(newAmount); // max checked in require above | ||
lockup.points = newPoints; | ||
|
||
// Update or create lockup | ||
if (lockupId != NEW_STAKE) { | ||
require(newEnd >= oldEnd, "Staking: New lockup must not be shorter"); | ||
require(newPoints > oldPoints, "Staking: Must have increased amount or duration"); | ||
lockups[to][uint256(lockupId)] = lockup; | ||
} else { | ||
lockups[to].push(lockup); | ||
uint256 numLockups = lockups[to].length; | ||
require(numLockups < uint256(type(int256).max), "Staking: Too many lockups"); | ||
lockupId = int256(numLockups - 1); | ||
// Delegate voting power to the receiver, if unregistered and first stake | ||
if (numLockups == 1 && delegates(to) == address(0)) { | ||
_delegate(to, to); | ||
} | ||
} | ||
_mint(to, newPoints - oldPoints); | ||
emit Stake(to, uint256(lockupId), newAmount, newEnd, newPoints); | ||
} | ||
|
||
/// @notice Collect staked asset for a lockup and any earned rewards. | ||
/// @param lockupId the id of the lockup to unstake | ||
function unstake(uint256 lockupId) external { | ||
Lockup memory lockup = lockups[msg.sender][lockupId]; | ||
uint256 amount = lockup.amount; | ||
uint256 end = lockup.end; | ||
uint256 points = lockup.points; | ||
require(end != 0, "Staking: Already unstaked this lockup"); | ||
_collectRewards(msg.sender, false); | ||
|
||
uint256 withdrawAmount = previewWithdraw(amount, end); | ||
uint256 penalty = amount - withdrawAmount; | ||
|
||
delete lockups[msg.sender][lockupId]; // Keeps empty in array, so indexes are stable | ||
_burn(msg.sender, points); | ||
if (penalty > 0) { | ||
asset.transfer(address(rewardsSource), penalty); | ||
emit Penalty(msg.sender, penalty); | ||
} | ||
asset.transfer(msg.sender, withdrawAmount); | ||
emit Unstake(msg.sender, lockupId, withdrawAmount, end, points); | ||
} | ||
|
||
// 3. Reward functions | ||
|
||
/// @notice Collect all earned asset rewards. | ||
function collectRewards() external { | ||
_collectRewards(msg.sender, false); | ||
} | ||
|
||
/// @dev Internal function to handle rewards accounting. | ||
/// | ||
/// 1. Collect new rewards for everyone | ||
/// 2. Calculate this user's rewards and accounting | ||
/// 3. Distribute this user's rewards | ||
/// | ||
/// This function *must* be called before any user balance changes. | ||
/// | ||
/// This will always update the user's rewardDebtPerShare to match | ||
/// accRewardPerShare, which is essential to the accounting. | ||
/// | ||
/// @param user to collect rewards for | ||
/// @param shouldRetainRewards if true user's rewards kept in this contract rather than sent | ||
/// @return retainedRewards amount of rewards not sent to user | ||
function _collectRewards(address user, bool shouldRetainRewards) internal returns (uint256) { | ||
uint256 supply = totalSupply(); | ||
if (supply > 0) { | ||
uint256 preBalance = asset.balanceOf(address(this)); | ||
try rewardsSource.collectRewards() {} | ||
catch { | ||
// Governance staking should continue, even if rewards fail | ||
} | ||
uint256 collected = asset.balanceOf(address(this)) - preBalance; | ||
accRewardPerShare += (collected * 1e12) / supply; | ||
} | ||
uint256 netRewardsPerShare = accRewardPerShare - rewardDebtPerShare[user]; | ||
uint256 netRewards = (balanceOf(user) * netRewardsPerShare) / 1e12; | ||
rewardDebtPerShare[user] = accRewardPerShare; | ||
if (netRewards == 0) { | ||
return 0; | ||
} | ||
emit Reward(user, netRewards); | ||
if (shouldRetainRewards) { | ||
return netRewards; | ||
} else { | ||
asset.transfer(user, netRewards); | ||
} | ||
} | ||
|
||
/// @notice Preview the number of points that would be returned for the | ||
/// given amount and duration. | ||
/// | ||
/// @param amount asset to be staked | ||
/// @param duration number of seconds to stake for | ||
/// @return points staking points that would be returned | ||
/// @return end staking period end date | ||
function previewPoints(uint256 amount, uint256 duration) public view returns (uint256, uint256) { | ||
require(duration <= maxStakeDuration, "Staking: Too long"); | ||
uint256 start = block.timestamp > epoch ? block.timestamp : epoch; | ||
uint256 end = start + duration; | ||
uint256 endYearpoc = ((end - epoch) * 1e18) / 365 days; | ||
uint256 multiplier = PRBMathUD60x18.pow(YEAR_BASE, endYearpoc); | ||
return ((amount * multiplier) / 1e18, end); | ||
} | ||
|
||
/// @notice Preview the amount of asset a user would receive if they collected | ||
/// rewards at this time. | ||
/// | ||
/// @param user to preview rewards for | ||
/// @return asset rewards amount | ||
function previewRewards(address user) external view returns (uint256) { | ||
uint256 supply = totalSupply(); | ||
if (supply == 0) { | ||
return 0; // No one has any points to even get rewards | ||
} | ||
uint256 _accRewardPerShare = accRewardPerShare; | ||
_accRewardPerShare += (rewardsSource.previewRewards() * 1e12) / supply; | ||
uint256 netRewardsPerShare = _accRewardPerShare - rewardDebtPerShare[user]; | ||
return (balanceOf(user) * netRewardsPerShare) / 1e12; | ||
} | ||
|
||
/// @notice Preview the amount that a user would receive if they withdraw now. | ||
/// This amount is after any early withdraw fees are removed for early withdraws. | ||
/// @param amount staked asset amount to be withdrawn | ||
/// @param end stake end date to be withdrawn from. | ||
/// @return withdrawAmount amount of assets that the user will receive from withdraw | ||
function previewWithdraw(uint256 amount, uint256 end) public view returns (uint256) { | ||
if (block.timestamp >= end) { | ||
return amount; | ||
} | ||
uint256 fullDuration = end - block.timestamp; | ||
(uint256 fullPoints,) = previewPoints(1e18, fullDuration); | ||
(uint256 currentPoints,) = previewPoints(1e36, 0); // 1e36 saves a later multiplication | ||
return amount * ((currentPoints / fullPoints)) / 1e18; | ||
} | ||
|
||
/// @notice Returns the total number of lockups the user has | ||
/// created so far (including expired & unstaked ones) | ||
/// @param user Address | ||
/// @return asset Number of lockups the user has had | ||
function lockupsCount(address user) external view returns (uint256) { | ||
return lockups[user].length; | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider custom errors?