Pooled staking
In this guide, we are going to deploy an Integration pool and implement the full deposit and withdrawal flows inside your platform.
1. Setup
Get a testnet instance
We strongly recommend to perform this guide using a testnet instance. These are deployed on the Holesky Ethereum testnet.
Kiln performs the deployment of these instances, which you can request by filling out this form.
Once deployed, you will have a dedicated testing environment with your own Integration Smart Contract following one of these ABI:
Public testing instance
In the meantime you can use
0x10341c4ad0357bbc1eab9b481df1e95b9eecafd1
(Native20) public testing instance on holesky network.
Manual interactions
We can know test our smart contract with basic interactions using foundry.
a. Get testnet ETH and RPC
Get Holesky testnet ETH using a faucet or by sending us your testnet wallets addresses on our shared channel.
b. Deposit 1 ETH
CONTRACT_ADDRESS=0x... # use the address of your testnet contract instance here
RPC_URL=https://... # use your holesky testnet rpc here
cast send $CONTRACT_ADDRESS "stake()" --value 1ether --rpc-url $RPC_URL -i # perform the stake tx
You have now successfully staked 1 ETH! Verify your deposit transaction on etherscan using the outputed "transaction hash" value.
Here is the output you should see:
This 1 ETH -value transaction did 4 things:
- send 1 ETH from your wallet to the integration contract (
$CONTRACT_ADDRESS
above) and - send 1 ETH from the integration contract to the underlying operator pool
- the operator pool minted
$VPS
tokens to represent the deposited ETH for the integration contract - the integration contract minted integrator shares which you received on your wallets (
$xrsETH
here). This token is transferable if the contract isLiquid20A
orLiquid20C
Exchange rate
Shares have an exchange rate upon the staked ETH, which will evolve in time as the pool accrues value, hence receiving an amount of shares < amount of deposited ETH is an expected behaviour.
2. Backend
Setup your API credentials
Connect to your kiln dashboard organization on testnet and get your api token.
Refer to this section of the documentation to get more infos on how to generate and manage your keys in your backend.
Get pool information
The goal here is to get information about the pool to showcase metrics like GRR (gross rewards rate) to your users.
GET /v1/eth/onchain/v2/network-stats?integration=
- Query using
?integration
parameter and specify your integration contract address as value
Example:
{
"data": {
"address": "0x5db5235b5c7e247488784986e58019fffd98fda4",
"name": "Pooled Staked ETH",
"symbol": "psETH",
"fee": 10,
"nrr": 3.407,
"total_supply": "104865118570632775697",
"total_underlying_supply": "103891951311279705404",
"pools": [
{
"address": "0x00a0be1bbc0c99898df7e6524bf16e893c1e3bb9",
"name": "Kiln",
"ratio": 100,
"commission": 10,
"total_deposited": "104865118570632775697",
"factory_address": "0xc63d9f0040d35f328274312fc8771a986fc4ba86",
"operator_address": "0xf9ef220543aaf0f4dc999382741883ce776064fb"
}
]
}
}
In this response you can see:
- your pool metadata (token name and symbol, address etc.)
- performance metrics (fee, nrr: annualised net rewards rate), useful to inform the user before it stakes
- adoption metrics (total supply)
- which operator pool(s) is your integration contract deployed upon
Get user position
Here we are going to fetch information about a user position: its staked balance, total rewards earned etc. for your user positions section.
GET /v1/eth/onchain/v2/stakes?wallets=
- Query using
?wallets
parameter and specify the user wallet(s) you want to query positions for⚠️ A user position only exists once a user performed a deposit transaction
Example:
{
"data": [
{
"owner": "0x41bf25fc8c52d292bd66d3bcecd8a919ecb9ef88",
"integrator": "Pooled Staked ETH",
"integrator_address": "0x5db5235b5c7e247488784986e58019fffd98fda4",
"balance": "300000000",
"rewards": "300000000",
"nrr": 3.407,
"structure": [
{
"pool": "Kiln",
"pool_address": "0x00a0be1bbc0c99898df7e6524bf16e893c1e3bb9",
"share": 0.5
}
],
"delegated_block": 123,
"delegated_at": "2023-01-14T01:13:59Z",
"updated_at": "2023-01-14T01:13:59Z"
}
]
}
In this response you can see:
- position metadata (owner, delegated_at, integrator name and address)
- position balances (staked balance, total rewards, annualised net reward rate of the position)
- position structure (ie split of the position between the different underlying operator pools of your contract)
- query metadata (updated_at, ie when the data of this position was last refreshed)
Get user history
Kiln also provides the ability to fetch 2 types of historical data:
- rewards distributed by the pool to the staker per day
- operations (like deposits or exits) of a user position
These can be useful to give your users access to financial report of their position that can help them in their accounting.
a - rewards
API -
GET /v1/eth/onchain/v2/rewards?wallets=
- Query using
?wallets
parameter and specify the user wallet(s) you want to query rewards for- You can also use the timeframe parameters to select a specific period (see api specs)
Example:
{
"data": [
{
"date": "2023-01-15",
"rewards": "1000",
"balance": "1000",
"nrr": 3.407,
"rewards_usd": 400,
"balance_usd": 3400
}
]
}
Rewards frequency
Rewards are snapshotted by the API every day at 00:00 UTC, in practice they are computed and distributed every day during the oracle report processing.
b - operations
API -
GET /v1/eth/onchain/v2/operations?wallets=
- Query using
?wallets
parameter and specify the user wallet(s) you want to query operations for- You can also use the timeframe parameters to select a specific period (see api specs)
Example:
{
"data": [
{
"type": "exit",
"ticket_id": "6125082604576892342340792916294922100547",
"ticket_status": "unfulfillable",
"size": "49982523094294339",
"size_shares": "50157843875857851",
"claimable": "0",
"claimable_shares": "0",
"cask_ids": [
"43"
]
},
{
"type": "claim",
"ticket_id": "string",
"ticket_status": "fulfilled",
"claimed": "string",
"claimed_shares": "string",
"remaining": "string",
"remaining_shares": "string",
"used_cask_ids": [
"string"
]
},
{
"type": "deposit",
"amount": "string",
"amount_shares": "string"
}
]
}
There are 3 types of operations:
- deposit: when a user deposits
- exit: when a user requires an exit of (part of) its positions, and mints and exit ticket
- claim: when a user burns (part of) a fulfilled exit ticket and receives its exited ETH
3. Frontend
Now that we have setup the backend data query, we can start the integration within the UI. There are 6 main flows to integrate:
- [VIEW] As a User, I can see the ETH pooling product and its GRR, so that I have a call-to-action to stake
- [TX] As a User, I can deposit ETH to the integrator contract, so that I start earning
- [VIEW] As a User, I can see my staked position and rewards, so that I see my position performance
- [TX] As a User, I can request exit of (part of) my position
- [VIEW] As a User, I can see my pending exit positions
- [TX] As a User, I can claim my fulfilled exit positions, so that I can get back ETH on my wallet
1 - [VIEW] As a User, I can see the ETH pooling product and its GRR, so that I have a call-to-action to stake
Use the nrr
we queried in the "Get pool information" section above to showcase the annualized average earnings for the user on your ETH asset display.
We also recommend displaying a risk notation for each asset yield product, showcasing that staking is the least risky product due to its native nature (protocol inflation).
2 - [TX] As a User, I can deposit ETH to the integrator contract, so that I start earning
TX -
function: stake()
- to: your integration contract address
- from: user wallet address
- value: amount to stake
This transaction directly deposits ETH to the staking pool, which means user position will start being processed for the rewards distribution.
We recommend having a transaction preview screen like the above to educate the user on:
- Expected return: using the API calls described before
- Expected unstaking period: ~4 days is a good average
- Rewards frequency: user position effectively earns rewards at every oracle report (every 24 hours at ~12:00pm UTC)
3 - [VIEW] As a User, I can see my staked position and rewards, so that I see my position performance
Use the balance
nrr
and rewards
queried in the "Get user position" section above and the daily data points gathered in the "Get user history - rewards" section to display rewards per timeframe.
We recommend adding a call to action to stake more that redirects to the beginning of the deposit tx flow.
4 - [TX] As a User, I can request exit of (part of) my position
TX -
function: requestExit(uint256 amount)
- to: your integration contract address
- from: user wallet address
- value: 0
- parameters:
- amount: the amount of shares to exit in wei (⚠️ not ETH value, shares value!)
🚧🚧:: add screenshot example
We recommend that in your withdraw tx screen you:
- input the amount to exit in ETH to the user and you convert it to a shares amount by applying the exchange rate of your integration contract shares to the inputed value.
- the exchange rate can be fetched by doing a view call on the integration contract rate function:
function rate() external view returns (uint256 rate)
and applying the returned rate this way:shares_amount = inputed_eth_amount / rate
- explain that the delay of exit processing is in average ~4 days
- explain that the user will receive an exit ticket (which is a soulbond NFT) representing its pending exit position
5 - [VIEW] As a User, I can see my pending exit positions
🚧🚧🚧🚧
6 - [TX] As a User, I can claim my fulfilled exit positions, so that I can get back ETH on my wallet
TX -
function: multiclaim(address[] exitQueues, uint256[][] ticketIds, uint32[][] casksIds)
- to: multiclaim contract helper address of your integration contract
- from: user wallet address
- value: 0
- parameters:
- exitQueues:
- ticketIds:
- caskIds:
🚧🚧: add recommendations
4. Testing
🚧🚧🚧🚧
Updated 6 months ago