Staking

Goals

This guide shows you how to Stake ETH using the Liquid Collective Protocol and receive LsETH in return.

By the end of this guide you will:

  • Have an understanding of the Liquid Collective protocol

  • Stake ETH

Pre-read

Review the Authentication Guide for the Alluvial API.

Dependencies

  • Ethers.js: You will use Ether.js, a popular library for interacting with Smart Contracts to query blockchain information and send transactions.

  • Application Binary Interface (ABI): The ABI for the River smart contract will be used to READ /WRITE to the contract.

  • Holesky ETH: You will be accessing the Holesky testnet, ensure you have a wallet with HoleskyETH. If you don't have Holesky ETH you can use a faucet.

After reading both guides, come back to continue the implementation of the staking workflow.

Implementation

Import libraries

In the backend (Node.js) application you will need to import one library.

const { ethers } = require("ethers");

The Liquid Collective uses a TUPProxy architecture. Below are the details about the Proxy address and implementation contract.

Ethereum NetworkProxyImplementation

Holesky

0x1d8b30cC38Dba8aBce1ac29Ea27d9cFd05379A09

0x6edbde63319df1c54ee94075191c3d2ac5a1bf81

Mainnet

0x8c1BEd5b9a0928467c9B1341Da1D7BD5e10b6549

0x48D93d8C45Fb25125F13cdd40529BbeaA97A6565

You can save the ABI to a local .json file and import into your javascript file like below:

Contract.json

{   
  "address": "0x1d8b30cC38Dba8aBce1ac29Ea27d9cFd05379A09",
  "abi": [...],
}

app.js

const Contract = require("./Contract.json");

Full code implementation can be see in Appendix, at bottom of the guide.

Next, add information about the RPC node Provider being used. This example uses this example you are using Chainstack, but there are several options for platforms to host an RPC node.

Staking transaction

(async () => {

     const nodeUrl = "https://ethereum-holesky.core.chainstack.com/<ID>";
     const provider = new ethers.providers.JsonRpcProvider(nodeUrl);

    const gasPrice = await provider.getGasPrice();
   
    const signer = new ethers.Wallet("<INSERT WALLET KEY>", provider);

    const LsETHContract = new ethers.Contract(Contract.address, Contract.abi, signer )
    
    const walletAddress = "<INSERT WALLET ADDRESS>"

})();

You can now create the transaction object. In the example below, you are transferring 0.0000001 from address 0xbe79ff177a8F6a0D9656cF47D8687f43666a4d1e to the River address.

Gas Limit uses a built in Ethers.js function. You may need to manually adjust when network has high demand. The code uses the estimateGas function.


    // Get estimate for gas limit
    const deposit_estimation = await LsETHContract.estimateGas.deposit( {
        from: walletAddress,
        value: ethers.utils.parseUnits('0.00000001', 'ether'),
        gasLimit: ethers.utils.hexlify(1),
        nonce: provider.getTransactionCount(walletAddress,'latest')
    });

    // Deposit tx
    let tx = await LsETHContract.deposit(
        {
            from: walletAddress,
            value: ethers.utils.parseUnits('0.00000001', 'ether'),
            gasPrice: gasPrice,
            gasLimit: deposit_estimation,
            nonce: provider.getTransactionCount(walletAddress,'latest')
        }
    )
    let receipt = await tx.wait();
    console.log(receipt)

Great job! You have successfully staked ETH. You can use the transaction hash returned to view more details. Or, you can view the transaction on Etherscan. Here is an example.

Congratulations! You've now staked ETH, received LsETH, and retrieved your balance. As a next step, review the guide on how to implement redemptions.

Appendix

Full Code

Full code for making deposit transaction

Code is meant for testing purposes and should not be used directly for production workloads.

index.js
const { ethers } = require("ethers");
const Contract = require("./Contract.json");
const walletAddress = "0xbe79ff177a8F6a0D9656cF47D8687f43666a4d1e"

