Pooled staking

In this guide, we are going to deploy an Integration pool and implement the full deposit and withdrawal flows inside your platform.

Examples of in-wallet pooling integration

Examples of in-wallet pooling integration

Mockup of deposit and view-my-position flows

Mockup of deposit and view-my-position flows

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:

  1. send 1 ETH from your wallet to the integration contract ($CONTRACT_ADDRESS above) and
  2. send 1 ETH from the integration contract to the underlying operator pool
  3. the operator pool minted $VPS tokens to represent the deposited ETH for the integration contract
  4. the integration contract minted integrator shares which you received on your wallets ($xrsETH here). This token is transferable if the contract is Liquid20A or Liquid20C

πŸ“˜

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:

  1. rewards distributed by the pool to the staker per day
  2. 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:

  1. deposit: when a user deposits
  2. exit: when a user requires an exit of (part of) its positions, and mints and exit ticket
  3. 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:

  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
  2. [TX] As a User, I can deposit ETH to the integrator contract, so that I start earning
  3. [VIEW] As a User, I can see my staked position and rewards, so that I see my position performance
  4. [TX] As a User, I can request exit of (part of) my 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

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


Example of crypto.com earn section

Example of crypto.com earn section


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.

Example of Coinbase Wallet transaction preview

Example of Coinbase Wallet transaction preview


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


Example of a fictive wallet

Example of a fictive wallet

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


🚧🚧🚧🚧