Staking: Reputation Score Rewards
To increase the engagement of each participant within a blockchain ecosystem, you can introduce a reputation score that motivates users to act in specific ways. This creates an environment where the reputation score serves as a tool to
incentivize positive actions, build trust, and promote accountability among participants.
The reputation score will be represented by ERC-20 tokens, which are minted when users stake ERC-20 tokens. To implement this, developers need to deploy a separate ERC-20 smart contract that mints new reputation tokens for stakers who become eligible for rewards by participating in a staking pool.
Note
This guide relies on the
viem
library, but you are free to use any other Web3 provider such asweb3.js
orethers.js
.
Preface
You will use Evergonlabs' proprietary tmi-protocol-api-client
and the underlying reputation
template to deploy both a staking platform and the staking pool. The deployment of the Reputation SC contract is beyond the scope of this material, but you will find helpful instructions on how to implement it and ensure it works in tandem with the deployed staking pool.
When a staking pool is deployed using the reputation
template, the rewardAssetHandler
contract is also deployed to route claim rewards requests to the Reputation SC contract. Ensure the rewardAssetHandler
has permission to call the mint
function on the Reputation SC contract.
There are three entities involved in the entire 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 all the key actions of each entity:

- Admin deploys the platform.
- Issuer sends a request to the staking platform to create a new staking pool.
- The staking platform processes the request and deploys the staking pool.
- Investor stakes the ERC-20 tokens.
- Investor requests a reward claim.
- The staking pool forwards the mint request to the assetRewardHandler.
- The assetRewardHandler forwards the mint request to the Reputation SC contract.
- The Reputation SC contract mints reputation tokens and sends them to the Investor's account.
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, partial unstaking and unstaking.
ERC-20 Reputation SC
We recommend deploying a smart contract for the ERC-20 reputation score tokens based on the OpenZeppelin standard, which provides the access control layer.
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20, AccessControl {
// Create a new role identifier for the minter role
bytes32 public constant MINTER_ROLE = keccak256("MINTER");
constructor(address minter) ERC20("MyToken", "TKN") {
// Grant the minter role to a specified account
_grantRole(MINTER_ROLE, minter);
}
function mint(address to, uint256 amount) public {
// Check that the calling account has the minter role
require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
_mint(to, amount);
}
}
When using the reputation
template, rewards are not minted upfront for the staking pool; instead, they are minted on-demand when users reach a specific time threshold and become eligible to claim rewards.
To ensure this process works correctly, you must grant the MINTER
role from the Reputation SC
to the rewardAssetHandler
of the pool.
Rate-Based Open Reward Distribution
The reputation
template relies on the Rate-Based Open reward distribution mechanism, and uses the interval-based multipliers, where the number of staked packets determines the multiplier applied to the stake.
Consider the following example array of arrays, which sets the corresponding thresholds for each multiplier. The value at the zero-based index determines the required number of packets to be staked in order to receive the multiplier specified at the first index position.
const packetsAndMultipliers = [[100, 2], [300, 4]]
The packetsAndMultipliers
sets the following multiplier rules for a staking pool:
- A position with fewer than 100 staked packets will not have an amount multiplier applied.
- A position with 100 to 299 staked packets will have an amount multiplier of 2 applied.
- A position with 300 or more staked packets will have an amount multiplier of 4 applied.
Important
All values specified for amount multipliers must be strictly in increasing order:
Correct:
[[100, 2], [300, 4]]
Incorrect:[[300, 4], [100, 2]]
The rate-based open reward distribution algorithm applies the following formulas to staking, restaking, partial unstaking, and rewards claiming actions:
Action | Formula |
---|---|
Unstaking | rate * (packetBalance * amountMultiplier) * activePeriod_unix |
Partial Unstaking | rate * (packetBalance * amountMultiplier) * activePeriod_unix |
Restaking | rate * (beforeRestakePacketBalance * amountMultiplier) * activePeriod_unix |
Claiming Rewards | rate * (packetBalance * amountMultipier) * activePeriod_unix |
Property | Description |
---|---|
rate | The number of reward packets an account earns per second for each input packet staked. |
packetBalance | The current packet balance an account has staked. |
amountMultiplier | The multiplier that gets selected depending on the staked amount. |
beforeRestakePacketBalance | The current packet balance an account has staked prior to performing the restake operation. |
activePeriod_unix | The total time elapsed since the last reward provision occurred for an account. |
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, parseEther} 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. |
Defining the API client
The @evergonlabs/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.reputation.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.reputation.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.reputation.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 the account address that will be granted the 'Admin' role for the staking platform. |
erc721 | Initializes the platform's staking position token collection, specifying the collection's name, symbol, and base URL. Each time someone stakes in a campaign, they receive a corresponding NFT that represents their staking position. |
isSoulbound | Determines whether the staking position NFT collection is non-transferable, ensuring that each issued NFT remains tied to a single 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 accounts: :
'Creator'
, and'Staker'
.
'Creator'
- Grants permission to deploy a staking pool.
'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 | Specify the address of the platform represented by 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.reputation.createPool({
client: apiClient,
body: {
platform: diamondAddress,
// when Staking Pool starts
startDate: new Date("2025-02-10T11:07:07.541Z"),
// min and max amounts per stake
amount: {
minAmount: parseEther("1").toString(), // min 1 packet
maxAmount: parseEther("100").toString(), // max 100 packets
},
// Specify the ERC-20 contract address whose tokens are used as input for pool
stakingErc20: {
contract: "0xffffffffffffffffffffffffffffffffffffffff", // contract address
amountOfTokensPerPacket: "20", // The number of tokens to stake per packet
},
// Specify the Reputation SC contract details for reward distribution
rewardErc20: {
contract: "0xffffffffffffffffffffffffffffffffffffffff", // contract address
amountOfTokensPerPacket: "1", // The number of tokens to yield per packet
},
// Specify how many packets allocated to a staker each second as a Reward
rate: parseEther("0.00000001").toString(), // ~0.02592 packets per month
// Specify what amount multiplier should be applied:
amountMult: [
["5", "2"], // for 5 staked packets your reward rate will be multiplied by 2
["20", "4"], // for 20 staked packets your reward rate will be multiplied by 4
],
},
})
);
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 will start at the specified future time. Must be of the Date type. |
amount | An object that specifies the minimum and maximum input packet amounts allowed for one staking position. |
minAmount | Determines the minimum input packet amount allowed for a staking position. |
maxAmount | Determines the maximum input packet amount allowed for a staking position. |
stakingERC20 | An object that specifies the ERC-20 contract address whose tokens are used as input for pool. |
contract | The address of the smart contract. |
amountOfTokensPerPacket | Defines the number of tokens required to form one input packet. |
rewardErc20 | An object that specifies the ERC-20 contract address whose tokens are used as a reward for pool. |
contract | The address of the smart contract. |
amountOfTokensPerPacket | The number of tokens contained in a single reward packet. |
rate | Specify the number of packets allocated to a staker per second as a reward. |
amountMult | An array of arrays that specifies the interval-based amount multipliers. |
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.reputation.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 tokens, 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, Admin must grant the 'Staker'
role to it.
Define the grantStakerRole
function whose invocation will rely on the adminClient
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 adminClient
, apiClient
and diamondAddress
arguments to the grantCreatorRole
function call. The successful invocation grants the Investor the 'Staker'
role.
const stakerRoleReceipt = await grantStakerRole(adminClient, apiClient, diamondAddress)
After successfully granting the role, the Investor must approve the staking platform to allow spending a specific amount of ERC-20 tokens intended for staking.
Define the approveERC20Tokens
function that will execute the approval by using the writeContract
function which was imported from the viem
library:
async function approveERC20Tokens(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 approveERC20Tokens
function passing all required arguments:
const amount = 50000
const tokenAddress = '0xffffffffffffffffffffffffffffffffffffffff'
const investorApproveHash = approveERC20Tokens(investorClient, diamondAddress,
amount, tokenAddress)
After the successful approval, you can begin staking the ERC-20 tokens.
Define the stakeTokens
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 stakeTokens(viemClient: any, apiClient: ApiClient,
diamondAddress: string, poolId: string,
amount: string): Promise<Hash>{
const transactionData = handleApiResponse(
await api.stakingTemplates.reputation.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 packets to be staked. |
Note that when executing the stakeTokens
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 stakeTokens(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.reputation.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.
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.reputation.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.reputation.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'
(i.e., 20.000 ERC20 input tokens) 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.reputation.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
Great job! You’ve successfully deployed a series of smart contracts and launched a staking pool, enabling users to earn reward reputation tokens. With a solid understanding of the Staking API
, the methods in the reputation
template, and the reward distribution strategy, you’re now equipped to fine-tune staking solutions to achieve your objectives.
Updated 1 day ago