Skip to content

Instantly share code, notes, and snippets.

@AE-0h
Created June 7, 2024 23:36
Show Gist options
  • Save AE-0h/00cc25e4f45759a03362eeb3cb31118a to your computer and use it in GitHub Desktop.
Save AE-0h/00cc25e4f45759a03362eeb3cb31118a to your computer and use it in GitHub Desktop.
StakingPoolFactory by playpen. Find it at https://www.cookbook.dev/contracts/playpen-StakingPoolFactory
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: BSD
pragma solidity ^0.8.4;
/// @title Clone
/// @author zefram.eth
/// @notice Provides helper functions for reading immutable args from calldata
contract Clone {
/// @notice Reads an immutable arg with type address
/// @param argOffset The offset of the arg in the packed data
/// @return arg The arg value
function _getArgAddress(uint256 argOffset)
internal
pure
returns (address arg)
{
uint256 offset = _getImmutableArgsOffset();
// solhint-disable-next-line no-inline-assembly
assembly {
arg := shr(0x60, calldataload(add(offset, argOffset)))
}
}
/// @notice Reads an immutable arg with type uint256
/// @param argOffset The offset of the arg in the packed data
/// @return arg The arg value
function _getArgUint256(uint256 argOffset)
internal
pure
returns (uint256 arg)
{
uint256 offset = _getImmutableArgsOffset();
// solhint-disable-next-line no-inline-assembly
assembly {
arg := calldataload(add(offset, argOffset))
}
}
/// @notice Reads a uint256 array stored in the immutable args.
/// @param argOffset The offset of the arg in the packed data
/// @param arrLen Number of elements in the array
/// @return arr The array
function _getArgUint256Array(uint256 argOffset, uint64 arrLen)
internal
pure
returns (uint256[] memory arr)
{
uint256 offset = _getImmutableArgsOffset();
uint256 el;
arr = new uint256[](arrLen);
for (uint64 i = 0; i < arrLen; i++) {
assembly {
// solhint-disable-next-line no-inline-assembly
el := calldataload(add(add(offset, argOffset), mul(i, 32)))
}
arr[i] = el;
}
return arr;
}
/// @notice Reads an immutable arg with type uint64
/// @param argOffset The offset of the arg in the packed data
/// @return arg The arg value
function _getArgUint64(uint256 argOffset)
internal
pure
returns (uint64 arg)
{
uint256 offset = _getImmutableArgsOffset();
// solhint-disable-next-line no-inline-assembly
assembly {
arg := shr(0xc0, calldataload(add(offset, argOffset)))
}
}
/// @notice Reads an immutable arg with type uint8
/// @param argOffset The offset of the arg in the packed data
/// @return arg The arg value
function _getArgUint8(uint256 argOffset) internal pure returns (uint8 arg) {
uint256 offset = _getImmutableArgsOffset();
// solhint-disable-next-line no-inline-assembly
assembly {
arg := shr(0xf8, calldataload(add(offset, argOffset)))
}
}
/// @return offset The offset of the packed immutable args in calldata
function _getImmutableArgsOffset() internal pure returns (uint256 offset) {
// solhint-disable-next-line no-inline-assembly
assembly {
offset := sub(
calldatasize(),
add(shr(240, calldataload(sub(calldatasize(), 2))), 2)
)
}
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: BSD
pragma solidity ^0.8.4;
/// @title ClonesWithImmutableArgs
/// @author wighawag, zefram.eth
/// @notice Enables creating clone contracts with immutable args
library ClonesWithImmutableArgs {
error CreateFail();
/// @notice Creates a clone proxy of the implementation contract, with immutable args
/// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length
/// @param implementation The implementation contract to clone
/// @param data Encoded immutable args
/// @return instance The address of the created clone
function clone(address implementation, bytes memory data)
internal
returns (address instance)
{
// unrealistic for memory ptr or data length to exceed 256 bits
unchecked {
uint256 extraLength = data.length + 2; // +2 bytes for telling how much data there is appended to the call
uint256 creationSize = 0x41 + extraLength;
uint256 runSize = creationSize - 10;
uint256 dataPtr;
uint256 ptr;
// solhint-disable-next-line no-inline-assembly
assembly {
ptr := mload(0x40)
// -------------------------------------------------------------------------------------------------------------
// CREATION (10 bytes)
// -------------------------------------------------------------------------------------------------------------
// 61 runtime | PUSH2 runtime (r) | r | –
mstore(
ptr,
0x6100000000000000000000000000000000000000000000000000000000000000
)
mstore(add(ptr, 0x01), shl(240, runSize)) // size of the contract running bytecode (16 bits)
// creation size = 0a
// 3d | RETURNDATASIZE | 0 r | –
// 81 | DUP2 | r 0 r | –
// 60 creation | PUSH1 creation (c) | c r 0 r | –
// 3d | RETURNDATASIZE | 0 c r 0 r | –
// 39 | CODECOPY | 0 r | [0-runSize): runtime code
// f3 | RETURN | | [0-runSize): runtime code
// -------------------------------------------------------------------------------------------------------------
// RUNTIME (55 bytes + extraLength)
// -------------------------------------------------------------------------------------------------------------
// 3d | RETURNDATASIZE | 0 | –
// 3d | RETURNDATASIZE | 0 0 | –
// 3d | RETURNDATASIZE | 0 0 0 | –
// 3d | RETURNDATASIZE | 0 0 0 0 | –
// 36 | CALLDATASIZE | cds 0 0 0 0 | –
// 3d | RETURNDATASIZE | 0 cds 0 0 0 0 | –
// 3d | RETURNDATASIZE | 0 0 cds 0 0 0 0 | –
// 37 | CALLDATACOPY | 0 0 0 0 | [0, cds) = calldata
// 61 | PUSH2 extra | extra 0 0 0 0 | [0, cds) = calldata
mstore(
add(ptr, 0x03),
0x3d81600a3d39f33d3d3d3d363d3d376100000000000000000000000000000000
)
mstore(add(ptr, 0x13), shl(240, extraLength))
// 60 0x37 | PUSH1 0x37 | 0x37 extra 0 0 0 0 | [0, cds) = calldata // 0x37 (55) is runtime size - data
// 36 | CALLDATASIZE | cds 0x37 extra 0 0 0 0 | [0, cds) = calldata
// 39 | CODECOPY | 0 0 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
// 36 | CALLDATASIZE | cds 0 0 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
// 61 extra | PUSH2 extra | extra cds 0 0 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
mstore(
add(ptr, 0x15),
0x6037363936610000000000000000000000000000000000000000000000000000
)
mstore(add(ptr, 0x1b), shl(240, extraLength))
// 01 | ADD | cds+extra 0 0 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
// 3d | RETURNDATASIZE | 0 cds 0 0 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
// 73 addr | PUSH20 0x123… | addr 0 cds 0 0 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
mstore(
add(ptr, 0x1d),
0x013d730000000000000000000000000000000000000000000000000000000000
)
mstore(add(ptr, 0x20), shl(0x60, implementation))
// 5a | GAS | gas addr 0 cds 0 0 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
// f4 | DELEGATECALL | success 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
// 3d | RETURNDATASIZE | rds success 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
// 3d | RETURNDATASIZE | rds rds success 0 0 | [0, cds) = calldata, [cds, cds+0x37) = extraData
// 93 | SWAP4 | 0 rds success 0 rds | [0, cds) = calldata, [cds, cds+0x37) = extraData
// 80 | DUP1 | 0 0 rds success 0 rds | [0, cds) = calldata, [cds, cds+0x37) = extraData
// 3e | RETURNDATACOPY | success 0 rds | [0, rds) = return data (there might be some irrelevant leftovers in memory [rds, cds+0x37) when rds < cds+0x37)
// 60 0x35 | PUSH1 0x35 | 0x35 sucess 0 rds | [0, rds) = return data
// 57 | JUMPI | 0 rds | [0, rds) = return data
// fd | REVERT | – | [0, rds) = return data
// 5b | JUMPDEST | 0 rds | [0, rds) = return data
// f3 | RETURN | – | [0, rds) = return data
mstore(
add(ptr, 0x34),
0x5af43d3d93803e603557fd5bf300000000000000000000000000000000000000
)
}
// -------------------------------------------------------------------------------------------------------------
// APPENDED DATA (Accessible from extcodecopy)
// (but also send as appended data to the delegatecall)
// -------------------------------------------------------------------------------------------------------------
extraLength -= 2;
uint256 counter = extraLength;
uint256 copyPtr = ptr + 0x41;
// solhint-disable-next-line no-inline-assembly
assembly {
dataPtr := add(data, 32)
}
for (; counter >= 32; counter -= 32) {
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(copyPtr, mload(dataPtr))
}
copyPtr += 32;
dataPtr += 32;
}
uint256 mask = ~(256**(32 - counter) - 1);
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(copyPtr, and(mload(dataPtr), mask))
}
copyPtr += counter;
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(copyPtr, shl(240, extraLength))
}
// solhint-disable-next-line no-inline-assembly
assembly {
instance := create(0, ptr, creationSize)
}
if (instance == address(0)) {
revert CreateFail();
}
}
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {Clone} from "./Clone.sol";
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 is Clone {
/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(
address indexed owner,
address indexed spender,
uint256 amount
);
/*///////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/*///////////////////////////////////////////////////////////////
METADATA
//////////////////////////////////////////////////////////////*/
function name() external pure returns (string memory) {
return string(abi.encodePacked(_getArgUint256(0)));
}
function symbol() external pure returns (string memory) {
return string(abi.encodePacked(_getArgUint256(0x20)));
}
function decimals() external pure returns (uint8) {
return _getArgUint8(0x40);
}
/*///////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 amount)
public
virtual
returns (bool)
{
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount)
public
virtual
returns (bool)
{
balanceOf[msg.sender] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max)
allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
/*///////////////////////////////////////////////////////////////
INTERNAL LOGIC
//////////////////////////////////////////////////////////////*/
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
// Cannot underflow because a user's balance
// will never be larger than the total supply.
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
function _getImmutableVariablesOffset()
internal
pure
returns (uint256 offset)
{
assembly {
offset := sub(
calldatasize(),
add(shr(240, calldataload(sub(calldatasize(), 2))), 2)
)
}
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.4;
import {Clone} from "./Clone.sol";
import {ERC20} from "./ERC20.sol";
import {SafeTransferLib} from "./SafeTransferLib.sol";
import {Ownable} from "./Ownable.sol";
import {FullMath} from "./FullMath.sol";
import {Multicall} from "./Multicall.sol";
import {SelfPermit} from "./SelfPermit.sol";
/// @title ERC20StakingPool
/// @author zefram.eth
/// @notice A modern, gas optimized staking pool contract for rewarding ERC20 stakers
/// with ERC20 tokens periodically and continuously
contract ERC20StakingPool is Ownable, Clone, Multicall, SelfPermit {
/// -----------------------------------------------------------------------
/// Library usage
/// -----------------------------------------------------------------------
using SafeTransferLib for ERC20;
/// -----------------------------------------------------------------------
/// Errors
/// -----------------------------------------------------------------------
error Error_ZeroOwner();
error Error_AlreadyInitialized();
error Error_NotRewardDistributor();
error Error_AmountTooLarge();
/// -----------------------------------------------------------------------
/// Events
/// -----------------------------------------------------------------------
event RewardAdded(uint256 reward);
event Staked(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 reward);
/// -----------------------------------------------------------------------
/// Constants
/// -----------------------------------------------------------------------
uint256 internal constant PRECISION = 1e30;
/// -----------------------------------------------------------------------
/// Storage variables
/// -----------------------------------------------------------------------
/// @notice The last Unix timestamp (in seconds) when rewardPerTokenStored was updated
uint64 public lastUpdateTime;
/// @notice The Unix timestamp (in seconds) at which the current reward period ends
uint64 public periodFinish;
/// @notice The per-second rate at which rewardPerToken increases
uint256 public rewardRate;
/// @notice The last stored rewardPerToken value
uint256 public rewardPerTokenStored;
/// @notice The total tokens staked in the pool
uint256 public totalSupply;
/// @notice Tracks if an address can call notifyReward()
mapping(address => bool) public isRewardDistributor;
/// @notice The amount of tokens staked by an account
mapping(address => uint256) public balanceOf;
/// @notice The rewardPerToken value when an account last staked/withdrew/withdrew rewards
mapping(address => uint256) public userRewardPerTokenPaid;
/// @notice The earned() value when an account last staked/withdrew/withdrew rewards
mapping(address => uint256) public rewards;
/// -----------------------------------------------------------------------
/// Immutable parameters
/// -----------------------------------------------------------------------
/// @notice The token being rewarded to stakers
function rewardToken() public pure returns (ERC20 rewardToken_) {
return ERC20(_getArgAddress(0));
}
/// @notice The token being staked in the pool
function stakeToken() public pure returns (ERC20 stakeToken_) {
return ERC20(_getArgAddress(0x14));
}
/// @notice The length of each reward period, in seconds
function DURATION() public pure returns (uint64 DURATION_) {
return _getArgUint64(0x28);
}
/// -----------------------------------------------------------------------
/// Initialization
/// -----------------------------------------------------------------------
/// @notice Initializes the owner, called by StakingPoolFactory
/// @param initialOwner The initial owner of the contract
function initialize(address initialOwner) external {
if (owner() != address(0)) {
revert Error_AlreadyInitialized();
}
if (initialOwner == address(0)) {
revert Error_ZeroOwner();
}
_transferOwnership(initialOwner);
}
/// -----------------------------------------------------------------------
/// User actions
/// -----------------------------------------------------------------------
/// @notice Stakes tokens in the pool to earn rewards
/// @param amount The amount of tokens to stake
function stake(uint256 amount) external {
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (amount == 0) {
return;
}
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 accountBalance = balanceOf[msg.sender];
uint64 lastTimeRewardApplicable_ = lastTimeRewardApplicable();
uint256 totalSupply_ = totalSupply;
uint256 rewardPerToken_ = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate
);
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
// accrue rewards
rewardPerTokenStored = rewardPerToken_;
lastUpdateTime = lastTimeRewardApplicable_;
rewards[msg.sender] = _earned(
msg.sender,
accountBalance,
rewardPerToken_,
rewards[msg.sender]
);
userRewardPerTokenPaid[msg.sender] = rewardPerToken_;
// stake
totalSupply = totalSupply_ + amount;
balanceOf[msg.sender] = accountBalance + amount;
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
stakeToken().safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
/// @notice Withdraws staked tokens from the pool
/// @param amount The amount of tokens to withdraw
function withdraw(uint256 amount) external {
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (amount == 0) {
return;
}
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 accountBalance = balanceOf[msg.sender];
uint64 lastTimeRewardApplicable_ = lastTimeRewardApplicable();
uint256 totalSupply_ = totalSupply;
uint256 rewardPerToken_ = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate
);
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
// accrue rewards
rewardPerTokenStored = rewardPerToken_;
lastUpdateTime = lastTimeRewardApplicable_;
rewards[msg.sender] = _earned(
msg.sender,
accountBalance,
rewardPerToken_,
rewards[msg.sender]
);
userRewardPerTokenPaid[msg.sender] = rewardPerToken_;
// withdraw stake
balanceOf[msg.sender] = accountBalance - amount;
// total supply has 1:1 relationship with staked amounts
// so can't ever underflow
unchecked {
totalSupply = totalSupply_ - amount;
}
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
stakeToken().safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
/// @notice Withdraws all staked tokens and earned rewards
function exit() external {
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
uint256 accountBalance = balanceOf[msg.sender];
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint64 lastTimeRewardApplicable_ = lastTimeRewardApplicable();
uint256 totalSupply_ = totalSupply;
uint256 rewardPerToken_ = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate
);
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
// give rewards
uint256 reward = _earned(
msg.sender,
accountBalance,
rewardPerToken_,
rewards[msg.sender]
);
if (reward > 0) {
rewards[msg.sender] = 0;
}
// accrue rewards
rewardPerTokenStored = rewardPerToken_;
lastUpdateTime = lastTimeRewardApplicable_;
userRewardPerTokenPaid[msg.sender] = rewardPerToken_;
// withdraw stake
balanceOf[msg.sender] = 0;
// total supply has 1:1 relationship with staked amounts
// so can't ever underflow
unchecked {
totalSupply = totalSupply_ - accountBalance;
}
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
// transfer stake
stakeToken().safeTransfer(msg.sender, accountBalance);
emit Withdrawn(msg.sender, accountBalance);
// transfer rewards
if (reward > 0) {
rewardToken().safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
/// @notice Withdraws all earned rewards
function getReward() external {
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 accountBalance = balanceOf[msg.sender];
uint64 lastTimeRewardApplicable_ = lastTimeRewardApplicable();
uint256 totalSupply_ = totalSupply;
uint256 rewardPerToken_ = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate
);
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
uint256 reward = _earned(
msg.sender,
accountBalance,
rewardPerToken_,
rewards[msg.sender]
);
// accrue rewards
rewardPerTokenStored = rewardPerToken_;
lastUpdateTime = lastTimeRewardApplicable_;
userRewardPerTokenPaid[msg.sender] = rewardPerToken_;
// withdraw rewards
if (reward > 0) {
rewards[msg.sender] = 0;
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
rewardToken().safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
/// -----------------------------------------------------------------------
/// Getters
/// -----------------------------------------------------------------------
/// @notice The latest time at which stakers are earning rewards.
function lastTimeRewardApplicable() public view returns (uint64) {
return
block.timestamp < periodFinish
? uint64(block.timestamp)
: periodFinish;
}
/// @notice The amount of reward tokens each staked token has earned so far
function rewardPerToken() external view returns (uint256) {
return
_rewardPerToken(
totalSupply,
lastTimeRewardApplicable(),
rewardRate
);
}
/// @notice The amount of reward tokens an account has accrued so far. Does not
/// include already withdrawn rewards.
function earned(address account) external view returns (uint256) {
return
_earned(
account,
balanceOf[account],
_rewardPerToken(
totalSupply,
lastTimeRewardApplicable(),
rewardRate
),
rewards[account]
);
}
/// -----------------------------------------------------------------------
/// Owner actions
/// -----------------------------------------------------------------------
/// @notice Lets a reward distributor start a new reward period. The reward tokens must have already
/// been transferred to this contract before calling this function. If it is called
/// when a reward period is still active, a new reward period will begin from the time
/// of calling this function, using the leftover rewards from the old reward period plus
/// the newly sent rewards as the reward.
/// @dev If the reward amount will cause an overflow when computing rewardPerToken, then
/// this function will revert.
/// @param reward The amount of reward tokens to use in the new reward period.
function notifyRewardAmount(uint256 reward) external {
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (reward == 0) {
return;
}
if (!isRewardDistributor[msg.sender]) {
revert Error_NotRewardDistributor();
}
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 rewardRate_ = rewardRate;
uint64 periodFinish_ = periodFinish;
uint64 lastTimeRewardApplicable_ = block.timestamp < periodFinish_
? uint64(block.timestamp)
: periodFinish_;
uint64 DURATION_ = DURATION();
uint256 totalSupply_ = totalSupply;
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
// accrue rewards
rewardPerTokenStored = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate_
);
lastUpdateTime = lastTimeRewardApplicable_;
// record new reward
uint256 newRewardRate;
if (block.timestamp >= periodFinish_) {
newRewardRate = reward / DURATION_;
} else {
uint256 remaining = periodFinish_ - block.timestamp;
uint256 leftover = remaining * rewardRate_;
newRewardRate = (reward + leftover) / DURATION_;
}
// prevent overflow when computing rewardPerToken
if (newRewardRate >= ((type(uint256).max / PRECISION) / DURATION_)) {
revert Error_AmountTooLarge();
}
rewardRate = newRewardRate;
lastUpdateTime = uint64(block.timestamp);
periodFinish = uint64(block.timestamp + DURATION_);
emit RewardAdded(reward);
}
/// @notice Lets the owner add/remove accounts from the list of reward distributors.
/// Reward distributors can call notifyRewardAmount()
/// @param rewardDistributor The account to add/remove
/// @param isRewardDistributor_ True to add the account, false to remove the account
function setRewardDistributor(
address rewardDistributor,
bool isRewardDistributor_
) external onlyOwner {
isRewardDistributor[rewardDistributor] = isRewardDistributor_;
}
/// -----------------------------------------------------------------------
/// Internal functions
/// -----------------------------------------------------------------------
function _earned(
address account,
uint256 accountBalance,
uint256 rewardPerToken_,
uint256 accountRewards
) internal view returns (uint256) {
return
FullMath.mulDiv(
accountBalance,
rewardPerToken_ - userRewardPerTokenPaid[account],
PRECISION
) + accountRewards;
}
function _rewardPerToken(
uint256 totalSupply_,
uint256 lastTimeRewardApplicable_,
uint256 rewardRate_
) internal view returns (uint256) {
if (totalSupply_ == 0) {
return rewardPerTokenStored;
}
return
rewardPerTokenStored +
FullMath.mulDiv(
(lastTimeRewardApplicable_ - lastUpdateTime) * PRECISION,
rewardRate_,
totalSupply_
);
}
function _getImmutableVariablesOffset()
internal
pure
returns (uint256 offset)
{
assembly {
offset := sub(
calldatasize(),
add(shr(240, calldataload(sub(calldatasize(), 2))), 2)
)
}
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Modern, minimalist, and gas efficient ERC-721 implementation.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol)
/// @dev Note that balanceOf does not revert if passed the zero address, in defiance of the ERC.
abstract contract ERC721 {
/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 indexed id);
event Approval(address indexed owner, address indexed spender, uint256 indexed id);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/*///////////////////////////////////////////////////////////////
METADATA STORAGE/LOGIC
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
function tokenURI(uint256 id) public view virtual returns (string memory);
/*///////////////////////////////////////////////////////////////
ERC721 STORAGE
//////////////////////////////////////////////////////////////*/
mapping(address => uint256) public balanceOf;
mapping(uint256 => address) public ownerOf;
mapping(uint256 => address) public getApproved;
mapping(address => mapping(address => bool)) public isApprovedForAll;
/*///////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
}
/*///////////////////////////////////////////////////////////////
ERC721 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 id) public virtual {
address owner = ownerOf[id];
require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED");
getApproved[id] = spender;
emit Approval(owner, spender, id);
}
function setApprovalForAll(address operator, bool approved) public virtual {
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
function transferFrom(
address from,
address to,
uint256 id
) public virtual {
require(from == ownerOf[id], "WRONG_FROM");
require(to != address(0), "INVALID_RECIPIENT");
require(
msg.sender == from || msg.sender == getApproved[id] || isApprovedForAll[from][msg.sender],
"NOT_AUTHORIZED"
);
// Underflow of the sender's balance is impossible because we check for
// ownership above and the recipient's balance can't realistically overflow.
unchecked {
balanceOf[from]--;
balanceOf[to]++;
}
ownerOf[id] = to;
delete getApproved[id];
emit Transfer(from, to, id);
}
function safeTransferFrom(
address from,
address to,
uint256 id
) public virtual {
transferFrom(from, to, id);
require(
to.code.length == 0 ||
ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") ==
ERC721TokenReceiver.onERC721Received.selector,
"UNSAFE_RECIPIENT"
);
}
function safeTransferFrom(
address from,
address to,
uint256 id,
bytes memory data
) public virtual {
transferFrom(from, to, id);
require(
to.code.length == 0 ||
ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) ==
ERC721TokenReceiver.onERC721Received.selector,
"UNSAFE_RECIPIENT"
);
}
/*///////////////////////////////////////////////////////////////
ERC165 LOGIC
//////////////////////////////////////////////////////////////*/
function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata
}
/*///////////////////////////////////////////////////////////////
INTERNAL MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/
function _mint(address to, uint256 id) internal virtual {
require(to != address(0), "INVALID_RECIPIENT");
require(ownerOf[id] == address(0), "ALREADY_MINTED");
// Counter overflow is incredibly unrealistic.
unchecked {
balanceOf[to]++;
}
ownerOf[id] = to;
emit Transfer(address(0), to, id);
}
function _burn(uint256 id) internal virtual {
address owner = ownerOf[id];
require(ownerOf[id] != address(0), "NOT_MINTED");
// Ownership check above ensures no underflow.
unchecked {
balanceOf[owner]--;
}
delete ownerOf[id];
delete getApproved[id];
emit Transfer(owner, address(0), id);
}
/*///////////////////////////////////////////////////////////////
INTERNAL SAFE MINT LOGIC
//////////////////////////////////////////////////////////////*/
function _safeMint(address to, uint256 id) internal virtual {
_mint(to, id);
require(
to.code.length == 0 ||
ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") ==
ERC721TokenReceiver.onERC721Received.selector,
"UNSAFE_RECIPIENT"
);
}
function _safeMint(
address to,
uint256 id,
bytes memory data
) internal virtual {
_mint(to, id);
require(
to.code.length == 0 ||
ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) ==
ERC721TokenReceiver.onERC721Received.selector,
"UNSAFE_RECIPIENT"
);
}
}
/// @notice A generic interface for a contract which properly accepts ERC721 tokens.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol)
interface ERC721TokenReceiver {
function onERC721Received(
address operator,
address from,
uint256 id,
bytes calldata data
) external returns (bytes4);
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.4;
import {Clone} from "./Clone.sol";
import {ERC20} from "./ERC20.sol";
import {ERC721, ERC721TokenReceiver} from "./ERC721.sol";
import {SafeTransferLib} from "./SafeTransferLib.sol";
import {Ownable} from "./Ownable.sol";
import {FullMath} from "./FullMath.sol";
/// @title ERC721StakingPool
/// @author zefram.eth
/// @notice A modern, gas optimized staking pool contract for rewarding ERC721 stakers
/// with ERC20 tokens periodically and continuously
contract ERC721StakingPool is Ownable, Clone, ERC721TokenReceiver {
/// -----------------------------------------------------------------------
/// Library usage
/// -----------------------------------------------------------------------
using SafeTransferLib for ERC20;
/// -----------------------------------------------------------------------
/// Errors
/// -----------------------------------------------------------------------
error Error_ZeroOwner();
error Error_AlreadyInitialized();
error Error_NotRewardDistributor();
error Error_AmountTooLarge();
error Error_NotTokenOwner();
error Error_NotStakeToken();
/// -----------------------------------------------------------------------
/// Events
/// -----------------------------------------------------------------------
event RewardAdded(uint256 reward);
event Staked(address indexed user, uint256[] idList);
event Withdrawn(address indexed user, uint256[] idList);
event RewardPaid(address indexed user, uint256 reward);
/// -----------------------------------------------------------------------
/// Constants
/// -----------------------------------------------------------------------
uint256 internal constant PRECISION = 1e30;
address internal constant BURN_ADDRESS = address(0xdead);
/// -----------------------------------------------------------------------
/// Storage variables
/// -----------------------------------------------------------------------
/// @notice The last Unix timestamp (in seconds) when rewardPerTokenStored was updated
uint64 public lastUpdateTime;
/// @notice The Unix timestamp (in seconds) at which the current reward period ends
uint64 public periodFinish;
/// @notice The per-second rate at which rewardPerToken increases
uint256 public rewardRate;
/// @notice The last stored rewardPerToken value
uint256 public rewardPerTokenStored;
/// @notice The total tokens staked in the pool
uint256 public totalSupply;
/// @notice Tracks if an address can call notifyReward()
mapping(address => bool) public isRewardDistributor;
/// @notice The owner of a staked ERC721 token
mapping(uint256 => address) public ownerOf;
/// @notice The amount of tokens staked by an account
mapping(address => uint256) public balanceOf;
/// @notice The rewardPerToken value when an account last staked/withdrew/withdrew rewards
mapping(address => uint256) public userRewardPerTokenPaid;
/// @notice The earned() value when an account last staked/withdrew/withdrew rewards
mapping(address => uint256) public rewards;
/// -----------------------------------------------------------------------
/// Immutable parameters
/// -----------------------------------------------------------------------
/// @notice The token being rewarded to stakers
function rewardToken() public pure returns (ERC20 rewardToken_) {
return ERC20(_getArgAddress(0));
}
/// @notice The token being staked in the pool
function stakeToken() public pure returns (ERC721 stakeToken_) {
return ERC721(_getArgAddress(0x14));
}
/// @notice The length of each reward period, in seconds
function DURATION() public pure returns (uint64 DURATION_) {
return _getArgUint64(0x28);
}
/// -----------------------------------------------------------------------
/// Initialization
/// -----------------------------------------------------------------------
/// @notice Initializes the owner, called by StakingPoolFactory
/// @param initialOwner The initial owner of the contract
function initialize(address initialOwner) external {
if (owner() != address(0)) {
revert Error_AlreadyInitialized();
}
if (initialOwner == address(0)) {
revert Error_ZeroOwner();
}
_transferOwnership(initialOwner);
}
/// -----------------------------------------------------------------------
/// User actions
/// -----------------------------------------------------------------------
/// @notice Stakes a list of ERC721 tokens in the pool to earn rewards
/// @param idList The list of ERC721 token IDs to stake
function stake(uint256[] calldata idList) external {
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (idList.length == 0) {
return;
}
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 accountBalance = balanceOf[msg.sender];
uint64 lastTimeRewardApplicable_ = lastTimeRewardApplicable();
uint256 totalSupply_ = totalSupply;
uint256 rewardPerToken_ = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate
);
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
// accrue rewards
rewardPerTokenStored = rewardPerToken_;
lastUpdateTime = lastTimeRewardApplicable_;
rewards[msg.sender] = _earned(
msg.sender,
accountBalance,
rewardPerToken_,
rewards[msg.sender]
);
userRewardPerTokenPaid[msg.sender] = rewardPerToken_;
// stake
totalSupply = totalSupply_ + idList.length;
balanceOf[msg.sender] = accountBalance + idList.length;
unchecked {
for (uint256 i = 0; i < idList.length; i++) {
ownerOf[idList[i]] = msg.sender;
}
}
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
unchecked {
for (uint256 i = 0; i < idList.length; i++) {
stakeToken().safeTransferFrom(
msg.sender,
address(this),
idList[i]
);
}
}
emit Staked(msg.sender, idList);
}
/// @notice Withdraws staked tokens from the pool
/// @param idList The list of ERC721 token IDs to stake
function withdraw(uint256[] calldata idList) external {
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (idList.length == 0) {
return;
}
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 accountBalance = balanceOf[msg.sender];
uint64 lastTimeRewardApplicable_ = lastTimeRewardApplicable();
uint256 totalSupply_ = totalSupply;
uint256 rewardPerToken_ = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate
);
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
// accrue rewards
rewardPerTokenStored = rewardPerToken_;
lastUpdateTime = lastTimeRewardApplicable_;
rewards[msg.sender] = _earned(
msg.sender,
accountBalance,
rewardPerToken_,
rewards[msg.sender]
);
userRewardPerTokenPaid[msg.sender] = rewardPerToken_;
// withdraw stake
balanceOf[msg.sender] = accountBalance - idList.length;
// total supply has 1:1 relationship with staked amounts
// so can't ever underflow
unchecked {
totalSupply = totalSupply_ - idList.length;
for (uint256 i = 0; i < idList.length; i++) {
// verify ownership
address tokenOwner = ownerOf[idList[i]];
if (tokenOwner != msg.sender || tokenOwner == BURN_ADDRESS) {
revert Error_NotTokenOwner();
}
// keep the storage slot dirty to save gas
// if someone else stakes the same token again
ownerOf[idList[i]] = BURN_ADDRESS;
}
}
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
unchecked {
for (uint256 i = 0; i < idList.length; i++) {
stakeToken().safeTransferFrom(
address(this),
msg.sender,
idList[i]
);
}
}
emit Withdrawn(msg.sender, idList);
}
/// @notice Withdraws specified staked tokens and earned rewards
function exit(uint256[] calldata idList) external {
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (idList.length == 0) {
return;
}
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 accountBalance = balanceOf[msg.sender];
uint64 lastTimeRewardApplicable_ = lastTimeRewardApplicable();
uint256 totalSupply_ = totalSupply;
uint256 rewardPerToken_ = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate
);
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
// give rewards
uint256 reward = _earned(
msg.sender,
accountBalance,
rewardPerToken_,
rewards[msg.sender]
);
if (reward > 0) {
rewards[msg.sender] = 0;
}
// accrue rewards
rewardPerTokenStored = rewardPerToken_;
lastUpdateTime = lastTimeRewardApplicable_;
userRewardPerTokenPaid[msg.sender] = rewardPerToken_;
// withdraw stake
balanceOf[msg.sender] = accountBalance - idList.length;
// total supply has 1:1 relationship with staked amounts
// so can't ever underflow
unchecked {
totalSupply = totalSupply_ - idList.length;
for (uint256 i = 0; i < idList.length; i++) {
// verify ownership
address tokenOwner = ownerOf[idList[i]];
if (tokenOwner != msg.sender || tokenOwner == BURN_ADDRESS) {
revert Error_NotTokenOwner();
}
// keep the storage slot dirty to save gas
// if someone else stakes the same token again
ownerOf[idList[i]] = BURN_ADDRESS;
}
}
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
// transfer stake
unchecked {
for (uint256 i = 0; i < idList.length; i++) {
stakeToken().safeTransferFrom(
address(this),
msg.sender,
idList[i]
);
}
}
emit Withdrawn(msg.sender, idList);
// transfer rewards
if (reward > 0) {
rewardToken().safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
/// @notice Withdraws all earned rewards
function getReward() external {
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 accountBalance = balanceOf[msg.sender];
uint64 lastTimeRewardApplicable_ = lastTimeRewardApplicable();
uint256 totalSupply_ = totalSupply;
uint256 rewardPerToken_ = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate
);
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
uint256 reward = _earned(
msg.sender,
accountBalance,
rewardPerToken_,
rewards[msg.sender]
);
// accrue rewards
rewardPerTokenStored = rewardPerToken_;
lastUpdateTime = lastTimeRewardApplicable_;
userRewardPerTokenPaid[msg.sender] = rewardPerToken_;
// withdraw rewards
if (reward > 0) {
rewards[msg.sender] = 0;
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
rewardToken().safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
/// -----------------------------------------------------------------------
/// Getters
/// -----------------------------------------------------------------------
/// @notice The latest time at which stakers are earning rewards.
function lastTimeRewardApplicable() public view returns (uint64) {
return
block.timestamp < periodFinish
? uint64(block.timestamp)
: periodFinish;
}
/// @notice The amount of reward tokens each staked token has earned so far
function rewardPerToken() external view returns (uint256) {
return
_rewardPerToken(
totalSupply,
lastTimeRewardApplicable(),
rewardRate
);
}
/// @notice The amount of reward tokens an account has accrued so far. Does not
/// include already withdrawn rewards.
function earned(address account) external view returns (uint256) {
return
_earned(
account,
balanceOf[account],
_rewardPerToken(
totalSupply,
lastTimeRewardApplicable(),
rewardRate
),
rewards[account]
);
}
/// @dev ERC721 compliance
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external view override returns (bytes4) {
if (msg.sender != address(stakeToken())) {
revert Error_NotStakeToken();
}
return this.onERC721Received.selector;
}
/// -----------------------------------------------------------------------
/// Owner actions
/// -----------------------------------------------------------------------
/// @notice Lets a reward distributor start a new reward period. The reward tokens must have already
/// been transferred to this contract before calling this function. If it is called
/// when a reward period is still active, a new reward period will begin from the time
/// of calling this function, using the leftover rewards from the old reward period plus
/// the newly sent rewards as the reward.
/// @dev If the reward amount will cause an overflow when computing rewardPerToken, then
/// this function will revert.
/// @param reward The amount of reward tokens to use in the new reward period.
function notifyRewardAmount(uint256 reward) external {
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (reward == 0) {
return;
}
if (!isRewardDistributor[msg.sender]) {
revert Error_NotRewardDistributor();
}
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 rewardRate_ = rewardRate;
uint64 periodFinish_ = periodFinish;
uint64 lastTimeRewardApplicable_ = block.timestamp < periodFinish_
? uint64(block.timestamp)
: periodFinish_;
uint64 DURATION_ = DURATION();
uint256 totalSupply_ = totalSupply;
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
// accrue rewards
rewardPerTokenStored = _rewardPerToken(
totalSupply_,
lastTimeRewardApplicable_,
rewardRate_
);
lastUpdateTime = lastTimeRewardApplicable_;
// record new reward
uint256 newRewardRate;
if (block.timestamp >= periodFinish_) {
newRewardRate = reward / DURATION_;
} else {
uint256 remaining = periodFinish_ - block.timestamp;
uint256 leftover = remaining * rewardRate_;
newRewardRate = (reward + leftover) / DURATION_;
}
// prevent overflow when computing rewardPerToken
if (newRewardRate >= ((type(uint256).max / PRECISION) / DURATION_)) {
revert Error_AmountTooLarge();
}
rewardRate = newRewardRate;
lastUpdateTime = uint64(block.timestamp);
periodFinish = uint64(block.timestamp + DURATION_);
emit RewardAdded(reward);
}
/// @notice Lets the owner add/remove accounts from the list of reward distributors.
/// Reward distributors can call notifyRewardAmount()
/// @param rewardDistributor The account to add/remove
/// @param isRewardDistributor_ True to add the account, false to remove the account
function setRewardDistributor(
address rewardDistributor,
bool isRewardDistributor_
) external onlyOwner {
isRewardDistributor[rewardDistributor] = isRewardDistributor_;
}
/// -----------------------------------------------------------------------
/// Internal functions
/// -----------------------------------------------------------------------
function _earned(
address account,
uint256 accountBalance,
uint256 rewardPerToken_,
uint256 accountRewards
) internal view returns (uint256) {
return
FullMath.mulDiv(
accountBalance,
rewardPerToken_ - userRewardPerTokenPaid[account],
PRECISION
) + accountRewards;
}
function _rewardPerToken(
uint256 totalSupply_,
uint256 lastTimeRewardApplicable_,
uint256 rewardRate_
) internal view returns (uint256) {
if (totalSupply_ == 0) {
return rewardPerTokenStored;
}
return
rewardPerTokenStored +
FullMath.mulDiv(
(lastTimeRewardApplicable_ - lastUpdateTime) * PRECISION,
rewardRate_,
totalSupply_
);
}
function _getImmutableVariablesOffset()
internal
pure
returns (uint256 offset)
{
assembly {
offset := sub(
calldatasize(),
add(shr(240, calldataload(sub(calldatasize(), 2))), 2)
)
}
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
/// @title Contains 512-bit math functions
/// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision
/// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits
library FullMath {
/// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
/// @param a The multiplicand
/// @param b The multiplier
/// @param denominator The divisor
/// @return result The 256-bit result
/// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv
function mulDiv(
uint256 a,
uint256 b,
uint256 denominator
) internal pure returns (uint256 result) {
unchecked {
// 512-bit multiply [prod1 prod0] = a * b
// Compute the product mod 2**256 and mod 2**256 - 1
// then use the Chinese Remainder Theorem to reconstruct
// the 512 bit result. The result is stored in two 256
// variables such that product = prod1 * 2**256 + prod0
uint256 prod0; // Least significant 256 bits of the product
uint256 prod1; // Most significant 256 bits of the product
assembly {
let mm := mulmod(a, b, not(0))
prod0 := mul(a, b)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
// Handle non-overflow cases, 256 by 256 division
if (prod1 == 0) {
require(denominator > 0);
assembly {
result := div(prod0, denominator)
}
return result;
}
// Make sure the result is less than 2**256.
// Also prevents denominator == 0
require(denominator > prod1);
///////////////////////////////////////////////
// 512 by 256 division.
///////////////////////////////////////////////
// Make division exact by subtracting the remainder from [prod1 prod0]
// Compute remainder using mulmod
uint256 remainder;
assembly {
remainder := mulmod(a, b, denominator)
}
// Subtract 256 bit number from 512 bit number
assembly {
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
// Factor powers of two out of denominator
// Compute largest power of two divisor of denominator.
// Always >= 1.
uint256 twos = (type(uint256).max - denominator + 1) & denominator;
// Divide denominator by power of two
assembly {
denominator := div(denominator, twos)
}
// Divide [prod1 prod0] by the factors of two
assembly {
prod0 := div(prod0, twos)
}
// Shift in bits from prod1 into prod0. For this we need
// to flip `twos` such that it is 2**256 / twos.
// If twos is zero, then it becomes one
assembly {
twos := add(div(sub(0, twos), twos), 1)
}
prod0 |= prod1 * twos;
// Invert denominator mod 2**256
// Now that denominator is an odd number, it has an inverse
// modulo 2**256 such that denominator * inv = 1 mod 2**256.
// Compute the inverse by starting with a seed that is correct
// correct for four bits. That is, denominator * inv = 1 mod 2**4
uint256 inv = (3 * denominator) ^ 2;
// Now use Newton-Raphson iteration to improve the precision.
// Thanks to Hensel's lifting lemma, this also works in modular
// arithmetic, doubling the correct bits in each step.
inv *= 2 - denominator * inv; // inverse mod 2**8
inv *= 2 - denominator * inv; // inverse mod 2**16
inv *= 2 - denominator * inv; // inverse mod 2**32
inv *= 2 - denominator * inv; // inverse mod 2**64
inv *= 2 - denominator * inv; // inverse mod 2**128
inv *= 2 - denominator * inv; // inverse mod 2**256
// Because the division is now exact we can divide by multiplying
// with the modular inverse of denominator. This will give us the
// correct result modulo 2**256. Since the precoditions guarantee
// that the outcome is less than 2**256, this is the final result.
// We don't need to compute the high bits of the result and prod1
// is no longer required.
result = prod0 * inv;
return result;
}
}
/// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
/// @param a The multiplicand
/// @param b The multiplier
/// @param denominator The divisor
/// @return result The 256-bit result
function mulDivRoundingUp(
uint256 a,
uint256 b,
uint256 denominator
) internal pure returns (uint256 result) {
result = mulDiv(a, b, denominator);
unchecked {
if (mulmod(a, b, denominator) > 0) {
require(result < type(uint256).max);
result++;
}
}
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.4;
/// @title Multicall
/// @notice Enables calling multiple methods in a single call to the contract
abstract contract Multicall {
function multicall(bytes[] calldata data)
external
payable
returns (bytes[] memory results)
{
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(
data[i]
);
if (!success) {
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
if (result.length < 68) revert();
assembly {
result := add(result, 0x04)
}
revert(abi.decode(result, (string)));
}
results[i] = result;
}
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.4;
abstract contract Ownable {
error Ownable_NotOwner();
error Ownable_NewOwnerZeroAddress();
address private _owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/// @dev Returns the address of the current owner.
function owner() public view virtual returns (address) {
return _owner;
}
/// @dev Throws if called by any account other than the owner.
modifier onlyOwner() {
if (owner() != msg.sender) revert Ownable_NotOwner();
_;
}
/// @dev Transfers ownership of the contract to a new account (`newOwner`).
/// Can only be called by the current owner.
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) revert Ownable_NewOwnerZeroAddress();
_transferOwnership(newOwner);
}
/// @dev Transfers ownership of the contract to a new account (`newOwner`).
/// Internal function without access restriction.
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {ERC20} from "./ERC20.sol";
/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @author Modified from Gnosis (https://github.com/gnosis/gp-v2-contracts/blob/main/src/contracts/libraries/GPv2SafeERC20.sol)
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer.
library SafeTransferLib {
/*///////////////////////////////////////////////////////////////
ETH OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferETH(address to, uint256 amount) internal {
bool callStatus;
assembly {
// Transfer the ETH and store if it succeeded or not.
callStatus := call(gas(), to, amount, 0, 0, 0, 0)
}
require(callStatus, "ETH_TRANSFER_FAILED");
}
/*///////////////////////////////////////////////////////////////
ERC20 OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferFrom(
ERC20 token,
address from,
address to,
uint256 amount
) internal {
bool callStatus;
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata to memory piece by piece:
mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) // Begin with the function selector.
mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "from" argument.
mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument.
mstore(add(freeMemoryPointer, 68), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value.
// Call the token and store if it succeeded or not.
// We use 100 because the calldata length is 4 + 32 * 3.
callStatus := call(gas(), token, 0, freeMemoryPointer, 100, 0, 0)
}
require(didLastOptionalReturnCallSucceed(callStatus), "TRANSFER_FROM_FAILED");
}
function safeTransfer(
ERC20 token,
address to,
uint256 amount
) internal {
bool callStatus;
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata to memory piece by piece:
mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) // Begin with the function selector.
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value.
// Call the token and store if it succeeded or not.
// We use 68 because the calldata length is 4 + 32 * 2.
callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0)
}
require(didLastOptionalReturnCallSucceed(callStatus), "TRANSFER_FAILED");
}
function safeApprove(
ERC20 token,
address to,
uint256 amount
) internal {
bool callStatus;
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata to memory piece by piece:
mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000) // Begin with the function selector.
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Mask and append the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value.
// Call the token and store if it succeeded or not.
// We use 68 because the calldata length is 4 + 32 * 2.
callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0)
}
require(didLastOptionalReturnCallSucceed(callStatus), "APPROVE_FAILED");
}
/*///////////////////////////////////////////////////////////////
INTERNAL HELPER LOGIC
//////////////////////////////////////////////////////////////*/
function didLastOptionalReturnCallSucceed(bool callStatus) private pure returns (bool success) {
assembly {
// Get how many bytes the call returned.
let returnDataSize := returndatasize()
// If the call reverted:
if iszero(callStatus) {
// Copy the revert message into memory.
returndatacopy(0, 0, returnDataSize)
// Revert with the same message.
revert(0, returnDataSize)
}
switch returnDataSize
case 32 {
// Copy the return data into memory.
returndatacopy(0, 0, returnDataSize)
// Set success to whether it returned true.
success := iszero(iszero(mload(0)))
}
case 0 {
// There was no return data.
success := 1
}
default {
// It returned some malformed input.
success := 0
}
}
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0
pragma solidity >=0.5.0;
import {ERC20} from "./ERC20.sol";
/// @title Self Permit
/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route
/// @dev These functions are expected to be embedded in multicalls to allow EOAs to approve a contract and call a function
/// that requires an approval in a single transaction.
abstract contract SelfPermit {
function selfPermit(
ERC20 token,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public payable {
token.permit(msg.sender, address(this), value, deadline, v, r, s);
}
function selfPermitIfNecessary(
ERC20 token,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external payable {
if (token.allowance(msg.sender, address(this)) < value)
selfPermit(token, value, deadline, v, r, s);
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.11;
import {ClonesWithImmutableArgs} from "./ClonesWithImmutableArgs.sol";
import {ERC20} from "./ERC20.sol";
import {ERC721} from "./ERC721.sol";
import {xERC20} from "./xERC20.sol";
import {ERC20StakingPool} from "./ERC20StakingPool.sol";
import {ERC721StakingPool} from "./ERC721StakingPool.sol";
/// @title StakingPoolFactory
/// @author zefram.eth
/// @notice Factory for deploying ERC20StakingPool and ERC721StakingPool contracts cheaply
contract StakingPoolFactory {
/// -----------------------------------------------------------------------
/// Library usage
/// -----------------------------------------------------------------------
using ClonesWithImmutableArgs for address;
/// -----------------------------------------------------------------------
/// Events
/// -----------------------------------------------------------------------
event CreateXERC20(xERC20 stakingPool);
event CreateERC20StakingPool(ERC20StakingPool stakingPool);
event CreateERC721StakingPool(ERC721StakingPool stakingPool);
/// -----------------------------------------------------------------------
/// Immutable parameters
/// -----------------------------------------------------------------------
/// @notice The contract used as the template for all xERC20 contracts created
xERC20 public immutable xERC20Implementation;
/// @notice The contract used as the template for all ERC20StakingPool contracts created
ERC20StakingPool public immutable erc20StakingPoolImplementation;
/// @notice The contract used as the template for all ERC721StakingPool contracts created
ERC721StakingPool public immutable erc721StakingPoolImplementation;
constructor(
xERC20 xERC20Implementation_,
ERC20StakingPool erc20StakingPoolImplementation_,
ERC721StakingPool erc721StakingPoolImplementation_
) {
xERC20Implementation = xERC20Implementation_;
erc20StakingPoolImplementation = erc20StakingPoolImplementation_;
erc721StakingPoolImplementation = erc721StakingPoolImplementation_;
}
/// @notice Creates an xERC20 contract
/// @dev Uses a modified minimal proxy contract that stores immutable parameters in code and
/// passes them in through calldata. See ClonesWithImmutableArgs.
/// @param name The name of the xERC20 token
/// @param symbol The symbol of the xERC20 token
/// @param decimals The decimals of the xERC20 token
/// @param stakeToken The token being staked in the pool
/// @param DURATION The length of each reward period, in seconds
/// @return stakingPool The created xERC20 contract
function createXERC20(
bytes32 name,
bytes32 symbol,
uint8 decimals,
ERC20 stakeToken,
uint64 DURATION
) external returns (xERC20 stakingPool) {
bytes memory data = abi.encodePacked(
name,
symbol,
decimals,
stakeToken,
DURATION
);
stakingPool = xERC20(address(xERC20Implementation).clone(data));
stakingPool.initialize(msg.sender);
emit CreateXERC20(stakingPool);
}
/// @notice Creates an ERC20StakingPool contract
/// @dev Uses a modified minimal proxy contract that stores immutable parameters in code and
/// passes them in through calldata. See ClonesWithImmutableArgs.
/// @param rewardToken The token being rewarded to stakers
/// @param stakeToken The token being staked in the pool
/// @param DURATION The length of each reward period, in seconds
/// @return stakingPool The created ERC20StakingPool contract
function createERC20StakingPool(
ERC20 rewardToken,
ERC20 stakeToken,
uint64 DURATION
) external returns (ERC20StakingPool stakingPool) {
bytes memory data = abi.encodePacked(rewardToken, stakeToken, DURATION);
stakingPool = ERC20StakingPool(
address(erc20StakingPoolImplementation).clone(data)
);
stakingPool.initialize(msg.sender);
emit CreateERC20StakingPool(stakingPool);
}
/// @notice Creates an ERC721StakingPool contract
/// @dev Uses a modified minimal proxy contract that stores immutable parameters in code and
/// passes them in through calldata. See ClonesWithImmutableArgs.
/// @param rewardToken The token being rewarded to stakers
/// @param stakeToken The token being staked in the pool
/// @param DURATION The length of each reward period, in seconds
/// @return stakingPool The created ERC721StakingPool contract
function createERC721StakingPool(
ERC20 rewardToken,
ERC721 stakeToken,
uint64 DURATION
) external returns (ERC721StakingPool stakingPool) {
bytes memory data = abi.encodePacked(rewardToken, stakeToken, DURATION);
stakingPool = ERC721StakingPool(
address(erc721StakingPoolImplementation).clone(data)
);
stakingPool.initialize(msg.sender);
emit CreateERC721StakingPool(stakingPool);
}
}
/*
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██████ ███████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ █████ ██████ ██ ██ ██ ██ █████ ██ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██████ ██████ ██ ██ ██████ ██████ ██████ ██ ██ ██ ██████ ███████ ████
Find any smart contract, and build your project faster: https://www.cookbook.dev
Twitter: https://twitter.com/cookbook_dev
Discord: https://discord.gg/WzsfPcfHrk
Find this contract on Cookbook: https://www.cookbook.dev/contracts/playpen-StakingPoolFactory/?utm=code
*/
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.4;
import {ERC20} from "./ERC20.sol";
import {SafeTransferLib} from "./SafeTransferLib.sol";
import {Ownable} from "./Ownable.sol";
import {FullMath} from "./FullMath.sol";
import {ERC20 as CloneERC20} from "./ERC20.sol";
import {Multicall} from "./Multicall.sol";
import {SelfPermit} from "./SelfPermit.sol";
/// @title xERC20
/// @author zefram.eth
/// @notice A special type of ERC20 staking pool where the reward token is the same as
/// the stake token. This enables stakers to receive an xERC20 token representing their
/// stake that can then be transferred or plugged into other things (e.g. Uniswap).
/// @dev xERC20 is inspired by xSUSHI, but is superior because rewards are distributed over time rather
/// than immediately, which prevents MEV bots from stealing the rewards or malicious users staking immediately
/// before the reward distribution and unstaking immediately after.
contract xERC20 is CloneERC20, Ownable, Multicall, SelfPermit {
/// -----------------------------------------------------------------------
/// Library usage
/// -----------------------------------------------------------------------
using SafeTransferLib for ERC20;
/// -----------------------------------------------------------------------
/// Errors
/// -----------------------------------------------------------------------
error Error_ZeroOwner();
error Error_AlreadyInitialized();
error Error_NotRewardDistributor();
error Error_ZeroSupply();
/// -----------------------------------------------------------------------
/// Events
/// -----------------------------------------------------------------------
event RewardAdded(uint128 reward);
event Staked(
address indexed user,
uint256 stakeTokenAmount,
uint256 xERC20Amount
);
event Withdrawn(
address indexed user,
uint256 stakeTokenAmount,
uint256 xERC20Amount
);
/// -----------------------------------------------------------------------
/// Constants
/// -----------------------------------------------------------------------
uint256 internal constant PRECISION = 1e18;
/// -----------------------------------------------------------------------
/// Storage variables
/// -----------------------------------------------------------------------
uint64 public currentUnlockEndTimestamp;
uint64 public lastRewardTimestamp;
uint128 public lastRewardAmount;
/// @notice Tracks if an address can call notifyReward()
mapping(address => bool) public isRewardDistributor;
/// -----------------------------------------------------------------------
/// Immutable parameters
/// -----------------------------------------------------------------------
/// @notice The token being staked in the pool
function stakeToken() public pure returns (ERC20) {
return ERC20(_getArgAddress(0x41));
}
/// @notice The length of each reward period, in seconds
function DURATION() public pure returns (uint64) {
return _getArgUint64(0x55);
}
/// -----------------------------------------------------------------------
/// Initialization
/// -----------------------------------------------------------------------
/// @notice Initializes the owner, called by StakingPoolFactory
/// @param initialOwner The initial owner of the contract
function initialize(address initialOwner) external {
if (owner() != address(0)) {
revert Error_AlreadyInitialized();
}
if (initialOwner == address(0)) {
revert Error_ZeroOwner();
}
_transferOwnership(initialOwner);
}
/// -----------------------------------------------------------------------
/// User actions
/// -----------------------------------------------------------------------
/// @notice Stake tokens to receive xERC20 tokens
/// @param stakeTokenAmount The amount of tokens to stake
/// @return xERC20Amount The amount of xERC20 tokens minted
function stake(uint256 stakeTokenAmount)
external
virtual
returns (uint256 xERC20Amount)
{
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (stakeTokenAmount == 0) {
return 0;
}
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
xERC20Amount = FullMath.mulDiv(
stakeTokenAmount,
PRECISION,
getPricePerFullShare()
);
_mint(msg.sender, xERC20Amount);
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
stakeToken().safeTransferFrom(
msg.sender,
address(this),
stakeTokenAmount
);
emit Staked(msg.sender, stakeTokenAmount, xERC20Amount);
}
/// @notice Withdraw tokens by burning xERC20 tokens
/// @param xERC20Amount The amount of xERC20 to burn
/// @return stakeTokenAmount The amount of staked tokens withdrawn
function withdraw(uint256 xERC20Amount)
external
virtual
returns (uint256 stakeTokenAmount)
{
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (xERC20Amount == 0) {
return 0;
}
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
stakeTokenAmount = FullMath.mulDiv(
xERC20Amount,
getPricePerFullShare(),
PRECISION
);
_burn(msg.sender, xERC20Amount);
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
stakeToken().safeTransfer(msg.sender, stakeTokenAmount);
emit Withdrawn(msg.sender, stakeTokenAmount, xERC20Amount);
}
/// -----------------------------------------------------------------------
/// Getters
/// -----------------------------------------------------------------------
/// @notice Compute the amount of staked tokens that can be withdrawn by burning
/// 1 xERC20 token. Increases linearly during a reward distribution period.
/// @dev Initialized to be PRECISION (representing 1:1)
/// @return The amount of staked tokens that can be withdrawn by burning
/// 1 xERC20 token
function getPricePerFullShare() public view returns (uint256) {
uint256 totalShares = totalSupply;
uint256 stakeTokenBalance = stakeToken().balanceOf(address(this));
if (totalShares == 0 || stakeTokenBalance == 0) {
return PRECISION;
}
uint256 lastRewardAmount_ = lastRewardAmount;
uint256 currentUnlockEndTimestamp_ = currentUnlockEndTimestamp;
if (
lastRewardAmount_ == 0 ||
block.timestamp >= currentUnlockEndTimestamp_
) {
// no rewards or rewards fully unlocked
// entire balance is withdrawable
return FullMath.mulDiv(stakeTokenBalance, PRECISION, totalShares);
} else {
// rewards not fully unlocked
// deduct locked rewards from balance
uint256 lastRewardTimestamp_ = lastRewardTimestamp;
// can't overflow since lockedRewardAmount < lastRewardAmount
uint256 lockedRewardAmount = (lastRewardAmount_ *
(currentUnlockEndTimestamp_ - block.timestamp)) /
(currentUnlockEndTimestamp_ - lastRewardTimestamp_);
return
FullMath.mulDiv(
stakeTokenBalance - lockedRewardAmount,
PRECISION,
totalShares
);
}
}
/// -----------------------------------------------------------------------
/// Owner actions
/// -----------------------------------------------------------------------
/// @notice Distributes rewards to xERC20 holders
/// @dev When not in a distribution period, start a new one with rewardUnlockPeriod seconds.
/// When in a distribution period, add rewards to current period.
function distributeReward(uint128 rewardAmount) external {
/// -----------------------------------------------------------------------
/// Validation
/// -----------------------------------------------------------------------
if (totalSupply == 0) {
revert Error_ZeroSupply();
}
if (!isRewardDistributor[msg.sender]) {
revert Error_NotRewardDistributor();
}
/// -----------------------------------------------------------------------
/// Storage loads
/// -----------------------------------------------------------------------
uint256 currentUnlockEndTimestamp_ = currentUnlockEndTimestamp;
/// -----------------------------------------------------------------------
/// State updates
/// -----------------------------------------------------------------------
if (block.timestamp >= currentUnlockEndTimestamp_) {
// start new reward period
currentUnlockEndTimestamp = uint64(block.timestamp + DURATION());
lastRewardAmount = rewardAmount;
} else {
// add rewards to current reward period
// can't overflow since lockedRewardAmount < lastRewardAmount
uint256 lockedRewardAmount = (lastRewardAmount *
(currentUnlockEndTimestamp_ - block.timestamp)) /
(currentUnlockEndTimestamp_ - lastRewardTimestamp);
// will revert if lastRewardAmount overflows
lastRewardAmount = uint128(rewardAmount + lockedRewardAmount);
}
lastRewardTimestamp = uint64(block.timestamp);
/// -----------------------------------------------------------------------
/// Effects
/// -----------------------------------------------------------------------
stakeToken().safeTransferFrom(msg.sender, address(this), rewardAmount);
emit RewardAdded(rewardAmount);
}
/// @notice Lets the owner add/remove accounts from the list of reward distributors.
/// Reward distributors can call notifyRewardAmount()
/// @param rewardDistributor The account to add/remove
/// @param isRewardDistributor_ True to add the account, false to remove the account
function setRewardDistributor(
address rewardDistributor,
bool isRewardDistributor_
) external onlyOwner {
isRewardDistributor[rewardDistributor] = isRewardDistributor_;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment