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.totalRewardfrommsg.senderto 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
totalRewardUnclaimedofrewardTokento 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
ownertofrom.- Setting
ownertofromensures 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.senderis theowneron that Deposit. - Check that
numberOfStakeson that Deposit is 0.
Effects
- Delete the Deposit
delete deposits[tokenId].
Interactions
safeTransferFromthe token totowithdata.- 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.senderto withdraw all of their reward balance in a specific token to a specifiedtoaddress. -
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
Stakefor the provided key (with exists=true).
Effects
- Deletes the Stake.
- Decrements
numberOfStakesfor the Deposit by 1. totalRewardsUnclaimedis decremented byreward.totalSecondsClaimedis 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
snapshotCumulativesInsidefrom the Uniswap v3 core contract to getsecondsPerLiquidityInRangeX128, and subtractingsecondsPerLiquidityInRangeInitialX128. - Multiplying that by
stake.liquidityto get the total seconds accrued by the liquidity in that period
- using
-
Note that X128 means it's a
UQ32X128. -
It computes
totalSecondsUnclaimedby takingmax(endTime, block.timestamp) - startTime, casting it as a Q128, and subtractingtotalSecondsClaimedX128. -
It computes
rewardRatefor the Incentive castingincentive.totalRewardUnclaimedas a Q128, then dividing it bytotalSecondsUnclaimedX128. -
rewardis then calculated assecondsInsideX128times therewardRate, scaled down to a regular uint128.