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

Token merger deploy script #414

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4f205b0
Add OGNRewardsSource contract
shahthepro Apr 17, 2024
8e45acc
Make collectRewards only callable by RewardsTarget
shahthepro Apr 17, 2024
ef72063
Draft xOGN staking contract
DanielVF Apr 17, 2024
1126cf2
Correct maxStakeDuration
DanielVF Apr 18, 2024
cd1bfda
Add penalty event
DanielVF Apr 18, 2024
ff30762
Change names
shahthepro Apr 21, 2024
7997625
Merge branch 'DanielVF/xogn2' into shah/ogn-rewards-source
shahthepro Apr 21, 2024
3f51c2d
Fix lockup ID
shahthepro Apr 21, 2024
c02d6b5
Revert change and cast properly
shahthepro Apr 21, 2024
f3db15a
Merge branch 'DanielVF/xogn2' into shah/ogn-rewards-source
shahthepro Apr 21, 2024
7e0daae
Gas opts
shahthepro Apr 22, 2024
db52be3
Remove casting
shahthepro Apr 24, 2024
820fccf
Add `getLockupsCount` method (#411)
shahthepro Apr 24, 2024
3784333
Allow non-duration change amount increase staking extends
DanielVF Apr 24, 2024
1864b68
Add tests, add move lockupid code
DanielVF Apr 25, 2024
50a0021
Merge branch 'DanielVF/xogn2' into shah/ogn-rewards-source
shahthepro Apr 26, 2024
3c3b799
Add Migrator (#410)
shahthepro Apr 26, 2024
230755a
Return excess OGN rather than burn
DanielVF Apr 26, 2024
d0fd40f
Simplify calculation
DanielVF Apr 26, 2024
e3b6244
Add script
shahthepro May 3, 2024
85dd6a9
Fix tooling
shahthepro May 6, 2024
b205685
Update deploy script
shahthepro May 8, 2024
0096c88
Add xOGN Governance deploy script
shahthepro May 9, 2024
e05a6e2
Multisig proposal
shahthepro May 10, 2024
fa77631
Fix brownie tests
shahthepro May 10, 2024
4c94b86
Add fork test script
shahthepro May 10, 2024
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
44 changes: 30 additions & 14 deletions common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
from brownie import *
import brownie

BLOCKS_PER_DAY = 86400 / 12 # 12 blocks per second
BLOCK_INTERVAL = 12 # 1 block every 12 seconds
BLOCKS_PER_DAY = 86400 / BLOCK_INTERVAL

STRATEGIST = '0xF14BBdf064E3F67f51cd9BD646aE3716aD938FDC'
GOV_MULTISIG = '0xbe2AB3d3d8F6a32b96414ebbd865dBD276d3d899'
GOVERNOR_FIVE = '0x3cdd07c16614059e66344a7b579dab4f9516c0b6'
TIMELOCK = '0x35918cDE7233F2dD33fA41ae3Cb6aE0e42E0e69F'

class TemporaryFork:
def __enter__(self):
Expand All @@ -24,15 +26,29 @@ def governanceProposal(deployment):
is_mainnet = web3.chain_id == 1 and not local_provider
is_fork = web3.chain_id == 1337 or local_provider
is_proposal_mode = os.getenv('MODE') == 'build_ogv_gov_proposal'

deploymentInfo = deployment(deployer, FROM_DEPLOYER, local_provider, is_mainnet, is_fork, is_proposal_mode)

impersonate_sim = os.getenv('IMPERSONATE_SIM') == 'true'

if not is_proposal_mode:
return

actions = deploymentInfo['actions']

if (len(actions) > 0):
ogvGovernor = Contract.from_explorer(GOVERNOR_FIVE)
if impersonate_sim:
# Impersonate timelock and simulate the actions
# bypassing the proposal flow (makes testing faster)
timelock = accounts.at(TIMELOCK, force=True)
for action in actions:
fn = getattr(action['contract'], action['signature'][:action['signature'].index('(')])
fn(*action['args'], {'from': timelock})

return

ogvGovernor = Governance.at(GOVERNOR_FIVE)

proposal_args = [
[action['contract'].address for action in actions],
[0 for action in actions],
Expand All @@ -45,14 +61,19 @@ def governanceProposal(deployment):
deploymentInfo['name'],
]

propose_data = ogvGovernor.propose.encode_input(
*proposal_args
)

if is_fork:
print('Creating governance proposal on fork')
ogvGovernor.propose(
propose_tx = ogvGovernor.propose(
*proposal_args,
{'from': GOV_MULTISIG}
)
# Simulate execution on fork
proposalId = propose_tx.events['ProposalCreated'][0][0]['proposalId']
print("Created proposal", proposalId)

# Move forward 30s so that we can vote
timetravel(30)
Expand All @@ -61,23 +82,18 @@ def governanceProposal(deployment):
print("Voting on the proposal")
ogvGovernor.castVote(proposalId, 1, {'from': GOV_MULTISIG})

# 3 days to queue (+2 for buffer since contract has a different BLOCKS_PER_DAY now)
timetravel(86400 * 5)
# 2 days to queue (+0.5 day for buffer)
timetravel(86400 * 2.5)

print("Queueing proposal")
ogvGovernor.queue(proposalId, {'from': GOV_MULTISIG})

# 2 day timelock (+1 day for buffer)
timetravel(86400 * 3)
# 2 day timelock (+0.5 day for buffer)
timetravel(86400 * 2.5)

print("Executing proposal")
ogvGovernor.execute(proposalId, {'from': GOV_MULTISIG})

else:
propose_data = ogvGovernor.propose.encode_input(
*proposal_args
)

print("Raw Args", proposal_args)

print("Execute the following transaction to create OGV Governance proposal")
Expand All @@ -86,5 +102,5 @@ def governanceProposal(deployment):


def timetravel(seconds):
brownie.chain.sleep(seconds + 1)
brownie.chain.mine(int(seconds / BLOCKS_PER_DAY) + 1)
brownie.chain.sleep(int(seconds) + 1)
brownie.chain.mine(int(seconds / BLOCK_INTERVAL) + 1)
279 changes: 279 additions & 0 deletions contracts/ExponentialStaking.sol
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;
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;
}
}
Loading
Loading