Uniswap announced its 3rd version of the swap/staking protocol. With the new model, the liquidity provision mechanism was also updated. There are no more ERC20 LP Tokens as they were replaced by ERC721 NFTs. However, most of the already deployed staking contracts relied on the former standard, which rises the question — how do I distribute rewards for wallets that provide liquidity for my custom tokens in V3?
There is a Uniswap V3 staker contract — which does all the calculations and distributes tokens proportionally. However, currently, there is no staking UI, therefore it is quite challenging to figure out the incentive mechanism creation and NFT position staking.
This article covers Uniswap V3 contract interaction steps. By the end, the reader should know how to create incentives and store correct data for future integrations in DAPPs.
Several documents describe the Uniswap V3 staking solution to a great extent (linked in the references). Still, there were some new issues to solve and relevant information to find out. I decided to write simple instructions with real transactions and examples that included everything I needed to create the incentive in one place. Hope it helps :)
Testnet of choice: Goerli
Main topics:
- Main data/contract introduction
- Step 1. Pool creation
- Step 2. Incentive creation
- Step 3. Staking
- Step 4. Unstaking and Claiming Rewards
UNISWAP Staking Steps
There are several contracts to be acquainted with before proceeding:
- Uniswap V3 ERC721 (NFT position manager) contract: 0xc364…11fe88
Used to transfer/stake tokens. - Uniswap V3 staker contract: 0xe34…c6fE65
Used for incentive creation, staking / unstaking, and withdrawing.
An exemplary data:
- Token 0: 0xbfb…6a8c69
- Token 1: ETH (native currency)
- Reward token: 0xFe0f…54623f
For the sake of clarity — in this example, token 0 and reward token are named: PIZZA and SALAD, respectively.
1. Pool Creation
If you already have the Uniswap V3 pool (or position) — you can skip this section and proceed to incentive creation. Just make sure to know the pool address.
Before creating any staking program — the user has to initialize the pool by creating the first position in the Uniswap V3 app.
The NFT position replaces LP token logic and acts as proof of liquidity provision — read more in Uniswap V3 blog).
After pool creation — everything from the regular user’s perspective is the same. The user provides PIZZA and ETH tokens, then the Uniswap contract deposits them into the pool and creates a new position, ready for staking.
Result:
- PIZZA / ETH Pool Address: 0xb97a…d171e1.
The pool address is stored in event logs and the factory contract. - PIZZA / ETH Position: tokenID — 33332. You can check it out in OpenSea.
2. Incentive creation
Before creating an incentive - approve the V3 staker contract to accept reward tokens!
SALAD ERC20 contract “approve” function parameters:
spender: 0xe34139463bA50bD61336E0c446Bd8C0867c6fE65 //(staker)
amount: 1000000000000000000000 //(1000 SALAD)
Now moving on to incentive creation. There are several properties in the staking incentive object (with current examples):
- Reward token address
Example: 0xFe0f…54623f (SALAD) - Start date / End date — timestamp without milliseconds. You can see the conversion example in the epoch converter.
Example: 1663607753, 1666022484 - Pool address
Example: 0xb97a… d171e1 (PIZZA/ETH). - Refundee — address of the owner that will receive all non-distributed reward tokens after the incentive period ends.
Example: 0xa3b8…ba2e5f
Now form those values in the tuple format:
["Reward token ", "pool address", start date, end date, "refundee"]
In this case, the incentive key tuple is:
Key: ["0xFe0fA96d3F01315cFf559B5F06Ac4f58A254623f","0xb97a651a6d0c164d4355a61f7fb9a43a32d171e1", 1663607753, 1666022484, "0xa3b8EAaB1eaF1E6DEAB42b864C93C70E2Eba2e5f"]
Another field is the reward token amount. Make sure to pass the absolute value. For example: 1000 tokens = 1000 * 10 ** 18. You can try the conversion here.
Reward: 1000000000000000000000
Now we can proceed to the createIncentive function:
Some reasons for possible errors:
- The pool address is incorrect;
- The incentive start date is in the past;
- Reward tokens are not approved;
- The incentive period is too long (maximum incentive duration is 63072000 seconds or 730 days).
Successful transaction example — 0x0247...e29a2e.
Incentive keys:
After creating the incentive, the owner should save/encode the incentive key tuple. Safe transfer functions or views use encoded key values.
The incentive key tuple to encode is:
["0xFe0fA96d3F01315cFf559B5F06Ac4f58A254623f","0xb97a651a6d0c164d4355a61f7fb9a43a32d171e1", 1663607753, 1666022484, "0xa3b8EAaB1eaF1E6DEAB42b864C93C70E2Eba2e5f"]
There is a contract example in Mark Curchin’s blog, which I used as a base for computing the required incentive hash. Updated functions:
- Calculating method for the unhashed incentive key;
- Decoding method to retrieve the tuple (for self-check).
You can copy and paste it in Remix IDE (deploy it in local javascript VM) and check the values:
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;
pragma abicoder v2;// Referenced from: https://holdex.io/c/learn/how-to-stake-tokens-with-uniswap-v3-staking-programcontract EncodeIncentivesUniV3 {// Used for encoding/decoding
struct IncentiveKey {
address rewardToken;
address pool;
uint256 startTime;
uint256 endTime;
address refundee;
}// Return value used for staking via safeTransfer in the "NFT position manager" contract.
function computeUnhashedKey(IncentiveKey memory key) public pure returns (bytes memory) {
return abi.encode(key);
}// Return value used for staking view functions in "Uniswap V3 staker" contract.
function compute(IncentiveKey memory key) public pure returns (bytes32 incentiveId) {
return keccak256(computeUnhashedKey(key));
}// Decoding unhashed key.
function decode(bytes memory data) public pure returns (IncentiveKey memory) {
return abi.decode(data, (IncentiveKey));
}
}
Example in remix:
Result:
- Unhashed incentive key will use this as data for staking via the NFT position manager:
0x000000000000000000000000fe0fa96d3f01315cff559b5f06ac4f58a254623f000000000000000000000000b97a651a6d0c164d4355a61f7fb9a43a32d171e1000000000000000000000000000000000000000000000000000000006328a3c900000000000000000000000000000000000000000000000000000000634d7c54000000000000000000000000a3b8eaab1eaf1e6deab42b864c93c70e2eba2e5f
- Incentive key: will be used for views:
0x04e5c56d3f7dd9987947c7d3c9a1ab4302ec85f4440fa26ba09ad8cc8c72721a
- Decoded unhashed incentive key: for self-check :)
tuple(address,address,uint256,uint256,address): 0xFe0fA96d3F01315cFf559B5F06Ac4f58A254623f,
0xb97a651A6d0C164d4355A61f7FB9a43a32d171E1,
1663607753,
1666022484,
0xa3b8EAaB1eaF1E6DEAB42b864C93C70E2Eba2e5f
3. Staking
Users with one NFT position can stake in many incentives for the same pool (if they exist). That is why sometimes users can unstake tokens from the staking incentive and leave them in the staker contract.
However, for simplicity, in this example, we cover only 1 to 1 staking (one position in one incentive).
Now, the staking can happen using either staker or NFT positions manager. However, it is easier with the latter (fewer steps due to implemented hook in the staker contract):
General idea:
1. Approve NFT position token:
to: 0xe34139463bA50bD61336E0c446Bd8C0867c6fE65 //(staker)
tokenId: 33332 //(position created after putting PIZZA/ETH tokens)
2. Use the safeTransferFrom function to transfer the token. Pass the unhashed incentive key to the contract so it would stake it automatically). Main values:
from: 0xa3b8EAaB1eaF1E6DEAB42b864C93C70E2Eba2e5f //(owner of token)to: 0xe34139463bA50bD61336E0c446Bd8C0867c6fE65 //(staker)tokenId: 33332 //(position created after putting PIZZA/ETH tokens)_data: 0x000000000000000000000000fe0fa96d3f01315cff559b5f06ac4f58a254623f000000000000000000000000b97a651a6d0c164d4355a61f7fb9a43a32d171e1000000000000000000000000000000000000000000000000000000006328a3c900000000000000000000000000000000000000000000000000000000634d7c54000000000000000000000000a3b8eaab1eaf1e6deab42b864c93c70e2eba2e5f
// unhashed incentive key
Example:
To confirm staking success and see preliminary reward information, call the getRewardInfo function (pass tuple incentive id):
However, users can claim the rewards only after the token is unstaked.
The number of staked tokens in this particular incentive can be checked in the incentives view (by passing hashed incentive id):
4. Unstaking and Claiming Rewards
Users can unstake tokens, but the contract will not automatically transfer NFT back to the owner (unless it is unstaked via client-side using a multicall function. More about this will be in Part II).
All unstaking / withdrawing steps:
- unstakeToken by passing incentive tuple:
2. claim rewards by passing reward token address and amount (put 0 to retrieve all accumulated tokens).
3. Withdraw the token:
That is it :)
The article aimed to introduce Uniswap V3 staking logic and interacting components before proceeding with the next part of front-end / web3 integration.
If you have any questions, feel free to ask :)
And make sure to check out mentioned articles (linked below).
References
- Uniswap V3 blog: https://uniswap.org/blog/uniswap-v3;
- Uniswap V3 documentation: https://docs.uniswap.org/protocol/reference/periphery/staker/UniswapV3Staker
- Mark Curchin Uniswap V3 staking article: https://holdex.io/c/learn/how-to-stake-tokens-with-uniswap-v3-staking-program
Special thanks to Justinas for the help :)