Skip to content

Commit

Permalink
Add sGYD staker (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
danhper committed Sep 23, 2024
1 parent e95c63f commit f9a4ac0
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 10 deletions.
42 changes: 42 additions & 0 deletions script/deployments/DeploySGydStaker.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import {console} from "forge-std/console.sol";
import {UUPSProxy} from "./UUPSProxy.sol";

import {sGydStaker} from "../../src/sGydStaker.sol";
import {Deployment} from "./Deployment.sol";

contract DeploySGyd is Deployment {
function run() public {
vm.startBroadcast(deployerPrivateKey);

address gyd_;
address distributor;
address governance;
address treasury;
address sgyd = _getDeployed(SGYD);
if (block.chainid == 1) {
gyd_ = gyd;
distributor = _getDeployed(GYD_DISTRIBUTOR);
governance = l1Governance;
revert("add treasury address");
} else {
gyd_ = l2Gyd;
distributor = _getDeployed(L2_GYD_DISTRIBUTOR);
governance = l2Governance;
treasury = 0x391714d83db20fde7110Cb80DC3857637c14E251;
}
console.log("gyd", gyd_);
console.log("distributor", distributor);
console.log("governance", governance);
console.log("treasury", treasury);

sGydStaker sgydStaker = new sGydStaker();

bytes memory data = abi.encodeWithSelector(sGydStaker.initialize.selector, sgyd, gyd_, treasury, governance);
bytes memory creationCode =
abi.encodePacked(type(UUPSProxy).creationCode, abi.encode(address(sgydStaker), data));
console.log("sGydStaker", _deploy(SGYD_STAKER, creationCode));
}
}
15 changes: 5 additions & 10 deletions script/deployments/Deployment.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ contract Deployment is Script {
string public constant GYD_DISTRIBUTOR = "GydDistributorV1";
string public constant L2_GYD_DISTRIBUTOR = "L2GydDistributorV1";
string public constant SGYD = "sGYD";
string public constant SGYD_STAKER = "sGydStaker";
string public constant DISTRIBUTION_MANAGER = "GydDistributionManagerV1";

// https://etherscan.io/address/0x93FEC2C00BfE902F733B57c5a6CeeD7CD1384AE1
ICREATE3Factory public factory =
ICREATE3Factory(0x93FEC2C00BfE902F733B57c5a6CeeD7CD1384AE1);
ICREATE3Factory public factory = ICREATE3Factory(0x93FEC2C00BfE902F733B57c5a6CeeD7CD1384AE1);

// https://etherscan.io/address/0x78EcF97572c3890eD02221A611014F30219f6219
address public l1Governance = 0x78EcF97572c3890eD02221A611014F30219f6219;
Expand All @@ -35,23 +35,18 @@ contract Deployment is Script {
address public deployer = 0x8bc920001949589258557412A32F8d297A74F244;

// https://etherscan.io/address/0xA8D612739354a4106072a91aA4Ca1458E1b5f9e9
address public distributionExecutor =
0xA8D612739354a4106072a91aA4Ca1458E1b5f9e9;
address public distributionExecutor = 0xA8D612739354a4106072a91aA4Ca1458E1b5f9e9;

// https://etherscan.io/address/0xb0307AB3e2C0886a70b2C84897Bca7Ee9b237a50
address public distributionSubmitter =
0xb0307AB3e2C0886a70b2C84897Bca7Ee9b237a50;
address public distributionSubmitter = 0xb0307AB3e2C0886a70b2C84897Bca7Ee9b237a50;

uint256 public deployerPrivateKey;

function setUp() public virtual {
deployerPrivateKey = uint256(vm.envBytes32("PRIVATE_KEY"));
}

function _deploy(
string memory name,
bytes memory creationCode
) internal returns (address) {
function _deploy(string memory name, bytes memory creationCode) internal returns (address) {
bytes32 salt = keccak256(bytes(name));
return factory.deploy(salt, creationCode);
}
Expand Down
136 changes: 136 additions & 0 deletions src/LiquidityMining.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import "./interfaces/ILiquidityMining.sol";
import "./libraries/ScaledMath.sol";

import "oz/proxy/utils/Initializable.sol";
import "oz/token/ERC20/IERC20.sol";
import "oz/token/ERC20/utils/SafeERC20.sol";

/// @dev Base contract for liquidity mining.
/// `startMining` and `stopMining` would typically be implemented by the subcontract to perform
/// its own authorization and then call the underscore versions
/// NOTE: this is the same as the LiquidityMining contract in the governance repo
abstract contract LiquidityMining is ILiquidityMining {
using ScaledMath for uint256;
using SafeERC20 for IERC20;

uint256 public override totalStaked;

uint256 internal _totalStakedIntegral;
uint256 internal _lastCheckpointTime;
/// @dev This contract only tracks these, we don't use it; but it may be convenient for inheriting contracts.
uint256 internal _totalUnclaimedRewards;
mapping(address => uint256) internal _perUserStakedIntegral;
mapping(address => uint256) internal _perUserShare;
mapping(address => uint256) internal _perUserStaked;

uint256 internal _rewardsEmissionRate;
uint256 public override rewardsEmissionEndTime;

IERC20 public rewardToken;
address public daoTreasury;

function __LiquidityMining_init(address _rewardToken, address _daoTreasury) internal {
_lastCheckpointTime = block.timestamp;
rewardToken = IERC20(_rewardToken);
daoTreasury = _daoTreasury;
}

function claimRewards() external returns (uint256) {
userCheckpoint(msg.sender);
uint256 amount = _perUserShare[msg.sender];
if (amount == 0) return 0;
delete _perUserShare[msg.sender];
emit Claim(msg.sender, amount);
_totalUnclaimedRewards -= amount;
return _mintRewards(msg.sender, amount);
}

function claimableRewards(address beneficiary) external view virtual returns (uint256) {
uint256 totalStakedIntegral = _totalStakedIntegral;
uint256 rewardsTimestamp = _rewardsTimestamp();
if (totalStaked > 0 && rewardsTimestamp > _lastCheckpointTime) {
uint256 elapsedTime = rewardsTimestamp - _lastCheckpointTime;
totalStakedIntegral += (_rewardsEmissionRate * elapsedTime).div(totalStaked);
}

return _perUserShare[beneficiary]
+ stakedBalanceOf(beneficiary).mul(totalStakedIntegral - _perUserStakedIntegral[beneficiary]);
}

function stakedBalanceOf(address account) public view returns (uint256) {
return _perUserStaked[account];
}

function globalCheckpoint() public {
uint256 rewardsTimestamp = _rewardsTimestamp();
uint256 totalStaked_ = totalStaked;
if (totalStaked_ > 0 && rewardsTimestamp > _lastCheckpointTime) {
uint256 elapsedTime = rewardsTimestamp - _lastCheckpointTime;
uint256 newRewards = _rewardsEmissionRate * elapsedTime;
_totalStakedIntegral += newRewards.div(totalStaked_);
_totalUnclaimedRewards += newRewards;
}
_lastCheckpointTime = rewardsTimestamp;
}

function userCheckpoint(address account) public virtual {
globalCheckpoint();
uint256 totalStakedIntegral = _totalStakedIntegral;
_perUserShare[account] += stakedBalanceOf(account).mul(totalStakedIntegral - _perUserStakedIntegral[account]);
_perUserStakedIntegral[account] = totalStakedIntegral;
}

/// @dev this is a helper function to be used by the inheriting contract
/// this does not perform any checks on the amount that `account` may or not have deposited
/// and should be used with caution. All checks should be performed in the inheriting contract
function _stake(address account, uint256 amount) internal {
userCheckpoint(account);
totalStaked += amount;
_perUserStaked[account] += amount;
emit Stake(account, amount);
}

/// @dev same as `_stake` but for unstaking
function _unstake(address account, uint256 amount) internal {
userCheckpoint(account);
_perUserStaked[account] -= amount;
totalStaked -= amount;
emit Unstake(account, amount);
}

/// @dev Helper function for the inheriting contract. Authorization should be performed by the inheriting contract.
function _startMining(address rewardsFrom, uint256 amount, uint256 endTime) internal {
globalCheckpoint();
rewardToken.safeTransferFrom(rewardsFrom, address(this), amount);
_rewardsEmissionRate = amount / (endTime - block.timestamp);
rewardsEmissionEndTime = endTime;
_lastCheckpointTime = block.timestamp;
emit StartMining(amount, endTime);
}

/// @dev same as `_startLiquidityMining` but for stopping.
function _stopMining() internal {
globalCheckpoint();
uint256 reimbursementAmount = rewardToken.balanceOf(address(this)) - _totalUnclaimedRewards;
rewardToken.safeTransfer(daoTreasury, reimbursementAmount);
rewardsEmissionEndTime = 0;
_rewardsEmissionRate = 0;
emit StopMining();
}

function _mintRewards(address beneficiary, uint256 amount) internal virtual returns (uint256) {
rewardToken.safeTransfer(beneficiary, amount);
return amount;
}

function rewardsEmissionRate() external view override returns (uint256) {
return block.timestamp <= rewardsEmissionEndTime ? _rewardsEmissionRate : 0;
}

function _rewardsTimestamp() internal view returns (uint256) {
return block.timestamp <= rewardsEmissionEndTime ? block.timestamp : rewardsEmissionEndTime;
}
}
45 changes: 45 additions & 0 deletions src/interfaces/ILiquidityMining.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

// NOTE: This is the same code as ILiquidityMining.sol in the governance repo.

interface ILiquidityMining {
event Stake(address indexed account, uint256 amount);
event Unstake(address indexed account, uint256 amount);
event Claim(address indexed beneficiary, uint256 amount);

event StartMining(uint256 amount, uint256 endTime);
event StopMining();

/// @notice claims rewards for caller
function claimRewards() external returns (uint256);

/// @notice returns the amount of claimable rewards by `beneficiary`
function claimableRewards(address beneficiary) external view returns (uint256);

/// @notice the total amount of tokens staked in the contract
function totalStaked() external view returns (uint256);

/// @notice the amount of tokens staked by `account`
function stakedBalanceOf(address account) external view returns (uint256);

/// @notice returns the number of rewards token that will be given per second for the contract
/// This emission will be given to all stakers in the contract proportionally to their stake
function rewardsEmissionRate() external view returns (uint256);

/// @notice time when rewards emission ends
function rewardsEmissionEndTime() external view returns (uint256);

/// @dev these functions will be called internally but can typically be called by anyone
/// to update the internal tracking state of the contract
function globalCheckpoint() external;

function userCheckpoint(address account) external;

/// @notice Deposit `amount` of the reward token from `rewardsFrom` and enable rewards until `endTime`. Typically governanceOnly. `amount` is spread evenly over the time period.
function startMining(address rewardsFrom, uint256 amount, uint256 endTime) external;

/// @notice Stop liquidity mining early and reimburse leftover rewards to the DAO treasury.
/// This may also be needed after the mining period has ended when we had `totalStaked() == 0` for a while, where no rewards accrue.
function stopMining() external;
}
4 changes: 4 additions & 0 deletions src/libraries/ScaledMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ library ScaledMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
return (a * b) / 1e18;
}

function div(uint256 a, uint256 b) internal pure returns (uint256) {
return (a * 1e18) / b;
}
}
58 changes: 58 additions & 0 deletions src/sGydStaker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

import {AccessControlDefaultAdminRulesUpgradeable} from
"ozu/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol";
import {LiquidityMining} from "./LiquidityMining.sol";
import {IERC20} from "oz/token/ERC20/IERC20.sol";
import {UUPSUpgradeable} from "ozu/proxy/utils/UUPSUpgradeable.sol";
import {ERC4626Upgradeable} from "ozu/token/ERC20/extensions/ERC4626Upgradeable.sol";

contract sGydStaker is
ERC4626Upgradeable,
AccessControlDefaultAdminRulesUpgradeable,
LiquidityMining,
UUPSUpgradeable
{
constructor() {
_disableInitializers();
}

function initialize(IERC20 _depositToken, IERC20 _rewardToken, address _daoTreasury, address _initialAdmin)
external
initializer
{
__UUPSUpgradeable_init();
__ERC20_init("Staked sGYD", "ssGYD");
__ERC4626_init(_depositToken);
__AccessControlDefaultAdminRules_init(0, _initialAdmin);
__LiquidityMining_init(address(_rewardToken), _daoTreasury);
}

function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}

function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
super._deposit(caller, receiver, assets, shares);
_stake(receiver, shares);
}

function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
internal
override
{
super._withdraw(caller, receiver, owner, assets, shares);
_unstake(receiver, shares);
}

function startMining(address rewardsFrom, uint256 amount, uint256 endTime)
external
override
onlyRole(DEFAULT_ADMIN_ROLE)
{
_startMining(rewardsFrom, amount, endTime);
}

function stopMining() external override onlyRole(DEFAULT_ADMIN_ROLE) {
_stopMining();
}
}
Loading

0 comments on commit f9a4ac0

Please sign in to comment.