(async () => {

    const nodeUrl = "https://ethereum-holesky.core.chainstack.com/<ID>";
    const provider = new ethers.providers.JsonRpcProvider(nodeUrl);

    const gasPrice = await provider.getGasPrice();
   
    const signer = new ethers.Wallet("<INSERT WALLET KEY>", provider);

    const LsETHContract = new ethers.Contract(Contract.address, Contract.abi, signer );
    
    const walletAddress = "<INSERT WALLET ADDRESS>";

    const value = ethers.utils.parseEther("0.000000000001");

    const deposit_estimation = await LsETHContract.estimateGas.deposit( {
        from: walletAddress,
        value: ethers.utils.parseUnits('0.00000001', 'ether'),
        gasLimit: ethers.utils.hexlify(1),
        nonce: provider.getTransactionCount(walletAddress,'latest')
    });

     let tx = await LsETHContract.deposit(
        {
            from: walletAddress,
            value: ethers.utils.parseUnits('0.00000001', 'ether'),
            gasPrice: gasPrice,
            gasLimit: deposit_estimation,
            nonce: provider.getTransactionCount(walletAddress,'latest')
        }
    )
    let receipt = await tx.wait();
    console.log(receipt)

})();
Contract.json
{
  "address": "0x1d8b30cC38Dba8aBce1ac29Ea27d9cFd05379A09",
  "abi": [
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "_from",
          "type": "address"
        },
        {
          "internalType": "address",
          "name": "_operator",
          "type": "address"
        },
        {
          "internalType": "uint256",
          "name": "_allowance",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "_value",
          "type": "uint256"
        }
      ],
      "name": "AllowanceTooLow",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "BalanceTooLow",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "account",
          "type": "address"
        }
      ],
      "name": "Denied",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "EmptyDeposit",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "ErrorOnDeposit",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InconsistentPublicKeys",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InconsistentSignatures",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidArgument",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidCall",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "currentValidatorsExitedBalance",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "newValidatorsExitedBalance",
          "type": "uint256"
        }
      ],
      "name": "InvalidDecreasingValidatorsExitedBalance",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "currentValidatorsSkimmedBalance",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "newValidatorsSkimmedBalance",
          "type": "uint256"
        }
      ],
      "name": "InvalidDecreasingValidatorsSkimmedBalance",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidEmptyString",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "epoch",
          "type": "uint256"
        }
      ],
      "name": "InvalidEpoch",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidFee",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "version",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "expectedVersion",
          "type": "uint256"
        }
      ],
      "name": "InvalidInitialization",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidPublicKeyCount",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "requested",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "received",
          "type": "uint256"
        }
      ],
      "name": "InvalidPulledClFundsAmount",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidSignatureCount",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "providedValidatorCount",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "depositedValidatorCount",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "lastReportedValidatorCount",
          "type": "uint256"
        }
      ],
      "name": "InvalidValidatorCountReport",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidWithdrawalCredentials",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "InvalidZeroAddress",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "NoAvailableValidatorKeys",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "NotEnoughFunds",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "NullTransfer",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "SliceOutOfBounds",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "SliceOverflow",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "prevTotalEthIncludingExited",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "postTotalEthIncludingExited",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "timeElapsed",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "relativeLowerBound",
          "type": "uint256"
        }
      ],
      "name": "TotalValidatorBalanceDecreaseOutOfBound",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "prevTotalEthIncludingExited",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "postTotalEthIncludingExited",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "timeElapsed",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "annualAprUpperBound",
          "type": "uint256"
        }
      ],
      "name": "TotalValidatorBalanceIncreaseOutOfBound",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "caller",
          "type": "address"
        }
      ],
      "name": "Unauthorized",
      "type": "error"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "_from",
          "type": "address"
        },
        {
          "internalType": "address",
          "name": "_to",
          "type": "address"
        }
      ],
      "name": "UnauthorizedTransfer",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "ZeroMintedShares",
      "type": "error"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "owner",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "spender",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "Approval",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "validatorCount",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "validatorTotalBalance",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "bytes32",
          "name": "roundId",
          "type": "bytes32"
        }
      ],
      "name": "ConsensusLayerDataUpdate",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "version",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "bytes",
          "name": "cdata",
          "type": "bytes"
        }
      ],
      "name": "Initialize",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "components": [
            {
              "internalType": "uint256",
              "name": "epoch",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "validatorsBalance",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "validatorsSkimmedBalance",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "validatorsExitedBalance",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "validatorsExitingBalance",
              "type": "uint256"
            },
            {
              "internalType": "uint32",
              "name": "validatorsCount",
              "type": "uint32"
            },
            {
              "internalType": "uint32[]",
              "name": "stoppedValidatorCountPerOperator",
              "type": "uint32[]"
            },
            {
              "internalType": "bool",
              "name": "rebalanceDepositToRedeemMode",
              "type": "bool"
            },
            {
              "internalType": "bool",
              "name": "slashingContainmentMode",
              "type": "bool"
            }
          ],
          "indexed": false,
          "internalType": "struct IOracleManagerV1.ConsensusLayerReport",
          "name": "report",
          "type": "tuple"
        },
        {
          "components": [
            {
              "internalType": "uint256",
              "name": "rewards",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "pulledELFees",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "pulledRedeemManagerExceedingEthBuffer",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "pulledCoverageFunds",
              "type": "uint256"
            }
          ],
          "indexed": false,
          "internalType": "struct IOracleManagerV1.ConsensusLayerDataReportingTrace",
          "name": "trace",
          "type": "tuple"
        }
      ],
      "name": "ProcessedConsensusLayerReport",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "pulledSkimmedEthAmount",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "pullExitedEthAmount",
          "type": "uint256"
        }
      ],
      "name": "PulledCLFunds",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        }
      ],
      "name": "PulledCoverageFunds",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        }
      ],
      "name": "PulledELFees",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        }
      ],
      "name": "PulledRedeemManagerExceedingEth",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "redeemManagerDemand",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "suppliedRedeemManagerDemand",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "suppliedRedeemManagerDemandInEth",
          "type": "uint256"
        }
      ],
      "name": "ReportedRedeemManager",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "_collector",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "_oldTotalUnderlyingBalance",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "_oldTotalSupply",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "_newTotalUnderlyingBalance",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "_newTotalSupply",
          "type": "uint256"
        }
      ],
      "name": "RewardsEarned",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "admin",
          "type": "address"
        }
      ],
      "name": "SetAdmin",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "allowlist",
          "type": "address"
        }
      ],
      "name": "SetAllowlist",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "oldAmount",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newAmount",
          "type": "uint256"
        }
      ],
      "name": "SetBalanceCommittedToDeposit",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "oldAmount",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newAmount",
          "type": "uint256"
        }
      ],
      "name": "SetBalanceToDeposit",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "oldAmount",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newAmount",
          "type": "uint256"
        }
      ],
      "name": "SetBalanceToRedeem",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "annualAprUpperBound",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "relativeLowerBound",
          "type": "uint256"
        }
      ],
      "name": "SetBounds",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",