diff --git a/include/seeds.rainbows.hpp b/include/seeds.rainbows.hpp index 33518af8..9ef18db7 100644 --- a/include/seeds.rainbows.hpp +++ b/include/seeds.rainbows.hpp @@ -4,27 +4,32 @@ #include #include #include - #include +#ifdef RAINBOW_STANDALONE +#undef RAINBOW_SEEDS_ECOSYS +#else +#define RAINBOW_SEEDS_ECOSYS +#endif + + using namespace eosio; using std::string; /** - * The `rainbows` experimental contract implements the functionality described in the design document + * The `rainbows` contract implements the functionality described in the design document * https://rieki-cordon.medium.com/1fb713efd9b1 . * In the development process we are building on the eosio.token code. * * The token contract defines the structures and actions that allow users to create, issue, and manage - * tokens for EOSIO based blockchains. It exemplifies one way to implement a smart contract which - * allows for creation and management of tokens. + * tokens for EOSIO based blockchains. * * The `rainbows` contract class also implements a public static method: `get_balance`. This allows * one to check the balance of a token for a specified account. * - * The `rainbows` contract manages the set of tokens, stakes, accounts and their corresponding balances, - * by using four internal tables: the `accounts`, `stats`, `configs`, and `stakes`. The `accounts` + * The `rainbows` contract manages the set of tokens, backings, accounts and their corresponding balances, + * by using four internal tables: the `accounts`, `stats`, `configs`, and `backings`. The `accounts` * multi-index table holds, for each row, instances of `account` object and the `account` object * holds information about the balance of one token. The `accounts` table is scoped to an eosio * account, and it keeps the rows indexed based on the token's symbol. This means that when one @@ -39,22 +44,27 @@ using namespace eosio; * * The first two tables (`accounts` and `stats`) are structured identically to the `eosio.token` * tables, making "rainbow tokens" compatible with most EOSIO wallet and block explorer applications. - * The two remaining tables (`configs` and `stakes`) provide additional data specific to the rainbow + * The two remaining tables (`configs` and `backings`) provide additional data specific to the rainbow * token. * * The `configs` singleton table contains names of administration accounts (e.g. membership_mgr, * freeze_mgr) and some configuration flags. The `configs` table is scoped to the token symbol_code * and has a single row per scope. * - * The `stakes` table contains staking relationships (staked currency, staking ratio, escrow account). + * The `backings` table contains backing relationships (backing currency, backing ratio, escrow account). * It is scoped by the token symbol_code and may contain 1 or more rows. It has a secondary index - * based on the staked currency type. + * based on the backing currency type. * * In addition, the `displays` singleton table contains json metadata intended for applications * (e.g. wallets) to use in UI display, such as a logo symbol url. It is scoped by token symbol_code. * * The `symbols` table is a housekeeping list of all the tokens managed by the contract. It is * scoped to the contract. + * + * The contract can require a prepaid fee (defined by the `fee_ext_sym` and `submission_fee` initializers + * in the rainbows constructor) in order to create a token. The prepaid balances are + * recorded in the `feebalance` table. + * */ CONTRACT rainbows : public contract { @@ -62,7 +72,15 @@ using namespace eosio; using contract::contract; rainbows( name receiver, name code, datastream ds ) : contract( receiver, code, ds ), - symboltable( receiver, receiver.value ) + symboltable( receiver, receiver.value ), + approval_required( true ), + #if defined RAINBOW_SEEDS_ECOSYS + fee_ext_sym( symbol( "SEEDS",4 ), "token.seeds"_n ), + submission_fee( 1000000, symbol( "SEEDS",4 ) ) + #else + fee_ext_sym( ), + submission_fee( ) + #endif {} /** @@ -70,7 +88,8 @@ using namespace eosio; * specified characteristics. * If the token does not exist, a new row in the stats table for token symbol scope is created * with the specified characteristics. At creation, its' approval flag is false, preventing - * tokens from being issued. + * tokens from being issued. (The approval requirement can be bypassed by setting the + * data member `approval_required` to false.) * If a token of this symbol does exist and update is permitted, the characteristics are updated. * * @param issuer - the account that creates the token, @@ -78,7 +97,7 @@ using namespace eosio; * @param withdrawal_mgr - the account with authority to withdraw tokens from any account, * @param withdraw_to - the account to which withdrawn tokens are deposited, * @param freeze_mgr - the account with authority to freeze transfer actions, - * @param redeem_locked_until - an ISO8601 date string; user redemption of stake is + * @param redeem_locked_until - an ISO8601 date string; user redemption of backings is * disallowed until this time; blank string is equivalent to "now" (i.e. unlocked). * @param config_locked_until - an ISO8601 date string; changes to token characteristics * are disallowed until this time; blank string is equivalent to "now" (i.e. unlocked). @@ -105,7 +124,10 @@ using namespace eosio; * @pre broker_symbol must be an existing frozen token of zero precision on this contract, or empty * @pre cred_limit_symbol must be an existing frozen token of matching precision on this contract, or empty * @pre pos_limit_symbol must be an existing frozen token of matching precision on this contract, or empty - + * @pre if data member `fee_ext_sym` is null, the issuer must provide sufficient RAM resources; + * otherwise, the issuer must have prepaid the required fee to the contract and + * no RAM is required from issuer. + * */ ACTION create( const name& issuer, const asset& maximum_supply, @@ -119,10 +141,26 @@ using namespace eosio; const string& cred_limit_symbol, const string& pos_limit_symbol ); + /** + * This action watches for fee transfers into the contract account and updates the + * `feebalance` table. + */ + [[eosio::on_notify("*::transfer")]] + void ontransfer(name from, name to, asset quantity, string memo); + + /** + * This action returns any fee balance associated with an account. This balance is + * typically related to an issuance fee. + * + * @param account - account + * + */ + ACTION returnfee( const name& account ); /** * By this action the contract owner approves or rejects the creation of the token. Until - * this approval, no tokens may be issued. If rejected, and no issued tokens are outstanding, + * this approval, no tokens may be issued. (However automatic approval upon creation occurs if + * the data member `approval_required` is false.) If rejected, and no issued tokens are outstanding, * the table entries for this token are deleted. * * @param symbolcode - the symbol_code of the token to execute the close action for. @@ -134,47 +172,46 @@ using namespace eosio; /** - * Allows `issuer` account to create a staking relationship for a token. A new row in the - * stakes table for token symbol scope gets created with the specified characteristics. + * Allows `issuer` account to create a backing relationship for a token. A new row in the + * backings table for token symbol scope gets created with the specified characteristics. * * @param token_bucket - a reference quantity of the token, - * @param stake_per_bucket - the number of stake tokens (e.g. Seeds) staked per "bucket" of tokens, - * @param stake_token_contract - the staked token contract account (e.g. token.seeds), - * @param stake_to - the escrow account where stake is held - * @param proportional - redeem by proportion of escrow rather than by staking ratio. + * @param backs_per_bucket - the number of backing tokens (e.g. Seeds) placed in escrow per "bucket" of tokens, + * @param backing_token_contract - the backing token contract account (e.g. token.seeds), + * @param escrow - the escrow account where backing tokens are held + * @param proportional - redeem by proportion of escrow rather than by backing ratio. * @param reserve_fraction - minimum reserve ratio (as percent) of escrow balance to redemption liability. * @param memo - the memo string to accompany the transaction. * * @pre Token symbol must have already been created by this issuer * @pre The config_locked_until field in the configs table must be in the past, - * @pre issuer must have a (possibly zero) balance of the stake token, - * @pre stake_per_bucket must be non-negative + * @pre issuer must have a (possibly zero) balance of the backing token, + * @pre backs_per_bucket must be non-negative * @param reserve_fraction must be non-negative * @pre issuer active permissions must include rainbowcontract@eosio.code - * @pre stake_to active permissions must include rainbowcontract@eosio.code + * @pre escrow active permissions must include rainbowcontract@eosio.code * * Note: the contract cannot internally check the required permissions status */ - ACTION setstake( const asset& token_bucket, - const asset& stake_per_bucket, - const name& stake_token_contract, - const name& stake_to, + ACTION setbacking( const asset& token_bucket, + const asset& backs_per_bucket, + const name& backing_token_contract, + const name& escrow, const bool& proportional, const uint32_t& reserve_fraction, const string& memo); /** - * Allows `issuer` account to delete a staking relationship. Staked tokens are returned - * to the issuer account. Deferred stake relationships are reverted. The row is removed - * from the stakes table. + * Allows `issuer` account to delete a backing relationship. Backing tokens are returned + * to the issuer account. The row is removed from the backings table. * - * @param stake_index - the index field in the `stakes` table - * @param symbolcode - the staked token + * @param backing_index - the index field in the `backings` table + * @param symbolcode - the backing token * @param memo - memo string * * @pre the config_locked_until field in the configs table must be in the past */ - ACTION deletestake( const uint64_t& stake_index, + ACTION deletebacking( const uint64_t& backing_index, const symbol_code& symbolcode, const string& memo ); @@ -204,7 +241,7 @@ using namespace eosio; /** * This action issues a `quantity` of tokens to the issuer account, and transfers - * a proportional amount of stake to escrow if staking is configured. + * a proportional amount of backing tokens to escrow if backing is configured. * * @param quantity - the amount of tokens to be issued, * @memo - the memo string that accompanies the token issue transaction. @@ -215,20 +252,24 @@ using namespace eosio; /** * The opposite for issue action, if all validations succeed, - * it debits the statstable.supply amount. Any staked tokens are released from escrow in - * proportion to the quantity of tokens retired. + * it debits the statstable.supply amount. If `do_redeem` flag is true, + * any backing tokens are released from escrow in proportion to the + * quantity of tokens retired. * * @param owner - the account containing tokens to retire, * @param quantity - the quantity of tokens to retire, + * @param do_redeem - if true, send backing tokens to owner, + * if false, they remain in escrow, * @param memo - the memo string to accompany the transaction. * * @pre the redeem_locked_until configuration must be in the past (except that * this action is always permitted to the issuer.) - * @pre If any staking relationships exist, for each relationship : - * 1. the proportional unstaking flag must be configured true, OR + * @pre If any backing relationships exist, for each relationship : + * 1. the proportional redemption flag must be configured true, OR * 2. the balance in the escrow account must meet the reserve_fraction criterion */ - ACTION retire( const name& owner, const asset& quantity, const string& memo ); + ACTION retire( const name& owner, const asset& quantity, + const bool& do_redeem, const string& memo ); /** * Allows `from` account to transfer to `to` account the `quantity` tokens. @@ -247,9 +288,6 @@ using namespace eosio; * credit if configured with credit_limit_symbol in `create` operation) * @pre If configured with positive_limit_symbol in `create` operation, the transfer * must not put the `to` account over its maximum limit - * - * Note: Transfer of zero tokens can effectively "open" an account (create a new - * balance entry in the `accounts` table) using `from` acct's ram. */ ACTION transfer( const name& from, const name& to, @@ -329,7 +367,7 @@ using namespace eosio; } private: - const int max_stake_count = 8; // don't use too much cpu time to complete transaction + const int max_backings_count = 8; // don't use too much cpu time to complete transaction const uint64_t no_index = static_cast(-1); // flag for nonexistent defer_table link static const asset null_asset; const uint32_t VISITOR = 1; @@ -368,18 +406,19 @@ using namespace eosio; }; - TABLE stake_stats { // scoped on token symbol code + TABLE backing_stats { // scoped on token symbol code uint64_t index; asset token_bucket; - asset stake_per_bucket; - name stake_token_contract; - name stake_to; + asset backs_per_bucket; + name backing_token_contract; + name escrow; uint32_t reserve_fraction; bool proportional; uint64_t primary_key()const { return index; }; uint128_t by_secondary() const { - return (uint128_t)stake_per_bucket.symbol.raw()<<64 | stake_token_contract.value; + return (uint128_t)backs_per_bucket.symbol.raw()<<64 | + backing_token_contract.value; } }; @@ -389,35 +428,48 @@ using namespace eosio; uint64_t primary_key()const { return symbolcode.raw(); }; }; + TABLE feebalance { // scoped on account name + asset balance; + + uint64_t primary_key()const { return balance.symbol.code().raw(); } + }; + typedef eosio::multi_index< "accounts"_n, account > accounts; typedef eosio::multi_index< "stat"_n, currency_stats > stats; typedef eosio::singleton< "configs"_n, currency_config > configs; typedef eosio::multi_index< "configs"_n, currency_config > dump_for_config; typedef eosio::singleton< "displays"_n, currency_display > displays; typedef eosio::multi_index< "displays"_n, currency_display > dump_for_display; - typedef eosio::multi_index< "stakes"_n, stake_stats, indexed_by - < "staketoken"_n, - const_mem_fun + typedef eosio::multi_index< "backings"_n, backing_stats, indexed_by + < "backingtoken"_n, + const_mem_fun > - > stakes; + > backs; typedef eosio::multi_index< "symbols"_n, symbolt > symbols; + typedef eosio::multi_index< "feebalance"_n, feebalance > fees; symbols symboltable; + extended_symbol fee_ext_sym; + asset submission_fee; + bool approval_required; void sub_balance( const name& owner, const asset& value, const symbol_code& limit_symbol ); void add_balance( const name& owner, const asset& value, const name& ram_payer, const symbol_code& limit_symbol ); void sister_check(const string& sym_name, uint32_t precision); - void stake_all( const name& owner, const asset& quantity ); - void unstake_all( const name& owner, const asset& quantity ); - void stake_one( const stake_stats& sk, const name& owner, const asset& quantity ); - void unstake_one( const stake_stats& sk, const name& owner, const asset& quantity ); + void set_all_backings( const name& owner, const asset& quantity ); + void redeem_all_backings( const name& owner, const asset& quantity ); + void set_one_backing( const backing_stats& bk, const name& owner, const asset& quantity ); + void redeem_one_backing( const backing_stats& bk, const name& owner, const asset& quantity ); void reset_one( const symbol_code symbolcode, const bool all, const uint32_t limit, uint32_t& counter ); }; +/* EOSIO_DISPATCH(rainbows, - (create)(approve)(setstake)(setdisplay)(issue)(retire)(transfer) + (create)(approve)(setbacking)(deletebacking)(setdisplay)(issue)(retire)(transfer) (open)(close)(freeze)(reset)(resetacct) ); +*/ + diff --git a/src/seeds.rainbows.cpp b/src/seeds.rainbows.cpp index f2cd8ea1..485e372d 100644 --- a/src/seeds.rainbows.cpp +++ b/src/seeds.rainbows.cpp @@ -2,6 +2,7 @@ #include #include <../capi/eosio/action.h> + void rainbows::create( const name& issuer, const asset& maximum_supply, const name& withdrawal_mgr, @@ -15,6 +16,17 @@ void rainbows::create( const name& issuer, const string& pos_limit_symbol ) { require_auth( issuer ); + auto fee_sym_code_raw = fee_ext_sym.get_symbol().code().raw(); + name ram_payer = fee_sym_code_raw ? get_self() : issuer; + if( fee_sym_code_raw && submission_fee.amount>0 ) { + fees feebals( get_self(), issuer.value ); + const auto& f = feebals.get( fee_sym_code_raw, "no fee has been submitted" ); + check( f.balance.amount >= submission_fee.amount, + "insufficient balance "+f.balance.to_string()+"; fee is "+submission_fee.to_string() ); + feebals.modify(f, get_self(), [&](auto &s) { + s.balance.amount -= submission_fee.amount; + }); + } auto sym = maximum_supply.symbol; check( sym.is_valid(), "invalid symbol name" ); check( maximum_supply.is_valid(), "invalid supply"); @@ -71,15 +83,15 @@ void rainbows::create( const name& issuer, cf.broker = symbol_code( broker_symbol ); cf.cred_limit = symbol_code( cred_limit_symbol ); cf.positive_limit = symbol_code( pos_limit_symbol ); - configtable.set( cf, issuer ); + configtable.set( cf, ram_payer ); return; } // new token symbols symboltable( get_self(), get_self().value ); - symboltable.emplace( issuer, [&]( auto& s ) { + symboltable.emplace( ram_payer, [&]( auto& s ) { s.symbolcode = sym.code(); }); - statstable.emplace( issuer, [&]( auto& s ) { + statstable.emplace( ram_payer, [&]( auto& s ) { s.supply.symbol = maximum_supply.symbol; s.max_supply = maximum_supply; s.issuer = issuer; @@ -92,16 +104,16 @@ void rainbows::create( const name& issuer, .redeem_locked_until = redeem_locked_until, .config_locked_until = config_locked_until, .transfers_frozen = false, - .approved = false, + .approved = !approval_required, .membership = symbol_code( membership_symbol ), .broker = symbol_code( broker_symbol ), .cred_limit = symbol_code( cred_limit_symbol ), .positive_limit = symbol_code( pos_limit_symbol ) }; - configtable.set( new_config, issuer ); + configtable.set( new_config, ram_payer ); displays displaytable( get_self(), sym.code().raw() ); currency_display new_display{ "" }; - displaytable.set( new_display, issuer ); + displaytable.set( new_display, ram_payer ); } void rainbows::sister_check(const string& sym_name, uint32_t precision) { @@ -116,6 +128,51 @@ void rainbows::sister_check(const string& sym_name, uint32_t precision) { } } +void rainbows::returnfee( const name& account ) +{ + auto fee_sym_code_raw = fee_ext_sym.get_symbol().code().raw(); + check( fee_sym_code_raw, "no fees in contract" ); + if( ~has_auth( get_self() ) ) { + require_auth( account ); + } + fees feebals( get_self(), account.value ); + const auto& f = feebals.get( fee_sym_code_raw, "no fee balance to return" ); + if( f.balance.amount > 0 ) { + action( + permission_level{get_self(),"active"_n}, + fee_ext_sym.get_contract(), + "transfer"_n, + std::make_tuple(get_self(), + account, + f.balance, + std::string("rainbow fee return") ) + ).send(); + feebals.erase( f ); + } +} + +void rainbows::ontransfer(name from, name to, asset quantity, string memo) +{ + if ( to != get_self() || from == get_self() ) { + return; + } + if( get_first_receiver() != fee_ext_sym.get_contract() || + quantity.symbol != fee_ext_sym.get_symbol() ) { + return; // transfer to rainbow contract is not in fee currency + } + check(quantity.amount > 0, "fee transfer must be positive"); + fees feebals(get_self(), from.value); + auto f_it = feebals.find(quantity.symbol.code().raw()); + if (f_it != feebals.end()) + feebals.modify(f_it, get_self(), [&](auto &s) { + s.balance.amount += quantity.amount; + }); + else + feebals.emplace(get_self(), [&](auto &s) { + s.balance = quantity; + }); +} + void rainbows::approve( const symbol_code& symbolcode, const bool& reject_and_clear ) { require_auth( get_self() ); @@ -127,9 +184,9 @@ void rainbows::approve( const symbol_code& symbolcode, const bool& reject_and_cl displays displaytable( get_self(), sym_code_raw ); if( reject_and_clear ) { check( st.supply.amount == 0, "cannot clear with outstanding tokens" ); - stakes stakestable( get_self(), sym_code_raw ); - for( auto itr = stakestable.begin(); itr != stakestable.end(); ) { - itr = stakestable.erase(itr); + backs backingtable( get_self(), sym_code_raw ); + for( auto itr = backingtable.begin(); itr != backingtable.end(); ) { + itr = backingtable.erase(itr); } configtable.remove( ); displaytable.remove( ); @@ -139,15 +196,17 @@ void rainbows::approve( const symbol_code& symbolcode, const bool& reject_and_cl symboltable.erase( sym ); } else { cf.approved = true; - configtable.set (cf, st.issuer ); + auto fee_sym_code_raw = fee_ext_sym.get_symbol().code().raw(); + name ram_payer = fee_sym_code_raw ? get_self() : st.issuer; + configtable.set (cf, ram_payer ); } } -void rainbows::setstake( const asset& token_bucket, - const asset& stake_per_bucket, - const name& stake_token_contract, - const name& stake_to, +void rainbows::setbacking( const asset& token_bucket, + const asset& backs_per_bucket, + const name& backing_token_contract, + const name& escrow, const bool& proportional, const uint32_t& reserve_fraction, const string& memo ) @@ -157,60 +216,61 @@ void rainbows::setstake( const asset& token_bucket, const auto& st = statstable.get( sym_code_raw, "token with symbol does not exist" ); require_auth( st.issuer ); check( memo.size() <= 256, "memo has more than 256 bytes" ); - auto stake_sym = stake_per_bucket.symbol; - uint128_t stake_token = (uint128_t)stake_sym.raw()<<64 | stake_token_contract.value; - check( stake_sym.is_valid(), "invalid stake symbol name" ); - check( stake_per_bucket.is_valid(), "invalid stake"); - check( stake_per_bucket.amount >= 0, "stake per token must be non-negative"); - check( is_account( stake_token_contract ), "stake token contract account does not exist"); - check( stake_sym.code().raw() != sym_code_raw || stake_token_contract != get_self(), - "cannot stake own token"); - accounts accountstable( stake_token_contract, st.issuer.value ); - const auto stake_bal = accountstable.find( stake_sym.code().raw() ); - check( stake_bal != accountstable.end(), "issuer must have a stake token balance"); - check( stake_bal->balance.symbol == stake_sym, "mismatched stake token precision" ); - check( is_account( stake_to ), "stake_to account does not exist"); + auto backing_sym = backs_per_bucket.symbol; + check( backing_sym.is_valid(), "invalid backing symbol name" ); + check( backs_per_bucket.is_valid(), "invalid backing"); + check( backs_per_bucket.amount >= 0, "backing per token must be non-negative"); + check( is_account( backing_token_contract ), "backing token contract account does not exist"); + check( backing_sym.code().raw() != sym_code_raw || backing_token_contract != get_self(), + "cannot back with own token"); + accounts accountstable( backing_token_contract, st.issuer.value ); + const auto backing_bal = accountstable.find( backing_sym.code().raw() ); + check( backing_bal != accountstable.end(), "issuer must have a backing token balance"); + check( backing_bal->balance.symbol == backing_sym, "mismatched backing token precision" ); + check( is_account( escrow ), "escrow account does not exist"); check( token_bucket.amount > 0, "token bucket must be > 0" ); configs configtable( get_self(), sym_code_raw ); const auto& cf = configtable.get(); check( cf.config_locked_until.time_since_epoch() < current_time_point().time_since_epoch(), "token reconfiguration is locked" ); - stakes stakestable( get_self(), sym_code_raw ); - int existing_stake_count = std::distance(stakestable.cbegin(),stakestable.cend()); - check( existing_stake_count <= max_stake_count, "stake count exceeded" ); - const auto& sk = *stakestable.emplace( st.issuer, [&]( auto& s ) { - s.index = stakestable.available_primary_key(); + backs backingtable( get_self(), sym_code_raw ); + int existing_backing_count = std::distance(backingtable.cbegin(),backingtable.cend()); + check( existing_backing_count <= max_backings_count, "max backings count exceeded" ); + auto fee_sym_code_raw = fee_ext_sym.get_symbol().code().raw(); + name ram_payer = fee_sym_code_raw ? get_self() : st.issuer; + const auto& bk = *backingtable.emplace( ram_payer, [&]( auto& s ) { + s.index = backingtable.available_primary_key(); s.token_bucket = token_bucket; - s.stake_per_bucket = stake_per_bucket; - s.stake_token_contract = stake_token_contract; - s.stake_to = stake_to; + s.backs_per_bucket = backs_per_bucket; + s.backing_token_contract = backing_token_contract; + s.escrow = escrow; s.proportional = proportional; s.reserve_fraction = reserve_fraction; }); if( st.supply.amount != 0 ) { - stake_one( sk, st.issuer, st.supply ); + set_one_backing( bk, st.issuer, st.supply ); } } -void rainbows::deletestake( const uint64_t& stake_index, +void rainbows::deletebacking( const uint64_t& back_index, const symbol_code& symbolcode, const string& memo ) { auto sym_code_raw = symbolcode.raw(); stats statstable( get_self(), sym_code_raw ); const auto& st = statstable.get( sym_code_raw, "token with symbol does not exist" ); - stakes stakestable( get_self(), sym_code_raw ); - const auto& sk = stakestable.get( stake_index, "stake index does not exist" ); + backs backingtable( get_self(), sym_code_raw ); + const auto& bk = backingtable.get( back_index, "backing index does not exist" ); configs configtable( get_self(), sym_code_raw ); const auto& cf = configtable.get(); check( cf.config_locked_until.time_since_epoch() < current_time_point().time_since_epoch(), "token reconfiguration is locked" ); require_auth( st.issuer ); if( st.supply.amount != 0 ) { - unstake_one( sk, st.issuer, st.supply ); + redeem_one_backing( bk, st.issuer, st.supply ); } - stakestable.erase( sk ); + backingtable.erase( bk ); } void rainbows::setdisplay( const symbol_code& symbolcode, @@ -225,7 +285,9 @@ void rainbows::setdisplay( const symbol_code& symbolcode, check( json_meta.size() <= 2048, "json metadata has more than 2048 bytes" ); // TODO check json_meta string for safety, parse json, and check name length dt.json_meta = json_meta; - displaytable.set( dt, st.issuer ); + auto fee_sym_code_raw = fee_ext_sym.get_symbol().code().raw(); + name ram_payer = fee_sym_code_raw ? get_self() : st.issuer; + displaytable.set( dt, ram_payer ); } void rainbows::issue( const asset& quantity, const string& memo ) @@ -250,83 +312,85 @@ void rainbows::issue( const asset& quantity, const string& memo ) s.supply += quantity; }); - stake_all( st.issuer, quantity ); + set_all_backings( st.issuer, quantity ); add_balance( st.issuer, quantity, st.issuer, cf.positive_limit ); } -void rainbows::stake_one( const stake_stats& sk, const name& owner, const asset& quantity ) { - if( sk.stake_per_bucket.amount > 0 ) { // TBD: use stake ratio = 0 as placeholder for proportional? - asset stake_quantity = sk.stake_per_bucket; - stake_quantity.amount = (int64_t)((int128_t)quantity.amount*sk.stake_per_bucket.amount/sk.token_bucket.amount); +void rainbows::set_one_backing( + const backing_stats& bk, + const name& owner, + const asset& quantity ) { + if( bk.backs_per_bucket.amount > 0 ) { + asset backing_quantity = bk.backs_per_bucket; + backing_quantity.amount = (int64_t)((int128_t)quantity.amount*bk.backs_per_bucket.amount/bk.token_bucket.amount); action( permission_level{owner, "active"_n}, - sk.stake_token_contract, + bk.backing_token_contract, "transfer"_n, std::make_tuple(owner, - sk.stake_to, - stake_quantity, - std::string("rainbow stake")) + bk.escrow, + backing_quantity, + std::string("rainbow backing")) ).send(); } } -void rainbows::stake_all( const name& owner, const asset& quantity ) { - stakes stakestable( get_self(), quantity.symbol.code().raw() ); - for( auto itr = stakestable.begin(); itr != stakestable.end(); itr++ ) { - stake_one( *itr, owner, quantity ); +void rainbows::set_all_backings( const name& owner, const asset& quantity ) { + backs backingtable( get_self(), quantity.symbol.code().raw() ); + for( auto itr = backingtable.begin(); itr != backingtable.end(); itr++ ) { + set_one_backing( *itr, owner, quantity ); } } -void rainbows::unstake_one( const stake_stats& sk, const name& owner, const asset& quantity ) { +void rainbows::redeem_one_backing( const backing_stats& bk, const name& owner, const asset& quantity ) { // get balance in escrow - auto stake_in_escrow = get_balance( sk.stake_token_contract, sk.stake_to, sk.stake_per_bucket.symbol.code() ); - // stake proportion = (qty being unstaked)/(token supply) - // TODO: consider whether negative balances (mutual credit) should count as supply for this calculation - uint64_t sym_code_raw = sk.token_bucket.symbol.code().raw(); + auto backing_in_escrow = get_balance( bk.backing_token_contract, bk.escrow, bk.backs_per_bucket.symbol.code() ); + // backing proportion = (qty being redeemed)/(token supply) + uint64_t sym_code_raw = bk.token_bucket.symbol.code().raw(); stats statstable( get_self(), sym_code_raw ); - const auto& st = statstable.get( sym_code_raw, "unstake: no symbol" ); - check( st.supply.amount > 0, "no supply to unstake" ); - int64_t proportional_amount = (int64_t)((int128_t)stake_in_escrow.amount*quantity.amount/st.supply.amount); - asset stake_quantity = sk.stake_per_bucket; + const auto& st = statstable.get( sym_code_raw, "redeem backing: no symbol" ); + check( st.supply.amount > 0, "no backing supply to redeem" ); + int64_t proportional_amount = (int64_t)((int128_t)backing_in_escrow.amount*quantity.amount/st.supply.amount); + asset backing_quantity = bk.backs_per_bucket; string memo; - if( sk.proportional) { - stake_quantity.amount = proportional_amount; + if( bk.proportional) { + backing_quantity.amount = proportional_amount; memo = "proportional "; } else { - stake_quantity.amount = (int64_t)((int128_t)quantity.amount*sk.stake_per_bucket.amount/sk.token_bucket.amount); + backing_quantity.amount = (int64_t)((int128_t)quantity.amount*bk.backs_per_bucket.amount/bk.token_bucket.amount); // check whether this redemption would put escrow below reserve fraction - auto stake_remaining = stake_in_escrow.amount - stake_quantity.amount; + auto backing_remaining = backing_in_escrow.amount - backing_quantity.amount; auto supply_remaining = st.supply.amount - quantity.amount; - auto escrow_needed = (int64_t)((int128_t)supply_remaining*sk.reserve_fraction*sk.stake_per_bucket.amount/ - (100*sk.token_bucket.amount)); - if( escrow_needed > stake_remaining ) { - check( false, "can't unstake, escrow underfunded in " + - sk.stake_per_bucket.symbol.code().to_string() + - //std::to_string(escrow_needed) +":"+ std::to_string(stake_remaining) + - " (" + std::to_string(sk.reserve_fraction) + "% reserve)" ); + auto escrow_needed = (int64_t)((int128_t)supply_remaining*bk.reserve_fraction*bk.backs_per_bucket.amount/ + (100*bk.token_bucket.amount)); + if( escrow_needed > backing_remaining ) { + check( false, "can't redeem, escrow underfunded in " + + bk.backs_per_bucket.symbol.code().to_string() + + " (" + std::to_string(bk.reserve_fraction) + "% reserve)" ); } } - memo += "rainbow unstake"; - if( stake_quantity.amount > 0 ) { + memo += "rainbow redeem"; + if( backing_quantity.amount > 0 ) { action( - permission_level{sk.stake_to,"active"_n}, - sk.stake_token_contract, + permission_level{bk.escrow,"active"_n}, + bk.backing_token_contract, "transfer"_n, - std::make_tuple(sk.stake_to, + std::make_tuple(bk.escrow, owner, - stake_quantity, + backing_quantity, memo) ).send(); } } -void rainbows::unstake_all( const name& owner, const asset& quantity ) { - stakes stakestable( get_self(), quantity.symbol.code().raw() ); - for( auto itr = stakestable.begin(); itr != stakestable.end(); itr++ ) { - unstake_one( *itr, owner, quantity ); +void rainbows::redeem_all_backings( const name& owner, const asset& quantity ) { + backs backingtable( get_self(), quantity.symbol.code().raw() ); + for( auto itr = backingtable.begin(); itr != backingtable.end(); itr++ ) { + redeem_one_backing( *itr, owner, quantity ); } } -void rainbows::retire( const name& owner, const asset& quantity, const string& memo ) +void rainbows::retire( const name& owner, const asset& quantity, + const bool& do_redeem, const string& memo ) { auto sym = quantity.symbol; check( sym.is_valid(), "invalid symbol name" ); @@ -335,20 +399,21 @@ void rainbows::retire( const name& owner, const asset& quantity, const string& m stats statstable( get_self(), sym.code().raw() ); const auto& st = statstable.get( sym.code().raw(), "token with symbol does not exist" ); configs configtable( get_self(), sym.code().raw() ); - const auto& cf = configtable.get(); - if( cf.redeem_locked_until.time_since_epoch() < current_time_point().time_since_epoch() ) { - check( !cf.transfers_frozen, "transfers are frozen"); - } else { - check( owner == st.issuer, "bearer redeem is disabled"); - } require_auth( owner ); check( quantity.is_valid(), "invalid quantity" ); check( quantity.amount > 0, "must retire positive quantity" ); - - check( quantity.symbol == st.supply.symbol, "symbol precision mismatch" ); - - unstake_all( owner, quantity ); - + check( quantity.symbol == st.supply.symbol, "symbol or precision mismatch" ); + if( do_redeem ) { + const auto& cf = configtable.get(); + if( cf.redeem_locked_until.time_since_epoch() < + current_time_point().time_since_epoch() ) { + check( !cf.transfers_frozen, "transfers are frozen"); + } else { + check( owner == st.issuer, "bearer redeem is disabled"); + } + + redeem_all_backings( owner, quantity ); + } sub_balance( owner, quantity, symbol_code(0) ); statstable.modify( st, same_payer, [&]( auto& s ) { s.supply -= quantity; @@ -402,13 +467,16 @@ void rainbows::transfer( const name& from, sub_balance( from, quantity, cf.cred_limit ); add_balance( to, quantity, payer, cf.positive_limit ); + stats statstable2( get_self(), sym_code_raw ); // use updated statstable for supply value + const auto& st2 = statstable2.get( sym_code_raw ); + check( st2.max_supply.amount >= st2.supply.amount, "new credit exceeds available supply"); + } void rainbows::sub_balance( const name& owner, const asset& value, const symbol_code& limit_symbol ) { accounts from_acnts( get_self(), owner.value ); - const auto& from = from_acnts.get( value.symbol.code().raw(), "no balance object found" ); - uint64_t limit = 0; + int64_t limit = 0; if( limit_symbol != symbol_code(0) ) { auto cred = from_acnts.find( limit_symbol.raw() ); if( cred != from_acnts.end() ) { @@ -417,15 +485,25 @@ void rainbows::sub_balance( const name& owner, const asset& value, const symbol_ limit = lim.balance.amount; } } - int64_t new_balance = from.balance.amount - value.amount; - check( new_balance + limit >= 0, "overdrawn balance" ); - int64_t credit_increase = std::min( from.balance.amount, 0LL ) - std::min( new_balance, 0LL ); - from_acnts.modify( from, same_payer, [&]( auto& a ) { - a.balance.amount = new_balance; + int64_t new_amount, old_amount; + const auto fr = from_acnts.find( value.symbol.code().raw() ); + if( fr == from_acnts.end() ) { + old_amount = 0; + new_amount = -value.amount; + from_acnts.emplace( owner, [&]( auto& a ){ + a.balance = asset{new_amount, value.symbol}; }); + } else { + old_amount = fr->balance.amount; + new_amount = old_amount - value.amount; + from_acnts.modify( fr, same_payer, [&]( auto& a ) { + a.balance.amount = new_amount; + }); + } + check( new_amount + limit >= 0, "overdrawn balance" ); + int64_t credit_increase = std::min( old_amount, 0LL ) - std::min( new_amount, 0LL ); stats statstable( get_self(), value.symbol.code().raw() ); const auto& st = statstable.get( value.symbol.code().raw() ); - check( credit_increase <= st.max_supply.amount - st.supply.amount, "new credit exceeds available supply"); statstable.modify( st, same_payer, [&]( auto& s ) { s.supply.amount += credit_increase; }); @@ -511,7 +589,9 @@ void rainbows::freeze( const symbol_code& symbolcode, const bool& freeze, const check( memo.size() <= 256, "memo has more than 256 bytes" ); require_auth( cf.freeze_mgr ); cf.transfers_frozen = freeze; - configtable.set (cf, st.issuer ); + auto fee_sym_code_raw = fee_ext_sym.get_symbol().code().raw(); + name ram_payer = fee_sym_code_raw ? get_self() : st.issuer; + configtable.set (cf, ram_payer ); } void rainbows::reset( const bool all, const uint32_t limit ) @@ -554,7 +634,7 @@ void rainbows::reset_one( const symbol_code symbolcode, const bool all, const ui if( ++counter > limit ) { goto CountedOut; } } { - stakes tbl(get_self(),scope); + backs tbl(get_self(),scope); auto itr = tbl.begin(); while (itr != tbl.end()) { itr = tbl.erase(itr); diff --git a/test/rainbows.test.js b/test/rainbows.test.js index fbc06139..0d1fe8bb 100644 --- a/test/rainbows.test.js +++ b/test/rainbows.test.js @@ -28,6 +28,16 @@ const get_scope = async ( code ) => { return res } +const get_supply = async( code, symbol ) => { + const resp = await getTableRows({ + code: code, + scope: symbol, + table: 'stat', + json: true + }) + const res = await resp; + return res.rows[0].supply; +} describe('rainbows', async assert => { @@ -69,6 +79,7 @@ describe('rainbows', async assert => { console.log('add eosio.code permissions') await addActorPermission(issuer, 'active', rainbows, 'eosio.code') + await addActorPermission(rainbows, 'active', rainbows, 'eosio.code') await addActorPermission(toke_escrow, 'active', rainbows, 'eosio.code') console.log('reset') @@ -86,19 +97,21 @@ describe('rainbows', async assert => { expected: { rows: [], more: '' } }) - await setSeedsBalance(issuer, '10000000.0000 SEEDS') + await setSeedsBalance(issuer, '10001500.0000 SEEDS') await setSeedsBalance(seconduser, '10000000.0000 SEEDS') await setSeedsBalance(thirduser, '5000000.0000 SEEDS') await setSeedsBalance(fourthuser, '10000000.0000 SEEDS') - const issuerInitialBalance = await getBalance(issuer) console.log('create token') + await contracts.token.transfer(issuer, rainbows, '1500.0000 SEEDS', 'fee', + { authorization: `${issuer}@active` }) + const issuerInitialBalance = await getBalance(issuer) await contracts.rainbows.create(issuer, '1000000.00 TOKES', issuer, withdraw_to, issuer, starttime.toISOString(), starttime.toISOString(), '', '', '', '', { authorization: `${issuer}@active` } ) - console.log('set stake') - await contracts.rainbows.setstake('5.00 TOKES', '2.0000 SEEDS', 'token.seeds', toke_escrow, false, 100, '', + console.log('set backing') + await contracts.rainbows.setbacking('5.00 TOKES', '2.0000 SEEDS', 'token.seeds', toke_escrow, false, 100, '', { authorization: `${issuer}@active` } ) console.log('approve token') @@ -112,12 +125,13 @@ describe('rainbows', async assert => { should: 'see token created & issued', actual: await get_scope(rainbows), expected: { - rows: [ {"code":"rainbo.seeds","scope":".....ou5dhbp4","table":"configs","payer":"seedsuseraaa","count":1}, - {"code":"rainbo.seeds","scope":".....ou5dhbp4","table":"displays","payer":"seedsuseraaa","count":1}, - {"code":"rainbo.seeds","scope":".....ou5dhbp4","table":"stakes","payer":"seedsuseraaa","count":2}, - {"code":"rainbo.seeds","scope":".....ou5dhbp4","table":"stat","payer":"seedsuseraaa","count":1}, - {"code":"rainbo.seeds","scope":"rainbo.seeds","table":"symbols","payer":"seedsuseraaa","count":1}, - {"code":"rainbo.seeds","scope":"seedsuseraaa","table":"accounts","payer":"seedsuseraaa","count":1} ], + rows: [ {"code":"rainbo.seeds","scope":".....ou5dhbp4","table":"backings","payer":"rainbo.seeds","count":2}, +{"code":"rainbo.seeds","scope":".....ou5dhbp4","table":"configs","payer":"rainbo.seeds","count":1}, + {"code":"rainbo.seeds","scope":".....ou5dhbp4","table":"displays","payer":"rainbo.seeds","count":1}, + {"code":"rainbo.seeds","scope":".....ou5dhbp4","table":"stat","payer":"rainbo.seeds","count":1}, + {"code":"rainbo.seeds","scope":"rainbo.seeds","table":"symbols","payer":"rainbo.seeds","count":1}, + {"code":"rainbo.seeds","scope":"seedsuseraaa","table":"accounts","payer":"seedsuseraaa","count":1}, + {"code":"rainbo.seeds","scope":"seedsuseraaa","table":"feebalance","payer":"rainbo.seeds","count":1} ], more: '' } }) @@ -133,17 +147,18 @@ describe('rainbows', async assert => { assert({ given: 'transfer token to new user', - should: 'see tokens in users account', + should: 'see tokens in users account, correct supply', actual: [ await eos.getCurrencyBalance(token, toke_escrow, 'SEEDS'), await eos.getCurrencyBalance(rainbows, fourthuser, 'TOKES'), + await get_supply(rainbows, 'TOKES'), ], - expected: [ [ '10000200.0000 SEEDS' ], [ '20.00 TOKES' ] ] + expected: [ [ '10000200.0000 SEEDS' ], [ '20.00 TOKES' ], '500.00 TOKES' ] }) console.log('redeem & return') - await contracts.rainbows.retire(fourthuser, '20.00 TOKES', 'redeemed by user', { authorization: `${fourthuser}@active` }) - await contracts.rainbows.retire(withdraw_to, '80.00 TOKES', 'redeemed by user', { authorization: `${withdraw_to}@active` }) - await contracts.rainbows.retire(issuer, '400.00 TOKES', 'redeemed by issuer', { authorization: `${issuer}@active` }) + await contracts.rainbows.retire(fourthuser, '20.00 TOKES', true, 'redeemed by user', { authorization: `${fourthuser}@active` }) + await contracts.rainbows.retire(withdraw_to, '80.00 TOKES', true, 'redeemed by user', { authorization: `${withdraw_to}@active` }) + await contracts.rainbows.retire(issuer, '400.00 TOKES', true, 'redeemed by issuer', { authorization: `${issuer}@active` }) await contracts.token.transfer(fourthuser, issuer, '8.0000 SEEDS', 'restore SEEDS balance', { authorization: `${fourthuser}@active` }) await contracts.token.transfer(withdraw_to, issuer, '32.0000 SEEDS', 'restore SEEDS balance', @@ -156,7 +171,23 @@ describe('rainbows', async assert => { expected: issuerInitialBalance }) + console.log('delete backing') + await contracts.rainbows.deletebacking(0, 'TOKES', '', { authorization: `${issuer}@active` }) + assert({ + given: 'delete backing', + should: 'see backing entry gone', + actual: (await getTableRows({ + code: rainbows, + scope: 'TOKES', + table: 'backings', + json: true + }))['rows'], + expected: [] + }) + + console.log('create credit limit token') + await contracts.rainbows.create(issuer, '1000000.00 CREDS', issuer, issuer, issuer, starttime.toISOString(), starttime.toISOString(), '', '', '', '', { authorization: `${issuer}@active` } ) @@ -164,20 +195,58 @@ describe('rainbows', async assert => { await contracts.rainbows.issue('1000000.00 CREDS', '', { authorization: `${issuer}@active` }) await contracts.rainbows.freeze('CREDS', true, '', { authorization: `${issuer}@active` }) await contracts.rainbows.open(fourthuser, 'CREDS', issuer, { authorization: `${issuer}@active` }) - await contracts.rainbows.transfer(issuer, fourthuser, '100.00 CREDS', '', { authorization: `${issuer}@active` }) + await contracts.rainbows.transfer(issuer, fourthuser, '50.00 CREDS', '', { authorization: `${issuer}@active` }) + await contracts.rainbows.transfer(issuer, fifthuser, '100.00 CREDS', '', { authorization: `${issuer}@active` }) console.log('reconfigure token') - await contracts.rainbows.create(issuer, '1000000.00 TOKES', issuer, withdraw_to, issuer, + await contracts.rainbows.create(issuer, '100.00 TOKES', issuer, withdraw_to, issuer, starttime.toISOString(), starttime.toISOString(), '', '', 'CREDS', '', { authorization: `${issuer}@active` } ) - console.log('make transfer against credit limit') + console.log('make transfers against credit limit') await contracts.rainbows.transfer(fourthuser, issuer, '50.00 TOKES', '', { authorization: `${fourthuser}@active` }) + await contracts.rainbows.transfer(fifthuser, issuer, '50.00 TOKES', '', { authorization: `${fifthuser}@active` }) assert({ given: 'transfer tokens', should: 'see negative currency balance', - actual: await eos.getCurrencyBalance(rainbows, fourthuser, 'TOKES'), - expected: [ '-50.00 TOKES' ] + actual: [ await eos.getCurrencyBalance(rainbows, fourthuser, 'TOKES'), + await get_supply(rainbows, 'TOKES') ], + expected: [ [ '-50.00 TOKES' ], '100.00 TOKES' ] + }) + + console.log('return some TOKES') + await contracts.rainbows.transfer(fifthuser, fourthuser, '20.00 TOKES', '', { authorization: `${fifthuser}@active` }) + await contracts.rainbows.transfer(issuer, fourthuser, '20.00 TOKES', '', { authorization: `${issuer}@active` }) + assert({ + given: 'transfer tokens back', + should: 'see expected currency balance and supply', + actual: [ await eos.getCurrencyBalance(rainbows, fourthuser, 'TOKES'), + await eos.getCurrencyBalance(rainbows, fifthuser, 'TOKES'), + await get_supply(rainbows, 'TOKES') ], + expected: [ [ '-10.00 TOKES' ], [ '-70.00 TOKES' ], '80.00 TOKES' ] + }) + + console.log('overdraw credits') + let actionProperlyBlocked = true + try { + await contracts.rainbows.transfer(fourthuser, fifthuser, '41.00 TOKES', '', { authorization: `${fourthuser}@active` }) + actionProperlyBlocked = false + } catch (err) { + actionProperlyBlocked &&= err.toString().includes('overdrawn balance') + console.log( (actionProperlyBlocked ? "" : "un") + "expected error "+err) + } + try { + await contracts.rainbows.transfer(fourthuser, issuer, '21.00 TOKES', '', { authorization: `${fourthuser}@active` }) + actionProperlyBlocked = false + } catch (err) { + actionProperlyBlocked &&= err.toString().includes('new credit exceeds available supply') + console.log( (actionProperlyBlocked ? "" : "un") + "expected error "+err) + } + assert({ + given: 'trying use too much credit', + should: 'fail', + actual: actionProperlyBlocked, + expected: true }) console.log('reset CREDS') @@ -185,9 +254,13 @@ describe('rainbows', async assert => { if( bal[0] != '0.00 CREDS' ) { await contracts.rainbows.transfer(fourthuser, issuer, bal[0], 'withdraw CREDS', { authorization: `${issuer}@active` } ) } + bal = await eos.getCurrencyBalance(rainbows, fifthuser, 'CREDS') + if( bal[0] != '0.00 CREDS' ) { + await contracts.rainbows.transfer(fifthuser, issuer, bal[0], 'withdraw CREDS', { authorization: `${issuer}@active` } ) + } - console.log('create proportional staked token') + console.log('create proportional backed token') await contracts.rainbows.create(issuer, '1000000.0000 PROPS', issuer, withdraw_to, issuer, starttime.toISOString(), starttime.toISOString(), '', '', '', '', @@ -199,8 +272,8 @@ describe('rainbows', async assert => { await setSeedsBalance(fifthuser, '0.0000 SEEDS') - console.log('set stake') - await contracts.rainbows.setstake('1.0000 PROPS', '2.0000 SEEDS', 'token.seeds', fifthuser, true, 100, '', + console.log('set backing') + await contracts.rainbows.setbacking('1.0000 PROPS', '2.0000 SEEDS', 'token.seeds', fifthuser, true, 100, '', { authorization: `${issuer}@active` } ) await addActorPermission(fifthuser, 'active', rainbows, 'eosio.code') @@ -215,21 +288,22 @@ describe('rainbows', async assert => { should: 'see token created & issued', actual: await get_scope(rainbows), expected: { - rows: [ { code: 'rainbo.seeds', scope: '.....ou4cpd43', table: 'configs', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: '.....ou4cpd43', table: 'displays', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: '.....ou4cpd43', table: 'stat', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: '.....ou5dhbp4', table: 'configs', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: '.....ou5dhbp4', table: 'displays', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: '.....ou5dhbp4', table: 'stakes', payer: 'seedsuseraaa', count: 2 }, - { code: 'rainbo.seeds', scope: '.....ou5dhbp4', table: 'stat', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: '.....oukdxd5', table: 'configs', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: '.....oukdxd5', table: 'displays', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: '.....oukdxd5', table: 'stakes', payer: 'seedsuseraaa', count: 2 }, - { code: 'rainbo.seeds', scope: '.....oukdxd5', table: 'stat', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: 'rainbo.seeds', table: 'symbols', payer: 'seedsuseraaa', count: 3 }, + rows: [ { code: 'rainbo.seeds', scope: '.....ou4cpd43', table: 'configs', payer: 'rainbo.seeds', count: 1 }, + { code: 'rainbo.seeds', scope: '.....ou4cpd43', table: 'displays', payer: 'rainbo.seeds', count: 1 }, + { code: 'rainbo.seeds', scope: '.....ou4cpd43', table: 'stat', payer: 'rainbo.seeds', count: 1 }, + { code: 'rainbo.seeds', scope: '.....ou5dhbp4', table: 'configs', payer: 'rainbo.seeds', count: 1 }, + { code: 'rainbo.seeds', scope: '.....ou5dhbp4', table: 'displays', payer: 'rainbo.seeds', count: 1 }, + { code: 'rainbo.seeds', scope: '.....ou5dhbp4', table: 'stat', payer: 'rainbo.seeds', count: 1 }, + { code: 'rainbo.seeds', scope: '.....oukdxd5', table: 'backings', payer: 'rainbo.seeds', count: 2 }, + { code: 'rainbo.seeds', scope: '.....oukdxd5', table: 'configs', payer: 'rainbo.seeds', count: 1 }, + { code: 'rainbo.seeds', scope: '.....oukdxd5', table: 'displays', payer: 'rainbo.seeds', count: 1 }, + { code: 'rainbo.seeds', scope: '.....oukdxd5', table: 'stat', payer: 'rainbo.seeds', count: 1 }, + { code: 'rainbo.seeds', scope: 'rainbo.seeds', table: 'symbols', payer: 'rainbo.seeds', count: 3 }, { code: 'rainbo.seeds', scope: 'seedsuseraaa', table: 'accounts', payer: 'seedsuseraaa', count: 3 }, + { code: 'rainbo.seeds', scope: 'seedsuseraaa', table: 'feebalance', payer: 'rainbo.seeds', count: 1 }, { code: 'rainbo.seeds', scope: 'seedsuserccc', table: 'accounts', payer: 'seedsuseraaa', count: 1 }, - { code: 'rainbo.seeds', scope: 'seedsuserxxx', table: 'accounts', payer: 'seedsuseraaa', count: 2 } + { code: 'rainbo.seeds', scope: 'seedsuserxxx', table: 'accounts', payer: 'seedsuseraaa', count: 2 }, + { code: 'rainbo.seeds', scope: 'seedsuseryyy', table: 'accounts', payer: 'seedsuseraaa', count: 2 } ], more: '' } }) @@ -238,7 +312,7 @@ describe('rainbows', async assert => { await contracts.rainbows.transfer(issuer, fourthuser, '100.0000 PROPS', 'test nonmember', { authorization: `${issuer}@active` }) console.log('redeem some') - await contracts.rainbows.retire(fourthuser, '20.0000 PROPS', 'redeemed by user', { authorization: `${fourthuser}@active` }) + await contracts.rainbows.retire(fourthuser, '20.0000 PROPS', true, 'redeemed by user', { authorization: `${fourthuser}@active` }) assert({ @@ -253,7 +327,7 @@ describe('rainbows', async assert => { await contracts.token.transfer(issuer, fifthuser, '480.0000 SEEDS', '+50% escrow', { authorization: `${issuer}@active` }) console.log('redeem some more') - await contracts.rainbows.retire(fourthuser, '20.0000 PROPS', 'redeemed by user', { authorization: `${fourthuser}@active` }) + await contracts.rainbows.retire(fourthuser, '20.0000 PROPS', true, 'redeemed by user', { authorization: `${fourthuser}@active` }) assert({ given: 'proportional reedeem', @@ -264,8 +338,8 @@ describe('rainbows', async assert => { }) console.log('redeem & return') - await contracts.rainbows.retire(fourthuser, '60.0000 PROPS', 'redeemed by user', { authorization: `${fourthuser}@active` }) - await contracts.rainbows.retire(issuer, '400.0000 PROPS', 'redeemed by issuer', { authorization: `${issuer}@active` }) + await contracts.rainbows.retire(fourthuser, '60.0000 PROPS', true, 'redeemed by user', { authorization: `${fourthuser}@active` }) + await contracts.rainbows.retire(issuer, '400.0000 PROPS', true, 'redeemed by issuer', { authorization: `${issuer}@active` }) await contracts.token.transfer(fourthuser, issuer, '280.0000 SEEDS', 'restore SEEDS balance', { authorization: `${fourthuser}@active` }) @@ -287,9 +361,12 @@ describe('rainbows', async assert => { assert({ given: 'reset all', - should: 'clear table RAM', + should: 'clear table RAM (except feebalance)', actual: await get_scope(rainbows), - expected: { rows: [], more: '' } + expected: { + rows: [ { code: 'rainbo.seeds', scope: 'seedsuseraaa', table: 'feebalance', payer: 'rainbo.seeds', count: 1 }], + more: '' + } }) console.log('create token') @@ -298,7 +375,7 @@ describe('rainbows', async assert => { { authorization: `${issuer}@active` } ) console.log('issue tokens without approval') - let actionProperlyBlocked = true + actionProperlyBlocked = true try { await contracts.rainbows.issue('500.00 TOKES', '', { authorization: `${issuer}@active` }) actionProperlyBlocked = false @@ -397,7 +474,7 @@ describe('rainbows', async assert => { expected: [ '60.00 TOKES' ] }) - console.log('create fractional staked token') + console.log('create fractional backed token') await contracts.rainbows.create(issuer, '1000000.0000 FRACS', issuer, withdraw_to, issuer, starttime.toISOString(), starttime.toISOString(), '', '', '', '', @@ -409,8 +486,8 @@ describe('rainbows', async assert => { await setSeedsBalance(fifthuser, '0.0000 SEEDS') - console.log('set stake') - await contracts.rainbows.setstake('1.0000 FRACS', '2.0000 SEEDS', 'token.seeds', fifthuser, false, 30, '', + console.log('set backing') + await contracts.rainbows.setbacking('1.0000 FRACS', '2.0000 SEEDS', 'token.seeds', fifthuser, false, 30, '', { authorization: `${issuer}@active` } ) await addActorPermission(fifthuser, 'active', rainbows, 'eosio.code') @@ -425,7 +502,7 @@ describe('rainbows', async assert => { await contracts.token.transfer(fifthuser, issuer, '500.0000 SEEDS', '', { authorization: `${fifthuser}@active` }) console.log('redeem some') - await contracts.rainbows.retire(fourthuser, '20.0000 FRACS', 'redeemed by user', { authorization: `${fourthuser}@active` }) + await contracts.rainbows.retire(fourthuser, '20.0000 FRACS', true, 'redeemed by user', { authorization: `${fourthuser}@active` }) assert({ @@ -441,10 +518,10 @@ describe('rainbows', async assert => { actionProperlyBlocked = true try { await contracts.token.transfer(fifthuser, issuer, '145.0000 SEEDS', '', { authorization: `${fifthuser}@active` }) - await contracts.rainbows.retire(fourthuser, '20.0000 FRACS', 'redeemed by user', { authorization: `${fourthuser}@active` }) + await contracts.rainbows.retire(fourthuser, '20.0000 FRACS', true, 'redeemed by user', { authorization: `${fourthuser}@active` }) actionProperlyBlocked = false } catch (err) { - actionProperlyBlocked &&= err.toString().includes('can\'t unstake, escrow underfunded in SEEDS') + actionProperlyBlocked &&= err.toString().includes('can\'t redeem, escrow underfunded in SEEDS') console.log( (actionProperlyBlocked ? "" : "un") + "expected error "+err) } assert({ @@ -454,7 +531,7 @@ describe('rainbows', async assert => { expected: true }) - console.log('---begin dSeeds stake tests---') + console.log('---begin dSeeds redemption tests---') const dseed_escrow = fifthuser @@ -499,12 +576,15 @@ describe('rainbows', async assert => { assert({ given: 'reset all', - should: 'clear table RAM', + should: 'clear table RAM (except feebalance)', actual: await get_scope(rainbows), - expected: { rows: [], more: '' } + expected: { + rows: [ { code: 'rainbo.seeds', scope: 'seedsuseraaa', table: 'feebalance', payer: 'rainbo.seeds', count: 1 }], + more: '' + } }) - console.log('create dSeed-staked token ARCOS') + console.log('create dSeed-backed token ARCOS') await contracts.rainbows.create(issuer, '1000000.0000 ARCOS', issuer, withdraw_to, issuer, starttime.toISOString(), starttime.toISOString(), '', '', '', '', @@ -556,13 +636,13 @@ describe('rainbows', async assert => { console.log('transfer HPOOL to issuer') await contracts.pool.transfer(thirduser, issuer, '500.0000 HPOOL', '', { authorization: `${thirduser}@active` }) - console.log('set placeholder Seeds stake') - await contracts.rainbows.setstake('1.0000 ARCOS', '0.0000 SEEDS', 'token.seeds', dseed_escrow, true, 100, '', + console.log('set placeholder Seeds backing') + await contracts.rainbows.setbacking('1.0000 ARCOS', '0.0000 SEEDS', 'token.seeds', dseed_escrow, true, 100, '', { authorization: `${issuer}@active` } ) await addActorPermission(dseed_escrow, 'active', rainbows, 'eosio.code') - console.log('set dSeeds stake') - await contracts.rainbows.setstake('1.0000 ARCOS', '1.0000 HPOOL', 'pool.seeds', dseed_escrow, true, 100, '', + console.log('set dSeeds backing') + await contracts.rainbows.setbacking('1.0000 ARCOS', '1.0000 HPOOL', 'pool.seeds', dseed_escrow, true, 100, '', { authorization: `${issuer}@active` } ) console.log('approve token') @@ -605,7 +685,7 @@ describe('rainbows', async assert => { }) console.log('user redeem some') - await contracts.rainbows.retire(fourthuser, '150.0000 ARCOS', 'redeemed by user', { authorization: `${fourthuser}@active` }) + await contracts.rainbows.retire(fourthuser, '150.0000 ARCOS', true, 'redeemed by user', { authorization: `${fourthuser}@active` }) assert({ given: 'user redeem', @@ -635,8 +715,8 @@ describe('rainbows', async assert => { }) console.log('redeem all') - await contracts.rainbows.retire(issuer, '200.0000 ARCOS', 'redeemed by issuer', { authorization: `${issuer}@active` }) - await contracts.rainbows.retire(fourthuser, '150.0000 ARCOS', 'redeemed by user', { authorization: `${fourthuser}@active` }) + await contracts.rainbows.retire(issuer, '200.0000 ARCOS', true, 'redeemed by issuer', { authorization: `${issuer}@active` }) + await contracts.rainbows.retire(fourthuser, '150.0000 ARCOS', true, 'redeemed by user', { authorization: `${fourthuser}@active` }) assert({ given: 'all the payouts completed and redeemed', @@ -649,7 +729,20 @@ describe('rainbows', async assert => { [ [ '0.0000 ARCOS' ], [], [], [ '0.0000 ARCOS' ], [] ] ] }) + assert({ + given: 'check remaining fee balance', + should: 'see correct value', + actual: (await getTableRows({ + code: rainbows, + scope: issuer, + table: 'feebalance', + json: true + }))['rows'], + expected: [ { balance: '500.0000 SEEDS' } ] + }) + console.log('return fee balance') + await contracts.rainbows.returnfee(issuer, { authorization: `${issuer}@active` }) })