Skip to content

Instantly share code, notes, and snippets.

@nsjames
Created February 12, 2025 16:11
Show Gist options
  • Save nsjames/2a4a9c0ba064d528e8234b681976a2a2 to your computer and use it in GitHub Desktop.
Save nsjames/2a4a9c0ba064d528e8234b681976a2a2 to your computer and use it in GitHub Desktop.

EOS Staking Integration Guide

The EOS Network emits a fixed percentage of the out of circulation supply of EOS to various entities (miners, foundations, staking rewards, etc.) on the network constantly. The network has a built-in mechanism to distribute these rewards to the stakers of EOS. The staking rewards are distributed to the stakers of EOS in the form of REX tokens.

REX tokens are a representation of the amount of EOS that a user has staked on the network. REX tokens cannot be transferred to other users, but they can be sold back to the network for EOS.

As the network rewards are distributed to the REX staking pool, the price of REX tokens increases. This means that the conversion rate of EOS tokens to REX increases over time and never decreases.

The amount of REX tokens a user holds does not change, but the value of the REX tokens increases over time. This means that the user can always sell their REX tokens back to the network for more EOS than they initially staked.

Flow of Staking Rewards

Every 4 years the amount of tokens that is distributed to the various entities is halved. The current rate of emission for staking rewards is 31,250,000 tokens per year. You can see in the chart below that tokens first flow through the eosio.saving account, then to the eosio.reward account, and finally to the eosio.rex account.

graph TD
    eosio --> |EOS emissions | eosio.saving
    eosio.saving --> |31,250,000 EOS per year| eosio.reward
    eosio.reward --> weights_rewards{eosio.rex}
Loading

Smart contract actions

The contract that handles the staking rewards is the eosio system contract.

Note: The eosio contract will soon be wrapped with another contract for a rebrand. Once this happens you can simply change your code to use the new contract account name, and change the ticker from EOS to the new name. All other parameters will stay the same.

The following actions are available on the eosio contract:

Staking

Staking requires you to deposit EOS into the contract, and then to buy REX with that deposited EOS.

void deposit( const name& owner, const asset& amount );
void buyrex( const name& from, const asset& amount );

You need to complete both of these actions, and you can do them both within the same transaction.

Example transaction: https://unicove.com/en/eos/transaction/104910588aeaa372d4b2511c630aec316b5d85fa8940f286671bf12564ffda84

Note: This guide uses the WharfKit SDK to sign transactions. If you do not know how to set up WharfKit, please refer to the WharfKit documentation.

const result = session.transact({
    actions: [
        {
            account: "eosio",
            name: "deposit",
            authorization: [session.permissionLevel],
            data: {
                owner: session.actor,
                amount: "100.0000 EOS"
            }
        },
        {
            account: "eosio",
            name: "buyrex",
            authorization: [session.permissionLevel],
            data: {
                from: session.actor,
                amount: "100.0000 EOS"
            }
        }
    ]
});

Unstaking

Unstaking will start the timer that matures your staked position. Until you trigger unstaking, your position is staked perpetually.

void mvfrsavings( const name& owner, const asset& rex );

Example transaction: https://unicove.com/en/eos/transaction/2feff4c6d76bbfa6abc5ee3d92a07097e3568aa9a61fa64d04147f824a4c9ae5

session.transact({
    actions: [
        {
            account: "eosio",
            name: "mvfrsavings",
            authorization: [session.permissionLevel],
            data: {
                owner: session.actor,
                rex: `100.0000 REX`
            }
        }
    ]
});

This will trigger a 21-day maturation period. The user still accumulates rewards during this period.

Claiming rewards

Once the 21-day maturation period is over, the user can then claim their rewards. They will first need to sell their rex position, and then withdraw the EOS.

void sellrex( const name& from, const asset& rex );
void withdraw( const name& owner, const asset& amount );

One point of note is that you have to specify the EOS amount you want to withdraw, which you won't know until you sell your REX position. You can calculate this, but it will not be readily available without calculation as a stored number because the conversion rate potentially changes every block.

Example transaction: https://unicove.com/en/eos/transaction/892eed7631a33ceb4b93d99ad0dd8d06d586ed766dd0b99ecaa3a041dc16cc52

session.transact({
    actions: [
        {
            account: "eosio",
            name: "sellrex",
            authorization: [session.permissionLevel],
            data: {
                from: session.actor,
                rex: `100.0000 REX`
            }
        },
        {
            account: "eosio",
            name: "withdraw",
            authorization: [session.permissionLevel],
            data: {
                owner: session.actor,
                amount: `100.0000 EOS`
            }
        }
    ]
});

donatetorex

void donatetorex( const name& payer, const asset& quantity, const std::string& memo );

This action allows any user to donate EOS to the REX pool. The donation pushes up the price of REX tokens, which benefits all REX holders. The user can specify the amount of EOS they want to donate and a memo.

