Uniswap V3 Staker Design
The liquidity mining staker design is comprised of one canonical position staking contract, Staker. The technical reference for this contract is here and the source code is here.
Data Structures
struct Incentive {
  uint128 totalRewardUnclaimed;
  uint128 numberOfStakes;
  uint160 totalSecondsClaimedX128;
}
struct Deposit {
  address owner;
  uint96 numberOfStakes;
}
struct Stake {
  uint160 secondsPerLiquidityInsideInitialX128;
  uint128 liquidity;
}
struct IncentiveKey {
        IERC20Minimal rewardToken;
        IUniswapV3Pool pool;
        uint256 startTime;
        uint256 endTime;
        address refundee;
}
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;
}
Incentives
createIncentive(CreateIncentiveParams memory params)
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(EndIncentiveParams memory params)
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 
Deposit/Withdraw Token
Interactions
nonfungiblePositionManager.safeTransferFrom(sender, this, tokenId)- This transfer triggers the onERC721Received hook
 
onERC721Received(address, address from, uint256 tokenId, bytes calldata data)
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 
withdrawToken(uint256 tokenId, address to, bytes memory data)
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)) 
Stake/Unstake/Rewards
stakeToken
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
 
claimReward
Interactions
msg.senderto withdraw all of their reward balance in a specific token to a specifiedtoaddress.emit RewardClaimed(to, reward)
unstakeToken
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. 
getRewardInfo
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.