1

In my case, I need to write a contract which has an action can stake my own token (issued by my account with eosio.token, like TADO). If A has 10 TADO already, when he stakes 4 TADO, his balance still show 10 TADO, but when he try to transfer 8 TADO to B, alert error.

I have read the source of eosio.system, I notice that the system contract used 'transfer' to implement stake, so user's balance will change. e.g. I stake 1 EOS to get cpu, eosio.system will transfer 1 EOS from my account to eosio.stake.

But this is not what I want, I can't let normal users to trust my contract, so user don't want to transfer TADO token to another account, so I need a way to lock user's balance for staking, and user's balance will not change, but also can not transfer out the locked part.

I found some EOS games have this function, like betdice.one, eosmax.io . They have an account with contract that have stake and unstake actions, I can understand this, if a user stake 1 DICE, user will invoke stake action in stack account named betdicestake, and it will write a record into table. But I can't understand how token account named betdicetoken know how many token don't locked when a user try to transfer his token. I don't see any records that betdicetoken invoked betdicestake.

Has anyone who can help me?

Thanks!

2

This problem I have solved by using table query. I hope my solution can help you.

First I publish my stake contract to account A, it has "stake, unstake" actions, and has "stat, staking_log, unstaking_log, account" tables. "stat" save the total staking and unstaking data, "staking_log" save user staked quantity, "unstaking_log" save user unstaking data, "account" save user quantity sum with staked and unstaking.

Second I publish my token contract to account B, it depends on eosio.token, but I add a stake query in transfer action.

When user stake TADO, user invoke stake action on A, the action will query user TADO balance and query locked balance in account table, then save stake quantity into staking_log, stat, account.

Then user try to transfer TADO, account B will query table account in account A, then account B can assert user could transfer or not.

And yes, user's TADO balance will not change, and that balance will stay in user's account.

Here's my stake contract code:

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>
#include <eosiolib/asset.hpp>
#include <eosiolib/symbol.hpp>
#include <eosiolib/transaction.hpp>
#include "eosio.token.hpp"

using namespace eosio;