This action is used by the system to donate REX as well, which makes all on-chain actions transparent.

Example transaction: https://unicove.com/en/eos/transaction/be4bd8a8a5c1172d2776616a4367c58b4974e1d29d1fe68bbbd2002dfe209965/traces

This wouldn't be an action you as an integrator needs.

Data and Calculations

You will need to fetch data from the chain in order to know how much REX or EOS the user has, and also to calculate various things like REX/EOS conversion rates, APR, etc.

Fetching data from the chain

You can fetch data from the chain using the get_table_rows RPC call. All data will exist on the eosio contract, or the token contract.

I will be using JavaScript's fetch in the examples below, but you can use any HTTP client you prefer to fetch data, and if you're using JavaScript, I would recommend using a library like WharfKit.

Getting the REX pool data

REX pool data is stored in the rexpool table. It includes information you need to calculate the REX/EOS conversion rate.

const response = await fetch('https://eos.greymass.com/v1/chain/get_table_rows', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        code: 'eosio',
        scope: 'eosio',
        table: 'rexpool',
        json: true,
    }),
}).then(x => x.json());

// Result: 
// {
//     rows: [
//         {
//             version: 0,
//             total_lent: "24743362.9593 EOS",
//             total_unlent: "148138217.9863 EOS",
//             total_rent: "4118.5517 EOS",
//             total_lendable: "172881580.9456 EOS",
//             total_rex: "1389119024325.2628 REX",
//             namebid_proceeds: "0.0000 EOS",
//             loan_num: 505265,
//         }
//     ],
//         more: false,
//     next_key: "",
// }

You can also use WharfKit to get the data instead of fetch:

const response = await session.client.v1.chain.get_table_rows({
    code: 'eosio',
    scope: 'eosio',
    table: 'rexpool',
    json: true,
});

Note: From here on out, I will just show the body JSON object that you need to send to the get_table_rows RPC call.

Getting the user's REX balance

The user's REX balance is stored in the rexbal table. You will need to specify the user's account name in both the lower_bound and upper_bound fields.

{
  "code": "eosio",
  "scope": "eosio",
  "lower_bound": "someaccount",
  "upper_bound": "someaccount",
  "table": "rexbal",
  "json": true
}

// Result:
//{
//  "rows": [
//    {
//      "version": 0,
//      "owner": "someaccount",
//      "vote_stake": "100.0000 EOS",
//      "rex_balance": "100000.0000 REX",
//      "matured_rex": 0,
//      "rex_maturities": [
//        {
//          "first": "2024-07-29T00:00:00",
//          "second": "100000000"
//        },
//        {
//          "first": "2106-02-07T06:28:15",
//          "second": 10000
//        }
//      ]
//    }
//  ],
//  "more": false,
//  "next_key": ""
//}

You'll notice that there are two rex_maturities objects. One object will have a first value that is very far in the future. This is the user's indefinitely staked REX position. Any other rex_maturities object is a maturing REX position, and if it is in the past, then it has passed the required maturation period and can be sold and claimed.

The user might also have a matured_rex balance, which is another amount of REX that has matured and can be claimed.

Calculating how much REX you will get for EOS

To calculate the REX <> EOS conversion rate, you will need to use the rexpool data.

function convertRexToEos(rex:number){
    const pool = ... // get the pool here.
    const S0 = parseFloat(pool.total_lendable.split(' ')[0]);
    const R0 = parseFloat(pool.total_rex.split(' ')[0]);
    const R1 = R0 + rex;
    const S1 = (S0 * R1) / R0;
    return parseFloat(parseFloat((S1 - S0).toString()).toFixed(4));
}

Note: You will notice the split function in the code above. This is because the total_lendable and total_rex fields are strings that contain the amount and the token symbol. You will need to split the string to get the amount.

Calculating how much EOS you will get for REX

To do the reverse of the above, you will need to use the rexpool data again.

function convertEosToRex(eos:number){
    const pool = ... // get the pool here.
    const S0 = parseFloat(pool.total_lendable.split(' ')[0]);
    const S1 = S0 + eos;
    const R0 = parseFloat(pool.total_rex.split(' ')[0]);
    const R1 = (S1 * R0) / S0;
    return parseFloat(parseFloat((R1 - R0).toString()).toFixed(4));
}

Calculating APR

By far the easiest way to calculate the APR (since we know the static inflow amounts) is to take the annual reward, and the total amount of staked EOS and then calculate the percentage.

function calculateAPR(){
    const pool = ... // get the pool here.
    const annualReward = 31250000;
    const totalStaked = parseInt(pool.total_lendable.split(' ')[0]);
    return parseFloat(((annualReward / totalStaked) * 100).toString()).toFixed(2);
} 

This will give you a fixed annual percentage rate. You can also calculate the daily rate by dividing the annual rate by 365.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment