Guides

RWA Staking Pools

This guide provides instructions for deploying a staking platform based on the EIP-2535 multi-facet diamond proxy. Details include the list of required facets for configuring the platform, enabling the creation of staking pools with specified settings, as well as step-by-step instructions for proxy storage initialization.

About RWA Staking Use Case

The RWA Staking use case describes the scenario when an Eligible Issuer creates a digital-twin NFT which acts as the representation of any real-world asset; then the Issuer locks the asset in the Vault and fractionalises it using the Fraction Protocol into tokens that can be either ERC-20 or ERC-1155 (fractions).

Each fraction token acts as a share that represents the partial ownership of the original digital-twin NFT. Users can buy fractions by participating in a sale that an Issuer runs.

After users acquire fractions, a developer must deploy a staking platform, which enables an Issuer to initiate a staking pool that permits users to stake their ERC-20/1155 fractions. For every staking position, an Issuer allocates rewards in the chosen currency for the staking pool. These rewards can be any ERC-20 token, such as USDC or other wrapped assets.

Note

The staking platform is designed to support multiple staking pools rather than being limited to just one.

Roles

The staking platform revolve around three main entities:

  1. Administrator - Manages a staking platform: granting or revoking roles and other.
  2. Eligible Issuer - Creates staking pools within the platform using a predefined configuration, which is initially set during the deployment of the staking platform. Each pool's configuration is limited to the functionality defined at the platform level.
  3. Eligible User - Participates in the staking pool.

Facets

The table below lists the facets that must be added to the diamond proxy in order to create a platform that supports such RWA staking use case. Refer to the documentation for each facet to learn more about the underlying functionality and also you can check the address of the smart contract on the blockchain explorer.

Note

Refer to the GitHub repository of the Solid State diamond proxy for more details.

Instructions

Setting Up The Environment

Create a new directory and initialize the Node project by running:

npm init

Install the Typescript and TS execution engine:

npm install --save-dev typescript ts-node

Install the Hardhat npm package:

npm install hardhat 

Begin by initializing the Hardhat project. When prompted, confirm each option by entering Y (yes) for all configuration choices.

npx hardhat init

Then install all necessary dependencies required for smart contract deployment:

npm install @solidstate/contracts @nomicfoundation/hardhat-toolbox ethers

In your project root folder find the tsconfig.json file and add the resolveJsonModule property to your configuration and set it to true:

{
  "compilerOptions": {
    "resolveJsonModule": true
    /// ... other properties
  }
}

This property ensures that Typescript can read the data from the .json files.

Diamond Proxy Deployment

In your project, find the contracts folder and, inside it, create a new file that will contain the implementation of the diamond proxy smart contract. For demonstration purposes, the name of the file will be MyDiamond.sol.

Insert the following code into the MyDiamond.sol file:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
 
import "./proxy/diamond/SolidstateDiamondProxy.sol";
 
contract StakingDiamond is SolidStateDiamond {}

In the root folder of your project, create a new TypeScript file called deploy.ts. This file will contain the code required to:

  1. Deploy the diamond proxy smart contract.
  2. Add facets and initialize the diamond storage.

Insert the code provided below into the deploy.ts file and follow the comments.

import { ethers } from "hardhat";
// 1. Import the StakingDiamond.json file to read the ABI and the Bytecode.
import SolidStateDiamond from "./artifacts/contracts/MyDiamond.sol/StakingDiamond.json"
 
// 2. Set up provider (using a testnet like Sepolia). 
// Ensure you have set up the environment variable that contains the provider details.
const provider = new ethers.JsonRpcProvider(process.env.SEPOLIA_RPC);
 
// 3. Set up wallet (replace with your private key).
const wallet = new ethers.Wallet("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", provider);
 
// 4. Define ABI and Bytecode.
const abi = SolidStateDiamond.abi
const bytecode = SolidStateDiamond.bytecode
 
// 5. Initialize the variable for the diamond proxy address.
let diamondAddress: string;
 
// 6. Create a ContractFactory
const contractFactory = new ethers.ContractFactory(abi, bytecode, wallet);
 
// 7. Deploy the contract 
async function deployContract() {
  console.log("Deploying contract...");
 
  // 8. Deploy the contract
  const contract = await contractFactory.deploy();
 
  // 9. Wait for deployment to be mined
  await contract.waitForDeployment();
  console.log("Contract deployed at:", contract.target);
  diamondAddress = contract.target
}
 
deployContract().catch(console.error);

Deploy the diamond proxy smart contract by running it in your terminal:

npx hardhat run deploy.ts

Obtaining Selectors

At this stage, you need to obtain the function selectors corresponding to the facet signatures. This guide outlines two approaches you can use to generate these selectors.

Approach 1: Manual Conversion

For this approach, you need to use the signature-to-selector converter, which allows generating a valid selector for a given signature.

Use the data table provided in this topic to refer to the corresponding smart contract documentation for function signatures.

For each facet, you need to create the corresponding array where you will store selectors. For demonstration purposes, only two selector arrays are provided below:

const AccessControlStakingFacetSelectors = [
    "0xd494a6e3"
]
 
const StakingSkeletonFacetSelectors = [
    "0xa638f2e2",
    "0x73eb8039",
    "0xce733e6d",
    "0xc00bb6dc"
    /// ... add other selectors
]

Approach 2: Automatic Conversion

This approach relies on the automatic generation of selectors based on the human-readable-abi standard.

For all function signatures, refer to the corresponding facet documentation that you can access through the data table provided in this topic.

Create arrays that will store the function signatures. For demonstration purposes, only two signature arrays are provided below:

const AccessControlStakingFacetAbi = [
    "function initAccessControlFacet(address admin) external"
]
 
