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 Network | Proxy | Implementation |
---|---|---|
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.
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)
})();
{
"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",