CONTRACT token_stake : public contract
{
public:
    using contract::contract;

    token_stake(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), configtable(_self, _self.value), statstables(_self, _self.value), stakinglogtables(_self, _self.value), unstakinglogtables(_self, _self.value)
    {
    }

    ACTION stake(name from, asset quantity)
    {
        require_auth(from);
        assert_status();
        eosio_assert(quantity.symbol == symbol("TADO", 4), "only accepts TADO for deposits");
        eosio_assert(quantity.is_valid(), "Invalid token transfer");
        eosio_assert(quantity.amount > 0, "Quantity must be positive");

        //query from account balance
        eosio::token t(get_tadoaccount(), _code, _ds);
        const auto from_balance = t.get_balance(from, quantity.symbol.code());
        const auto locked_balance = get_locked_balance(from);
        eosio_assert((from_balance - locked_balance - quantity).amount >= 0, "TADO balance not enough.");

        //save staking log
        auto staking = stakinglogtables.find(from.value);
        if (staking == stakinglogtables.end())
        {
            stakinglogtables.emplace(_self, [&](auto &stake) {
                stake.user = from;
                stake.asset = quantity;
            });
        }
        else
        {
            stakinglogtables.modify(staking, _self, [&](auto &row) {
                row.asset.amount += quantity.amount;
            });
        }

        //update stats
        auto stats = statstables.find(1);
        if (stats == statstables.end())
        {
            statstables.emplace(_self, [&](auto &stats) {
                stats.key = 1;
                stats.staking = quantity;
                stats.unstaking = asset(0, symbol("TADO", 4));
            });
        }
        else
        {
            statstables.modify(stats, _self, [&](auto &row) {
                row.staking.amount += quantity.amount;
            });
        }

        accounts_tables accountstable(_self, from.value);
        auto ac = accountstable.find(symbol("TADO", 4).code().raw());
        if (ac == accountstable.end())
        {
            accountstable.emplace(_self, [&](auto &account) {
                account.locked_balance = quantity;
            });
        }
        else
        {
            accountstable.modify(ac, _self, [&](auto &row) {
                row.locked_balance += quantity;
            });
        }
    }

    ACTION unstake(name from, asset quantity, int8_t isinstance)
    {
        require_auth(from);
        assert_status();
        eosio_assert(quantity.symbol == symbol("TADO", 4), "only accepts TADO for deposits");
        eosio_assert(quantity.is_valid(), "Invalid token transfer");
        eosio_assert(quantity.amount > 0, "Quantity must be positive");

        //update staking log
        auto staking = stakinglogtables.find(from.value);
        eosio_assert(staking != stakinglogtables.end(), "user has not stake yets. ");

        uint64_t stake_amount = staking->asset.amount;
        eosio_assert(stake_amount >= quantity.amount, "stack quantity not enough.");

        if (quantity.amount == stake_amount)
        {
            stakinglogtables.erase(staking);
        }
        else
        {
            stakinglogtables.modify(staking, _self, [&](auto &row) {
                row.asset.amount -= quantity.amount;
            });
        }

        if (isinstance)
        {
            require_auth(_self);

            //update stat
            auto stats = statstables.find(1);
            statstables.modify(stats, _self, [&](auto &row) {
                row.staking.amount -= quantity.amount;
            });

            accounts_tables accountstable(_self, from.value);
            auto ac = accountstable.find(symbol("TADO", 4).code().raw());
            if (quantity.amount == ac->locked_balance.amount)
            {
                accountstable.erase(ac);
            }
            else
            {
                accountstable.modify(ac, _self, [&](auto &row) {
                    row.locked_balance.amount -= quantity.amount;
                });
            }
        }
        else
        {
            //save unstaking log

            auto unstaking = unstakinglogtables.find(from.value);
            if (unstaking == unstakinglogtables.end())
            {
                unstakinglogtables.emplace(_self, [&](auto &unstake) {
                    unstake.user = from;
                    unstake.asset = quantity;
                    unstake.request_time = current_time();
                });
            }
            else
            {
                unstakinglogtables.modify(unstaking, _self, [&](auto &unstake) {
                    unstake.asset += quantity;
                    unstake.request_time = current_time();
                });
            }

            //update stat
            auto stats = statstables.find(1);
            statstables.modify(stats, _self, [&](auto &row) {
                row.unstaking.amount += quantity.amount;
                row.staking.amount -= quantity.amount;
            });

            //send defer release
            transaction out{};
            out.actions.emplace_back(permission_level{_self, "active"_n}, _self, "deferrelease"_n, std::make_tuple(from));
            out.delay_sec = get_unstake_time();
            cancel_deferred(from.value);
            out.send(from.value, _self, true);
        }
    }

    ACTION setstatus(uint64_t new_status)
    {
        require_auth(_self);

        auto itr = configtable.find("status"_n.value);
        if (itr == configtable.end())
        {
            configtable.emplace(_self, [&](auto &config) {
                config.key = "status"_n;
                config.uint64_value = new_status;
            });
        }
        else
        {
            configtable.modify(itr, _self, [&](auto &config) {
                config.uint64_value = new_status;
            });
        }
    }

    ACTION setust(uint64_t new_time_second)
    {
        require_auth(_self);

        auto itr = configtable.find("unstaketime"_n.value);
        if (itr == configtable.end())
        {
            //24*3600=86400
            configtable.emplace(_self, [&](auto &config) {
                config.key = "unstaketime"_n;
                config.uint64_value = new_time_second;
            });
        }
        else
        {
            configtable.modify(itr, _self, [&](auto &config) {
                config.uint64_value = new_time_second;
            });
        }
    }

    ACTION settadocode(name new_account)
    {
        require_auth(_self);

        auto itr = configtable.find("tadocode"_n.value);
        if (itr == configtable.end())
        {
            configtable.emplace(_self, [&](auto &config) {
                config.key = "tadocode"_n;
                config.name_value = new_account;
            });
        }
        else
        {
            configtable.modify(itr, _self, [&](auto &config) {
                config.name_value = new_account;
            });
        }
    }

    ACTION deferrelease(name from)
    {
        require_auth(_self);
        print("deferrelease\n", from, "\n");

        //erase unstaking log
        auto unstaking = unstakinglogtables.find(from.value);
        int64_t amount = unstaking->asset.amount;
        unstakinglogtables.erase(unstaking);

        print("stats\n");

        //update stats
        auto stats = statstables.find(1);
        statstables.modify(stats, _self, [&](auto &row) {
            row.unstaking.amount -= amount;
        });
        print("accounts\n", from, "\n");

        accounts_tables accountstable(_self, from.value);
        auto ac = accountstable.find(symbol("TADO", 4).code().raw());
        if (amount == ac->locked_balance.amount)
        {
            accountstable.erase(ac);
        }
        else
        {
            accountstable.modify(ac, _self, [&](auto &row) {
                row.locked_balance.amount -= amount;
            });
        }
    }

    inline asset get_locked_balance(name account)
    {
        accounts_tables accountstable(_self, account.value);
        auto ac = accountstable.find(symbol("TADO", 4).code().raw());
        if (ac == accountstable.end())
        {
            return asset(0, symbol("TADO", 4));
        }
        return ac->locked_balance;
    }

    TABLE config_table
    {
        name key;
        uint64_t uint64_value;
        asset asset_value;
        name name_value;
        std::string string_value;
        fixed_bytes<32> bytes_value;

        uint64_t primary_key() const { return key.value; }
    };

    TABLE stats
    {
        uint64_t key;
        asset staking;
        asset unstaking;

        uint64_t primary_key() const { return key; }
    };

    TABLE staking_log
    {
        name user;
        asset asset;

        uint64_t primary_key() const { return user.value; }
    };

    TABLE unstaking_log
    {
        name user;
        asset asset;
        uint64_t request_time;

        uint64_t primary_key() const { return user.value; }
    };

    TABLE account
    {
        asset locked_balance;

        uint64_t primary_key() const { return locked_balance.symbol.code().raw(); }
    };

    typedef multi_index<"config"_n, config_table> config_tables;
    typedef multi_index<"stats"_n, stats> stats_tables;
    typedef multi_index<"stakinglog"_n, staking_log> stakinglog_tables;
    typedef multi_index<"unstakinglog"_n, unstaking_log> unstakinglog_tables;
    typedef multi_index<"accounts"_n, account> accounts_tables;

private:
    config_tables configtable;
    stats_tables statstables;
    stakinglog_tables stakinglogtables;
    unstakinglog_tables unstakinglogtables;

    void assert_status()
    {
        auto itr = configtable.find("status"_n.value);
        eosio_assert(itr != configtable.end() && itr->uint64_value > 0, "current status do not allow doing this action.");
    }

    name get_tadoaccount()
    {
        auto tado_account = configtable.find("tadocode"_n.value);
        eosio_assert(tado_account != configtable.end(), "tado account not set.");

        return tado_account->name_value;
    }

    uint64_t get_unstake_time()
    {
        auto unstake_time = configtable.find("unstaketime"_n.value);
        eosio_assert(unstake_time != configtable.end(), "unstake time not set.");

        return unstake_time->uint64_value;
    }
};

EOSIO_DISPATCH(token_stake, (setstatus)(setust)(settadocode)(deferrelease)(stake)(unstake))

Here's my token contract code(partial):

void token::sub_balance(name owner, asset value)
{
  accounts from_acnts(_self, owner.value);

  const auto &from = from_acnts.get(value.symbol.code().raw(), "no balance object found");
  eosio_assert(from.balance.amount >= value.amount, "overdrawn balance");

  //query stake account in config table
  configs config(_self, _self.value);
  const auto &stake_account = config.get("stakeaccount"_n.value, "no stake account found");

  //query account locked balance from stake account
  ggs::token_stake stake(stake_account.name_value, _code, _ds);
  const auto from_locked_balance = stake.get_locked_balance(owner);
  eosio_assert(from.balance.amount - value.amount - from_locked_balance.amount >= 0, "balance has locked by stake.");

  from_acnts.modify(from, owner, [&](auto &a) {
     a.balance -= value;
  });
}

If you have problem in query other contract table, you can use this way.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.