const StakingSkeletonFacetAbi = [
    "function stake(uint256 campaignId, uint256 amountOfPackets, uint256 timeLockPeriod) external",
    "function _stakeBeneficiary(uint256 campaignId, uint256 amountOfPackets, uint256 timeLockPeriod, address beneficiary) internal",
    "function restake(uint256 nftId, uint256 amountOfPackets, uint256 timeLockPeriod) external",
    "function restakeBeneficiary(uint256 nftId, uint256 amountOfPackets, uint256 timeLockPeriod, address beneficiary) external"
    /// ,,, add other function signatures
]

Then you have to use the functionality of the ethers library to iterate through all signatures in the arrays and convert them into selectors. Ensure that you replace the value passed to the Interface instance.

import { Interface, id, FunctionFragment, hashMessage, dataLength } from "ethers";
 
//  Create an Interface
const iface = new Interface(AccessControlStakingFacetAbi);
 
// Loop over all function signatures
let StakingSkeletonFacetSelectorsResult = []
 
for (const fragment of iface.fragments) {
  if (fragment.type === "function") {
    const fnFragment = fragment as FunctionFragment;
    const signature = fnFragment.format(); // canonical signature
    const selector = id(signature).slice(0, 10); // 4-byte selector
    console.log(`${selector}`);
    StakingSkeletonFacetSelectorsResult.push(selector);
  }
}

Adding Facets To Diamond Proxy

Next, you have to create an array of FacetCut objects containing the function selectors that will be added to the diamond proxy. In the Solidity code, the FacetCut is represented as the following struct:

struct FacetCut {
  address target;
  FacetCutAction action;
  bytes4[] selectors;
}

The FacetCutAction represents an enum that determines how to handle the facet cut process. In our case, we need to use the ADD member of the enum, which is represented by the index position 0:

enum FacetCutAction {
 ADD,
 REPLACE,
 REMOVE
}

You need to specify the address of the facet, the FacetCutAction member, which is 0, and an array of selectors for each facet that you want to add to the diamond proxy.

const AccessControlStakingFacetAddress = "0x5109E51FD38E75b8f08F14F6267A9051395b8EC6"
const StakingSkeletonFacetAddress = "0x581A6a7443d8f90491Ad7C3311Ca2EC0Ccc3774d"
 
let facet1 = { target: AccessControlStakingFacetAddress, action: 0, selectors: AccessControlStakingFacetSelectors}
let facet2 = { target: StakingSkeletonFacetAddress, action: 0, selectors: StakingSkeletonFacetSelectors}

Now we have prepared all the required FacetCut objects, and we are ready to add them to the diamond proxy.

Next, you need to create an instance of the diamond proxy you just deployed:

const diamondProxyContract = new ethers.Contract(diamondAddress, abi, wallet);

We also need to define an async operation to execute the diamondCut function on the instance of the diamond proxy smart contract.

async function diamondCut() {
  const result = await diamondProxyContract.diamondCut([facet1, facet2], ethers.ZeroAddress, "0x");
  console.log(result)
}

As you can see, the first argument expects an array of facet arrays containing selectors; the second argument expects the zero address, which we pass using ethers.ZeroAddress; while the third argument expects the init data, which is empty ("0x").

Now you can call the diamondCut() function, which will return the result of the operation:

diamondCut()

After adding the facets to the diamond proxy, you can verify their presence by using the Louper web tool.
Insert the diamond proxy address into the search bar and verify the newly added functions.

Diamond Proxy Storage Initialization

After adding facets to the diamond proxy, we need to initialize them.

Note

You only need to initialize those facets that have an init function available. For example, the AccessControlStakingFacet has the initAccessControlFacet function, which must be called.

Other facets don't require initialization.

Create instances of the facets that need initialization. These instances must be called at the address of the diamond proxy in order to interact with them through the diamond proxy:

const facet1AtDiamond = new ethers.Contract(diamondAddress, iface, wallet);

Define the async function to call initAccessControlFacet on the facet contract:

async function initializeFacet() {
  const result = await facet1AtDiamond.initAccessControlFacet(wallet.address);
  console.log(result)
}

Now, by calling the initializeFacet function, you initialize the diamond storage for the selected facet. In case you have added more facets to your diamond contract, you need to initialize them too.

initializeFacet()

If you need to pass any other data, you have to encode it properly. For instance, this guide requires initializing the WhitelistedCampaignCreatorsFacet by calling the initCreatorEligibilityFacet() function with the initCreatorEligibilityData calldata. To obtain the data, you need to refer to the corresponding FacetStorage smart contract - which in this case is: WhitelistedCampaignCreatorsFacetStorage.

After that, you need to find the initCreatorEligibilityData parameter in the table provided in the Reference.

Finally, you have to encode the data properly by using the following function:

ethers.utils.defaultAbiCoder.encode(["bytes"], ["YOUR_DATA_IS_HERE"]);

Interacting with the Staking Platform

Suppose you need to interact with the platform we've set up—specifically, to stake in a campaign. First, obtain an instance of the facet that provides the required functionality (i.e., stake()) at the diamond proxy address:

let stakingSkeletonAtDiamond = new ethers.Contract(diamondAddress, StakingSkeletonFacetAbi, wallet);

Then you will call the function from this instance:

let campaignId = 1;
let amountOfPackets = 20;
let lockPeriod = 50; // seconds
 
async function stake() {
  const result = await stakingSkeletonAtDiamond.stake(campaignId, amountOfPackets, lockPeriod);
  console.log(result);
}

API Client

Evergonlabs provides a high-level API and an associated RWA staking pools template that supports rapid deployment of the staking platform and creation of staking pools. The @evergonlab/tmi-protocol-api-client library library handles low-level protocol interactions.

The high-level API offers a simplified interface for configuring and deploying staking use cases.