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

Add SnapshotStakingPool and SignedSnapshotStakingPool #2

Merged
merged 12 commits into from
Jul 4, 2024
32 changes: 32 additions & 0 deletions src/interfaces/staking/ISignedSnapshotStakingPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {ISnapshotStakingPool} from "./ISnapshotStakingPool.sol";

interface ISignedSnapshotStakingPool is ISnapshotStakingPool {

/// @notice Message to sign when staking
function message() external view returns (string memory);

/// @notice Mapping of approved stakers
function isApprovedStaker(address) external view returns (bool);

/// @notice Stake `amount` of stakeToken from `msg.sender` and mint staked tokens.
/// @param amount The amount of stakeToken to stake
/// @dev Must be an approved staker
function stake(uint256 amount) external;

/// @notice Stake `amount` of stakeToken from `msg.sender` and mint staked tokens.
/// @param amount The amount of stakeToken to stake
/// @param signature The signature of the message
/// @dev Approves the staker if not already approved
function stake(uint256 amount, bytes calldata signature) external;

/// @notice Approve the signer of the message as an approved staker
/// @param signature The signature of the message
function approveStaker(bytes calldata signature) external;

/// @notice Get the hashed digest of the message to be signed for staking
/// @return The hashed bytes to be signed
function getStakeSignatureDigest() external view returns (bytes32);
}
98 changes: 98 additions & 0 deletions src/interfaces/staking/ISnapshotStakingPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

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

interface ISnapshotStakingPool is IERC20 {

/// @notice Token to be distributed as rewards
function rewardToken() external view returns (IERC20);

/// @notice Token to be staked
function stakeToken() external view returns (IERC20);

/// @notice Distributor of rewards
function distributor() external view returns (address);

/// @notice Snapshot delay
function snapshotDelay() external view returns (uint256);

/// @notice Last snapshot time
function lastSnapshotTime() external view returns (uint256);

/// @notice Next snapshot id for `account` to claim
function nextClaimId(address account) external view returns (uint256);

/// @notice Reward snapshot at `snapshotId`
function rewardSnapshots(uint256) external view returns (uint256);

/// @notice Get the reward snapshots
function getRewardSnapshots() external view returns (uint256[] memory);

/// @notice Stake `amount` of stakeToken from `msg.sender` and mint staked tokens.
/// @param amount The amount of stakeToken to stake
function stake(uint256 amount) external;

/// @notice Unstake `amount` of stakeToken by `msg.sender`.
/// @param amount The amount of stakeToken to unstake
function unstake(uint256 amount) external;

/// @notice ONLY DISTRIBUTOR: Accrue rewardToken and update snapshot.
/// @param amount The amount of rewardToken to accrue
function accrue(uint256 amount) external;

/// @notice Claim the staking rewards from pending snapshots for `msg.sender`.
function claim() external;

/// @notice Claim partial staking rewards from pending snapshots for `msg.sender` from `_startClaimId` to `_endClaimId`.
/// @param startSnapshotId The snapshot id to start the partial claim
/// @param endSnapshotId The snapshot id to end the partial claim
function claimPartial(uint256 startSnapshotId, uint256 endSnapshotId) external;

/* ========== Admin Functions ========== */

/// @notice ONLY OWNER: Update the distributor address.
/// @param newDistributor The new distributor address
function setDistributor(address newDistributor) external;

/// @notice ONLY OWNER: Update the snapshot delay. Can set to 0 to disable snapshot delay.
/// @param newSnapshotDelay The new snapshot delay
function setSnapshotDelay(uint256 newSnapshotDelay) external;

/* ========== View Functions ========== */

/// @notice Get the current snapshot id.
/// @return The current snapshot id
function getCurrentSnapshotId() external view returns (uint256);

/// @notice Retrieves the rewards pending to be claimed by `account`.
/// @param account The account to retrieve pending rewards for
/// @return The rewards pending to be claimed by `account`
function getPendingRewards(address account) external view returns (uint256);

/// @notice Retrives the rewards of `account` in the range of `startSnapshotId` to `endSnapshotId`.
/// @param account The account to retrieve rewards for
/// @param startSnapshotId The start snapshot id
/// @param endSnapshotId The end snapshot id
/// @return The rewards of `account` in the range of `startSnapshotId` to `endSnapshotId`
function rewardOfInRange(address account, uint256 startSnapshotId, uint256 endSnapshotId) external view returns (uint256);

/// @notice Retrieves the rewards of `account` at the `snapshotId`.
/// @param account The account to retrieve rewards for
/// @param snapshotId The snapshot id
/// @return The rewards of `account` at the `snapshotId`
function rewardOfAt(address account, uint256 snapshotId) external view returns (uint256);

/// @notice Retrieves the total pool reward at the time `snapshotId`.
/// @param snapshotId The snapshot id
/// @return The total pool reward at the time `snapshotId`
function rewardAt(uint256 snapshotId) external view returns (uint256);

/// @notice Check if rewards can be accrued.
/// @return Boolean indicating if rewards can be accrued
function canAccrue() external view returns (bool);

/// @notice Get the time until the next snapshot.
/// @return The time until the next snapshot
function getTimeUntilNextSnapshot() external view returns (uint256);
}
100 changes: 100 additions & 0 deletions src/staking/SignedSnapshotStakingPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ISignedSnapshotStakingPool} from "../interfaces/staking/ISignedSnapshotStakingPool.sol";

import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
Copy link
Contributor

Choose a reason for hiding this comment

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

Great find of this SignatureChecker library with both ECDSA and SC signature support. Just what we needed 👍

import {SnapshotStakingPool} from "./SnapshotStakingPool.sol";

/// @title SignedSnapshotStakingPool
/// @author Index Cooperative
/// @notice A contract for staking `stakeToken` and receiving `rewardToken` based
/// on snapshots taken when rewards are accrued.
contract SignedSnapshotStakingPool is ISignedSnapshotStakingPool, SnapshotStakingPool, EIP712 {
string private constant MESSAGE_TYPE = "StakeMessage(string message)";

/* EVENTS */

event StakerApproved(address indexed staker);

/* STORAGE */

/// @inheritdoc ISignedSnapshotStakingPool
string public message;
/// @inheritdoc ISignedSnapshotStakingPool
mapping(address => bool) public isApprovedStaker;

/* CONSTRUCTOR */

/// @param eip712Name Name of the EIP712 signing domain
/// @param eip712Version Current major version of the EIP712 signing domain
/// @param stakeMessage The message to sign when staking
/// @param name Name of the staked token
/// @param symbol Symbol of the staked token
/// @param rewardToken Instance of the reward token
/// @param stakeToken Instance of the stake token
/// @param distributor Address of the distributor
/// @param snapshotDelay The minimum amount of time between snapshots
constructor(
string memory eip712Name,
string memory eip712Version,
string memory stakeMessage,
string memory name,
string memory symbol,
IERC20 rewardToken,
IERC20 stakeToken,
address distributor,
uint256 snapshotDelay
)
EIP712(eip712Name, eip712Version)
SnapshotStakingPool(name, symbol, rewardToken, stakeToken, distributor, snapshotDelay)
{
message = stakeMessage;
}

/* STAKER FUNCTIONS */

/// @inheritdoc ISignedSnapshotStakingPool
function stake(uint256 _amount) external override(SnapshotStakingPool, ISignedSnapshotStakingPool) nonReentrant {
require(isApprovedStaker[msg.sender], "Not approved staker");
_stake(msg.sender, _amount);
}

/// @inheritdoc ISignedSnapshotStakingPool
function stake(uint256 _amount, bytes calldata _signature) external nonReentrant {
_approveStaker(msg.sender, _signature);
_stake(msg.sender, _amount);
}

/// @inheritdoc ISignedSnapshotStakingPool
function approveStaker(bytes calldata _signature) external {
_approveStaker(msg.sender, _signature);
}

/* VIEW FUNCTIONS */

/// @inheritdoc ISignedSnapshotStakingPool
function getStakeSignatureDigest() public view returns (bytes32) {
return _hashTypedDataV4(
keccak256(
abi.encode(
keccak256(abi.encodePacked(MESSAGE_TYPE)),
keccak256(bytes(message))
)
)
);
}

/* INTERNAL FUNCTIONS */

/// @dev Approve the `staker` if the `signature` is valid
/// @param staker The staker to approve
/// @param signature The signature to verify
function _approveStaker(address staker, bytes calldata signature) internal {
require(SignatureChecker.isValidSignatureNow(staker, getStakeSignatureDigest(), signature), "Invalid signature");
isApprovedStaker[staker] = true;
emit StakerApproved(staker);
}
}
Loading