From 3f8591927eceadefe2aa015aa123fb39dff1f351 Mon Sep 17 00:00:00 2001 From: Zodomo Date: Thu, 12 Dec 2024 16:31:40 -0500 Subject: [PATCH] chore(contracts/core): migrate genesis contracts to monorepo (#2689) Migrated Genesis contracts into monorepo issue: none --- contracts/core/.gas-snapshot | 2 +- contracts/core/package.json | 1 + contracts/core/pnpm-lock.yaml | 8 + .../core/src/interfaces/IGenesisStake.sol | 33 ++ contracts/core/src/token/GenesisClaim.sol | 337 ++++++++++++++++++ contracts/core/src/token/GenesisStake.sol | 243 +++++++++++++ 6 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 contracts/core/src/interfaces/IGenesisStake.sol create mode 100644 contracts/core/src/token/GenesisClaim.sol create mode 100644 contracts/core/src/token/GenesisStake.sol diff --git a/contracts/core/.gas-snapshot b/contracts/core/.gas-snapshot index 9a85a89be..3eafa7511 100644 --- a/contracts/core/.gas-snapshot +++ b/contracts/core/.gas-snapshot @@ -43,7 +43,7 @@ OmniBridgeNative_Test:test_pauseBridging() (gas: 44379) OmniBridgeNative_Test:test_pauseWithdraws() (gas: 61240) OmniBridgeNative_Test:test_stub() (gas: 143) OmniBridgeNative_Test:test_withdraw() (gas: 279580) -OmniGasPump_Test:testFuzz_quote(uint32) (runs: 256, μ: 63943, ~: 63991) +OmniGasPump_Test:testFuzz_quote(uint32) (runs: 256, μ: 63941, ~: 63991) OmniGasPump_Test:test_fillUp() (gas: 237060) OmniGasPump_Test:test_pause() (gas: 62825) OmniGasPump_Test:test_setMaxSwap() (gas: 34771) diff --git a/contracts/core/package.json b/contracts/core/package.json index e0c7d6ff2..24c6db0cd 100644 --- a/contracts/core/package.json +++ b/contracts/core/package.json @@ -20,6 +20,7 @@ "dependencies": { "@nomad-xyz/excessively-safe-call": "github:nomad-xyz/ExcessivelySafeCall", "@openzeppelin-v4/contracts": "npm:@openzeppelin/contracts@4.9.6", + "@openzeppelin-v4/contracts-upgradeable": "npm:@openzeppelin/contracts-upgradeable@4.9.6", "@openzeppelin/contracts": "^5.0.2", "@openzeppelin/contracts-upgradeable": "^5.0.2", "eigenlayer-contracts": "github:Layr-Labs/eigenlayer-contracts", diff --git a/contracts/core/pnpm-lock.yaml b/contracts/core/pnpm-lock.yaml index 4c989c38a..c492d9a3e 100644 --- a/contracts/core/pnpm-lock.yaml +++ b/contracts/core/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@openzeppelin-v4/contracts': specifier: npm:@openzeppelin/contracts@4.9.6 version: '@openzeppelin/contracts@4.9.6' + '@openzeppelin-v4/contracts-upgradeable': + specifier: npm:@openzeppelin/contracts-upgradeable@4.9.6 + version: '@openzeppelin/contracts-upgradeable@4.9.6' '@openzeppelin/contracts': specifier: ^5.0.2 version: 5.0.2 @@ -224,6 +227,9 @@ packages: resolution: {integrity: sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==} engines: {node: '>= 12'} + '@openzeppelin/contracts-upgradeable@4.9.6': + resolution: {integrity: sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA==} + '@openzeppelin/contracts-upgradeable@5.0.2': resolution: {integrity: sha512-0MmkHSHiW2NRFiT9/r5Lu4eJq5UJ4/tzlOgYXNAIj/ONkQTVnz22pLxDvp4C4uZ9he7ZFvGn3Driptn1/iU7tQ==} peerDependencies: @@ -1635,6 +1641,8 @@ snapshots: '@nomicfoundation/solidity-analyzer-linux-x64-musl': 0.1.2 '@nomicfoundation/solidity-analyzer-win32-x64-msvc': 0.1.2 + '@openzeppelin/contracts-upgradeable@4.9.6': {} + '@openzeppelin/contracts-upgradeable@5.0.2(@openzeppelin/contracts@5.0.2)': dependencies: '@openzeppelin/contracts': 5.0.2 diff --git a/contracts/core/src/interfaces/IGenesisStake.sol b/contracts/core/src/interfaces/IGenesisStake.sol new file mode 100644 index 000000000..56768bd6d --- /dev/null +++ b/contracts/core/src/interfaces/IGenesisStake.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.24; + +interface IGenesisStake { + /** + * @notice Stake `amount` tokens. + * @param amount The amount of tokens to stake. + */ + function stake(uint256 amount) external; + + /** + * @notice Stake `amount` tokens for `recipient`, paid by the caller. + * @param recipient The recipient to stake tokens for. + * @param amount The amount of tokens to stake. + */ + function stakeFor(address recipient, uint256 amount) external; + + /** + * @notice Unstake your entire balance, starting the unbonding period. + */ + function unstake() external; + + /** + * @notice Withdraw your entire balance after the unbonding period. + */ + function withdraw() external; + + /** + * @notice Returns timestamp at which `account` can withdraw. + * Reverts if the account has not staked & unstaked. + */ + function canWithdrawAt(address account) external view returns (uint256); +} diff --git a/contracts/core/src/token/GenesisClaim.sol b/contracts/core/src/token/GenesisClaim.sol new file mode 100644 index 000000000..44a4b2af4 --- /dev/null +++ b/contracts/core/src/token/GenesisClaim.sol @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.24; + +import { OwnableUpgradeable } from "@openzeppelin-v4/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin-v4/contracts-upgradeable/security/PausableUpgradeable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IGenesisStake } from "../interfaces/IGenesisStake.sol"; + +contract GenesisClaim is OwnableUpgradeable, PausableUpgradeable { + /** + * @notice Emitted when rewards are set for an account. + * @param account The account that received rewards. + * @param amount The amount of rewards received. + */ + event RewardsSet(address indexed account, uint256 amount); + + /** + * @notice Emitted when an account claims rewards. + * @param account The account that claimed rewards. + * @param amount The amount of rewards claimed. + */ + event Claimed(address indexed account, uint256 amount); + + /** + * @notice Emitted when the GenesisStake contract is set. + * @param newGenesisStake The new GenesisStake contract address. + * @param prevGenesisStake The previous GenesisStake contract address. + */ + event GenesisStakeChanged(address indexed newGenesisStake, address indexed prevGenesisStake); + + /** + * @notice Emitted when the Clique signer address is changed. + * @param newClique The new Clique signer address. + * @param prevClique The previous Clique signer address. + */ + event CliqueChanged(address indexed newClique, address indexed prevClique); + + /** + * @notice Emitted when the Clique fee is changed. + * @param newCliqueFee The new Clique fee. + * @param prevCliqueFee The previous Clique fee. + */ + event CliqueFeeChanged(uint256 newCliqueFee, uint256 prevCliqueFee); + + /** + * @notice Emitted when claims are opened. + */ + event ClaimsOpened(); + + /** + * @notice Emitted when claims are closed. + */ + event ClaimsClosed(); + + /** + * @notice The token contract address. + */ + IERC20 public immutable token; + + /** + * @notice The Clique signer address. + */ + address public clique; + + /** + * @notice The GenesisStake contract address. + */ + IGenesisStake public genesisStake; + + /** + * @notice True if claims are open, false otherwise. + */ + bool public isOpen; + + /** + * @notice Timestamp at which claims were opened. + */ + uint256 public openedAt; + + /** + * @notice Clique fee + */ + uint256 public cliqueFee; + + /** + * @notice Rewards for each account. + */ + mapping(address => uint256) public rewards; + + /** + * @notice True if rewards have been set for an account, false otherwise. + */ + mapping(address => bool) public rewardsSet; + + /** + * @notice Restrict calls to the Clique address. + */ + modifier onlyClique() { + require(msg.sender == clique, "GenesisClaim: only clique"); + _; + } + + /** + * @notice Restrict calls to when claims are open. + */ + modifier whenOpen() { + require(isOpen, "GenesisClaim: not open"); + _; + } + + constructor(address token_) { + token = IERC20(token_); + _disableInitializers(); + } + + /** + * @notice Initialize the contract. + * @param owner_ The owner of the contract. + * @param genesisStake_ The GenesisStake contract address. + */ + function initialize(address owner_, address genesisStake_, address clique_, uint256 cliqueFee_, bool isOpen_) + external + initializer + { + __Ownable_init(); + __Pausable_init(); + _transferOwnership(owner_); + + _setGenesisStake(genesisStake_); + _setClique(clique_); + _setCliqueFee(cliqueFee_); + + if (isOpen_) _openClaims(); + } + + /** + * @notice Set the rewards for `account`. + * @param account The account to set rewards for. + * @param amount The amount of rewards to set. + */ + function setRewards(address account, uint256 amount) external onlyClique whenNotPaused { + _setRewards(account, amount); + } + + /** + * @notice Set rewards for multiple accounts. + * @param accounts The accounts to set rewards for. + * @param amounts The amounts of rewards to set. + */ + function batchSetRewards(address[] calldata accounts, uint256[] calldata amounts) + external + onlyClique + whenNotPaused + { + require(accounts.length == amounts.length, "GenesisClaim: length mismatch"); + + for (uint256 i = 0; i < accounts.length; i++) { + _setRewards(accounts[i], amounts[i]); + } + } + + function _setRewards(address account, uint256 amount) internal { + require(amount > 0, "GenesisClaim: amount must be > 0"); + require(!rewardsSet[account], "GenesisClaim: already set"); + require(account != address(0), "GenesisClaim: no zero address"); + + rewards[account] = amount; + rewardsSet[account] = true; + + emit RewardsSet(account, amount); + } + + /** + * @notice Reset rewards for `accounts`. + * @param accounts The accounts to reset rewards for. + */ + function resetRewards(address[] calldata accounts) external onlyOwner { + for (uint256 i = 0; i < accounts.length; i++) { + // this check is included to avoid resetting rewards for accounts that have already claimed + require(rewards[accounts[i]] > 0, "GenesisClaim: no rewards"); + + rewards[accounts[i]] = 0; + rewardsSet[accounts[i]] = false; + } + } + + /** + * @notice Return true if `account` has rewards, false otherwise. + * @param account The account to check. + */ + function hasRewards(address account) external view returns (bool) { + return rewards[account] > 0; + } + + /** + * @notice Claim all rewards for the caller. + */ + function claim() external payable whenNotPaused whenOpen { + require(msg.value >= cliqueFee, "GenesisClaim: insufficient fee"); + + uint256 amount = _markClaimed(msg.sender); + + IERC20(token).transfer(msg.sender, amount); + + emit Claimed(msg.sender, amount); + } + + /** + * @notice Claim all rewards for the caller, and stake them on behalf of the caller. + */ + function claimAndStake() external payable whenNotPaused whenOpen { + require(msg.value >= cliqueFee, "GenesisClaim: insufficient fee"); + + uint256 amount = _markClaimed(msg.sender); + + genesisStake.stakeFor(msg.sender, amount); + + emit Claimed(msg.sender, amount); + } + + /** + * @notice Mark rewards as claimed for `account`. Returns the amount claimed. + * @param account The account claiming rewards. + */ + function _markClaimed(address account) internal returns (uint256) { + uint256 amount = rewards[account]; + require(amount > 0, "GenesisClaim: no rewards"); + + rewards[account] = 0; + + return amount; + } + + /** + * @notice Open claims. + * @dev Just sets openedAt timestamp. + */ + function openClaims() external onlyOwner { + _openClaims(); + } + + /** + * @notice Pause the contract. + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @notice Unpause the contract. + */ + function unpause() external onlyOwner { + _unpause(); + } + + /** + * @notice Withdraw all unclaimed tokens to the `withdrawTo` address, and close claims. + */ + function withdrawAndClose(address withdrawTo) external onlyOwner whenOpen { + require(block.timestamp >= openedAt + 45 days, "GenesisClaim: not 45 days"); + isOpen = false; + token.transfer(withdrawTo, token.balanceOf(address(this))); + emit ClaimsClosed(); + } + + /** + * @notice Withdraw clique fees to the `withdrawTo` address. + */ + function withdrawFees(address payable withdrawTo) external onlyClique whenNotPaused { + (bool success,) = withdrawTo.call{ value: address(this).balance }(""); + require(success, "GenesisClaim: withdrawal failed"); + } + + /** + * @notice Set the GenesisStake contract address. + * @param genesisStake_ The new GenesisStake contract address. + */ + function setGenesisStake(address genesisStake_) external onlyOwner { + _setGenesisStake(genesisStake_); + } + + /** + * @notice Set the Clique address. + * @param clique_ The new Clique address. + */ + function setClique(address clique_) external onlyOwner { + _setClique(clique_); + } + + /** + * @notice Set the GenesisStake contract address. + * @param genesisStake_ The new GenesisStake contract address. + */ + function _setGenesisStake(address genesisStake_) internal { + require(genesisStake_ != address(0), "GenesisClaim: no zero address"); + + address prevGenesisStake = address(genesisStake); + genesisStake = IGenesisStake(genesisStake_); + + // Approve new GenesisStake contract to transfer tokens. + token.approve(genesisStake_, type(uint256).max); + + // Revoke approval from previous GenesisStake contract. + if (prevGenesisStake != address(0)) token.approve(prevGenesisStake, 0); + + emit GenesisStakeChanged(genesisStake_, prevGenesisStake); + } + + /** + * @notice Set the Clique address. + * @param clique_ The new Clique address. + */ + function _setClique(address clique_) internal { + require(clique_ != address(0), "GenesisClaim: no zero address"); + emit CliqueChanged(clique_, clique); + clique = clique_; + } + + /** + * @notice Set the Clique fee. + * @param cliqueFee_ The new Clique fee. + */ + function _setCliqueFee(uint256 cliqueFee_) internal { + emit CliqueFeeChanged(cliqueFee, cliqueFee_); + cliqueFee = cliqueFee_; + } + + /** + * @notice Open claims. + */ + function _openClaims() internal { + require(!isOpen, "GenesisClaim: already open"); + isOpen = true; + openedAt = block.timestamp; + emit ClaimsOpened(); + } +} diff --git a/contracts/core/src/token/GenesisStake.sol b/contracts/core/src/token/GenesisStake.sol new file mode 100644 index 000000000..428fc4e18 --- /dev/null +++ b/contracts/core/src/token/GenesisStake.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.24; + +import { OwnableUpgradeable } from "@openzeppelin-v4/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin-v4/contracts-upgradeable/security/PausableUpgradeable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IGenesisStake } from "../interfaces/IGenesisStake.sol"; + +/** + * @title GenesisStake + * @notice Omni's genesis staking contract. It allows + */ +contract GenesisStake is IGenesisStake, OwnableUpgradeable, PausableUpgradeable { + /** + * @notice Emitted when an account stakes. + * @param recipient The recipient of the stake. + * @param amount The amount of tokens staked. + */ + event Staked(address indexed recipient, uint256 amount); + + /** + * @notice Emitted when a user unstakes tokens. + * @param account The account that unstaked tokens. + * @param amount The amount of tokens unstaked. + */ + event Unstaked(address indexed account, uint256 amount); + + /** + * @notice Emitted when a user withdraws tokens. + * @param account The account that withdrew tokens. + * @param amount The amount of tokens withdrawn. + */ + event Withdrawn(address indexed account, uint256 amount); + + /** + * @notice Emitted when the unboding period is changed. + * @param newDuration The new unboding period. + * @param prevDuration The previous unboding period. + */ + event UnbondingPeriodChanged(uint256 newDuration, uint256 prevDuration); + + /** + * @notice Emitted when staking is opened. + */ + event Opened(); + + /** + * @notice Emitted when staking is closed. + */ + event Closed(); + + /** + * @notice Omni erc20 token. + */ + IERC20 public immutable token; + + /** + * @notice Duration (in seconds) that a user must wait to withdraw after unstaking. + */ + uint256 public unbondingPeriod; + + /** + * @notice The staked balance of each user. + */ + mapping(address => uint256) public balanceOf; + + /** + * @notice The timestamp at which each user unstaked. + */ + mapping(address => uint256) public unstakedAt; + + /** + * @notice True is staking is open, false otherwise. + */ + bool public isOpen; + + /** + * @notice Restrict function to when staking is open. + */ + modifier whenOpen() { + require(isOpen, "GenesisStake: not open"); + _; + } + + constructor(address token_) { + token = IERC20(token_); + _disableInitializers(); + } + + /** + * @notice Initialize the contract. + * @param owner_ The owner of the contract. + * @param unbondingPeriod_ The unboding period. + */ + function initialize(address owner_, uint256 unbondingPeriod_) external initializer { + __Ownable_init(); + __Pausable_init(); + _transferOwnership(owner_); + _setUnbondingPeriod(unbondingPeriod_); + _open(); + } + + /** + * @notice Stake `amount` tokens for `user`, paid by the caller. + * @param recipient The recipient of the stake. + * @param amount The amount of tokens to stake. + */ + function stakeFor(address recipient, uint256 amount) external whenNotPaused whenOpen { + _stake(recipient, msg.sender, amount); + } + + /** + * @notice Stake `amount` tokens for the caller. + * @param amount The amount of tokens to stake. + */ + function stake(uint256 amount) external whenNotPaused whenOpen { + _stake(msg.sender, msg.sender, amount); + } + + /** + * @notice Internal function to stake `amount` tokens for `recipient`, paid by `patron`. + * @param recipient The recipient of the stake. + * @param patron The account paying for the stake. + * @param amount The amount of tokens to stake. + */ + function _stake(address recipient, address patron, uint256 amount) internal { + require(amount > 0, "GenesisStake: amount must be > 0"); + require(unstakedAt[recipient] == 0, "GenesisStake: unstaked"); + + balanceOf[recipient] += amount; + + require(token.transferFrom(patron, address(this), amount), "GenesisStake: transfer failed"); + + emit Staked(recipient, amount); + } + + /** + * @notice Unstake your entire balance. + */ + function unstake() external whenNotPaused { + require(balanceOf[msg.sender] > 0, "GenesisStake: not staked"); + require(unstakedAt[msg.sender] == 0, "GenesisStake: already unstaked"); + + unstakedAt[msg.sender] = block.timestamp; + + emit Unstaked(msg.sender, balanceOf[msg.sender]); + } + + /** + * @notice Withdraw your entire balance after the unbonding period. + */ + function withdraw() external whenNotPaused { + require(balanceOf[msg.sender] > 0, "GenesisStake: not staked"); + require(unstakedAt[msg.sender] > 0, "GenesisStake: not unstaked"); + require(block.timestamp >= unstakedAt[msg.sender] + unbondingPeriod, "GenesisStake: not unbonded"); + + uint256 amount = balanceOf[msg.sender]; + + // reset balance & timestamps + balanceOf[msg.sender] = 0; + unstakedAt[msg.sender] = 0; + + require(token.transfer(msg.sender, amount), "GenesisStake: transfer failed"); + + emit Withdrawn(msg.sender, amount); + } + + /** + * @notice Returns timestamp at which `account` can withdraw. + * Reverts if the account has not staked & unstaked. + */ + function canWithdrawAt(address account) external view returns (uint256) { + require(balanceOf[account] > 0, "GenesisStake: not staked"); + require(unstakedAt[account] > 0, "GenesisStake: not unstaked"); + + return unstakedAt[account] + unbondingPeriod; + } + + /** + * @notice Set the unboding period. + * @param duration The unboding period. + */ + function setUnbondingPeriod(uint256 duration) external onlyOwner { + _setUnbondingPeriod(duration); + } + + /** + * @notice Pause the contract. + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @notice Unpause the contract. + */ + function unpause() external onlyOwner { + _unpause(); + } + + /** + * @notice Open staking. + */ + function open() external onlyOwner { + _open(); + } + + /** + * @notice Close staking. + */ + function close() external onlyOwner { + _close(); + } + + /** + * @notice Set the unboding period. + * @param duration The unboding period. + */ + function _setUnbondingPeriod(uint256 duration) internal { + require(duration > 0, "GenesisStake: dur must be > 0"); + uint256 prev = unbondingPeriod; + unbondingPeriod = duration; + emit UnbondingPeriodChanged(duration, prev); + } + + /** + * @notice Open staking. + */ + function _open() internal { + require(!isOpen, "GenesisStake: already open"); + isOpen = true; + emit Opened(); + } + + /** + * @notice Close staking. + */ + function _close() internal { + require(isOpen, "GenesisStake: already closed"); + isOpen = false; + emit Closed(); + } +}