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.