Staking: Profit Sharing Pool
In this guide, you will discover how to quickly deploy a new staking platform and run a staking pool for a real-world business use-case , that generates profit and distributes it to investors. Usually, investors must acquire a share of the business to be eligible for dividends.
The staking pool acts as a dividend distribution point, where investors stake ERC-20 fractions to
receive rewards in cryptocurrency. ERC-20 fractions act as shares, where each share is a percentage of the total asset, allowing multiple buyers to own a portion of the business proportional to their investment.
Before proceeding, you must first issue a digital twin NFT representing a real-world asset via the Issuance API. Then, fractionalize the NFT into ERC-20 fractions using the Fraction API. Issuance and fractionalization are beyond the scope of this material.
Note
This guide relies on the
viem
library, but you are free to use any other Web3 provider such asweb3.js
orethers.js
.
Preface
There are three entities involved in the process:
- Admin - Deploys a staking platform.
- Issuer - Deploys a staking pool.
- Investor - Stakes, claims rewards and unstakes its position.
Below is a simplified diagram outlining the entire process, along with the key actions of each entity:

- Admin deploys a platform.
- Issuer sends a request to the staking platform to create a new staking pool.
- The staking platform processes the request and deploys a staking pool.
- Investor stakes the ERC-20 fractions.
- Issuer transfers rewards to the staking pool for further distribution.
- Investor claims the distributed rewards.
The end result of this guide is a deployed staking platform, a staking pool, and the interaction of an Investor with the staking pool, including staking, claiming rewards, and unstaking.
Imports
Create a new typescript project, use the following imports and install the required dependencies.
import { api, handleApiResponse, getEnv, ApiResponse, Transaction, ApiClient} from "@evergonlabs/tmi-protocol-api-client";
import { Address, createWalletClient, Hash, http, writeContract} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
import { erc20Abi } from 'viem';
Import | Description |
---|---|
"evergonlabs/tmi-protocol-api-client" | This library is required to construct data objects that are passed as input when executing transactions via the viem.writeContract function and to handle the API responses. |
"viem" | The viem library is used to create the wallet client, which is then used to send transactions to deploy a platform, pool, distribute rewards, stake, claim rewards, and unstake. |
"viem/accounts" | The privateKeyToAccount import is used when defining a wallet client via the createWalletClient call. |
"sepolia" | This import is required because the platform will be deployed to the Sepolia testnet. |
erc20Abi | The ERC-20 ABI interface is used to approve the Issuer balance spending for rewards distribution and to approve the Staker balance spending for staking, unstaking, claiming rewards. |
Defining the API client
The @evergonlab/tmi-protocol-api-client
library uses an API client to send transactions. The env
property sets the environment to either "test"
or "prod"
. Begin by defining the API client in your code:
const apiClient = api.createClient({
env: getEnv("test"),
});
Deploying a staking platform
To begin the deployment you must first create the Admin client by using the createWalletClient
function:
const adminClient = createWalletClient({
account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
chain: sepolia,
transport: http("https://your-provider.com/api-key"),
});
Property | Description |
---|---|
account | Specify your private key using the privateKeyToAccount function. |
chain | Set the chain property to sepolia . |
transport | Pass your RPC provider URL to the transport property to interact with the EVM network (e.g., Alchemy ). |
Important
The process of creating Admin, Issuer, and Investor wallet accounts is identical across this material. As a result, the descriptions of parameters and similar input values apply to all wallet instances.
Define the sendTransaction
function that will be responsible for sending transactions using the writeContract
function of the viem
library.
The transaction
argument expects a Transaction
type which must be a data object containing transaction details. The viemClient
argument expects a client instance created using the createWalletClient
function.
The returned Hash
is used throughout this guide to get the transaction response.
async function sendTransaction(viemClient:any, transaction: Transaction): Promise<Hash> {
return await viemClient.writeContract({
args: transaction.details.args,
functionName: transaction.details.functionName,
abi: transaction.details.abi,
address: transaction.details.address as Address,
});
}
Define the deployPlatform
function that will:
- Create a data object for the transaction using the
await api.stakingTemplates.rwa.createPlatform
call. - Pass the data object to the
sendTransaction
function to deploy the staking platform. - Handle the returned transaction hash using the
handleApiResponse
function. - Return the
platformDetails
object, which contains the address of the staking platform to be used in subsequent function calls.
async function deployPlatform(apiClient: ApiClient) {
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.createPlatform({
client: apiClient,
body: {
// pay attention: we are using string literals here
chainId: `${sepolia.id}`,
isSoulbound: true,
adminAddress: adminClient.account.address,
erc721: {
symbol: "TT",
name: "Test",
baseUri: "ipfs://QmYwAPJzv5CZsnAzt8QvV6pMMN4HgfMNXYxa9avYo5bFwK",
},
},
}),
);
const platformDetails = handleApiResponse(
await api.stakingTemplates.rwa.getPlatformDeployEvent({
query: {
chainId: `${sepolia.id}`,
hash: await sendTransaction(adminClient, transactionData),
},
}),
);
return platformDetails;
}
Property | Description |
---|---|
client | Expects an instance of the api.createClient to make API calls. |
chainId | Specify the chain id using the imported sepolia . |
adminAddress | Represents an account address that must be granted the 'Admin' role for the staking platform. |
erc721 | Initializes the staking position token collection, including the collection name, symbol, and base URL. Each time someone stakes, the corresponding NFT will be issued to the user’s account as confirmation of the staking position. |
isSoulbound | Determines if the minted NFT will be non-transferable and tied to one address. |
Deploy the platform by passing the apiClient
to the deployPlatform
function call and store the result in the platformReceipt
variable. This will provide the address of the staking platform for use in subsequent calls.
const platformReceipt = await deployPlatform(apiClient)
Roles
When deploying, the Admin entity becomes the owner of the platform, with the
'Admin'
role assigned automatically. Only one'Admin'
is allowed per platform.The Admin entity can then grant the following roles to other entities: :
'Creator'
,'Open'
, and'Staker'
.
'Creator'
pool - Grants permission to deploy a staking pool.
'Open'
- Provides access to all accounts interacting with the platform.
'Staker'
- Enables staking capabilities for an account.
At this stage, we have successfully deployed the staking platform. The address of the platform can be obtained via the diamondAddress
property:
const diamondAddress = platformReceipt.diamondAddress
Deploying a staking pool
First, to deploy a staking pool, you must create the wallet client for the Issuer entity:
const issuerClient = createWalletClient({
account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
chain: sepolia,
transport: http("https://your-provider.com/api-key"),
});
To allow Issuer deploying a new staking pool, the Admin entity must grant the 'Creator'
role.
Define the grantCreatorRole
function, which generates a new transaction object using the grantRole
function, and then sends it via the sendTransaction
invocation.
async function grantCreatorRole(viemClient: any,
apiClient: ApiClient,
diamondAddress: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.staking.roles.grantRole({
client: apiClient,
body: {
platform: diamondAddress,
addresses: [issuerClient.account.address],
role: 'Creator'
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Property | Description |
---|---|
client | Expects an instance of the api.createClient to make API calls. |
platform | Set the address of the platform by specifying the diamondAddress variable. |
addresses | Specify the Issuer address in the array that must be granted with the 'Creator' role. |
role | Select the role to grant. For this code example, set it to 'Creator' . |
Pass the issuerClient
, apiClient
and diamondAddress
arguments to the grantCreatorRole
function call:
const roleReceipt = await grantCreatorRole(issuerClient, apiClient, diamondAddress)
After granting the 'Creator'
role to the Issuer you can begin deploying the staking pool.
Define the deployPool
function with viemClient
, apiClient
, and diamondAddress
as arguments to create a new data object for a transaction and send it using the sendTransaction
function:
async function deployPool(viemClient: any, apiClient: ApiClient, diamondAddress: string ): Promise<Hash> {
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.createPool({
client: apiClient,
body: {
platform: diamondAddress,
startDate: new Date(),
hardcapAmount: "100000",
distribution: {
duration: "3600",
distributor: issuerClient.account.address,
},
erc20Input: {
contract: "0xffffffffffffffffffffffffffffffffffffffff",
amountOfTokensPerPacket: "20",
},
erc20Reward: {
contract: "0xffffffffffffffffffffffffffffffffffffffff",
amountOfTokensPerPacket: "5",
},
},
})
);
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Property | Description |
---|---|
platform | Specifies the address of the platform, represented by the diamondAddress variable. |
startDate | Defines the time when a pool begins. The pool can start at a specified future time, if provided. Must be of the Date type. |
hardcapAmount | Specifies the maximum number of packets a pool can accept from users. |
duration | Defines the duration in seconds after which rewards are distributed. |
distribution | An object containing the details of reward distribution. |
distributor | An account address responsible for distributing rewards. |
erc20Input | An object containing details about the input ERC-20 fractions that users must stake in the pool. |
contract | Specifies the smart contract address of the ERC-20 asset used as input for the pool. |
amountOfTokensPerPacket | Defines the number of tokens required to form one input packet. |
erc20Reward | An object containing details about the ERC-20 reward asset that users will receive. |
contract | Specifies the smart contract address whose tokens are used as rewards in the pool. |
amountOfTokensPerPacket | Defines the number of reward tokens required to form one reward packet. |
Pass the issuerClient
, apiClient
and diamondAddress
arguments to the deployPool
function call to create a new staking pool.
const poolHash = await deployPool(issuerClient, apiClient, diamondAddress)
Important
The result of this operation is a transaction hash, which is assigned to the
poolHash
variable. This hash is necessary to obtain the unique pool identifier,poolId
, and is required for all entities that will interact with the staking pool later in this tutorial.
To obtain the poolId
, parse the transaction hash using the getCreatePoolEvent
function. The result will then be properly parsed for further use with the handleApiResponse
function.
const poolDetails = handleApiResponse(
await api.stakingTemplates.rwa.getCreatePoolEvent({
query: {
chainId: `${sepolia.id}`,
hash: poolHash
},
}),
);
For ease of use, we create a new poolId
variable and assign to it the value obtained from the poolDetails
:
const poolId = poolDetails.poolId
At this point, the staking pool has been successfully deployed, and the next steps will involve the Investor interacting with it.
Staking
To begin staking ERC-20 fractions, you must create the wallet client for the Investor entity:
const investorClient = createWalletClient({
account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
chain: sepolia,
transport: http("https://your-provider.com/api-key"),
});
To enable Investor interacting with the staking platform, Issuer must grant the 'Staker'
role to it.
Define the grantStakerRole
function whose invocation will rely on the investorClient
wallet data, apiClient
and the address(diamondAddress
), of the staking platform.
The result of this operation is optional:
async function grantStakerRole(viemClient: any, apiClient: ApiClient, diamondAddress: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.staking.roles.grantRole({
client: apiClient,
body: {
platform: diamondAddress,
addresses: [investorClient.account.address],
role: 'Staker'
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Pass the investorClient
, apiClient
and diamondAddress
arguments to the grantCreatorRole
function call. The successful invocation grants the Investor the 'Staker'
role.
const stakerRoleReceipt = await grantStakerRole(investorClient, apiClient, diamondAddress)
After successfully granting the role, the Investor must approve the staking platform to allow spending a specific amount of ERC-20 fractions intended for staking.
Define the approveERC20Fractions
function that will execute the approval by using the writeContract
function which was imported from the viem
library:
async function approveERC20Fractions(viemClient: any, spender:any,
amount: number, tokenAddress: string): Promise<Hash> {
const txHash = await viemClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spender, amount],
});
return txHash
}
Property | Description |
---|---|
address | The address of the smart contract whose ERC-20 tokens are used as input in the staking pool. |
abi | Refers to a predefined set of Application Binary Interfaces (ABIs) for interacting with ERC-20 tokens. This function depends on the erc20Abi import from the viem library. |
functionName | The name of the function to call on the smart contract; in this case, we call the 'approve' function on the smart contract defined in the address property. |
args | Data used in the invocation of the 'approve' function. It expects the spender and amount arguments. |
spender | Specifies the address of the staking platform that will spend the amount of ERC-20 tokens. |
amount | Determines the amount of ERC-20 tokens to be approved for spending on the staking operation. |
Invoke the approveERC20Fractions
function passing all required arguments:
const amount = 50000
const tokenAddress = '0xffffffffffffffffffffffffffffffffffffffff'
const investorApproveHash = approveERC20Fractions(investorClient, diamondAddress,
amount, tokenAddress)
After the successful approval, you can begin staking the ERC-20 fractions.
Define the stakeFractions
function, which will utilize the stake
function from the @evergonlabs
library. This function will create the transaction data object and send it via sendTransaction
.
async function stakeFractions(viemClient: any, apiClient: ApiClient,
diamondAddress: string, poolId: string,
amount: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.stake({
client: apiClient,
body: {
platform: diamondAddress,
poolId: poolId,
amount: amount
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Property | Description |
---|---|
platform | The address of the staking platform. |
poolId | The unique identifier of the pool where tokens are staked. |
amount | The amount of tokens to be staked. |
Note that when executing the stakeFractions
function transaction we pass the amountPackets
variable specifying the number of packets to stake.
The amountPackets
value ('2500'
) differs from amount
('50000'
) because, when deploying the staking pool, we set the amountOfTokensPerPacket
variable to "20"
. This means that to stake '50000'
tokens, we need to create 2500
packets. Therefore, we specify 2500
as the input value to indicate how many packets should be staked.
const amountPackets = '2500'
const stakeReceipt = await stakeFractions(investorClient, apiClient,
diamondAddress, poolId, amountPackets)
After invoking the function, the Investor successfully stakes 2500
packets, or 50000
tokens, in other words. This action triggers the staking platform to issue a staking position NFT, which is used by an account to confirm its ownership of the stake in the pool.
Important
The staking position NFT data is mandatory to unstake, restake, and claim rewards. You need to obtain it by parsing the hash of the
stake
operation
Parse the hash assigned to the stakeReceipt
variable by calling the getStakeEvent
function:
const stakeData = await api.stakingTemplates.rwa.getStakeEvent({
query: {
chainId: `${sepolia.id}`,
hash: stakeReceipt
}
})
Then assign the unique identifier of the staking position to the stakeId
variable:
const stakeId = stakeData.data!.stakeId
Now you can use the stakeId
as an input for the unstake
, restake
, and claimRewards
functions to manage your stake.
Distributing rewards
To begin distributing rewards for the Investor, the Issuer must first approve the staking platform to spend the specified amount of tokens.
Define the approveERC20Rewards
function that will execute the approval by using the writeContract
function which was imported from the viem
library:
async function approveERC20Rewards(viemClient: any, spender:any,
amount: number, tokenAddress: string): Promise<Hash> {
const txHash = await viemClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spender, amount],
});
return txHash
}
Property | Description |
---|---|
address | The address of the smart contract whose ERC-20 tokens are used as reward in the staking pool. |
abi | Refers to a predefined set of Application Binary Interfaces (ABIs) for interacting with ERC-20 tokens. This function depends on the erc20Abi import from the viem library. |
functionName | The name of the function to call on the smart contract; in this case, we call the 'approve' function on the smart contract defined in the address property. |
args | Data used in the invocation of the 'approve' function. It expects the spender and amount arguments. |
spender | Specifies the address of the staking platform that will spend the amount of ERC-20 tokens. |
amount | Determines the amount of ERC-20 reward tokens to be approved for spending. |
Then pass the required arguments to the approveERC20Rewards
function to invoke it:
const amountRewards = 500000
const rewardsSCAddress = '0xffffffffffffffffffffffffffffffffffffffff'
const issuerApproveHash = approveERC20Rewards(investorClient, diamondAddress,
amount, tokenAddress)
Now you can begin distributing rewards as an Issuer entity.
Define the distributeRewards
function, which will construct the data object via the notifyRewards
invocation and send a transaction using this object in the sendTransaction
call:
async function distributeRewards(viemClient: any, apiClient: ApiClient,
diamondAddress: string, poolId: string,
amount: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.notifyRewards({
client: apiClient,
body: {
platform: diamondAddress,
poolId: poolId,
amount: amount
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Property | Description |
---|---|
platform | The address of the staking platform. |
poolId | The unique identifier of the pool where rewards must be distributed. |
amount | The amount of tokens to be distributed as rewards. |
Then execute the distributeRewards
function by passing all required arguments. The result of this operation is optional:
const rewardsHash = distributeRewards(issuerClient, apiClient, diamondAddress, poolId, `50000`)
After executing the function, the staking pool has received rewards for distribution, and the Investor can now begin claiming rewards.
Claiming rewards
Define the claimRewards
function for the Investor to receive their distributed amount:
async function claimRewards(viemClient: any, apiClient: ApiClient,
diamondAddress: string, stakeId: string,): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.getReward({
client: apiClient,
body: {
platform: diamondAddress,
stakeId: stakeId
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Property | Description |
---|---|
platform | Specifies the address of the staking platform. The diamondAddress variable will be used as an input when invoking the claimRewards function. |
stakeId | The unique identifier of the staking position. |
Invoke the claimRewards
function with the required arguments. The result of this operation is optional:
const claimHash = claimRewards(investorClient, apiClient, diamondAddress, stakeId)
The Investor has successfully claimed their rewards. The following sections of this material cover partial unstaking and unstaking operations.
Partial unstaking
Partial unstaking refers to the process of withdrawing a portion of the staked tokens while leaving the remaining tokens staked.
Define the pUnstake
function which will construct the data object via the partialUnstake
invocation and send a transaction using this object in the sendTransaction
call:
async function pUnstake(viemClient: any, apiClient: ApiClient,
diamondAddress: string, stakeId: string,
amount: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.partialUnstake({
client: apiClient,
body: {
platform: diamondAddress,
stakeId: stakeId,
amount: amount
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Property | Description |
---|---|
platform | Specifies the address of the staking platform. The diamondAddress variable will be used as an input when invoking the pUnstake operation. |
stakeId | The unique identifier of the staking position. The stakeId variable will be used as an input when invoking the pUnstake operation. |
amount | The number of input packets to withdraw from the position, reducing its size. |
Invoke the pUnstake
operation by passing the required arguments. The result of this operation is optional:
const pUnstakeHash = pUnstake(investorClient, apiClient, diamondAddress, stakeId, '1000')
As you can see, the invocation withdraws '1000'
packets from the current staking position.
Unstaking
Unstaking means that an account withdraws all input packets from the current position, effectively closing it.
Define the closePosition
function which will construct the data object via the unstake
invocation and send a transaction using this object in the sendTransaction
call:
async function closePosition(viemClient: any, apiClient: ApiClient,
diamondAddress: string, stakeId: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.rwa.unstake({
client: apiClient,
body: {
platform: diamondAddress,
stakeId: stakeId,
}
})
)
const transactionResult = await sendTransaction(viemClient, transactionData)
return transactionResult
}
Property | Description |
---|---|
platform | Specifies the address of the staking platform. The diamondAddress variable will be used as an input when invoking the closePosition operation. |
stakeId | The unique identifier of the staking position. The stakeId variable will be used as an input when invoking the closePosition operation. |
Invoke the closePosition
function with the required arguments. The result of this operation is optional:
const closePositionHash = closePosition(investorClient, apiClient, diamondAddress, stakeId)
The Investor has withdrawn all input packets from their position.
Closing remarks
Congratulations! You have successfully mastered the key concepts of using the Staking API
and we have covered all methods provided in the RWA
template. It also means that you can take one step further and modify existing code examples to fit your needs better.
By now, you should have a solid understanding of how each process works and how to navigate them effectively. With this expertise, you're now equipped to implement, customize, and optimize staking solutions to suit your specific goals.
Updated 1 day ago