Liquid Collective Platforms can use Fireblocks to custody digital assets.
Fireblocks is an easy-to-use platform to create new blockchain-based products and manage day-to-day digital asset operations. Fireblocks provides an MPC-based wallet allowing users to store their digital assets.
This guide will provide Platforms with a step-by-step process for using Fireblocks to interact with the Liquid Collective protocol.
Fireblocks' offerings are third-party products that are not offered by or in partnership or affiliation with Alluvial. Products and services offered by Fireblocks and other third parties are subject to separate terms and conditions. Please visit https://www.fireblocks.com/ for more information. Any links provided are for your convenience and informational purposes only. Inclusion of any link does not constitute an endorsement or an approval of any such third-party products by Alluvial or any other Liquid Collective protocol service provider.
Architecture
Below is an example architecture for a Platform that uses Fireblocks as a custodian when the Platform's users stake ETH and/or redeem LsETH.
Implementation
This implementation will take advantage of the Fireblocks SDK, specifically the Javascript SDK. Please follow the Fireblocks Javascript Guide to install the appropriate dependencies.
This guide uses the Goerli network and the Alluvial Staging API.
Dependencies
Create a new Javascript file, such as index.js
.
Define dependencies at top of file.
Copy const fs = require ( 'fs' );
const path = require ( 'path' );
const { FireblocksSDK } = require ( 'fireblocks-sdk' );
const { inspect } = require ( 'util' );
Vault & wallet
Create or use an existing Fireblocks vault.
Copy const createVault = async () => {
const name = 'LsETH blog'
const vaultAccount = await fireblocks .createVaultAccount (name);
console .log ( inspect (vaultAccount , false , null , true ));
}
createVault ();
Response:
Copy {
"id" : "6" ,
"name" : "LsETH blog" ,
"hiddenOnUI" : false ,
"assets" : [] ,
"autoFuel" : false
}
Next, put digital assets into the Fireblocks account.
Admin permissions are needed to add ERC-20 digital assets to a Fireblocks account. Please review this information on the Fireblocks website .
Define the contract address for Goerli:
Copy const CONTRACT_ADDRESS = process . env . CONTRACT_ADDRESS ;
Call the supported digital assets in Fireblocks.
Copy
const getAssets = async () => {
const supportedAssets = await fireblocks .getSupportedAssets ();
supportedAssets .forEach ((asset , index , array) => {
if ( asset .contractAddress == CONTRACT_ADDRESS ) {
console .log ( JSON .stringify (asset))
)
}
})
}
getAssets ();
Response:
Copy {
"id" : "LSETH_ETH_TEST3_4E2A" ,
"name" : "Liquid Staked ETH" ,
"type" : "ERC20" ,
"contractAddress" : "0x3ecCAdA3e11c1Cc3e9B5a53176A67cc3ABDD3E46" , "nativeAsset" : "ETH_TEST3" ,
"decimals" : 18
}
Add the LsETH to the newly created vault.
Request:
Copy const addAssetToVault = async () => {
const vaultWallet = await fireblocks .createVaultAsset ( 6 , 'LSETH_ETH_TEST3_4E2A' );
console .log ( inspect (vaultWallet , false , null , true ));
}
addAssetToVault ()
Response:
Copy {
"id" : "6" ,
"address" : "<FIREBLOCKS ADDRESS>" ,
"legacyAddress" : "" ,
"tag" : ""
}
Creating Depositor and Allowlisting
Now that you have an address associated with your Fireblocks account, you will create a Depositor object and add the wallet address to the Liquid Collective protocol Allowlist via the Alluvial API.
The steps for onboarding and adding to the Allowlist can be found in the staking guide . Come back to this guide once your wallet(s) have been successfully added to the Allowlist.
Stake ETH
Now that you have successfully added your wallets to the Allowlist you can continue with staking.
In order to interact with the Liquid Collective protocol you will need to invoke Smart Contract functions.
This guide will use the Fireblocks Ethers.js provider, created by the Fireblocks team. To install read the Fireblocks documentation .
In the index.js
file add new dependencies
Copy const { FireblocksWeb3Provider , ChainId } = require ( "@fireblocks/fireblocks-web3-provider" )
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 0x3ecCAdA3e11c1Cc3e9B5a53176A67cc3ABDD3E46
0xF32fC26C9604a380c311e7eC0c5E545917e7934f
0x8c1BEd5b9a0928467c9B1341Da1D7BD5e10b6549
0x48D93d8C45Fb25125F13cdd40529BbeaA97A6565
Create a separate file called Contract.json
.
In the file, add the ABI for the Liquid Collective protocol:
Mainnet can be found here
Copy {
"abi" : [ { INSERT ABI FILE HERE } ] ,
}
Define the ABI address for Goerli.
Copy const ABI = require ( "./Contract.json" ).abi;
Define the EIP-1193 Provider.
Copy const eip1193Provider = new FireblocksWeb3Provider ({
privateKey : process . env . FIREBLOCKS_API_PRIVATE_KEY_PATH ,
apiKey : process . env . FIREBLOCKS_API_KEY ,
vaultAccountIds : process . env . FIREBLOCKS_VAULT_ACCOUNT_IDS ,
chainId : ChainId . GOERLI ,
// apiBaseUrl: ApiBaseUrl.Sandbox // If using a sandbox workspace
});
Create a function that calls the deposit function.
You will need to have ETH in your Fireblocks vault to fund the deposit & gas fee.
Request:
Copy const createDeposit = async () => {
const provider = new ethers . providers .Web3Provider (eip1193Provider);
const LsETHContract = new ethers .Contract ( CONTRACT_ADDRESS , ABI , provider .getSigner ());
const gasPrice = await provider .getGasPrice ();
const deposit_estimation = await LsETHContract . estimateGas .deposit ( {
from : FIREBLOCKS_ADDRESS ,
value : ethers . utils .parseUnits ( '0.00000001' , 'ether' ) ,
gasLimit : ethers . utils .hexlify ( 1 ) ,
nonce : provider .getTransactionCount ( FIREBLOCKS_ADDRESS , 'latest' )
});
let tx = await LsETHContract .deposit (
{
from : FIREBLOCKS_ADDRESS ,
value : ethers . utils .parseUnits ( '0.000000001' , 'ether' ) ,
gasPrice : gasPrice ,
gasLimit : deposit_estimation ,
nonce : provider .getTransactionCount ( FIREBLOCKS_ADDRESS , 'latest' )
}
)
let receipt = await tx .wait ();
console .log (receipt)
}
createDeposit ();
Submitting transactions will involve the Fireblocks TAP policy.
You've successfully staked ETH and should see LsETH returned in your Fireblocks vault.
Copy const getTx = async () => {
const transactions = await fireblocks .getTransactions ({txHash : '<INSERT TX HASH>' });
console .log ( JSON .stringify (transactions))
}
getTx ()
Response:
Copy {
"id" : "LSETH_ETH_TEST3_4E2A" ,
"total" : "0.000000000974731883" ,
"balance" : "0.000000000974731883" ,
"lockedAmount" : "0" ,
"available" : "0.000000000974731883" ,
"pending" : "0" ,
"frozen" : "0" ,
"staked" : "0" ,
"blockHeight" : "9212224" , "blockHash" : "0xc1d94ab995a5db95ddaa10b85ef47b24a38b9ea7249ac7c64bb46a1288fa33bc"
}
Now you can implement the LsETH redemption flow, providing your users the ability to redeem their LsETH for ETH.
Redeem LsETH
The next step is to allow the allowlisted wallets the ability to redeem their LsETH for ETH, thereby burning their LsETH.
There is a two-step process to receive the redeemed ETH. First, you will create a RedeemRequest that results in a redemption ID being returned. Once the redemption has been satisfied (full or partial) you can make a claimRedeemRequest call.
For more information check out Liquid Collective's redemption documentation .
Create a redemption request
The first function you will call is the requestRedeem .
Request:
Copy const createRedeemRequest = async () => {
const provider = new ethers . providers .Web3Provider (eip1193Provider);
const LsETHContract = new ethers .Contract ( CONTRACT_ADDRESS , ABI , provider .getSigner ());
const value = ethers . utils .parseEther ( "0.000001" );
const redeem_estimation = await LsETHContract . estimateGas .requestRedeem (value , FIREBLOCKS_ADDRESS , { gasLimit : 1 });
const tx = await LsETHContract .requestRedeem (value , FIREBLOCKS_ADDRESS , { gasLimit : redeem_estimation});
let receipt = await tx .wait ();
console .log (receipt)
}
createRedeemRequest ();
A request redeem ID will be generated.
You can retrieve the request redeem ID either via the Alluvial API or by listening the the Redeem Manager contract events. More information can be found here .
Resolve redeem request
In order to get the Withdrawal Event ID, invoke the resolveRedeemRequest or call via the Alluvial API .
The example below will show invoking the resolveRedeemRequest function.
Request:
Copy const resolveRedeemRequest = async () => {
const provider = new ethers . providers .Web3Provider (eip1193Provider);
const LsETHContract = new ethers .Contract ( CONTRACT_ADDRESS , ABI , provider .getSigner ());
const value = ethers . utils .parseEther ( "0.000001" );
const arrRequestId = [ 53 ];
const resolveRedeem = await LsETHContract .resolveRedeemRequests (arrRequestId);
console .log ( resolveRedeem .toString ())
}
resolveRedeemRequest ();
Response:
111
The resolveRedeemRequest function returns the status of the Withdrawal Event ID. More can be found here .
You can proceed to make a claim now that you have both your redemption ID (53) and Withdrawal Event ID (111).
Create a claim request
To make a claim request provide both the redemption request ID and the Withdrawal Event ID in an array.
Submitting transactions will involve the Fireblocks TAP policy.
Request:
Copy const createClaimRedeemRequest = async () => {
const provider = new ethers . providers .Web3Provider (eip1193Provider);
const LsETHContract = new ethers .Contract ( CONTRACT_ADDRESS , ABI , provider .getSigner ());
const arrRequestId = [ 53 ];
const arrWithdrawalId = [ 111 ];
const claim_estimation = await LsETHContract.estimateGas.claimRedeemRequests(arrRequestId, arrWithdrawalId, { gasLimit: 1});
const claimRedeemRequests = await LsETHContract.claimRedeemRequests(arrRequestId, arrWithdrawalId, { gasLimit: claim_estimation});
const receipt = await claimRedeemRequests .wait ();
console .log (claimRedeemRequests);
}
createClaimRedeemRequest ();
After the claim is processed you will see the the deposit of ETH into your Fireblocks wallet.
To see the status of the claim you can either listen for events or use the Alluvial API. More information on the claiming process can be found here .
Congratulations! You have now implemented the ETH staking and LsETH redemption flow using a Fireblocks account and SDK.
Appendix:
Below is the full code from the snippets above:
To call a function uncomment the function declaration.
Copy
//General dependencies
require ( 'dotenv' ) .config ();
const fs = require ( 'fs' );
const path = require ( 'path' );
const { inspect } = require ( 'util' );
//Fireblocks SDKs
const { FireblocksSDK } = require ( 'fireblocks-sdk' );
const { FireblocksWeb3Provider , ChainId } = require ( "@fireblocks/fireblocks-web3-provider" );
const ethers = require ( "ethers" );
const baseUrl = "https://api.fireblocks.io" ;
const apiSecret = fs .readFileSync ( path .resolve ( "./fb.key" ) , "utf8" );
const apiKey = process . env . API_KEY ;
const ABI = require ( "./Contract.json" ).abi;
const CONTRACT_ADDRESS = process . env . CONTRACT_ADDRESS ;
const FIREBLOCKS_ADDRESS = process . env . FIREBLOCKS_ADDRESS ;
const fireblocks = new FireblocksSDK (apiSecret , apiKey , baseUrl);
const eip1193Provider = new FireblocksWeb3Provider ({
apiBaseUrl : baseUrl ,
privateKey : apiSecret ,
apiKey : apiKey ,
vaultAccountIds : 3 ,
chainId : ChainId . GOERLI ,
})
const createVault = async () => {
const name = 'LsETH blog'
const vaultAccount = await fireblocks .createVaultAccount (name);
console .log ( inspect (vaultAccount , false , null , true ));
}
//createVault()
const getAssets = async () => {
const supportedAssets = await fireblocks .getSupportedAssets ();
supportedAssets .forEach ((asset , index , array) => {
if ( asset .contractAddress == CONTRACT_ADDRESS ) {
console .log ( JSON .stringify (asset))
}
})
}
//getAssets()
const addAssetToVault = async () => {
const vaultWallet = await fireblocks .createVaultAsset ( 6 , 'LSETH_ETH_TEST3_4E2A' );
console .log ( inspect (vaultWallet , false , null , true ));
}
//addAssetToVault()
const createDeposit = async () => {
const provider = new ethers . providers .Web3Provider (eip1193Provider);
const LsETHContract = new ethers .Contract ( CONTRACT_ADDRESS , ABI , provider .getSigner ());
const gasPrice = await provider .getGasPrice ();
const deposit_estimation = await LsETHContract . estimateGas .deposit ( {
from : FIREBLOCKS_ADDRESS ,
value : ethers . utils .parseUnits ( '0.00000001' , 'ether' ) ,
gasLimit : ethers . utils .hexlify ( 1 ) ,
nonce : provider .getTransactionCount ( FIREBLOCKS_ADDRESS , 'latest' )
});
let tx = await LsETHContract .deposit (
{
from : FIREBLOCKS_ADDRESS ,
value : ethers . utils .parseUnits ( '0.000000001' , 'ether' ) ,
gasPrice : gasPrice ,
gasLimit : deposit_estimation ,
nonce : provider .getTransactionCount ( FIREBLOCKS_ADDRESS , 'latest' )
}
)
let receipt = await tx .wait ();
console .log (receipt)
}
//createDeposit()
const getTx = async () => {
const transactions = await fireblocks.getTransactions({txHash: '0x124e9ef06e38ab0e3ff78d066f8f2eea54b18210390a229c251a0d578853ae20'});
console .log ( JSON .stringify (transactions))
}
//getTx()
const getBalance = async () => {
const vaultAsset = await fireblocks .getVaultAccountAsset ( 3 , 'LSETH_ETH_TEST3_4E2A' );
console .log ( JSON .stringify (vaultAsset))
}
//getBalance()
const createRedeemRequest = async () => {
const provider = new ethers . providers .Web3Provider (eip1193Provider);
const LsETHContract = new ethers .Contract ( CONTRACT_ADDRESS , ABI , provider .getSigner ());
const value = ethers . utils .parseEther ( "0.000001" );
const redeem_estimation = await LsETHContract . estimateGas .requestRedeem (value , FIREBLOCKS_ADDRESS , { gasLimit : 1 });
const tx = await LsETHContract .requestRedeem (value , FIREBLOCKS_ADDRESS , { gasLimit : redeem_estimation});
let receipt = await tx .wait ();
console .log (receipt)
}
//createRedeemRequest();
const resolveRedeemRequest = async () => {
const provider = new ethers . providers .Web3Provider (eip1193Provider);
const LsETHContract = new ethers .Contract ( CONTRACT_ADDRESS , ABI , provider .getSigner ());
const value = ethers . utils .parseEther ( "0.000001" );
const arrRequestId = [ 53 ];
const resolveRedeem = await LsETHContract .resolveRedeemRequests (arrRequestId);
console .log ( resolveRedeem .toString ())
}
//resolveRedeemRequest();
const createClaimRedeemRequest = async () => {
const provider = new ethers . providers .Web3Provider (eip1193Provider);
const LsETHContract = new ethers .Contract ( CONTRACT_ADDRESS , ABI , provider .getSigner ());
const arrRequestId = [ 53 ];
const arrWithdrawalId = [ 111 ];
const claim_estimation = await LsETHContract.estimateGas.claimRedeemRequests(arrRequestId, arrWithdrawalId, { gasLimit: 1});
const claimRedeemRequests = await LsETHContract.claimRedeemRequests(arrRequestId, arrWithdrawalId, { gasLimit: claim_estimation});
const receipt = await claimRedeemRequests .wait ();
console .log (claimRedeemRequests);
}
//createClaimRedeemRequest();