There is a canonical position staking contract, Staker.
struct Incentive {
uint128 totalRewardUnclaimed;
uint128 numberOfStakes;
uint160 totalSecondsClaimedX128;
}
struct Deposit {
address owner;
uint96 numberOfStakes;
}
struct Stake {
uint160 secondsPerLiquidityInsideInitialX128;
uint128 liquidity;
}
State:
IUniswapV3Factory public immutable factory;
INonfungiblePositionManager public immutable nonfungiblePositionManager;
/// @dev bytes32 refers to the return value of IncentiveId.compute
mapping(bytes32 => Incentive) public incentives;
/// @dev deposits[tokenId] => Deposit
mapping(uint256 => Deposit) public deposits;
/// @dev stakes[tokenId][incentiveHash] => Stake
mapping(uint256 => mapping(bytes32 => Stake)) public stakes;
/// @dev rewards[rewardToken][msg.sender] => uint256
mapping(address => mapping(address => uint256)) public rewards;
Params:
struct CreateIncentiveParams {
address rewardToken;
address pool;
uint256 startTime;
uint256 endTime;
uint128 totalReward;
}
struct EndIncentiveParams {
address creator;
address rewardToken;
address pool;
uint256 startTime;
uint256 endTime;
}
createIncentive
creates a liquidity mining incentive program. The key used to look up an Incentive is the hash of its immutable properties.
Check:
- Incentive with these params does not already exist
- Timestamps:
params.endTime >= params.startTime
,params.startTime >= block.timestamp
- Incentive with this ID does not already exist.
Effects:
- Sets
incentives[key] = Incentive(totalRewardUnclaimed=totalReward, totalSecondsClaimedX128=0, rewardToken=rewardToken)
Interaction:
- Transfers
params.totalReward
frommsg.sender
to self.- Make sure there is a check here and it fails if the transfer fails
- Emits
IncentiveCreated
endIncentive
can be called by anyone to end an Incentive after the endTime
has passed, transferring totalRewardUnclaimed
of rewardToken
back to refundee
.
Check:
block.timestamp > params.endTime
- Incentive exists (
incentive.totalRewardUnclaimed != 0
)
Effects:
- deletes
incentives[key]
(This is a new change)
Interactions:
- safeTransfers
totalRewardUnclaimed
ofrewardToken
to the incentive creatormsg.sender
- emits
IncentiveEnded
Interactions
nonfungiblePositionManager.safeTransferFrom(sender, this, tokenId)
- This transfer triggers the onERC721Received hook
Check:
- Make sure sender is univ3 nft
Effects:
- Creates a deposit for the token setting deposit
owner
tofrom
.- Setting
owner
tofrom
ensures that the owner of the token also owns the deposit. Approved addresses and operators may first transfer the token to themselves before depositing for deposit ownership.
- Setting
- If
data.length>0
, stakes the token in one or more incentives
Checks
- Check that a Deposit exists for the token and that
msg.sender
is theowner
on that Deposit. - Check that
numberOfStakes
on that Deposit is 0.
Effects
- Delete the Deposit
delete deposits[tokenId]
.
Interactions
safeTransferFrom
the token toto
withdata
.- emit
DepositTransferred(token, deposit.owner, address(0))
Check:
deposits[params.tokenId].owner == msg.sender
- Make sure incentive actually exists and has reward that could be claimed (incentive.rewardToken != address(0))
- Check if this check can check totalRewardUnclaimed instead
- Make sure token not already staked
Interactions
-
msg.sender
to withdraw all of their reward balance in a specific token to a specifiedto
address. -
emit RewardClaimed(to, reward)
To unstake an NFT, you call unstakeToken
, which takes all the same arguments as stake
.
Checks
- It checks that you are the owner of the Deposit
- It checks that there exists a
Stake
for the provided key (with exists=true).
Effects
- Deletes the Stake.
- Decrements
numberOfStakes
for the Deposit by 1. totalRewardsUnclaimed
is decremented byreward
.totalSecondsClaimed
is incremented byseconds
.- Increments
rewards[rewardToken][msg.sender]
by the amount given bygetRewardInfo
.
-
It computes
secondsInsideX128
(the total liquidity seconds for which rewards are owed) for a given Stake, by:- using
snapshotCumulativesInside
from the Uniswap v3 core contract to getsecondsPerLiquidityInRangeX128
, and subtractingsecondsPerLiquidityInRangeInitialX128
. - Multiplying that by
stake.liquidity
to get the total seconds accrued by the liquidity in that period
- using
-
Note that X128 means it's a
UQ32X128
. -
It computes
totalSecondsUnclaimed
by takingmax(endTime, block.timestamp) - startTime
, casting it as a Q128, and subtractingtotalSecondsClaimedX128
. -
It computes
rewardRate
for the Incentive castingincentive.totalRewardUnclaimed
as a Q128, then dividing it bytotalSecondsUnclaimedX128
. -
reward
is then calculated assecondsInsideX128
times therewardRate
, scaled down to a regular uint128.