Contract detail
TimeWeightedStakingPool
Live Robinhood Chain account facts for 0xaf89d59a…6aa7426e with at least 25 observed transactions. The history total is updating as indexing continues.
- Address type
- Smart contract
- Transactions
- 25+ observed
- First indexed block
- #809381
- Latest indexed block
- #51057490
- History coverage
- Recent activity available; total is catching up
- Indexed token transfers
- 10
- ERC-20 transfers
- 19
- Emitted events
- 12
- Verification
- Verified
- Compiler
- v0.8.24+commit.e11b9ed9
- Verification match
- full
- Creation transaction
- 0xd9477709…1ad480d6
- Created at block
- #809381
- Language
- solidity
- EVM version
- cancun
- License
- none
- Compiler optimization
- Enabled · 200 runs
Related activity
Transactions
Verified contract
Contract source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
/// @title TimeWeightedStakingPool
/// @notice ERC20 staking pool platform with owner-listed tokens and time-weighted reward distribution.
/// @dev Pools use one ERC20 token for both stake and rewards. Rewards pay a per-pool platform fee snapshot.
/// Only trusted normal ERC20 tokens should be listed. Tokens with mutable tax, rebasing, blacklist, or
/// non-standard transfer behavior may break user expectations and are rejected when exact transfers fail.
contract TimeWeightedStakingPool is Ownable2Step, ReentrancyGuard {
using SafeERC20 for IERC20;
/// @notice Default staking duration used by the simple pool-creation overload.
uint64 public constant DEFAULT_DURATION = 14 days;
/// @notice Default post-end claim window used by pool-creation overloads when claimDuration is zero.
uint64 public constant DEFAULT_CLAIM_WINDOW = 30 days;
/// @notice Denominator for basis-point fee math.
uint16 public constant BPS_DENOMINATOR = 10_000;
/// @notice Maximum default platform fee, in basis points.
uint16 public constant MAX_PLATFORM_FEE_BPS = 500;
/// @notice Default platform fee for future pools, in basis points.
uint16 public defaultPlatformFeeBps;
/// @notice When enabled, only the owner can create pools.
bool public ownerOnlyPoolCreation = true;
/// @notice Default minimum reward amount required for new pools unless a token override is set.
/// @dev Uses 18-decimal token units. Set per-token overrides for tokens with other decimals.
uint256 public globalMinRewardAmount = 1000 ether;
struct Pool {
IERC20 token;
address creator;
uint256 rewardAmount;
uint64 startTime;
uint64 endTime;
uint64 claimDeadline;
uint16 platformFeeBps;
uint256 totalStaked;
uint256 totalPoints;
uint64 lastGlobalUpdate;
bool finalized;
uint256 rewardClaimed;
uint256 rewardRecovered;
}
struct UserInfo {
uint256 amount;
uint256 points;
uint64 lastUpdate;
bool claimed;
uint256 rewardPaid;
uint256 rewardFeePaid;
}
struct ListedToken {
address token;
string name;
string symbol;
string metadataURI;
address lister;
uint64 listedAt;
bool listed;
bool active;
}
error ZeroAddress();
error ZeroAmount();
error PoolDoesNotExist();
error PoolNotStarted();
error PoolEnded();
error PoolNotEnded();
error PoolAlreadyFinalized();
error PoolNotFinalized();
error ClaimDeadlinePassed();
error ClaimDeadlineNotPassed();
error AlreadyClaimed();
error InsufficientStake();
error TokenAmountReserved();
error NoRecoverableTokens();
error InvalidPoolTime();
error InsufficientPlatformFees();
error FeeTooHigh();
error PoolCreationRestricted();
error TokenNotListed();
error TokenAlreadyListed();
error TokenInactive();
error TokenNotContract();
error EmptyTokenMetadata();
error UnsupportedTokenTransfer();
error NotPoolCreatorOrOwner();
error RewardAmountTooLow();
event PoolCreated(
uint256 indexed poolId,
address indexed creator,
address indexed token,
uint256 rewardAmount,
uint64 startTime,
uint64 endTime,
uint64 claimDeadline,
uint16 platformFeeBps
);
event Staked(uint256 indexed poolId, address indexed user, uint256 amount);
event Withdrawn(uint256 indexed poolId, address indexed user, uint256 amount);
event Claimed(
uint256 indexed poolId, address indexed user, uint256 grossReward, uint256 platformFee, uint256 netReward
);
event PoolFinalized(uint256 indexed poolId, uint256 totalPoints);
event PlatformFeesWithdrawn(address indexed token, address indexed to, uint256 amount);
event TokenListed(address indexed token, address indexed lister, string name, string symbol, string metadataURI);
event ListedTokenUpdated(address indexed token, string name, string symbol, string metadataURI);
event ListedTokenActiveUpdated(address indexed token, bool active);
event DefaultPlatformFeeBpsUpdated(uint16 oldFeeBps, uint16 newFeeBps);
event OwnerOnlyPoolCreationUpdated(bool enabled);
event GlobalMinRewardAmountUpdated(uint256 oldAmount, uint256 newAmount);
event TokenMinRewardAmountUpdated(address indexed token, uint256 oldAmount, uint256 newAmount);
event RewardDustRecovered(uint256 indexed poolId, address indexed token, address indexed to, uint256 amount);
event TokenRecovered(address indexed token, address indexed to, uint256 amount);
Pool[] private _pools;
mapping(uint256 poolId => mapping(address user => UserInfo)) private _users;
mapping(address token => ListedToken info) private _listedTokens;
address[] private _listedTokenList;
mapping(address token => uint256 amount) private _tokenMinRewardAmount;
mapping(address token => uint256[] poolIds) private _poolIdsByToken;
mapping(address creator => uint256[] poolIds) private _poolIdsByCreator;
mapping(address token => uint256 amount) private _reservedStake;
mapping(address token => uint256 amount) private _reservedReward;
mapping(address token => uint256 amount) private _platformFees;
constructor(address initialOwner) Ownable(initialOwner) {
if (initialOwner == address(0)) revert ZeroAddress();
defaultPlatformFeeBps = 50;
ownerOnlyPoolCreation = true;
}
/// @notice Create a same-token staking/reward pool starting now with the default duration and claim window.
/// @param token Listed active ERC20 token users stake and receive as rewards.
/// @param rewardAmount Fixed reward budget deposited by the pool creator.
/// @return poolId The created pool id.
function createPool(address token, uint256 rewardAmount) external nonReentrant returns (uint256 poolId) {
return _createPoolFromDuration(token, rewardAmount, DEFAULT_DURATION, DEFAULT_CLAIM_WINDOW);
}
/// @notice Create a same-token staking/reward pool starting now with configurable duration and claim window.
/// @dev Passing duration as zero uses the default duration. Passing claimDuration as zero uses the default claim window.
/// @param token Listed active ERC20 token users stake and receive as rewards.
/// @param rewardAmount Fixed reward budget deposited by the pool creator.
/// @param duration Pool staking duration in seconds.
/// @param claimDuration Seconds after endTime during which users may claim; zero uses the default claim window.
/// @return poolId The created pool id.
function createPool(address token, uint256 rewardAmount, uint64 duration, uint64 claimDuration)
external
nonReentrant
returns (uint256 poolId)
{
uint64 poolDuration = duration == 0 ? DEFAULT_DURATION : duration;
uint64 actualClaimDuration = claimDuration == 0 ? DEFAULT_CLAIM_WINDOW : claimDuration;
return _createPoolFromDuration(token, rewardAmount, poolDuration, actualClaimDuration);
}
/// @notice Create a same-token staking/reward pool with explicit staking start and end timestamps.
/// @dev startTime must be now or later, and endTime must be after startTime.
/// Passing claimDuration as zero uses the default claim window.
/// @param token Listed active ERC20 token users stake and receive as rewards.
/// @param rewardAmount Fixed reward budget deposited by the pool creator.
/// @param startTime Unix timestamp when staking opens.
/// @param endTime Unix timestamp when staking closes and reward points stop accruing.
/// @param claimDuration Seconds after endTime during which users may claim; zero uses the default claim window.
/// @return poolId The created pool id.
function createPool(address token, uint256 rewardAmount, uint64 startTime, uint64 endTime, uint64 claimDuration)
external
nonReentrant
returns (uint256 poolId)
{
uint64 actualClaimDuration = claimDuration == 0 ? DEFAULT_CLAIM_WINDOW : claimDuration;
return _createPool(token, rewardAmount, startTime, endTime, actualClaimDuration);
}
/// @notice Stake ERC20 tokens while a pool is active.
/// @param poolId Pool id.
/// @param amount Amount of staking tokens to deposit.
function stake(uint256 poolId, uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroAmount();
Pool storage pool = _pool(poolId);
if (block.timestamp < pool.startTime) revert PoolNotStarted();
if (block.timestamp >= pool.endTime) revert PoolEnded();
_updatePool(pool);
UserInfo storage user = _users[poolId][msg.sender];
_updateUser(pool, user);
_safeTransferFromExact(pool.token, msg.sender, amount);
pool.totalStaked += amount;
user.amount += amount;
_reservedStake[address(pool.token)] += amount;
emit Staked(poolId, msg.sender, amount);
}
/// @notice Claim earned net rewards after the pool has ended.
/// @param poolId Pool id.
function claim(uint256 poolId) external nonReentrant {
Pool storage pool = _pool(poolId);
(,, uint256 netReward) = _prepareClaim(poolId, pool, msg.sender, true);
if (netReward != 0) {
pool.token.safeTransfer(msg.sender, netReward);
}
}
/// @notice Withdraw staked tokens after the pool has ended.
/// @param poolId Pool id.
/// @param amount Amount of staking tokens to withdraw.
function withdraw(uint256 poolId, uint256 amount) external nonReentrant {
Pool storage pool = _pool(poolId);
uint256 withdrawn = _prepareWithdraw(poolId, pool, msg.sender, amount);
pool.token.safeTransfer(msg.sender, withdrawn);
}
/// @notice Claim available net rewards and withdraw the caller's full stake after the pool has ended.
/// @dev If the claim deadline has passed, this still withdraws stake but does not claim rewards.
/// @param poolId Pool id.
function exit(uint256 poolId) external nonReentrant {
Pool storage pool = _pool(poolId);
if (block.timestamp < pool.endTime) revert PoolNotEnded();
_finalizePool(poolId, pool);
UserInfo storage user = _users[poolId][msg.sender];
uint256 netReward;
if (!user.claimed && !_isClaimDeadlinePassed(pool)) {
(,, netReward) = _prepareClaimAfterFinalized(poolId, pool, user, msg.sender);
}
uint256 withdrawn;
if (user.amount != 0) {
withdrawn = _prepareWithdrawAfterEnd(poolId, pool, user, msg.sender, user.amount);
}
if (netReward != 0) {
pool.token.safeTransfer(msg.sender, netReward);
}
if (withdrawn != 0) {
pool.token.safeTransfer(msg.sender, withdrawn);
}
}
/// @notice Finalize a pool after its end time so reward shares use final total points.
/// @param poolId Pool id.
function finalizePool(uint256 poolId) external {
Pool storage pool = _pool(poolId);
if (block.timestamp < pool.endTime) revert PoolNotEnded();
if (pool.finalized) revert PoolAlreadyFinalized();
_finalizePool(poolId, pool);
}
/// @notice Return a user's currently claimable net reward after platform fee.
/// @dev Reverts before endTime because final totalPoints are not available yet.
/// @param poolId Pool id.
/// @param account User address.
/// @return reward Pending net reward amount.
function pendingReward(uint256 poolId, address account) external view returns (uint256 reward) {
(,, reward) = pendingRewardBreakdown(poolId, account);
}
/// @notice Return a user's gross reward, platform fee, and net reward after the pool has ended.
/// @dev Reverts before endTime because final totalPoints are not available yet.
/// @param poolId Pool id.
/// @param account User address.
/// @return grossReward User reward before platform fee.
/// @return platformFee Platform fee deducted from gross reward.
/// @return netReward User reward after platform fee.
function pendingRewardBreakdown(uint256 poolId, address account)
public
view
returns (uint256 grossReward, uint256 platformFee, uint256 netReward)
{
Pool storage pool = _pool(poolId);
if (block.timestamp < pool.endTime) revert PoolNotEnded();
if (_isClaimDeadlinePassed(pool)) revert ClaimDeadlinePassed();
UserInfo storage user = _users[poolId][account];
if (user.claimed) return (0, 0, 0);
uint256 finalTotalPoints = _finalTotalPoints(pool);
if (finalTotalPoints == 0) return (0, 0, 0);
return
_rewardBreakdown(pool.rewardAmount, _currentUserPoints(pool, user), finalTotalPoints, pool.platformFeeBps);
}
/// @notice Return a user's time-weighted staking points, capped to the pool's active window.
/// @param poolId Pool id.
/// @param account User address.
/// @return points User points.
function userPoints(uint256 poolId, address account) external view returns (uint256 points) {
Pool storage pool = _pool(poolId);
return _currentUserPoints(pool, _users[poolId][account]);
}
/// @notice Return pool data.
/// @param poolId Pool id.
/// @return pool Pool info.
function poolInfo(uint256 poolId) external view returns (Pool memory pool) {
return _pool(poolId);
}
/// @notice Return a pool's creator address.
/// @param poolId Pool id.
/// @return creator Pool creator.
function poolCreator(uint256 poolId) external view returns (address creator) {
return _pool(poolId).creator;
}
/// @notice Return a pool's single staking/reward token address.
/// @param poolId Pool id.
/// @return token Pool ERC20 token.
function poolToken(uint256 poolId) external view returns (address token) {
return address(_pool(poolId).token);
}
/// @notice Return stored user data.
/// @param poolId Pool id.
/// @param account User address.
/// @return user User info.
function userInfo(uint256 poolId, address account) external view returns (UserInfo memory user) {
_pool(poolId);
return _users[poolId][account];
}
/// @notice Return the number of pools created.
/// @return count Pool count.
function poolCount() external view returns (uint256 count) {
return _pools.length;
}
/// @notice Return number of pools created for one token.
/// @param token ERC20 token address.
/// @return count Pool count for token.
function poolCountByToken(address token) external view returns (uint256 count) {
return _poolIdsByToken[token].length;
}
/// @notice Return a pool id for one token by index.
/// @param token ERC20 token address.
/// @param index Token pool index.
/// @return poolId Pool id.
function poolIdByTokenAt(address token, uint256 index) external view returns (uint256 poolId) {
return _poolIdsByToken[token][index];
}
/// @notice Return number of pools created by one creator.
/// @param creator Pool creator address.
/// @return count Pool count for creator.
function poolCountByCreator(address creator) external view returns (uint256 count) {
return _poolIdsByCreator[creator].length;
}
/// @notice Return a pool id for one creator by index.
/// @param creator Pool creator address.
/// @param index Creator pool index.
/// @return poolId Pool id.
function poolIdByCreatorAt(address creator, uint256 index) external view returns (uint256 poolId) {
return _poolIdsByCreator[creator][index];
}
/// @notice Return a paginated list of pool ids for one token.
/// @param token ERC20 token address.
/// @param offset Start index.
/// @param limit Maximum number of pool ids to return.
/// @return poolIds Pool ids.
function poolIdsByToken(address token, uint256 offset, uint256 limit)
external
view
returns (uint256[] memory poolIds)
{
return _sliceUintArray(_poolIdsByToken[token], offset, limit);
}
/// @notice Return a paginated list of pool ids for one creator.
/// @param creator Pool creator address.
/// @param offset Start index.
/// @param limit Maximum number of pool ids to return.
/// @return poolIds Pool ids.
function poolIdsByCreator(address creator, uint256 offset, uint256 limit)
external
view
returns (uint256[] memory poolIds)
{
return _sliceUintArray(_poolIdsByCreator[creator], offset, limit);
}
/// @notice Return accumulated withdrawable platform fees for one token.
/// @param token ERC20 token address.
/// @return amount Accumulated platform fees.
function platformFees(address token) external view returns (uint256 amount) {
return _platformFees[token];
}
/// @notice List a token for future pool creation.
/// @param token ERC20 token contract.
/// @param name Frontend display name.
/// @param symbol Frontend display symbol.
/// @param metadataURI IPFS or HTTPS metadata JSON URI.
function listToken(address token, string calldata name, string calldata symbol, string calldata metadataURI)
external
onlyOwner
{
_listToken(token, name, symbol, metadataURI);
}
/// @notice Update frontend metadata for a listed token.
/// @param token Listed token address.
/// @param name Frontend display name.
/// @param symbol Frontend display symbol.
/// @param metadataURI IPFS or HTTPS metadata JSON URI.
function updateListedTokenMetadata(
address token,
string calldata name,
string calldata symbol,
string calldata metadataURI
) external onlyOwner {
ListedToken storage info = _listedTokens[token];
if (!info.listed) revert TokenNotListed();
_requireTokenMetadata(name, symbol, metadataURI);
info.name = name;
info.symbol = symbol;
info.metadataURI = metadataURI;
emit ListedTokenUpdated(token, name, symbol, metadataURI);
}
/// @notice Enable or disable a listed token for new pool creation.
/// @param token Listed token address.
/// @param active Whether new pools can be created for the token.
function setListedTokenActive(address token, bool active) external onlyOwner {
ListedToken storage info = _listedTokens[token];
if (!info.listed) revert TokenNotListed();
info.active = active;
emit ListedTokenActiveUpdated(token, active);
}
/// @notice Return whether a token is listed.
/// @param token Token address.
/// @return listed True if listed.
function isTokenListed(address token) external view returns (bool listed) {
return _listedTokens[token].listed;
}
/// @notice Return whether a token is listed and active.
/// @param token Token address.
/// @return active True if active.
function isTokenActive(address token) external view returns (bool active) {
return _listedTokens[token].active;
}
/// @notice Return listed token metadata.
/// @param token Token address.
/// @return info Listed token info.
function listedTokenInfo(address token) external view returns (ListedToken memory info) {
return _listedTokens[token];
}
/// @notice Return listed token count.
/// @return count Listed token count.
function listedTokenCount() external view returns (uint256 count) {
return _listedTokenList.length;
}
/// @notice Return a listed token address by index.
/// @param index Listed token index.
/// @return token Token address.
function listedTokenAt(uint256 index) external view returns (address token) {
return _listedTokenList[index];
}
/// @notice Return a paginated list of listed token addresses.
/// @param offset Start index.
/// @param limit Maximum number of addresses to return.
/// @return tokens Listed token addresses.
function listedTokenAddresses(uint256 offset, uint256 limit) external view returns (address[] memory tokens) {
uint256 length = _listedTokenList.length;
if (offset >= length || limit == 0) return new address[](0);
uint256 remaining = length - offset;
uint256 resultLength = limit < remaining ? limit : remaining;
tokens = new address[](resultLength);
for (uint256 i = 0; i < resultLength; i++) {
tokens[i] = _listedTokenList[offset + i];
}
}
/// @notice Update the default platform fee used by future pools.
/// @param newFeeBps New fee in basis points.
function setDefaultPlatformFeeBps(uint16 newFeeBps) external onlyOwner {
if (newFeeBps > MAX_PLATFORM_FEE_BPS) revert FeeTooHigh();
uint16 oldFeeBps = defaultPlatformFeeBps;
defaultPlatformFeeBps = newFeeBps;
emit DefaultPlatformFeeBpsUpdated(oldFeeBps, newFeeBps);
}
/// @notice Toggle whether only the owner can create new pools.
/// @param enabled True to restrict pool creation to owner.
function setOwnerOnlyPoolCreation(bool enabled) external onlyOwner {
ownerOnlyPoolCreation = enabled;
emit OwnerOnlyPoolCreationUpdated(enabled);
}
/// @notice Update the default minimum reward amount required for future pools.
/// @param newAmount New global minimum reward amount.
function setGlobalMinRewardAmount(uint256 newAmount) external onlyOwner {
uint256 oldAmount = globalMinRewardAmount;
globalMinRewardAmount = newAmount;
emit GlobalMinRewardAmountUpdated(oldAmount, newAmount);
}
/// @notice Update the minimum reward amount override for one listed token.
/// @param token Listed token address.
/// @param newAmount New token-specific minimum reward amount; zero uses global minimum.
function setTokenMinRewardAmount(address token, uint256 newAmount) external onlyOwner {
if (token == address(0)) revert ZeroAddress();
if (!_listedTokens[token].listed) revert TokenNotListed();
uint256 oldAmount = _tokenMinRewardAmount[token];
_tokenMinRewardAmount[token] = newAmount;
emit TokenMinRewardAmountUpdated(token, oldAmount, newAmount);
}
/// @notice Return token-specific minimum reward amount override.
/// @param token Token address.
/// @return amount Token-specific minimum reward amount; zero means no override.
function tokenMinRewardAmount(address token) external view returns (uint256 amount) {
return _tokenMinRewardAmount[token];
}
/// @notice Return effective minimum reward amount for new pools using one token.
/// @param token Token address.
/// @return amount Token override if set, otherwise global minimum.
function effectiveMinRewardAmount(address token) public view returns (uint256 amount) {
uint256 tokenMin = _tokenMinRewardAmount[token];
return tokenMin == 0 ? globalMinRewardAmount : tokenMin;
}
/// @notice Withdraw accumulated platform fees for one token.
/// @param token ERC20 token to withdraw fees from.
/// @param to Recipient address.
/// @param amount Fee amount to withdraw.
function withdrawPlatformFees(address token, address to, uint256 amount) external onlyOwner nonReentrant {
if (token == address(0) || to == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
if (amount > _platformFees[token]) revert InsufficientPlatformFees();
_platformFees[token] -= amount;
IERC20(token).safeTransfer(to, amount);
emit PlatformFeesWithdrawn(token, to, amount);
}
/// @notice Withdraw all accumulated platform fees for one token.
/// @param token ERC20 token to withdraw fees from.
/// @param to Recipient address.
/// @return amount Fee amount withdrawn.
function withdrawAllPlatformFees(address token, address to)
external
onlyOwner
nonReentrant
returns (uint256 amount)
{
if (token == address(0) || to == address(0)) revert ZeroAddress();
amount = _platformFees[token];
if (amount == 0) revert NoRecoverableTokens();
_platformFees[token] = 0;
IERC20(token).safeTransfer(to, amount);
emit PlatformFeesWithdrawn(token, to, amount);
}
/// @notice Recover unclaimed reward dust after a pool's claim deadline. Callable by pool creator or owner.
/// @param poolId Pool id.
/// @param to Recipient address.
/// @return amount Amount recovered.
function recoverRewardDust(uint256 poolId, address to) external nonReentrant returns (uint256 amount) {
if (to == address(0)) revert ZeroAddress();
Pool storage pool = _pool(poolId);
if (block.timestamp < pool.endTime) revert PoolNotEnded();
_finalizePool(poolId, pool);
if (msg.sender != owner() && msg.sender != pool.creator) revert NotPoolCreatorOrOwner();
if (block.timestamp <= pool.claimDeadline) revert ClaimDeadlineNotPassed();
uint256 remaining = pool.rewardAmount - pool.rewardClaimed - pool.rewardRecovered;
if (remaining == 0) revert NoRecoverableTokens();
pool.rewardRecovered += remaining;
_reservedReward[address(pool.token)] -= remaining;
pool.token.safeTransfer(to, remaining);
emit RewardDustRecovered(poolId, address(pool.token), to, remaining);
return remaining;
}
/// @notice Recover tokens accidentally sent to this contract without touching reserved stake, rewards, or platform fees.
/// @param token ERC20 token to recover.
/// @param to Recipient address.
/// @param amount Amount to recover.
function recoverToken(IERC20 token, address to, uint256 amount) external onlyOwner nonReentrant {
if (address(token) == address(0) || to == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
uint256 protectedAmount =
_reservedStake[address(token)] + _reservedReward[address(token)] + _platformFees[address(token)];
uint256 balance = token.balanceOf(address(this));
if (balance < protectedAmount || amount > balance - protectedAmount) revert TokenAmountReserved();
token.safeTransfer(to, amount);
emit TokenRecovered(address(token), to, amount);
}
/// @notice Return aggregate staking-token reserves protected from normal owner recovery.
/// @param token ERC20 token address.
/// @return amount Reserved stake amount.
function reservedStake(address token) external view returns (uint256 amount) {
return _reservedStake[token];
}
/// @notice Return aggregate reward-token reserves protected from normal owner recovery.
/// @param token ERC20 token address.
/// @return amount Reserved reward amount.
function reservedReward(address token) external view returns (uint256 amount) {
return _reservedReward[token];
}
function _createPoolFromDuration(address token, uint256 rewardAmount, uint64 duration, uint64 claimDuration)
private
returns (uint256 poolId)
{
uint64 startTime = uint64(block.timestamp);
return _createPool(token, rewardAmount, startTime, startTime + duration, claimDuration);
}
function _createPool(address token, uint256 rewardAmount, uint64 startTime, uint64 endTime, uint64 claimDuration)
private
returns (uint256 poolId)
{
_enforcePoolCreationPermission();
if (token == address(0)) revert ZeroAddress();
_requireListedAndActiveToken(token);
if (rewardAmount == 0) revert ZeroAmount();
if (startTime < block.timestamp || endTime <= startTime) revert InvalidPoolTime();
if (rewardAmount < effectiveMinRewardAmount(token)) revert RewardAmountTooLow();
_safeTransferFromExact(IERC20(token), msg.sender, rewardAmount);
uint64 claimDeadline = endTime + claimDuration;
poolId = _pools.length;
_pools.push(
Pool({
token: IERC20(token),
creator: msg.sender,
rewardAmount: rewardAmount,
startTime: startTime,
endTime: endTime,
claimDeadline: claimDeadline,
platformFeeBps: defaultPlatformFeeBps,
totalStaked: 0,
totalPoints: 0,
lastGlobalUpdate: startTime,
finalized: false,
rewardClaimed: 0,
rewardRecovered: 0
})
);
_reservedReward[token] += rewardAmount;
_poolIdsByToken[token].push(poolId);
_poolIdsByCreator[msg.sender].push(poolId);
emit PoolCreated(
poolId, msg.sender, token, rewardAmount, startTime, endTime, claimDeadline, defaultPlatformFeeBps
);
}
function _prepareClaim(uint256 poolId, Pool storage pool, address account, bool enforceDeadline)
private
returns (uint256 grossReward, uint256 platformFee, uint256 netReward)
{
if (block.timestamp < pool.endTime) revert PoolNotEnded();
_finalizePool(poolId, pool);
if (enforceDeadline && _isClaimDeadlinePassed(pool)) revert ClaimDeadlinePassed();
UserInfo storage user = _users[poolId][account];
return _prepareClaimAfterFinalized(poolId, pool, user, account);
}
function _prepareClaimAfterFinalized(uint256 poolId, Pool storage pool, UserInfo storage user, address account)
private
returns (uint256 grossReward, uint256 platformFee, uint256 netReward)
{
if (!pool.finalized) revert PoolNotFinalized();
if (user.claimed) revert AlreadyClaimed();
_updateUser(pool, user);
if (pool.totalPoints != 0) {
(grossReward, platformFee, netReward) =
_rewardBreakdown(pool.rewardAmount, user.points, pool.totalPoints, pool.platformFeeBps);
}
user.claimed = true;
user.rewardPaid = netReward;
user.rewardFeePaid = platformFee;
pool.rewardClaimed += grossReward;
_reservedReward[address(pool.token)] -= grossReward;
_platformFees[address(pool.token)] += platformFee;
emit Claimed(poolId, account, grossReward, platformFee, netReward);
}
function _prepareWithdraw(uint256 poolId, Pool storage pool, address account, uint256 amount)
private
returns (uint256 withdrawn)
{
if (block.timestamp < pool.endTime) revert PoolNotEnded();
_finalizePool(poolId, pool);
UserInfo storage user = _users[poolId][account];
return _prepareWithdrawAfterEnd(poolId, pool, user, account, amount);
}
function _prepareWithdrawAfterEnd(
uint256 poolId,
Pool storage pool,
UserInfo storage user,
address account,
uint256 amount
) private returns (uint256 withdrawn) {
if (amount == 0) revert ZeroAmount();
if (user.amount < amount) revert InsufficientStake();
_updateUser(pool, user);
user.amount -= amount;
pool.totalStaked -= amount;
_reservedStake[address(pool.token)] -= amount;
emit Withdrawn(poolId, account, amount);
return amount;
}
function _finalizePool(uint256 poolId, Pool storage pool) private {
if (pool.finalized) return;
_updatePool(pool);
pool.finalized = true;
emit PoolFinalized(poolId, pool.totalPoints);
}
function _updatePool(Pool storage pool) private {
uint64 applicableTime = _applicableTime(pool);
if (applicableTime <= pool.lastGlobalUpdate) return;
pool.totalPoints += pool.totalStaked * (applicableTime - pool.lastGlobalUpdate);
pool.lastGlobalUpdate = applicableTime;
}
function _updateUser(Pool storage pool, UserInfo storage user) private {
uint64 applicableTime = _applicableTime(pool);
if (user.lastUpdate == 0 && user.amount == 0) {
user.lastUpdate = applicableTime;
return;
}
if (applicableTime <= user.lastUpdate) return;
user.points += user.amount * (applicableTime - user.lastUpdate);
user.lastUpdate = applicableTime;
}
function _currentUserPoints(Pool storage pool, UserInfo storage user) private view returns (uint256 points) {
points = user.points;
if (user.lastUpdate == 0 && user.amount == 0) return points;
uint64 applicableTime = _applicableTime(pool);
if (applicableTime > user.lastUpdate) {
points += user.amount * (applicableTime - user.lastUpdate);
}
}
function _finalTotalPoints(Pool storage pool) private view returns (uint256 points) {
points = pool.totalPoints;
uint64 applicableTime = _applicableTime(pool);
if (applicableTime > pool.lastGlobalUpdate) {
points += pool.totalStaked * (applicableTime - pool.lastGlobalUpdate);
}
}
function _rewardBreakdown(uint256 rewardAmount, uint256 userPoints_, uint256 totalPoints_, uint16 feeBps)
private
pure
returns (uint256 grossReward, uint256 platformFee, uint256 netReward)
{
grossReward = Math.mulDiv(rewardAmount, userPoints_, totalPoints_);
platformFee = Math.mulDiv(grossReward, feeBps, BPS_DENOMINATOR);
netReward = grossReward - platformFee;
}
function _applicableTime(Pool storage pool) private view returns (uint64) {
uint256 timestamp = block.timestamp;
if (timestamp < pool.startTime) return pool.startTime;
return timestamp < pool.endTime ? uint64(timestamp) : pool.endTime;
}
function _isClaimDeadlinePassed(Pool storage pool) private view returns (bool) {
return block.timestamp > pool.claimDeadline;
}
function _pool(uint256 poolId) private view returns (Pool storage pool) {
if (poolId >= _pools.length) revert PoolDoesNotExist();
return _pools[poolId];
}
function _listToken(address token, string calldata name, string calldata symbol, string calldata metadataURI)
private
{
if (token == address(0)) revert ZeroAddress();
if (token.code.length == 0) revert TokenNotContract();
if (_listedTokens[token].listed) revert TokenAlreadyListed();
_requireTokenMetadata(name, symbol, metadataURI);
_listedTokens[token] = ListedToken({
token: token,
name: name,
symbol: symbol,
metadataURI: metadataURI,
lister: msg.sender,
listedAt: uint64(block.timestamp),
listed: true,
active: true
});
_listedTokenList.push(token);
emit TokenListed(token, msg.sender, name, symbol, metadataURI);
}
function _requireTokenMetadata(string calldata name, string calldata symbol, string calldata metadataURI)
private
pure
{
if (bytes(name).length == 0 || bytes(symbol).length == 0 || bytes(metadataURI).length == 0) {
revert EmptyTokenMetadata();
}
}
function _enforcePoolCreationPermission() private view {
if (ownerOnlyPoolCreation && msg.sender != owner()) revert PoolCreationRestricted();
}
function _requireListedAndActiveToken(address token) private view {
ListedToken storage info = _listedTokens[token];
if (!info.listed) revert TokenNotListed();
if (!info.active) revert TokenInactive();
}
function _sliceUintArray(uint256[] storage source, uint256 offset, uint256 limit)
private
view
returns (uint256[] memory result)
{
uint256 length = source.length;
if (offset >= length || limit == 0) return new uint256[](0);
uint256 remaining = length - offset;
uint256 resultLength = limit < remaining ? limit : remaining;
result = new uint256[](resultLength);
for (uint256 i = 0; i < resultLength; i++) {
result[i] = source[offset + i];
}
}
function _safeTransferFromExact(IERC20 token, address from, uint256 amount) private {
uint256 beforeBalance = token.balanceOf(address(this));
token.safeTransferFrom(from, address(this), amount);
uint256 received = token.balanceOf(address(this)) - beforeBalance;
if (received != amount) revert UnsupportedTokenTransfer();
}
}