diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index adc5ba9f..25e9e469 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -93,6 +93,7 @@ setacctram setalimits setcode setinflation +setpayfactor setparams setpriv setram diff --git a/contracts/CMakeLists.txt b/contracts/CMakeLists.txt index d48cae0e..f96b0e45 100644 --- a/contracts/CMakeLists.txt +++ b/contracts/CMakeLists.txt @@ -51,5 +51,7 @@ add_subdirectory(eosio.msig) add_subdirectory(eosio.system) add_subdirectory(eosio.token) add_subdirectory(eosio.wrap) +add_subdirectory(eosio.fees) +add_subdirectory(eosio.bpay) add_subdirectory(test_contracts) diff --git a/contracts/eosio.bpay/CMakeLists.txt b/contracts/eosio.bpay/CMakeLists.txt new file mode 100644 index 00000000..09fe613e --- /dev/null +++ b/contracts/eosio.bpay/CMakeLists.txt @@ -0,0 +1,14 @@ +add_contract(eosio.bpay eosio.bpay ${CMAKE_CURRENT_SOURCE_DIR}/src/eosio.bpay.cpp) + +target_include_directories(eosio.bpay PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/../eosio.system/include + ${CMAKE_CURRENT_SOURCE_DIR}/../eosio.token/include) + +set_target_properties(eosio.bpay + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + +configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/ricardian/eosio.bpay.contracts.md.in ${CMAKE_CURRENT_BINARY_DIR}/ricardian/eosio.bpay.contracts.md @ONLY ) + +target_compile_options( eosio.bpay PUBLIC -R${CMAKE_CURRENT_SOURCE_DIR}/ricardian -R${CMAKE_CURRENT_BINARY_DIR}/ricardian ) diff --git a/contracts/eosio.bpay/include/eosio.bpay/eosio.bpay.hpp b/contracts/eosio.bpay/include/eosio.bpay/eosio.bpay.hpp new file mode 100644 index 00000000..042ebd3b --- /dev/null +++ b/contracts/eosio.bpay/include/eosio.bpay/eosio.bpay.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include + +using namespace std; + +namespace eosio { + /** + * The `eosio.bpay` contract handles system bpay distribution. + */ + class [[eosio::contract("eosio.bpay")]] bpay : public contract { + public: + using contract::contract; + + /** + * ## TABLE `rewards` + * + * @param owner - block producer owner account + * @param quantity - reward quantity in EOS + * + * ### example + * + * ```json + * [ + * { + * "owner": "alice", + * "quantity": "8.800 EOS" + * } + * ] + * ``` + */ + struct [[eosio::table("rewards")]] rewards_row { + name owner; + asset quantity; + + uint64_t primary_key() const { return owner.value; } + }; + typedef eosio::multi_index< "rewards"_n, rewards_row > rewards_table; + + /** + * Claim rewards for a block producer. + * + * @param owner - block producer owner account + */ + [[eosio::action]] + void claimrewards( const name owner); + + [[eosio::on_notify("eosio.token::transfer")]] + void on_transfer( const name from, const name to, const asset quantity, const string memo ); + + private: + }; +} /// namespace eosio diff --git a/contracts/eosio.bpay/ricardian/eosio.bpay.contracts.md.in b/contracts/eosio.bpay/ricardian/eosio.bpay.contracts.md.in new file mode 100644 index 00000000..85da666a --- /dev/null +++ b/contracts/eosio.bpay/ricardian/eosio.bpay.contracts.md.in @@ -0,0 +1,10 @@ +

claimrewards

+ +--- +spec_version: "0.2.0" +title: Claim Rewards +summary: '{{nowrap owner}} claims block production rewards' +icon: @ICON_BASE_URL@/@MULTISIG_ICON_URI@ +--- + +{{owner}} claims block production rewards accumulated through network fees. diff --git a/contracts/eosio.bpay/src/eosio.bpay.cpp b/contracts/eosio.bpay/src/eosio.bpay.cpp new file mode 100644 index 00000000..42999b01 --- /dev/null +++ b/contracts/eosio.bpay/src/eosio.bpay.cpp @@ -0,0 +1,73 @@ +#include + +namespace eosio { + +void bpay::claimrewards( const name owner ) { + require_auth( owner ); + + rewards_table _rewards( get_self(), get_self().value ); + + const auto& row = _rewards.get( owner.value, "no rewards to claim" ); + + eosio::token::transfer_action transfer( "eosio.token"_n, { get_self(), "active"_n }); + transfer.send( get_self(), owner, row.quantity, "producer block pay" ); + + _rewards.erase(row); +} + +void bpay::on_transfer( const name from, const name to, const asset quantity, const string memo ) { + if (from == get_self() || to != get_self()) { + return; + } + + // ignore eosio system incoming transfers (caused by bpay income transfers eosio => eosio.bpay => producer) + if ( from == "eosio"_n) return; + + symbol system_symbol = eosiosystem::system_contract::get_core_symbol(); + + check( quantity.symbol == system_symbol, "only core token allowed" ); + + rewards_table _rewards( get_self(), get_self().value ); + eosiosystem::producers_table _producers( "eosio"_n, "eosio"_n.value ); + + eosiosystem::global_state_singleton _global("eosio"_n, "eosio"_n.value); + check( _global.exists(), "global state does not exist"); + uint16_t producer_count = _global.get().last_producer_schedule_size; + + // get producer with the most votes + // using `by_votes` secondary index + auto idx = _producers.get_index<"prototalvote"_n>(); + auto prod = idx.begin(); + + // get top n producers by vote, excluding inactive + std::vector top_producers; + while (true) { + if (prod == idx.end()) break; + if (prod->is_active == false) continue; + + top_producers.push_back(prod->owner); + + if (top_producers.size() == producer_count) break; + + prod++; + } + + asset reward = quantity / top_producers.size(); + + // distribute rewards to top producers + for (auto producer : top_producers) { + auto row = _rewards.find( producer.value ); + if (row == _rewards.end()) { + _rewards.emplace( get_self(), [&](auto& row) { + row.owner = producer; + row.quantity = reward; + }); + } else { + _rewards.modify(row, get_self(), [&](auto& row) { + row.quantity += reward; + }); + } + } +} + +} /// namespace eosio diff --git a/contracts/eosio.fees/CMakeLists.txt b/contracts/eosio.fees/CMakeLists.txt new file mode 100644 index 00000000..268f506c --- /dev/null +++ b/contracts/eosio.fees/CMakeLists.txt @@ -0,0 +1,11 @@ +add_contract(eosio.fees eosio.fees ${CMAKE_CURRENT_SOURCE_DIR}/src/eosio.fees.cpp) + +target_include_directories(eosio.fees PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/../eosio.system/include) + +set_target_properties(eosio.fees + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + +target_compile_options( eosio.fees PUBLIC ) diff --git a/contracts/eosio.fees/include/eosio.fees/eosio.fees.hpp b/contracts/eosio.fees/include/eosio.fees/eosio.fees.hpp new file mode 100644 index 00000000..8580e848 --- /dev/null +++ b/contracts/eosio.fees/include/eosio.fees/eosio.fees.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include + +namespace eosiosystem { + class system_contract; +} + +namespace eosio { + + using std::string; + /** + * The eosio.fees smart contract facilitates the collection of transaction fees from system accounts and their subsequent distribution to the Resource Exchange (REX) pool. + * + * This contract serves as an essential component for inclusion in system-level unit tests. + * + * A comprehensive implementation of the eosio.fees contract can be accessed at EOS Network Foundation GitHub repository. + * https://github.com/eosnetworkfoundation/eosio.fees + */ + class [[eosio::contract("eosio.fees")]] fees : public contract { + public: + using contract::contract; + + [[eosio::on_notify("eosio.token::transfer")]] + void on_transfer( const name from, const name to, const asset quantity, const string memo ); + + [[eosio::action]] + void noop(); + }; + +} diff --git a/contracts/eosio.fees/src/eosio.fees.cpp b/contracts/eosio.fees/src/eosio.fees.cpp new file mode 100644 index 00000000..a36adc46 --- /dev/null +++ b/contracts/eosio.fees/src/eosio.fees.cpp @@ -0,0 +1,21 @@ +#include + +namespace eosio { + +void fees::on_transfer( const name from, const name to, const asset quantity, const string memo ) +{ + if ( to != get_self() ) { + return; + } + if (eosiosystem::system_contract::rex_available()) { + eosiosystem::system_contract::donatetorex_action donatetorex( "eosio"_n, { get_self(), "active"_n }); + donatetorex.send(get_self(), quantity, memo); + } +} + +void fees::noop() +{ + require_auth( get_self() ); +} + +} /// namespace eosio diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index d9e2b07e..ac57ddc2 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -97,9 +97,9 @@ namespace eosiosystem { /** * The `eosio.system` smart contract is provided by `block.one` as a sample system contract, and it defines the structures and actions needed for blockchain's core functionality. - * + * * Just like in the `eosio.bios` sample contract implementation, there are a few actions which are not implemented at the contract level (`newaccount`, `updateauth`, `deleteauth`, `linkauth`, `unlinkauth`, `canceldelay`, `onerror`, `setabi`, `setcode`), they are just declared in the contract so they will show in the contract's ABI and users will be able to push those actions to the chain via the account holding the `eosio.system` contract, but the implementation is at the EOSIO core level. They are referred to as EOSIO native actions. - * + * * - Users can stake tokens for CPU and Network bandwidth, and then vote for producers or * delegate their vote to a proxy. * - Producers register in order to be voted for, and can claim per-block and per-vote rewards. @@ -108,7 +108,7 @@ namespace eosiosystem { * - A resource exchange system (REX) allows token holders to lend their tokens, * and users to rent CPU and Network resources in return for a market-determined fee. */ - + // A name bid, which consists of: // - a `newname` name that the bid is for // - a `high_bidder` account name that is the one with the highest bid so far @@ -199,7 +199,16 @@ namespace eosiosystem { EOSLIB_SERIALIZE( eosio_global_state4, (continuous_rate)(inflation_pay_factor)(votepay_factor) ) }; + // Defines the schedule for pre-determined annual rate changes. + struct [[eosio::table, eosio::contract("eosio.system")]] schedules_info { + time_point_sec start_time; + double continuous_rate; + + uint64_t primary_key() const { return start_time.sec_since_epoch(); } + // explicit serialization macro is not necessary, used here only to improve compilation time + EOSLIB_SERIALIZE( schedules_info, (start_time)(continuous_rate) ) + }; inline eosio::block_signing_authority convert_to_block_signing_authority( const eosio::public_key& producer_key ) { return eosio::block_signing_authority_v0{ .threshold = 1, .keys = {{producer_key, 1}} }; @@ -411,6 +420,8 @@ namespace eosiosystem { typedef eosio::multi_index< "producers2"_n, producer_info2 > producers_table2; + typedef eosio::multi_index< "schedules"_n, schedules_info > schedules_table; + typedef eosio::singleton< "global"_n, eosio_global_state > global_state_singleton; typedef eosio::singleton< "global2"_n, eosio_global_state2 > global_state2_singleton; @@ -620,6 +631,14 @@ namespace eosiosystem { typedef eosio::multi_index< "rexqueue"_n, rex_order, indexed_by<"bytime"_n, const_mem_fun>> rex_order_table; + struct [[eosio::table("rexmaturity"),eosio::contract("eosio.system")]] rex_maturity { + uint32_t num_of_maturity_buckets = 5; + bool sell_matured_rex = false; + bool buy_rex_to_savings = false; + }; + + typedef eosio::singleton<"rexmaturity"_n, rex_maturity> rex_maturity_singleton; + struct rex_order_outcome { bool success; asset proceeds; @@ -631,6 +650,7 @@ namespace eosiosystem { asset quantity; int64_t bytes_sold; int64_t ram_bytes; + asset fee; }; struct action_return_buyram { @@ -639,6 +659,7 @@ namespace eosiosystem { asset quantity; int64_t bytes_purchased; int64_t ram_bytes; + asset fee; }; struct action_return_ramtransfer { @@ -799,6 +820,7 @@ namespace eosiosystem { eosio_global_state2 _gstate2; eosio_global_state3 _gstate3; eosio_global_state4 _gstate4; + schedules_table _schedules; rammarket _rammarket; rex_pool_table _rexpool; rex_return_pool_table _rexretpool; @@ -806,6 +828,7 @@ namespace eosiosystem { rex_fund_table _rexfunds; rex_balance_table _rexbalance; rex_order_table _rexorders; + rex_maturity_singleton _rexmaturity; public: static constexpr eosio::name active_permission{"active"_n}; @@ -818,6 +841,8 @@ namespace eosiosystem { static constexpr eosio::name names_account{"eosio.names"_n}; static constexpr eosio::name saving_account{"eosio.saving"_n}; static constexpr eosio::name rex_account{"eosio.rex"_n}; + static constexpr eosio::name fees_account{"eosio.fees"_n}; + static constexpr eosio::name powerup_account{"eosio.powup"_n}; static constexpr eosio::name reserve_account{"eosio.reserv"_n}; // cspell:disable-line static constexpr eosio::name null_account{"eosio.null"_n}; static constexpr symbol ramcore_symbol = symbol(symbol_code("RAMCORE"), 4); @@ -831,8 +856,21 @@ namespace eosiosystem { // @param system_account - the system account to get the core symbol for. static symbol get_core_symbol( name system_account = "eosio"_n ) { rammarket rm(system_account, system_account.value); - const static auto sym = get_core_symbol( rm ); - return sym; + auto itr = rm.find(ramcore_symbol.raw()); + check(itr != rm.end(), "system contract must first be initialized"); + return itr->quote.balance.symbol; + } + + // Returns true/false if the rex system is initialized + static bool rex_system_initialized( name system_account = "eosio"_n ) { + eosiosystem::rex_pool_table _rexpool( system_account, system_account.value ); + return _rexpool.begin() != _rexpool.end(); + } + + // Returns true/false if the rex system is available + static bool rex_available( name system_account = "eosio"_n ) { + eosiosystem::rex_pool_table _rexpool( system_account, system_account.value ); + return rex_system_initialized() && _rexpool.begin()->total_rex.amount > 0; } // Actions: @@ -909,6 +947,16 @@ namespace eosiosystem { [[eosio::action]] void activate( const eosio::checksum256& feature_digest ); + /** + * Logging for actions resulting in system fees. + * + * @param protocol - name of protocol fees were earned from. + * @param fee - the amount of fees collected by system. + * @param memo - (optional) the memo associated with the action. + */ + [[eosio::action]] + void logsystemfee( const name& protocol, const asset& fee, const std::string& memo ); + // functions defined in delegate_bandwidth.cpp /** @@ -968,12 +1016,9 @@ namespace eosiosystem { * @param from - owner account name, * @param amount - amount of tokens taken out of 'from' REX fund. * - * @pre A voting requirement must be satisfied before action can be executed. - * @pre User must vote for at least 21 producers or delegate vote to proxy before buying REX. - * * @post User votes are updated following this action. * @post Tokens used in purchase are added to user's voting power. - * @post Bought REX cannot be sold before 4 days counting from end of day of purchase. + * @post Bought REX cannot be sold before {num_of_maturity_buckets} days counting from end of day of purchase. */ [[eosio::action]] void buyrex( const name& from, const asset& amount ); @@ -987,12 +1032,9 @@ namespace eosiosystem { * @param from_net - amount of tokens to be unstaked from NET bandwidth and used for REX purchase, * @param from_cpu - amount of tokens to be unstaked from CPU bandwidth and used for REX purchase. * - * @pre A voting requirement must be satisfied before action can be executed. - * @pre User must vote for at least 21 producers or delegate vote to proxy before buying REX. - * * @post User votes are updated following this action. * @post Tokens used in purchase are added to user's voting power. - * @post Bought REX cannot be sold before 4 days counting from end of day of purchase. + * @post Bought REX cannot be sold before {num_of_maturity_buckets} days counting from end of day of purchase. */ [[eosio::action]] void unstaketorex( const name& owner, const name& receiver, const asset& from_net, const asset& from_cpu ); @@ -1121,7 +1163,7 @@ namespace eosiosystem { void rexexec( const name& user, uint16_t max ); /** - * Consolidate action, consolidates REX maturity buckets into one bucket that can be sold after 4 days + * Consolidate action, consolidates REX maturity buckets into one bucket that can be sold after {num_of_maturity_buckets} days * starting from the end of the day. * * @param owner - REX owner account name. @@ -1133,7 +1175,7 @@ namespace eosiosystem { * Mvtosavings action, moves a specified amount of REX into savings bucket. REX savings bucket * never matures. In order for it to be sold, it has to be moved explicitly * out of that bucket. Then the moved amount will have the regular maturity - * period of 4 days starting from the end of the day. + * period of {num_of_maturity_buckets} days starting from the end of the day. * * @param owner - REX owner account name. * @param rex - amount of REX to be moved. @@ -1143,7 +1185,7 @@ namespace eosiosystem { /** * Mvfrsavings action, moves a specified amount of REX out of savings bucket. The moved amount - * will have the regular REX maturity period of 4 days. + * will have the regular REX maturity period of {num_of_maturity_buckets} days. * * @param owner - REX owner account name. * @param rex - amount of REX to be moved. @@ -1165,6 +1207,29 @@ namespace eosiosystem { [[eosio::action]] void closerex( const name& owner ); + /** + * Facilitates the modification of REX maturity buckets + * + * @param num_of_maturity_buckets - used to calculate maturity time of purchase REX tokens from end of the day UTC. + * @param sell_matured_rex - if true, matured REX is sold immediately. + * https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 + * @param buy_rex_to_savings - if true, buying REX is moved immediately to REX savings. + * https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 + */ + [[eosio::action]] + void setrexmature(const std::optional num_of_maturity_buckets, const std::optional sell_matured_rex, const std::optional buy_rex_to_savings ); + + /** + * Donatetorex action, donates funds to REX, increases REX pool return buckets + * Executes inline transfer from payer to system contract of tokens will be executed. + * + * @param payer - the payer of donated funds. + * @param quantity - the quantity of tokens to donated to REX with. + * @param memo - the memo string to accompany the transaction. + */ + [[eosio::action]] + void donatetorex( const name& payer, const asset& quantity, const std::string& memo ); + /** * Undelegate bandwidth action, decreases the total tokens delegated by `from` to `receiver` and/or * frees the memory associated with the delegation if there is nothing @@ -1238,9 +1303,10 @@ namespace eosiosystem { * @param quantity - the quantity of tokens to buy ram with. * @param bytes - the quantity of ram to buy specified in bytes. * @param ram_bytes - the ram bytes held by receiver after the action. + * @param fee - the fee to be paid for the ram sold. */ [[eosio::action]] - void logbuyram( const name& payer, const name& receiver, const asset& quantity, int64_t bytes, int64_t ram_bytes ); + void logbuyram( const name& payer, const name& receiver, const asset& quantity, int64_t bytes, int64_t ram_bytes, const asset& fee ); /** * Sell ram action, reduces quota by bytes and then performs an inline transfer of tokens @@ -1259,9 +1325,10 @@ namespace eosiosystem { * @param quantity - the quantity of tokens to sell ram with. * @param bytes - the quantity of ram to sell specified in bytes. * @param ram_bytes - the ram bytes held by account after the action. + * @param fee - the fee to be paid for the ram sold. */ [[eosio::action]] - void logsellram( const name& account, const asset& quantity, int64_t bytes, int64_t ram_bytes ); + void logsellram( const name& account, const asset& quantity, int64_t bytes, int64_t ram_bytes, const asset& fee ); /** * Transfer ram action, reduces sender's quota by bytes and increase receiver's quota by bytes. @@ -1284,6 +1351,17 @@ namespace eosiosystem { [[eosio::action]] action_return_ramtransfer ramburn( const name& owner, int64_t bytes, const std::string& memo ); + /** + * Buy RAM and immediately burn RAM. + * An inline transfer from payer to system contract of tokens will be executed. + * + * @param payer - the payer of buy RAM & burn. + * @param quantity - the quantity of tokens to buy RAM & burn with. + * @param memo - the memo string to accompany the transaction. + */ + [[eosio::action]] + action_return_buyram buyramburn( const name& payer, const asset& quantity, const std::string& memo ); + /** * Logging for ram changes * @@ -1349,7 +1427,7 @@ namespace eosiosystem { /** * Action to permanently transition to Savanna consensus. * Create the first generation of finalizer policy and activate - * the policy by using `set_finalizers` host function + * the policy by using `set_finalizers` host function * * @pre Require the authority of the contract itself * @pre A sufficient numner of the top 21 block producers have registered a finalizer key @@ -1453,10 +1531,10 @@ namespace eosiosystem { /** * Update the vote weight for the producers or proxy `voter_name` currently votes for. This will also - * update the `staked` value for the `voter_name` by checking `rexbal` and all delegated NET and CPU. - * + * update the `staked` value for the `voter_name` by checking `rexbal` and all delegated NET and CPU. + * * @param voter_name - the account to update the votes for, - * + * * @post the voter.staked will be updated * @post previously voted for producers vote weight will be updated with new weight * @post previously voted for proxy vote weight will be updated with new weight @@ -1577,6 +1655,60 @@ namespace eosiosystem { [[eosio::action]] void setinflation( int64_t annual_rate, int64_t inflation_pay_factor, int64_t votepay_factor ); + /** + * Change how inflated or vested tokens will be distributed based on the following structure. + * + * @param inflation_pay_factor - Inverse of the fraction of the inflation used to reward block producers. + * The remaining inflation will be sent to the `eosio.saving` account. + * (eg. For 20% of inflation going to block producer rewards => inflation_pay_factor = 50000 + * For 100% of inflation going to block producer rewards => inflation_pay_factor = 10000). + * @param votepay_factor - Inverse of the fraction of the block producer rewards to be distributed proportional to blocks produced. + * The remaining rewards will be distributed proportional to votes received. + * (eg. For 25% of block producer rewards going towards block pay => votepay_factor = 40000 + * For 75% of block producer rewards going towards block pay => votepay_factor = 13333). + */ + [[eosio::action]] + void setpayfactor( int64_t inflation_pay_factor, int64_t votepay_factor ); + + /** + * Set the schedule for pre-determined annual rate changes. + * + * @param start_time - the time to start the schedule. + * @param continuous_rate - the inflation or distribution rate of the core token supply. + * (eg. For 5% => 0.05 + * For 1.5% => 0.015) + */ + [[eosio::action]] + void setschedule( const time_point_sec start_time, double continuous_rate ); + + /** + * Delete the schedule for pre-determined annual rate changes. + * + * @param start_time - the time to start the schedule. + */ + [[eosio::action]] + void delschedule( const time_point_sec start_time ); + + /** + * Executes the next schedule for pre-determined annual rate changes. + * + * Start time of the schedule must be in the past. + * + * Can be executed by any account. + */ + [[eosio::action]] + void execschedule(); + + /** + * Facilitates the removal of vested staked tokens from an account, ensuring that these tokens are reallocated to the system's pool. + * + * @param account - the target account from which tokens are to be unvested. + * @param unvest_net_quantity - the amount of NET tokens to unvest. + * @param unvest_cpu_quantity - the amount of CPU tokens to unvest. + */ + [[eosio::action]] + void unvest(const name account, const asset unvest_net_quantity, const asset unvest_cpu_quantity); + /** * Configure the `power` market. The market becomes available the first time this * action is invoked. @@ -1630,6 +1762,7 @@ namespace eosiosystem { using setacctnet_action = eosio::action_wrapper<"setacctnet"_n, &system_contract::setacctnet>; using setacctcpu_action = eosio::action_wrapper<"setacctcpu"_n, &system_contract::setacctcpu>; using activate_action = eosio::action_wrapper<"activate"_n, &system_contract::activate>; + using logsystemfee_action = eosio::action_wrapper<"logsystemfee"_n, &system_contract::logsystemfee>; using delegatebw_action = eosio::action_wrapper<"delegatebw"_n, &system_contract::delegatebw>; using deposit_action = eosio::action_wrapper<"deposit"_n, &system_contract::deposit>; using withdraw_action = eosio::action_wrapper<"withdraw"_n, &system_contract::withdraw>; @@ -1650,6 +1783,7 @@ namespace eosiosystem { using mvfrsavings_action = eosio::action_wrapper<"mvfrsavings"_n, &system_contract::mvfrsavings>; using consolidate_action = eosio::action_wrapper<"consolidate"_n, &system_contract::consolidate>; using closerex_action = eosio::action_wrapper<"closerex"_n, &system_contract::closerex>; + using donatetorex_action = eosio::action_wrapper<"donatetorex"_n, &system_contract::donatetorex>; using undelegatebw_action = eosio::action_wrapper<"undelegatebw"_n, &system_contract::undelegatebw>; using buyram_action = eosio::action_wrapper<"buyram"_n, &system_contract::buyram>; using buyrambytes_action = eosio::action_wrapper<"buyrambytes"_n, &system_contract::buyrambytes>; @@ -1658,6 +1792,7 @@ namespace eosiosystem { using logsellram_action = eosio::action_wrapper<"logsellram"_n, &system_contract::logsellram>; using ramtransfer_action = eosio::action_wrapper<"ramtransfer"_n, &system_contract::ramtransfer>; using ramburn_action = eosio::action_wrapper<"ramburn"_n, &system_contract::ramburn>; + using buyramburn_action = eosio::action_wrapper<"buyramburn"_n, &system_contract::buyramburn>; using logramchange_action = eosio::action_wrapper<"logramchange"_n, &system_contract::logramchange>; using refund_action = eosio::action_wrapper<"refund"_n, &system_contract::refund>; using regproducer_action = eosio::action_wrapper<"regproducer"_n, &system_contract::regproducer>; @@ -1677,35 +1812,30 @@ namespace eosiosystem { using setalimits_action = eosio::action_wrapper<"setalimits"_n, &system_contract::setalimits>; using setparams_action = eosio::action_wrapper<"setparams"_n, &system_contract::setparams>; using setinflation_action = eosio::action_wrapper<"setinflation"_n, &system_contract::setinflation>; + using setpayfactor_action = eosio::action_wrapper<"setpayfactor"_n, &system_contract::setpayfactor>; using cfgpowerup_action = eosio::action_wrapper<"cfgpowerup"_n, &system_contract::cfgpowerup>; using powerupexec_action = eosio::action_wrapper<"powerupexec"_n, &system_contract::powerupexec>; using powerup_action = eosio::action_wrapper<"powerup"_n, &system_contract::powerup>; + using execschedule_action = eosio::action_wrapper<"execschedule"_n, &system_contract::execschedule>; + using setschedule_action = eosio::action_wrapper<"setschedule"_n, &system_contract::setschedule>; + using delschedule_action = eosio::action_wrapper<"delschedule"_n, &system_contract::delschedule>; + using unvest_action = eosio::action_wrapper<"unvest"_n, &system_contract::unvest>; private: - // Implementation details: - - static symbol get_core_symbol( const rammarket& rm ) { - auto itr = rm.find(ramcore_symbol.raw()); - check(itr != rm.end(), "system contract must first be initialized"); - return itr->quote.balance.symbol; - } - //defined in eosio.system.cpp static eosio_global_state get_default_parameters(); static eosio_global_state4 get_default_inflation_parameters(); symbol core_symbol()const; void update_ram_supply(); + void channel_to_system_fees( const name& from, const asset& amount ); + bool execute_next_schedule(); // defined in rex.cpp void runrex( uint16_t max ); void update_rex_pool(); void update_resource_limits( const name& from, const name& receiver, int64_t delta_net, int64_t delta_cpu ); - void check_voting_requirement( const name& owner, - const char* error_msg = "must vote for at least 21 producers or for a proxy before buying REX" )const; rex_order_outcome fill_rex_order( const rex_balance_table::const_iterator& bitr, const asset& rex ); asset update_rex_account( const name& owner, const asset& proceeds, const asset& unstake_quant, bool force_vote_update = false ); - void channel_to_rex( const name& from, const asset& amount, bool required = false ); - void channel_namebid_to_rex( const int64_t highest_bid ); template int64_t rent_rex( T& table, const name& from, const name& receiver, const asset& loan_payment, const asset& loan_fund ); template @@ -1715,18 +1845,19 @@ namespace eosiosystem { void transfer_from_fund( const name& owner, const asset& amount ); void transfer_to_fund( const name& owner, const asset& amount ); bool rex_loans_available()const; - bool rex_system_initialized()const { return _rexpool.begin() != _rexpool.end(); } - bool rex_available()const { return rex_system_initialized() && _rexpool.begin()->total_rex.amount > 0; } - static time_point_sec get_rex_maturity(); + static time_point_sec get_rex_maturity(const name& system_account_name = "eosio"_n ); asset add_to_rex_balance( const name& owner, const asset& payment, const asset& rex_received ); asset add_to_rex_pool( const asset& payment ); void add_to_rex_return_pool( const asset& fee ); void process_rex_maturities( const rex_balance_table::const_iterator& bitr ); + void process_sell_matured_rex( const name owner ); + void process_buy_rex_to_savings( const name owner, const asset rex ); void consolidate_rex_balance( const rex_balance_table::const_iterator& bitr, const asset& rex_in_sell_order ); int64_t read_rex_savings( const rex_balance_table::const_iterator& bitr ); void put_rex_savings( const rex_balance_table::const_iterator& bitr, int64_t rex ); void update_rex_stake( const name& voter ); + void sell_rex( const name& from, const asset& rex ); void add_loan_to_rex_pool( const asset& payment, int64_t rented_tokens, bool new_loan ); void remove_loan_from_rex_pool( const rex_loan& loan ); @@ -1736,10 +1867,12 @@ namespace eosiosystem { // defined in delegate_bandwidth.cpp void changebw( name from, const name& receiver, const asset& stake_net_quantity, const asset& stake_cpu_quantity, bool transfer ); - void update_voting_power( const name& voter, const asset& total_update ); + int64_t update_voting_power( const name& voter, const asset& total_update ); void set_resource_ram_bytes_limits( const name& owner ); int64_t reduce_ram( const name& owner, int64_t bytes ); int64_t add_ram( const name& owner, int64_t bytes ); + void update_stake_delegated( const name from, const name receiver, const asset stake_net_delta, const asset stake_cpu_delta ); + void update_user_resources( const name from, const name receiver, const asset stake_net_delta, const asset stake_cpu_delta ); // defined in voting.cpp void register_producer( const name& producer, const eosio::block_signing_authority& producer_authority, const std::string& url, uint16_t location ); diff --git a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in index d82a209b..396f4be9 100644 --- a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in +++ b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in @@ -94,6 +94,17 @@ icon: @ICON_BASE_URL@/@REX_ICON_URI@ A sell order of the purchased amount can only be initiated after waiting for the maturity period of 4 to 5 days to pass. Even then, depending on the market conditions, the initiated sell order may not be executed immediately. +

donatetorex

+ +--- +spec_version: "0.2.0" +title: Donate system tokens to REX +summary: '{{nowrap payer}} donates {{nowrap quantity}} tokens to REX' +icon: @ICON_BASE_URL@/@REX_ICON_URI@ +--- + +{{quantity}} is taken out of {{payer}}’s token balance and given to REX with the included memo: "{{memo}}". +

canceldelay

--- @@ -402,6 +413,27 @@ icon: @ICON_BASE_URL@/@REX_ICON_URI@ Performs REX maintenance by processing a maximum of {{max}} REX sell orders and expired loans. Any account can execute this action. +

setrexmature

+ +--- +spec_version: "0.2.0" +title: Set REX Maturity Settings +summary: 'Sets the options for REX maturity buckets' +icon: @ICON_BASE_URL@/@REX_ICON_URI@ +--- + +{{#if num_of_maturity_buckets}} + Sets the numbers of maturity buckets to '{{num_of_maturity_buckets}}' +{{/if}} + +{{#if sell_matured_rex}} + Sets whether or not to immediately sell matured REX to '{{sell_matured_rex}}' +{{/if}} + +{{#if buy_rex_to_savings}} + Sets whether or not to immediately move purchased REX to savings to '{{buy_rex_to_savings}}' +{{/if}} +

rmvproducer

--- @@ -454,6 +486,21 @@ Burn {{bytes}} bytes of unused RAM from account {{owner}}. {{memo}} {{/if}} +

buyramburn

+ +--- +spec_version: "0.2.0" +title: Buy and Burn RAM +summary: 'Buy and immediately Burn {{quantity}} of RAM from {{nowrap payer}}' +icon: @ICON_BASE_URL@/@RESOURCE_ICON_URI@ +--- + +Buy and Burn {{quantity}} of RAM from account {{payer}}. + +{{#if memo}}There is a memo attached to the action stating: +{{memo}} +{{/if}} +

sellrex

--- @@ -463,9 +510,15 @@ summary: '{{nowrap from}} sells {{nowrap rex}} tokens' icon: @ICON_BASE_URL@/@REX_ICON_URI@ --- -{{from}} initiates a sell order to sell {{rex}} tokens at the market exchange rate during the time at which the order is ultimately executed. If {{from}} already has an open sell order in the sell queue, {{rex}} will be added to the amount of the sell order without change the position of the sell order within the queue. Once the sell order is executed, proceeds are added to {{from}}’s REX fund, the value of sold REX tokens is deducted from {{from}}’s vote stake, and votes are updated accordingly. +The 'rex' parameter no longer has an effect. -Depending on the market conditions, it may not be possible to fill the entire sell order immediately. In such a case, the sell order is added to the back of a sell queue. A sell order at the front of the sell queue will automatically be executed when the market conditions allow for the entire order to be filled. Regardless of the market conditions, the system is designed to execute this sell order within 30 days. {{from}} can cancel the order at any time before it is filled using the cnclrexorder action. +{{from}} initiates a sell order to sell all of their matured REX tokens at the market exchange rate during the time at which the order is ultimately executed. +If {{from}} already has an open sell order in the sell queue, {{rex}} will be added to the amount of the sell order without change the position of the sell order within the queue. +Once the sell order is executed, proceeds are added to {{from}}’s REX fund, the value of sold REX tokens is deducted from {{from}}’s vote stake, and votes are updated accordingly. + +Depending on the market conditions, it may not be possible to fill the entire sell order immediately. In such a case, the sell order is added to the back of a sell queue. +A sell order at the front of the sell queue will automatically be executed when the market conditions allow for the entire order to be filled. Regardless of the market conditions, +the system is designed to execute this sell order within 30 days. {{from}} can cancel the order at any time before it is filled using the cnclrexorder action.

setabi

@@ -626,6 +679,20 @@ icon: @ICON_BASE_URL@/@ADMIN_ICON_URI@ * Fraction of inflation used to reward block producers: 10000/{{inflation_pay_factor}} * Fraction of block producer rewards to be distributed proportional to blocks produced: 10000/{{votepay_factor}} +

setpayfactor

+ +--- +spec_version: "0.2.0" +title: Set Pay Factors +summary: 'Set pay factors' +icon: @ICON_BASE_URL@/@ADMIN_ICON_URI@ +--- + +{{$action.account}} sets the inflation parameters as follows: + +* Fraction of inflation used to reward block producers: 10000/{{inflation_pay_factor}} +* Fraction of block producer rewards to be distributed proportional to blocks produced: 10000/{{votepay_factor}} +

undelegatebw

--- @@ -752,4 +819,51 @@ summary: 'User may powerup to reserve resources' icon: @ICON_BASE_URL@/@RESOURCE_ICON_URI@ --- -Users may use the powerup action to reserve resources. \ No newline at end of file +Users may use the powerup action to reserve resources. + +

setschedule

+ +--- +spec_version: "0.2.0" +title: Set Annual Rate Schedule +summary: 'Set annual rate parameters' +icon: @ICON_BASE_URL@/@ADMIN_ICON_URI@ +--- + +{{$action.account}} sets a pre-determined inflation schedule to adjust parameters as follows: + +* Start time of the schedule: {{start_time}} +* The continuous rate of inflation: {{continuous_rate}} + +

delschedule

+ +--- +spec_version: "0.2.0" +title: Delete Annual Rate Schedule +summary: 'Delete annual rate schedule' +icon: @ICON_BASE_URL@/@ADMIN_ICON_URI@ +--- + +{{$action.account}} to delete a pre-determined inflation schedule from {{start_time}} start time. + +

execschedule

+ +--- +spec_version: "0.2.0" +title: Execute Next Annual Rate Schedule +summary: 'Execute next annual rate schedule' +icon: @ICON_BASE_URL@/@ADMIN_ICON_URI@ +--- + +{{$action.account}} to execute the next upcoming annual rate schedule. + +

unvest

+ +--- +spec_version: "0.2.0" +title: Unvest Tokens +summary: 'Reclaim and retire unvested tokens' +icon: @ICON_BASE_URL@/@ADMIN_ICON_URI@ +--- + +Reclaim and retire {{$action.unvest_net_quantity}} and {{$action.unvest_cpu_quantity}} worth of unvested tokens from the account {{$action.account}}. \ No newline at end of file diff --git a/contracts/eosio.system/src/delegate_bandwidth.cpp b/contracts/eosio.system/src/delegate_bandwidth.cpp index f9696edc..e4b5ea08 100644 --- a/contracts/eosio.system/src/delegate_bandwidth.cpp +++ b/contracts/eosio.system/src/delegate_bandwidth.cpp @@ -56,12 +56,12 @@ namespace eosiosystem { check( quant.symbol == core_symbol(), "must buy ram with core token" ); check( quant.amount > 0, "must purchase a positive amount" ); - auto fee = quant; + asset fee = quant; fee.amount = ( fee.amount + 199 ) / 200; /// .5% fee (round up) // fee.amount cannot be 0 since that is only possible if quant.amount is 0 which is not allowed by the assert above. // If quant.amount == 1, then fee.amount == 1, // otherwise if quant.amount > 1, then 0 < fee.amount < quant.amount. - auto quant_after_fee = quant; + asset quant_after_fee = quant; quant_after_fee.amount -= fee.amount; // quant_after_fee.amount should be > 0 if quant.amount > 1. // If quant.amount == 1, then quant_after_fee.amount == 0 and the next inline transfer will fail causing the buyram action to fail. @@ -72,7 +72,7 @@ namespace eosiosystem { if ( fee.amount > 0 ) { token::transfer_action transfer_act{ token_account, { {payer, active_permission} } }; transfer_act.send( payer, ramfee_account, fee, "ram fee" ); - channel_to_rex( ramfee_account, fee ); + channel_to_system_fees( ramfee_account, fee ); } int64_t bytes_out; @@ -91,13 +91,16 @@ namespace eosiosystem { // logging system_contract::logbuyram_action logbuyram_act{ get_self(), { {get_self(), active_permission} } }; - logbuyram_act.send( payer, receiver, quant, bytes_out, ram_bytes ); + system_contract::logsystemfee_action logsystemfee_act{ get_self(), { {get_self(), active_permission} } }; + + logbuyram_act.send( payer, receiver, quant, bytes_out, ram_bytes, fee ); + logsystemfee_act.send( ram_account, fee, "buy ram" ); // action return value - return action_return_buyram{ payer, receiver, quant, bytes_out, ram_bytes }; + return action_return_buyram{ payer, receiver, quant, bytes_out, ram_bytes, fee }; } - void system_contract::logbuyram( const name& payer, const name& receiver, const asset& quantity, int64_t bytes, int64_t ram_bytes ) { + void system_contract::logbuyram( const name& payer, const name& receiver, const asset& quantity, int64_t bytes, int64_t ram_bytes, const asset& fee ) { require_auth( get_self() ); require_recipient(payer); require_recipient(receiver); @@ -134,23 +137,26 @@ namespace eosiosystem { token::transfer_action transfer_act{ token_account, { {ram_account, active_permission}, {account, active_permission} } }; transfer_act.send( ram_account, account, asset(tokens_out), "sell ram" ); } - auto fee = ( tokens_out.amount + 199 ) / 200; /// .5% fee (round up) + const int64_t fee = ( tokens_out.amount + 199 ) / 200; /// .5% fee (round up) // since tokens_out.amount was asserted to be at least 2 earlier, fee.amount < tokens_out.amount if ( fee > 0 ) { token::transfer_action transfer_act{ token_account, { {account, active_permission} } }; transfer_act.send( account, ramfee_account, asset(fee, core_symbol()), "sell ram fee" ); - channel_to_rex( ramfee_account, asset(fee, core_symbol() )); + channel_to_system_fees( ramfee_account, asset(fee, core_symbol() )); } // logging system_contract::logsellram_action logsellram_act{ get_self(), { {get_self(), active_permission} } }; - logsellram_act.send( account, tokens_out, bytes, ram_bytes ); + system_contract::logsystemfee_action logsystemfee_act{ get_self(), { {get_self(), active_permission} } }; + + logsellram_act.send( account, tokens_out, bytes, ram_bytes, asset(fee, core_symbol() ) ); + logsystemfee_act.send( ram_account, asset(fee, core_symbol() ), "sell ram" ); // action return value - return action_return_sellram{ account, tokens_out, bytes, ram_bytes }; + return action_return_sellram{ account, tokens_out, bytes, ram_bytes, asset(fee, core_symbol() ) }; } - void system_contract::logsellram( const name& account, const asset& quantity, int64_t bytes, int64_t ram_bytes ) { + void system_contract::logsellram( const name& account, const asset& quantity, int64_t bytes, int64_t ram_bytes, const asset& fee ) { require_auth( get_self() ); require_recipient(account); } @@ -179,6 +185,20 @@ namespace eosiosystem { return ramtransfer( owner, null_account, bytes, memo ); } + /** + * This action will buy and then burn the purchased RAM bytes. + */ + action_return_buyram system_contract::buyramburn( const name& payer, const asset& quantity, const std::string& memo ) { + require_auth( payer ); + check( quantity.symbol == core_symbol(), "quantity must be core token" ); + check( quantity.amount > 0, "quantity must be positive" ); + + const auto return_buyram = buyram( payer, payer, quantity ); + ramburn( payer, return_buyram.bytes_purchased, memo ); + + return return_buyram; + } + [[eosio::action]] void system_contract::logramchange( const name& owner, int64_t bytes, int64_t ram_bytes ) { @@ -241,13 +261,25 @@ namespace eosiosystem { } } - void validate_b1_vesting( int64_t stake ) { + std::pair get_b1_vesting_info() { const int64_t base_time = 1527811200; /// Friday, June 1, 2018 12:00:00 AM UTC const int64_t current_time = 1638921540; /// Tuesday, December 7, 2021 11:59:00 PM UTC - const int64_t max_claimable = 100'000'000'0000ll; - const int64_t claimable = int64_t(max_claimable * double(current_time - base_time) / (10*seconds_per_year) ); + const int64_t total_vesting = 100'000'000'0000ll; + const int64_t vested = int64_t(total_vesting * double(current_time - base_time) / (10*seconds_per_year) ); + return { total_vesting, vested }; + } + + + void validate_b1_vesting( int64_t new_stake, asset stake_change ) { + const auto [total_vesting, vested] = get_b1_vesting_info(); + auto unvestable = total_vesting - vested; - check( max_claimable - claimable <= stake, "b1 can only claim their tokens over 10 years" ); + auto hasAlreadyUnvested = new_stake < unvestable + && stake_change.amount < 0 + && new_stake + std::abs(stake_change.amount) < unvestable; + if(hasAlreadyUnvested) return; + + check( new_stake >= unvestable, "b1 can only claim what has already vested" ); } void system_contract::changebw( name from, const name& receiver, @@ -264,77 +296,8 @@ namespace eosiosystem { from = receiver; } - // update stake delegated from "from" to "receiver" - { - del_bandwidth_table del_tbl( get_self(), from.value ); - auto itr = del_tbl.find( receiver.value ); - if( itr == del_tbl.end() ) { - itr = del_tbl.emplace( from, [&]( auto& dbo ){ - dbo.from = from; - dbo.to = receiver; - dbo.net_weight = stake_net_delta; - dbo.cpu_weight = stake_cpu_delta; - }); - } - else { - del_tbl.modify( itr, same_payer, [&]( auto& dbo ){ - dbo.net_weight += stake_net_delta; - dbo.cpu_weight += stake_cpu_delta; - }); - } - check( 0 <= itr->net_weight.amount, "insufficient staked net bandwidth" ); - check( 0 <= itr->cpu_weight.amount, "insufficient staked cpu bandwidth" ); - if ( itr->is_empty() ) { - del_tbl.erase( itr ); - } - } // itr can be invalid, should go out of scope - - // update totals of "receiver" - { - user_resources_table totals_tbl( get_self(), receiver.value ); - auto tot_itr = totals_tbl.find( receiver.value ); - if( tot_itr == totals_tbl.end() ) { - tot_itr = totals_tbl.emplace( from, [&]( auto& tot ) { - tot.owner = receiver; - tot.net_weight = stake_net_delta; - tot.cpu_weight = stake_cpu_delta; - }); - } else { - totals_tbl.modify( tot_itr, from == receiver ? from : same_payer, [&]( auto& tot ) { - tot.net_weight += stake_net_delta; - tot.cpu_weight += stake_cpu_delta; - }); - } - check( 0 <= tot_itr->net_weight.amount, "insufficient staked total net bandwidth" ); - check( 0 <= tot_itr->cpu_weight.amount, "insufficient staked total cpu bandwidth" ); - - { - bool ram_managed = false; - bool net_managed = false; - bool cpu_managed = false; - - auto voter_itr = _voters.find( receiver.value ); - if( voter_itr != _voters.end() ) { - ram_managed = has_field( voter_itr->flags1, voter_info::flags1_fields::ram_managed ); - net_managed = has_field( voter_itr->flags1, voter_info::flags1_fields::net_managed ); - cpu_managed = has_field( voter_itr->flags1, voter_info::flags1_fields::cpu_managed ); - } - - if( !(net_managed && cpu_managed) ) { - int64_t ram_bytes, net, cpu; - get_resource_limits( receiver, ram_bytes, net, cpu ); - - set_resource_limits( receiver, - ram_managed ? ram_bytes : std::max( tot_itr->ram_bytes + ram_gift_bytes, ram_bytes ), - net_managed ? net : tot_itr->net_weight.amount, - cpu_managed ? cpu : tot_itr->cpu_weight.amount ); - } - } - - if ( tot_itr->is_empty() ) { - totals_tbl.erase( tot_itr ); - } - } // tot_itr can be invalid, should go out of scope + update_stake_delegated( from, receiver, stake_net_delta, stake_cpu_delta ); + update_user_resources( from, receiver, stake_net_delta, stake_cpu_delta ); // create refund or update from existing refund if ( stake_account != source_stake_from ) { //for eosio both transfer and refund make no sense @@ -406,10 +369,84 @@ namespace eosiosystem { } vote_stake_updater( from ); - update_voting_power( from, stake_net_delta + stake_cpu_delta ); + const int64_t staked = update_voting_power( from, stake_net_delta + stake_cpu_delta ); + if ( from == "b1"_n ) { + validate_b1_vesting( staked, stake_net_delta + stake_cpu_delta ); + } + } + + void system_contract::update_stake_delegated( const name from, const name receiver, const asset stake_net_delta, const asset stake_cpu_delta ) + { + del_bandwidth_table del_tbl( get_self(), from.value ); + auto itr = del_tbl.find( receiver.value ); + if( itr == del_tbl.end() ) { + itr = del_tbl.emplace( from, [&]( auto& dbo ){ + dbo.from = from; + dbo.to = receiver; + dbo.net_weight = stake_net_delta; + dbo.cpu_weight = stake_cpu_delta; + }); + } else { + del_tbl.modify( itr, same_payer, [&]( auto& dbo ){ + dbo.net_weight += stake_net_delta; + dbo.cpu_weight += stake_cpu_delta; + }); + } + check( 0 <= itr->net_weight.amount, "insufficient staked net bandwidth" ); + check( 0 <= itr->cpu_weight.amount, "insufficient staked cpu bandwidth" ); + if ( itr->is_empty() ) { + del_tbl.erase( itr ); + } + } + + void system_contract::update_user_resources( const name from, const name receiver, const asset stake_net_delta, const asset stake_cpu_delta ) + { + user_resources_table totals_tbl( get_self(), receiver.value ); + auto tot_itr = totals_tbl.find( receiver.value ); + if( tot_itr == totals_tbl.end() ) { + tot_itr = totals_tbl.emplace( from, [&]( auto& tot ) { + tot.owner = receiver; + tot.net_weight = stake_net_delta; + tot.cpu_weight = stake_cpu_delta; + }); + } else { + totals_tbl.modify( tot_itr, from == receiver ? from : same_payer, [&]( auto& tot ) { + tot.net_weight += stake_net_delta; + tot.cpu_weight += stake_cpu_delta; + }); + } + check( 0 <= tot_itr->net_weight.amount, "insufficient staked total net bandwidth" ); + check( 0 <= tot_itr->cpu_weight.amount, "insufficient staked total cpu bandwidth" ); + + { + bool ram_managed = false; + bool net_managed = false; + bool cpu_managed = false; + + auto voter_itr = _voters.find( receiver.value ); + if( voter_itr != _voters.end() ) { + ram_managed = has_field( voter_itr->flags1, voter_info::flags1_fields::ram_managed ); + net_managed = has_field( voter_itr->flags1, voter_info::flags1_fields::net_managed ); + cpu_managed = has_field( voter_itr->flags1, voter_info::flags1_fields::cpu_managed ); + } + + if( !(net_managed && cpu_managed) ) { + int64_t ram_bytes, net, cpu; + get_resource_limits( receiver, ram_bytes, net, cpu ); + + set_resource_limits( receiver, + ram_managed ? ram_bytes : std::max( tot_itr->ram_bytes + ram_gift_bytes, ram_bytes ), + net_managed ? net : tot_itr->net_weight.amount, + cpu_managed ? cpu : tot_itr->cpu_weight.amount ); + } + } + + if ( tot_itr->is_empty() ) { + totals_tbl.erase( tot_itr ); + } // tot_itr can be invalid, should go out of scope } - void system_contract::update_voting_power( const name& voter, const asset& total_update ) + int64_t system_contract::update_voting_power( const name& voter, const asset& total_update ) { auto voter_itr = _voters.find( voter.value ); if( voter_itr == _voters.end() ) { @@ -425,13 +462,10 @@ namespace eosiosystem { check( 0 <= voter_itr->staked, "stake for voting cannot be negative" ); - if( voter == "b1"_n ) { - validate_b1_vesting( voter_itr->staked ); - } - if( voter_itr->producers.size() || voter_itr->proxy ) { update_votes( voter, voter_itr->proxy, voter_itr->producers, false ); } + return voter_itr->staked; } void system_contract::delegatebw( const name& from, const name& receiver, @@ -460,7 +494,6 @@ namespace eosiosystem { changebw( from, receiver, -unstake_net_quantity, -unstake_cpu_quantity, false); } // undelegatebw - void system_contract::refund( const name& owner ) { require_auth( owner ); @@ -474,5 +507,32 @@ namespace eosiosystem { refunds_tbl.erase( req ); } + void system_contract::unvest(const name account, const asset unvest_net_quantity, const asset unvest_cpu_quantity) + { + require_auth( get_self() ); + + check( account == "b1"_n, "only b1 account can unvest"); + + check( unvest_cpu_quantity.amount >= 0, "must unvest a positive amount" ); + check( unvest_net_quantity.amount >= 0, "must unvest a positive amount" ); + + const auto [total_vesting, vested] = get_b1_vesting_info(); + const asset unvesting = unvest_net_quantity + unvest_cpu_quantity; + check( unvesting.amount <= total_vesting - vested , "can only unvest what is not already vested"); + + // reduce staked from account + update_voting_power( account, -unvesting ); + update_stake_delegated( account, account, -unvest_net_quantity, -unvest_cpu_quantity ); + update_user_resources( account, account, -unvest_net_quantity, -unvest_cpu_quantity ); + vote_stake_updater( account ); + + // transfer unvested tokens to `eosio` + token::transfer_action transfer_act{ token_account, { {stake_account, active_permission} } }; + transfer_act.send( stake_account, get_self(), unvesting, "unvest" ); + + // retire unvested tokens + token::retire_action retire_act{ token_account, { {"eosio"_n, active_permission} } }; + retire_act.send( unvesting, "unvest" ); + } // unvest } //namespace eosiosystem diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index feb2a1ac..d345087a 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -28,13 +28,15 @@ namespace eosiosystem { _global2(get_self(), get_self().value), _global3(get_self(), get_self().value), _global4(get_self(), get_self().value), + _schedules(get_self(), get_self().value), _rammarket(get_self(), get_self().value), _rexpool(get_self(), get_self().value), _rexretpool(get_self(), get_self().value), _rexretbuckets(get_self(), get_self().value), _rexfunds(get_self(), get_self().value), _rexbalance(get_self(), get_self().value), - _rexorders(get_self(), get_self().value) + _rexorders(get_self(), get_self().value), + _rexmaturity(get_self(), get_self().value) { _gstate = _global.exists() ? _global.get() : get_default_parameters(); _gstate2 = _global2.exists() ? _global2.get() : eosio_global_state2{}; @@ -57,7 +59,7 @@ namespace eosiosystem { } symbol system_contract::core_symbol()const { - const static auto sym = get_core_symbol( _rammarket ); + const static auto sym = get_core_symbol(); return sym; } @@ -113,6 +115,11 @@ namespace eosiosystem { _gstate2.new_ram_per_block = bytes_per_block; } + void system_contract::channel_to_system_fees( const name& from, const asset& amount ) { + token::transfer_action transfer_act{ token_account, { from, active_permission } }; + transfer_act.send( from, fees_account, amount, "transfer from " + from.to_string() + " to " + fees_account.to_string() ); + } + #ifdef SYSTEM_BLOCKCHAIN_PARAMETERS extern "C" [[eosio::wasm_import]] void set_parameters_packed(const void*, size_t); #endif @@ -394,6 +401,10 @@ namespace eosiosystem { preactivate_feature( feature_digest ); } + void system_contract::logsystemfee( const name& protocol, const asset& fee, const std::string& memo ) { + require_auth( get_self() ); + } + void system_contract::rmvproducer( const name& producer ) { require_auth( get_self() ); auto prod = _producers.find( producer.value ); @@ -427,6 +438,67 @@ namespace eosiosystem { _global4.set( _gstate4, get_self() ); } + void system_contract::setpayfactor( int64_t inflation_pay_factor, int64_t votepay_factor ) { + require_auth(get_self()); + if ( inflation_pay_factor < pay_factor_precision ) { + check( false, "inflation_pay_factor must not be less than " + std::to_string(pay_factor_precision) ); + } + if ( votepay_factor < pay_factor_precision ) { + check( false, "votepay_factor must not be less than " + std::to_string(pay_factor_precision) ); + } + _gstate4.inflation_pay_factor = inflation_pay_factor; + _gstate4.votepay_factor = votepay_factor; + _global4.set( _gstate4, get_self() ); + } + + void system_contract::setschedule( const time_point_sec start_time, double continuous_rate ) + { + require_auth( get_self() ); + + check(continuous_rate >= 0, "continuous_rate can't be negative"); + check(continuous_rate <= 1, "continuous_rate can't be over 100%"); + + auto itr = _schedules.find( start_time.sec_since_epoch() ); + + if( itr == _schedules.end() ) { + _schedules.emplace( get_self(), [&]( auto& s ) { + s.start_time = start_time; + s.continuous_rate = continuous_rate; + }); + } else { + _schedules.modify( itr, same_payer, [&]( auto& s ) { + s.continuous_rate = continuous_rate; + }); + } + } + + void system_contract::delschedule( const time_point_sec start_time ) + { + require_auth( get_self() ); + + auto itr = _schedules.require_find( start_time.sec_since_epoch(), "schedule not found" ); + _schedules.erase( itr ); + } + + void system_contract::execschedule() + { + check(execute_next_schedule(), "no schedule to execute"); + } + + bool system_contract::execute_next_schedule() + { + auto itr = _schedules.begin(); + if (itr == _schedules.end()) return false; // no schedules to execute + + if ( current_time_point().sec_since_epoch() >= itr->start_time.sec_since_epoch() ) { + _gstate4.continuous_rate = itr->continuous_rate; + _global4.set( _gstate4, get_self() ); + _schedules.erase( itr ); + return true; + } + return false; + } + /** * Called after a new account is created. This code enforces resource-limits rules * for new accounts as well as new account naming conventions. diff --git a/contracts/eosio.system/src/powerup.cpp b/contracts/eosio.system/src/powerup.cpp index adc92e71..8717a489 100644 --- a/contracts/eosio.system/src/powerup.cpp +++ b/contracts/eosio.system/src/powerup.cpp @@ -387,12 +387,16 @@ void system_contract::powerup(const name& payer, const name& receiver, uint32_t adjust_resources(payer, receiver, core_symbol, net_amount, cpu_amount, true); adjust_resources(get_self(), reserve_account, core_symbol, net_delta_available, cpu_delta_available, true); - channel_to_rex(payer, fee, true); + channel_to_system_fees(payer, fee); state_sing.set(state, get_self()); // inline noop action powup_results::powupresult_action powupresult_act{ reserve_account, std::vector{ } }; powupresult_act.send( fee, net_amount, cpu_amount ); + + // logging + system_contract::logsystemfee_action logsystemfee_act{ get_self(), { {get_self(), active_permission} } }; + logsystemfee_act.send( powerup_account, fee, "buy powerup" ); } } // namespace eosiosystem diff --git a/contracts/eosio.system/src/producer_pay.cpp b/contracts/eosio.system/src/producer_pay.cpp index e1a98dd0..1096b085 100644 --- a/contracts/eosio.system/src/producer_pay.cpp +++ b/contracts/eosio.system/src/producer_pay.cpp @@ -64,7 +64,12 @@ namespace eosiosystem { (current_time_point() - _gstate.thresh_activated_stake_time) > microseconds(14 * useconds_per_day) ) { _gstate.last_name_close = timestamp; - channel_namebid_to_rex( highest->high_bid ); + channel_to_system_fees( names_account, asset( highest->high_bid, core_symbol() ) ); + + // logging + system_contract::logsystemfee_action logsystemfee_act{ get_self(), { {get_self(), active_permission} } }; + logsystemfee_act.send( names_account, asset( highest->high_bid, core_symbol() ), "buy name" ); + idx.modify( highest, same_payer, [&]( auto& b ){ b.high_bid = -b.high_bid; }); @@ -76,7 +81,8 @@ namespace eosiosystem { void system_contract::claimrewards( const name& owner ) { require_auth( owner ); - const auto& prod = _producers.get( owner.value ); + execute_next_schedule(); + const auto& prod = _producers.get( owner.value, "producer not registered" ); check( prod.active(), "producer does not have an active key" ); check( _gstate.thresh_activated_stake_time != time_point(), @@ -87,6 +93,8 @@ namespace eosiosystem { check( ct - prod.last_claim_time > microseconds(useconds_per_day), "already claimed rewards within past day" ); const asset token_supply = token::get_supply(token_account, core_symbol().code() ); + const asset token_max_supply = token::get_max_supply(token_account, core_symbol().code() ); + const asset token_balance = token::get_balance(token_account, get_self(), core_symbol().code() ); const auto usecs_since_last_fill = (ct - _gstate.last_pervote_bucket_fill).count(); if( usecs_since_last_fill > 0 && _gstate.last_pervote_bucket_fill > time_point() ) { @@ -101,14 +109,22 @@ namespace eosiosystem { int64_t to_per_vote_pay = to_producers - to_per_block_pay; if( new_tokens > 0 ) { + // issue new tokens or use existing eosio token balance { - token::issue_action issue_act{ token_account, { {get_self(), active_permission} } }; - issue_act.send( get_self(), asset(new_tokens, core_symbol()), "issue tokens for producer pay and savings" ); + // issue new tokens if circulating supply does not exceed max supply + if ( token_supply.amount + new_tokens <= token_max_supply.amount ) { + token::issue_action issue_act{ token_account, { {get_self(), active_permission} } }; + issue_act.send( get_self(), asset(new_tokens, core_symbol()), "issue tokens for producer pay and savings" ); + + // use existing eosio token balance if circulating supply exceeds max supply + } else { + check( token_balance.amount >= new_tokens, "insufficient system token balance for claiming rewards"); + } } { token::transfer_action transfer_act{ token_account, { {get_self(), active_permission} } }; if( to_savings > 0 ) { - transfer_act.send( get_self(), saving_account, asset(to_savings, core_symbol()), "unallocated inflation" ); + transfer_act.send( get_self(), saving_account, asset(to_savings, core_symbol()), "unallocated bucket" ); } if( to_per_block_pay > 0 ) { transfer_act.send( get_self(), bpay_account, asset(to_per_block_pay, core_symbol()), "fund per-block bucket" ); diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index 99542c13..43bbac5f 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -8,6 +8,21 @@ namespace eosiosystem { using eosio::token; using eosio::seconds; + void system_contract::setrexmature(const std::optional num_of_maturity_buckets, const std::optional sell_matured_rex, const std::optional buy_rex_to_savings ) + { + require_auth(get_self()); + + auto state = _rexmaturity.get_or_default(); + + check(*num_of_maturity_buckets > 0, "num_of_maturity_buckets must be positive"); + check(*num_of_maturity_buckets <= 30, "num_of_maturity_buckets must be less than or equal to 30"); + + if ( num_of_maturity_buckets ) state.num_of_maturity_buckets = *num_of_maturity_buckets; + if ( sell_matured_rex ) state.sell_matured_rex = *sell_matured_rex; + if ( buy_rex_to_savings ) state.buy_rex_to_savings = *buy_rex_to_savings; + _rexmaturity.set(state, get_self()); + } + void system_contract::deposit( const name& owner, const asset& amount ) { require_auth( owner ); @@ -43,12 +58,15 @@ namespace eosiosystem { check( amount.symbol == core_symbol(), "asset must be core token" ); check( 0 < amount.amount, "must use positive amount" ); - check_voting_requirement( from ); transfer_from_fund( from, amount ); const asset rex_received = add_to_rex_pool( amount ); const asset delta_rex_stake = add_to_rex_balance( from, amount, rex_received ); runrex(2); update_rex_account( from, asset( 0, core_symbol() ), delta_rex_stake ); + + process_buy_rex_to_savings( from, rex_received ); + process_sell_matured_rex( from ); + // dummy action added so that amount of REX tokens purchased shows up in action trace rex_results::buyresult_action buyrex_act( rex_account, std::vector{ } ); buyrex_act.send( rex_received ); @@ -61,7 +79,6 @@ namespace eosiosystem { check( from_net.symbol == core_symbol() && from_cpu.symbol == core_symbol(), "asset must be core token" ); check( (0 <= from_net.amount) && (0 <= from_cpu.amount) && (0 < from_net.amount || 0 < from_cpu.amount), "must unstake a positive amount to buy rex" ); - check_voting_requirement( owner ); { del_bandwidth_table dbw_table( get_self(), owner.value ); @@ -89,6 +106,10 @@ namespace eosiosystem { auto rex_stake_delta = add_to_rex_balance( owner, payment, rex_received ); runrex(2); update_rex_account( owner, asset( 0, core_symbol() ), rex_stake_delta - payment, true ); + + process_buy_rex_to_savings( owner, rex_received ); + process_sell_matured_rex( owner ); + // dummy action added so that amount of REX tokens purchased shows up in action trace rex_results::buyresult_action buyrex_act( rex_account, std::vector{ } ); buyrex_act.send( rex_received ); @@ -97,7 +118,12 @@ namespace eosiosystem { void system_contract::sellrex( const name& from, const asset& rex ) { require_auth( from ); + sell_rex( from, rex ); + process_sell_matured_rex( from ); + } + void system_contract::sell_rex( const name& from, const asset& rex ) + { runrex(2); auto bitr = _rexbalance.require_find( from.value, "user must first buyrex" ); @@ -353,6 +379,26 @@ namespace eosiosystem { } } + /** + * @brief Updates REX pool and deposits token to eosio.rex + * + * @param payer - the payer of donated funds. + * @param quantity - the quantity of tokens to donated to REX with. + * @param memo - the memo string to accompany the transfer. + */ + void system_contract::donatetorex( const name& payer, const asset& quantity, const std::string& memo ) + { + require_auth( payer ); + check( rex_available(), "rex system is not initialized" ); + check( quantity.symbol == core_symbol(), "quantity must be core token" ); + check( quantity.amount > 0, "quantity must be positive" ); + + add_to_rex_return_pool( quantity ); + // inline transfer to rex_account + token::transfer_action transfer_act{ token_account, { payer, active_permission } }; + transfer_act.send( payer, rex_account, quantity, memo ); + } + /** * @brief Updates account NET and CPU resource limits * @@ -411,19 +457,6 @@ namespace eosiosystem { } } - /** - * @brief Checks if account satisfies voting requirement (voting for a proxy or 21 producers) - * for buying REX - * - * @param owner - account buying or already holding REX tokens - * @err_msg - error message - */ - void system_contract::check_voting_requirement( const name& owner, const char* error_msg )const - { - auto vitr = _voters.find( owner.value ); - check( vitr != _voters.end() && ( vitr->proxy || 21 <= vitr->producers.size() ), error_msg ); - } - /** * @brief Checks if CPU and Network loans are available * @@ -453,7 +486,7 @@ namespace eosiosystem { */ void system_contract::add_loan_to_rex_pool( const asset& payment, int64_t rented_tokens, bool new_loan ) { - add_to_rex_return_pool( payment ); + channel_to_system_fees( get_self(), payment ); _rexpool.modify( _rexpool.begin(), same_payer, [&]( auto& rt ) { // add payment to total_rent rt.total_rent.amount += payment.amount; @@ -546,14 +579,6 @@ namespace eosiosystem { return { delete_loan, delta_stake }; }; - /// transfer from eosio.names to eosio.rex - if ( pool->namebid_proceeds.amount > 0 ) { - channel_to_rex( names_account, pool->namebid_proceeds ); - _rexpool.modify( pool, same_payer, [&]( auto& rt ) { - rt.namebid_proceeds.amount = 0; - }); - } - /// process cpu loans { rex_cpu_loan_table cpu_loans( get_self(), get_self().value ); @@ -754,6 +779,10 @@ namespace eosiosystem { rex_results::rentresult_action rentresult_act{ rex_account, std::vector{ } }; rentresult_act.send( asset{ rented_tokens, core_symbol() } ); + + // logging + system_contract::logsystemfee_action logsystemfee_act{ get_self(), { {get_self(), active_permission} } }; + logsystemfee_act.send( rex_account, payment, "rent rex" ); return rented_tokens; } @@ -916,52 +945,15 @@ namespace eosiosystem { } /** - * @brief Channels system fees to REX pool - * - * @param from - account from which asset is transferred to REX pool - * @param amount - amount of tokens to be transferred - * @param required - if true, asserts when the system is not configured to channel fees into REX - */ - void system_contract::channel_to_rex( const name& from, const asset& amount, bool required ) - { -#if CHANNEL_RAM_AND_NAMEBID_FEES_TO_REX - if ( rex_available() ) { - add_to_rex_return_pool( amount ); - // inline transfer to rex_account - token::transfer_action transfer_act{ token_account, { from, active_permission } }; - transfer_act.send( from, rex_account, amount, - std::string("transfer from ") + from.to_string() + " to eosio.rex" ); - return; - } -#endif - eosio::check( !required, "can't channel fees to rex" ); - } - - /** - * @brief Updates namebid proceeds to be transferred to REX pool - * - * @param highest_bid - highest bidding amount of closed namebid - */ - void system_contract::channel_namebid_to_rex( const int64_t highest_bid ) - { -#if CHANNEL_RAM_AND_NAMEBID_FEES_TO_REX - if ( rex_available() ) { - _rexpool.modify( _rexpool.begin(), same_payer, [&]( auto& rp ) { - rp.namebid_proceeds.amount += highest_bid; - }); - } -#endif - } - - /** - * @brief Calculates maturity time of purchased REX tokens which is 4 days from end + * @brief Calculates maturity time of purchased REX tokens which is {num_of_maturity_buckets} days from end * of the day UTC * * @return time_point_sec */ - time_point_sec system_contract::get_rex_maturity() + time_point_sec system_contract::get_rex_maturity( const name& system_account_name ) { - const uint32_t num_of_maturity_buckets = 5; + rex_maturity_singleton _rexmaturity(system_account_name, system_account_name.value); + const uint32_t num_of_maturity_buckets = _rexmaturity.get_or_default().num_of_maturity_buckets; // default 5 static const uint32_t now = current_time_point().sec_since_epoch(); static const uint32_t r = now % seconds_per_day; static const time_point_sec rms{ now - r + num_of_maturity_buckets * seconds_per_day }; @@ -984,6 +976,38 @@ namespace eosiosystem { }); } + /** + * @brief Sells matured REX tokens + * https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 + * + * @param owner - owner account name + */ + void system_contract::process_sell_matured_rex( const name owner ) + { + const auto rex_maturity_state = _rexmaturity.get_or_default(); + if ( rex_maturity_state.sell_matured_rex == false ) return; // skip selling matured REX + + const auto itr = _rexbalance.find( owner.value ); + if ( itr->matured_rex > 0 ) { + sell_rex(owner, asset(itr->matured_rex, rex_symbol)); + } + } + + /** + * @brief Move new REX tokens to savings + * https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 + * + * @param owner - owner account name + * @param rex - amount of REX tokens to be moved to savings + */ + void system_contract::process_buy_rex_to_savings( const name owner, const asset rex ) + { + const auto rex_maturity_state = _rexmaturity.get_or_default(); + if ( rex_maturity_state.buy_rex_to_savings && rex.amount > 0 ) { + mvtosavings( owner, rex ); + } + } + /** * @brief Consolidates REX maturity buckets into one * diff --git a/contracts/eosio.system/src/voting.cpp b/contracts/eosio.system/src/voting.cpp index 3636a575..89ff3420 100644 --- a/contracts/eosio.system/src/voting.cpp +++ b/contracts/eosio.system/src/voting.cpp @@ -234,10 +234,6 @@ namespace eosiosystem { vote_stake_updater( voter_name ); update_votes( voter_name, proxy, producers, true ); - auto rex_itr = _rexbalance.find( voter_name.value ); - if( rex_itr != _rexbalance.end() && rex_itr->rex_balance.amount > 0 ) { - check_voting_requirement( voter_name, "voter holding REX tokens must vote for at least 21 producers or for a proxy" ); - } } void system_contract::voteupdate( const name& voter_name ) { diff --git a/contracts/eosio.token/include/eosio.token/eosio.token.hpp b/contracts/eosio.token/include/eosio.token/eosio.token.hpp index ce8756c7..2ee84fc5 100644 --- a/contracts/eosio.token/include/eosio.token/eosio.token.hpp +++ b/contracts/eosio.token/include/eosio.token/eosio.token.hpp @@ -15,11 +15,11 @@ namespace eosio { /** * The `eosio.token` sample system contract defines the structures and actions that allow users to create, issue, and manage tokens for EOSIO based blockchains. It demonstrates one way to implement a smart contract which allows for creation and management of tokens. It is possible for one to create a similar contract which suits different needs. However, it is recommended that if one only needs a token with the below listed actions, that one uses the `eosio.token` contract instead of developing their own. - * + * * The `eosio.token` contract class also implements two useful public static methods: `get_supply` and `get_balance`. The first allows one to check the total supply of a specified token, created by an account and the second allows one to check the balance of a token for a specified account (the token creator account has to be specified as well). - * + * * The `eosio.token` contract manages the set of tokens, accounts and their corresponding balances, by using two internal multi-index structures: the `accounts` and `stats`. 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 queries the `accounts` multi-index table for an account name the result is all the tokens that account holds at the moment. - * + * * Similarly, the `stats` multi-index table, holds instances of `currency_stats` objects for each row, which contains information about current supply, maximum supply, and the creator account for a symbol token. The `stats` table is scoped to the token symbol. Therefore, when one queries the `stats` table for a token symbol the result is one single entry/row corresponding to the queried symbol token if it was previously created, or nothing, otherwise. */ class [[eosio::contract("eosio.token")]] token : public contract { @@ -45,11 +45,30 @@ namespace eosio { * * @param to - the account to issue tokens to, it must be the same as the issuer, * @param quantity - the amount of tokens to be issued, - * @memo - the memo string that accompanies the token issue transaction. + * @param memo - the memo string that accompanies the token issue transaction. */ [[eosio::action]] void issue( const name& to, const asset& quantity, const string& memo ); + /** + * Issues only the necessary tokens to bridge the gap between the current supply and the targeted total. + * + * @param to - the account to issue tokens to, it must be the same as the issuer, + * @param supply - the target total supply for the token. + * @param memo - the memo string that accompanies the token issue transaction. + */ + [[eosio::action]] + void issuefixed( const name& to, const asset& supply, const string& memo ); + + /** + * Set the maximum supply of the token. + * + * @param issuer - the issuer account setting the maximum supply. + * @param maximum_supply - the maximum supply of the token. + */ + [[eosio::action]] + void setmaxsupply( const name& issuer, const asset& maximum_supply ); + /** * The opposite for create action, if all validations succeed, * it debits the statstable.supply amount. @@ -104,15 +123,25 @@ namespace eosio { static asset get_supply( const name& token_contract_account, const symbol_code& sym_code ) { stats statstable( token_contract_account, sym_code.raw() ); - const auto& st = statstable.get( sym_code.raw(), "invalid supply symbol code" ); - return st.supply; + return statstable.get( sym_code.raw(), "invalid supply symbol code" ).supply; + } + + static asset get_max_supply( const name& token_contract_account, const symbol_code& sym_code ) + { + stats statstable( token_contract_account, sym_code.raw() ); + return statstable.get( sym_code.raw(), "invalid supply symbol code" ).max_supply; + } + + static name get_issuer( const name& token_contract_account, const symbol_code& sym_code ) + { + stats statstable( token_contract_account, sym_code.raw() ); + return statstable.get( sym_code.raw(), "invalid supply symbol code" ).issuer; } static asset get_balance( const name& token_contract_account, const name& owner, const symbol_code& sym_code ) { accounts accountstable( token_contract_account, owner.value ); - const auto& ac = accountstable.get( sym_code.raw(), "no balance with specified symbol" ); - return ac.balance; + return accountstable.get( sym_code.raw(), "no balance with specified symbol" ).balance; } using create_action = eosio::action_wrapper<"create"_n, &token::create>; @@ -121,7 +150,9 @@ namespace eosio { using transfer_action = eosio::action_wrapper<"transfer"_n, &token::transfer>; using open_action = eosio::action_wrapper<"open"_n, &token::open>; using close_action = eosio::action_wrapper<"close"_n, &token::close>; - private: + using issuefixed_action = eosio::action_wrapper<"issuefixed"_n, &token::issuefixed>; + using setmaxsupply_action = eosio::action_wrapper<"setmaxsupply"_n, &token::setmaxsupply>; + struct [[eosio::table]] account { asset balance; @@ -139,6 +170,7 @@ namespace eosio { typedef eosio::multi_index< "accounts"_n, account > accounts; typedef eosio::multi_index< "stat"_n, currency_stats > stats; + private: void sub_balance( const name& owner, const asset& value ); void add_balance( const name& owner, const asset& value, const name& ram_payer ); }; diff --git a/contracts/eosio.token/ricardian/eosio.token.contracts.md.in b/contracts/eosio.token/ricardian/eosio.token.contracts.md.in index f050eec7..dc857fc1 100644 --- a/contracts/eosio.token/ricardian/eosio.token.contracts.md.in +++ b/contracts/eosio.token/ricardian/eosio.token.contracts.md.in @@ -28,6 +28,19 @@ This action will not result any any tokens being issued into circulation. RAM will deducted from {{$action.account}}’s resources to create the necessary records. +

setmaxsupply

+ +--- +spec_version: "0.2.0" +title: Set Max Supply +summary: 'Set max supply for token' +icon: @ICON_BASE_URL@/@TOKEN_ICON_URI@ +--- + +{{issuer}} will be allowed to issue tokens into circulation, up to a maximum supply of {{maximum_supply}}. + +This action will not result any any tokens being issued into circulation. +

issue

--- @@ -47,6 +60,25 @@ If {{to}} does not have a balance for {{asset_to_symbol_code quantity}}, or the This action does not allow the total quantity to exceed the max allowed supply of the token. +

issuefixed

+ +--- +spec_version: "0.2.0" +title: Issue Fixed Supply of Tokens into Circulation +summary: 'Issue up to {{nowrap supply}} supply into circulation and transfer into {{nowrap to}}’s account' +icon: @ICON_BASE_URL@/@TOKEN_ICON_URI@ +--- + +The token manager agrees to issue tokens up to {{supply}} fixed supply into circulation, and transfer it into {{to}}’s account. + +{{#if memo}}There is a memo attached to the transfer stating: +{{memo}} +{{/if}} + +If {{to}} does not have a balance for {{asset_to_symbol_code quantity}}, or the token manager does not have a balance for {{asset_to_symbol_code quantity}}, the token manager will be designated as the RAM payer of the {{asset_to_symbol_code quantity}} token balance for {{to}}. As a result, RAM will be deducted from the token manager’s resources to create the necessary records. + +This action does not allow the total quantity to exceed the max allowed supply of the token. +

open

--- diff --git a/contracts/eosio.token/src/eosio.token.cpp b/contracts/eosio.token/src/eosio.token.cpp index 33a31cec..f70dd58a 100644 --- a/contracts/eosio.token/src/eosio.token.cpp +++ b/contracts/eosio.token/src/eosio.token.cpp @@ -49,6 +49,33 @@ void token::issue( const name& to, const asset& quantity, const string& memo ) add_balance( st.issuer, quantity, st.issuer ); } +void token::issuefixed( const name& to, const asset& supply, const string& memo ) +{ + const asset circulating_supply = get_supply( get_self(), supply.symbol.code() ); + check( circulating_supply.symbol == supply.symbol, "symbol precision mismatch" ); + const asset quantity = supply - circulating_supply; + issue( to, quantity, memo ); +} + +void token::setmaxsupply( const name& issuer, const asset& maximum_supply ) +{ + auto sym = maximum_supply.symbol; + check( maximum_supply.is_valid(), "invalid supply"); + check( maximum_supply.amount > 0, "max-supply must be positive"); + + stats statstable( get_self(), sym.code().raw() ); + auto & st = statstable.get( sym.code().raw(), "token supply does not exist" ); + check( issuer == st.issuer, "only issuer can set token maximum supply" ); + require_auth( st.issuer ); + + check( maximum_supply.symbol == st.supply.symbol, "symbol precision mismatch" ); + check( maximum_supply.amount >= st.supply.amount, "max supply is less than available supply"); + + statstable.modify( st, same_payer, [&]( auto& s ) { + s.max_supply = maximum_supply; + }); +} + void token::retire( const asset& quantity, const string& memo ) { auto sym = quantity.symbol; diff --git a/tests/contracts.hpp.in b/tests/contracts.hpp.in index ba33a662..cce660dc 100644 --- a/tests/contracts.hpp.in +++ b/tests/contracts.hpp.in @@ -7,6 +7,7 @@ struct contracts { static std::vector system_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/eosio.system/eosio.system.wasm"); } static std::vector system_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/eosio.system/eosio.system.abi"); } static std::vector token_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/eosio.token/eosio.token.wasm"); } + static std::vector fees_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/eosio.fees/eosio.fees.wasm"); } static std::vector token_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/eosio.token/eosio.token.abi"); } static std::vector msig_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/eosio.msig/eosio.msig.wasm"); } static std::vector msig_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/eosio.msig/eosio.msig.abi"); } @@ -14,6 +15,8 @@ struct contracts { static std::vector wrap_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/eosio.wrap/eosio.wrap.abi"); } static std::vector bios_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/eosio.bios/eosio.bios.wasm"); } static std::vector bios_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/eosio.bios/eosio.bios.abi"); } + static std::vector bpay_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/eosio.bpay/eosio.bpay.wasm"); } + static std::vector bpay_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/eosio.bpay/eosio.bpay.abi"); } struct util { static std::vector reject_all_wasm() { return read_wasm("${CMAKE_CURRENT_SOURCE_DIR}/test_contracts/reject_all.wasm"); } diff --git a/tests/eosio.bpay_tests.cpp b/tests/eosio.bpay_tests.cpp new file mode 100644 index 00000000..19beb4f7 --- /dev/null +++ b/tests/eosio.bpay_tests.cpp @@ -0,0 +1,101 @@ +#include "eosio.system_tester.hpp" + +using namespace eosio_system; + +BOOST_AUTO_TEST_SUITE(eosio_bpay_tests); + +account_name voter = "alice1111111"_n; +account_name standby = "bp.standby"_n; +account_name inactive = "bp.inactive"_n; +account_name fees = "eosio.fees"_n; +account_name bpay = "eosio.bpay"_n; + +BOOST_FIXTURE_TEST_CASE( bpay_test, eosio_system_tester ) try { + + + // Transferring some tokens to the fees account + // since tokens from eosio will not be directly accepted as contributions to + // the bpay contract + transfer( config::system_account_name, fees, core_sym::from_string("100000.0000"), config::system_account_name ); + + + // Setting up the producers, standby and inactive producers, and voting them in + setup_producer_accounts({standby, inactive}); + auto producer_names = active_and_vote_producers(); + + BOOST_REQUIRE_EQUAL( success(), regproducer(standby) ); + BOOST_REQUIRE_EQUAL( success(), regproducer(inactive) ); + vector top_producers_and_inactive = {inactive}; + top_producers_and_inactive.insert( top_producers_and_inactive.end(), producer_names.begin(), producer_names.begin()+21 ); + + BOOST_REQUIRE_EQUAL( success(), vote( voter, top_producers_and_inactive ) ); + produce_blocks( 250 ); + + + BOOST_REQUIRE_EQUAL( 0, get_producer_info( standby )["unpaid_blocks"].as() ); + BOOST_REQUIRE_EQUAL( get_producer_info( producer_names[0] )["unpaid_blocks"].as() > 0, true ); + + // TODO: Check nothing happened here, no rewards since it comes from system account + + asset rewards_sent = core_sym::from_string("1000.0000"); + transfer( fees, bpay, rewards_sent, fees); + + // rewards / 21 + asset balance_per_producer = core_sym::from_string("47.6190"); + + auto rewards = get_bpay_rewards(producer_names[0]); + + // bp.inactive is still active, so should be included in the rewards + BOOST_REQUIRE_EQUAL( get_bpay_rewards(inactive)["quantity"].as(), balance_per_producer ); + // Random sample + BOOST_REQUIRE_EQUAL( get_bpay_rewards(producer_names[11])["quantity"].as(), balance_per_producer ); + + + // Deactivating a producer + BOOST_REQUIRE_EQUAL( success(), push_action(config::system_account_name, "rmvproducer"_n, mvo()("producer", inactive) ) ); + BOOST_REQUIRE_EQUAL( false, get_producer_info( inactive )["is_active"].as() ); + + transfer( fees, bpay, rewards_sent, fees); + BOOST_REQUIRE_EQUAL( get_bpay_rewards(inactive)["quantity"].as(), balance_per_producer ); + BOOST_REQUIRE_EQUAL( get_bpay_rewards(producer_names[11])["quantity"].as(), core_sym::from_string("95.2380") ); + + // BP should be able to claim their rewards + { + auto prod = producer_names[11]; + BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_balance( prod ) ); + BOOST_REQUIRE_EQUAL( success(), bpay_claimrewards( prod ) ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("95.2380"), get_balance( prod ) ); + BOOST_REQUIRE_EQUAL( true, get_bpay_rewards(prod).is_null() ); + + // should still have rewards for another producer + BOOST_REQUIRE_EQUAL( get_bpay_rewards(producer_names[10])["quantity"].as(), core_sym::from_string("95.2380") ); + } + + // Should be able to claim rewards from a producer that is no longer active + { + BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_balance( inactive ) ); + BOOST_REQUIRE_EQUAL( success(), bpay_claimrewards( inactive ) ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("47.6190"), get_balance( inactive ) ); + BOOST_REQUIRE_EQUAL( true, get_bpay_rewards(inactive).is_null() ); + } + + // Should not have rewards for a producer that was never active + { + BOOST_REQUIRE_EQUAL( true, get_bpay_rewards(standby).is_null() ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_balance( standby ) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("no rewards to claim"), bpay_claimrewards( standby ) ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_balance( standby ) ); + } + + // Tokens transferred from the eosio account should be ignored + { + transfer( config::system_account_name, bpay, rewards_sent, config::system_account_name ); + BOOST_REQUIRE_EQUAL( get_bpay_rewards(producer_names[10])["quantity"].as(), core_sym::from_string("95.2380") ); + } + + + +} FC_LOG_AND_RETHROW() + + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/tests/eosio.fees_tests.cpp b/tests/eosio.fees_tests.cpp new file mode 100644 index 00000000..63b24321 --- /dev/null +++ b/tests/eosio.fees_tests.cpp @@ -0,0 +1,53 @@ +#include "eosio.system_tester.hpp" + +using namespace eosio_system; + +BOOST_AUTO_TEST_SUITE(eosio_fees_tests); + +BOOST_FIXTURE_TEST_CASE( fees_with_rex, eosio_system_tester ) try { + // init REX + const std::vector accounts = { "alice"_n }; + const account_name alice = accounts[0]; + const asset init_balance = core_sym::from_string("1000.0000"); + setup_rex_accounts( accounts, init_balance ); + buyrex( alice, core_sym::from_string("10.0000")); + + // manual token transfer to fees account + const asset fees_before = get_balance( "eosio.fees" ); + const asset rex_before = get_balance( "eosio.rex" ); + const asset eosio_before = get_balance( "eosio" ); + + const asset fee = core_sym::from_string("100.0000"); + transfer( config::system_account_name, "eosio.fees"_n, fee, config::system_account_name ); + + const asset fees_after = get_balance( "eosio.fees" ); + const asset rex_after = get_balance( "eosio.rex" ); + const asset eosio_after = get_balance( "eosio" ); + + BOOST_REQUIRE_EQUAL( fees_before, fees_after ); + BOOST_REQUIRE_EQUAL( rex_before + fee, rex_after ); + BOOST_REQUIRE_EQUAL( eosio_before - fee, eosio_after ); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( fees_without_rex, eosio_system_tester ) try { + // manual token transfer to fees account + const asset fees_before = get_balance( "eosio.fees" ); + const asset rex_before = get_balance( "eosio.rex" ); + const asset eosio_before = get_balance( "eosio" ); + + const asset fee = core_sym::from_string("100.0000"); + transfer( config::system_account_name, "eosio.fees"_n, fee, config::system_account_name ); + + const asset fees_after = get_balance( "eosio.fees" ); + const asset rex_after = get_balance( "eosio.rex" ); + const asset eosio_after = get_balance( "eosio" ); + + BOOST_REQUIRE_EQUAL( fees_before + fee, fees_after ); + BOOST_REQUIRE_EQUAL( rex_before, rex_after ); + BOOST_REQUIRE_EQUAL( eosio_before - fee, eosio_after ); + +} FC_LOG_AND_RETHROW() + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/eosio.msig_tests.cpp b/tests/eosio.msig_tests.cpp index a6de8707..7ff3d4a1 100644 --- a/tests/eosio.msig_tests.cpp +++ b/tests/eosio.msig_tests.cpp @@ -19,7 +19,7 @@ using mvo = fc::mutable_variant_object; class eosio_msig_tester : public tester { public: eosio_msig_tester() { - create_accounts( { "eosio.msig"_n, "eosio.stake"_n, "eosio.ram"_n, "eosio.ramfee"_n, "alice"_n, "bob"_n, "carol"_n } ); + create_accounts( { "eosio.msig"_n, "eosio.stake"_n, "eosio.ram"_n, "eosio.ramfee"_n, "eosio.fees"_n, "alice"_n, "bob"_n, "carol"_n } ); produce_block(); auto trace = base_tester::push_action(config::system_account_name, "setpriv"_n, @@ -448,7 +448,8 @@ BOOST_FIXTURE_TEST_CASE( update_system_contract_all_approve, eosio_msig_tester ) create_account_with_resources( "carol1111111"_n, "eosio"_n, core_sym::from_string("1.0000"), false ); BOOST_REQUIRE_EQUAL( core_sym::from_string("1000000000.0000"), - get_balance(config::system_account_name) + get_balance("eosio.ramfee"_n) + get_balance("eosio.stake"_n) + get_balance("eosio.ram"_n) ); + get_balance(config::system_account_name) + get_balance("eosio.ramfee"_n) + get_balance("eosio.stake"_n) + + get_balance("eosio.ram"_n) + get_balance("eosio.fees"_n) ); vector perm = { { "alice"_n, config::active_name }, { "bob"_n, config::active_name }, {"carol"_n, config::active_name} }; @@ -568,7 +569,8 @@ BOOST_FIXTURE_TEST_CASE( update_system_contract_major_approve, eosio_msig_tester create_account_with_resources( "carol1111111"_n, "eosio"_n, core_sym::from_string("1.0000"), false ); BOOST_REQUIRE_EQUAL( core_sym::from_string("1000000000.0000"), - get_balance(config::system_account_name) + get_balance("eosio.ramfee"_n) + get_balance("eosio.stake"_n) + get_balance("eosio.ram"_n) ); + get_balance(config::system_account_name) + get_balance("eosio.ramfee"_n) + get_balance("eosio.stake"_n) + + get_balance("eosio.ram"_n) + get_balance("eosio.fees"_n) ); vector perm = { { "alice"_n, config::active_name }, { "bob"_n, config::active_name }, {"carol"_n, config::active_name}, {"apple"_n, config::active_name}}; diff --git a/tests/eosio.powerup_tests.cpp b/tests/eosio.powerup_tests.cpp index fc7c0d71..d9c0b8a2 100644 --- a/tests/eosio.powerup_tests.cpp +++ b/tests/eosio.powerup_tests.cpp @@ -646,9 +646,6 @@ BOOST_AUTO_TEST_CASE(rent_tests) try { BOOST_REQUIRE_EQUAL( t.wasm_assert_msg("max_payment is less than calculated fee: 3000000.0000 TST"), // t.powerup("bob111111111"_n, "alice1111111"_n, 30, powerup_frac, powerup_frac, asset::from_string("1.0000 TST"))); - BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("can't channel fees to rex"), // - t.powerup("bob111111111"_n, "alice1111111"_n, 30, powerup_frac, powerup_frac, - asset::from_string("3000000.0000 TST"))); } // net:100%, cpu:100% diff --git a/tests/eosio.system_ram_tests.cpp b/tests/eosio.system_ram_tests.cpp index b5b3e298..e88fa5bb 100644 --- a/tests/eosio.system_ram_tests.cpp +++ b/tests/eosio.system_ram_tests.cpp @@ -82,7 +82,8 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_ram_validate, eosio_system_tester ) try { "receiver": "alice", "quantity": "0.1462 TST", "bytes_purchased": 9991, - "ram_bytes": 17983 + "ram_bytes": 17983, + "fee": "0.0008 TST" } )====="; validate_buyrambytes_return(alice, alice, 10000, @@ -93,7 +94,8 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_ram_validate, eosio_system_tester ) try { "account": "alice", "quantity": "0.1455 TST", "bytes_sold": 10000, - "ram_bytes": 7983 + "ram_bytes": 7983, + "fee": "0.0008 TST" } )====="; validate_sellram_return(alice, 10000, @@ -105,7 +107,9 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_ram_validate, eosio_system_tester ) try { "receiver": "alice", "quantity": "2.0000 TST", "bytes_purchased": 136750, - "ram_bytes": 144733 + "ram_bytes": 144733, + "fee": "0.0100 TST" + } )====="; validate_buyram_return(bob, alice, core_sym::from_string("2.0000"), @@ -131,7 +135,8 @@ BOOST_FIXTURE_TEST_CASE( ram_burn, eosio_system_tester ) try { "receiver": "bob", "quantity": "10.0000 TST", "bytes_purchased": 683747, - "ram_bytes": 691739 + "ram_bytes": 691739, + "fee": "0.0500 TST" } )====="; validate_buyramself_return(bob, core_sym::from_string("10.0000"), @@ -153,7 +158,8 @@ BOOST_FIXTURE_TEST_CASE( ram_burn, eosio_system_tester ) try { "to": "eosio.null", "bytes": 1, "from_ram_bytes": 691738, - "to_ram_bytes": 12992 + "to_ram_bytes": 12992, + "fee": "1.0000 TST" } )====="; validate_ramburn_return(bob, 1, "burn RAM memo", @@ -161,6 +167,45 @@ BOOST_FIXTURE_TEST_CASE( ram_burn, eosio_system_tester ) try { } FC_LOG_AND_RETHROW() +// buyramburn +BOOST_FIXTURE_TEST_CASE( buy_ram_burn, eosio_system_tester ) try { + const std::vector accounts = { "alice"_n }; + const account_name alice = accounts[0]; + const account_name null_account = "eosio.null"_n; + + const char* expected_buyramburn_return_data = R"=====( +{ + "payer": "alice", + "receiver": "alice", + "quantity": "1.0000 TST", + "bytes_purchased": 68374, + "ram_bytes": 86357, + "fee": "0.0050 TST" +} +)====="; + + create_accounts_with_resources( accounts ); + transfer( config::system_account_name, alice, core_sym::from_string("100.0000"), config::system_account_name ); + + BOOST_REQUIRE_EQUAL( success(), buyrambytes( alice, alice, 10000 ) ); + BOOST_REQUIRE_EQUAL( success(), buyrambytes( alice, null_account, 10000 ) ); + + const uint64_t null_before_buyramburn = get_total_stake( null_account )["ram_bytes"].as_uint64(); + const uint64_t alice_before_buyramburn = get_total_stake( alice )["ram_bytes"].as_uint64(); + const asset initial_alice_balance = get_balance(alice); + const asset ten_core_token = core_sym::from_string("10.0000"); + + // buy ram burn action + BOOST_REQUIRE_EQUAL( success(), buyramburn( alice, ten_core_token, "burn RAM burn memo" ) ); + const uint64_t alice_after_buyramburn = get_total_stake( alice )["ram_bytes"].as_uint64(); + const uint64_t null_after_buyramburn = get_total_stake( null_account )["ram_bytes"].as_uint64(); + BOOST_REQUIRE_EQUAL( alice_before_buyramburn, alice_after_buyramburn ); + BOOST_REQUIRE_EQUAL( true, null_before_buyramburn < null_after_buyramburn ); + BOOST_REQUIRE_EQUAL( initial_alice_balance - ten_core_token, get_balance(alice)); + + validate_buyramburn_return(alice, core_sym::from_string("1.0000"), "burn RAM memo", + "action_return_buyram", expected_buyramburn_return_data ); +} FC_LOG_AND_RETHROW() // buyramself BOOST_FIXTURE_TEST_CASE( buy_ram_self, eosio_system_tester ) try { @@ -180,7 +225,8 @@ BOOST_FIXTURE_TEST_CASE( buy_ram_self, eosio_system_tester ) try { "receiver": "alice", "quantity": "2.0000 TST", "bytes_purchased": 136750, - "ram_bytes": 213117 + "ram_bytes": 213117, + "fee": "0.0100 TST" } )====="; diff --git a/tests/eosio.system_rex_matured_tests.cpp b/tests/eosio.system_rex_matured_tests.cpp new file mode 100644 index 00000000..c7699bcd --- /dev/null +++ b/tests/eosio.system_rex_matured_tests.cpp @@ -0,0 +1,103 @@ +#include + +#include "eosio.system_tester.hpp" + +using namespace eosio_system; + +BOOST_AUTO_TEST_SUITE(eosio_system_rex_tests) + +bool within_error(int64_t a, int64_t b, int64_t err) { return std::abs(a - b) <= err; }; +bool within_one(int64_t a, int64_t b) { return within_error(a, b, 1); } + +BOOST_FIXTURE_TEST_CASE( buy_sell_matured_rex, eosio_system_tester ) try { + // @param num_of_maturity_buckets - used to calculate maturity time of purchase REX tokens from end of the day UTC. + // @param sell_matured_rex - if true, matured REX is sold immediately. + // @param buy_rex_to_savings - if true, buying REX is moved immediately to REX savings. + // + // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/132 + // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 + // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 + + // setup accounts + const int64_t ratio = 10000; + const asset init_rent = core_sym::from_string("20000.0000"); + const asset init_balance = core_sym::from_string("1000.0000"); + const std::vector accounts = { "alice"_n, "bob"_n, "charly"_n, "david"_n, "mark"_n }; + account_name alice = accounts[0], bob = accounts[1], charly = accounts[2], david = accounts[3], mark = accounts[4]; + setup_rex_accounts( accounts, init_balance ); + + + // 1. set `num_of_maturity_buckets=21` to test increasing maturity time of buying REX tokens. + BOOST_REQUIRE_EQUAL( success(), setrexmature( 21, false, false ) ); + BOOST_REQUIRE_EQUAL( asset::from_string("25000.0000 REX"), get_buyrex_result( alice, core_sym::from_string("2.5000") ) ); + produce_blocks(2); + produce_block(fc::days(5)); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("insufficient available rex"), sellrex( alice, asset::from_string("25000.0000 REX") ) ); + produce_block(fc::days(16)); + + BOOST_REQUIRE_EQUAL( core_sym::from_string("2.5000"), get_sellrex_result( alice, asset::from_string("25000.0000 REX") ) ); + BOOST_REQUIRE_EQUAL( success(), buyrex( alice, core_sym::from_string("13.0000") ) ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("13.0000"), get_rex_vote_stake( alice ) ); + BOOST_REQUIRE_EQUAL( success(), buyrex( alice, core_sym::from_string("17.0000") ) ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("30.0000"), get_rex_vote_stake( alice ) ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("970.0000"), get_rex_fund(alice) ); + BOOST_REQUIRE_EQUAL( get_rex_balance(alice).get_amount(), ratio * asset::from_string("30.0000 REX").get_amount() ); + auto rex_pool = get_rex_pool(); + BOOST_REQUIRE_EQUAL( core_sym::from_string("30.0000"), rex_pool["total_lendable"].as() ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("30.0000"), rex_pool["total_unlent"].as() ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), rex_pool["total_lent"].as() ); + BOOST_REQUIRE_EQUAL( init_rent, rex_pool["total_rent"].as() ); + BOOST_REQUIRE_EQUAL( get_rex_balance(alice), rex_pool["total_rex"].as() ); + + // 2. set `sell_matured_rex=true` and `buy_rex_to_savings=false` to test buying REX without moving it to REX savings + BOOST_REQUIRE_EQUAL( success(), setrexmature( 21, true, false ) ); + BOOST_REQUIRE_EQUAL( asset::from_string("25000.0000 REX"), get_buyrex_result( bob, core_sym::from_string("2.5000") ) ); + produce_blocks(2); + produce_block(fc::days(5)); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("insufficient available rex"), sellrex( bob, asset::from_string("25000.0000 REX") ) ); + produce_block(fc::days(16)); + BOOST_REQUIRE_EQUAL( core_sym::from_string("2.5000"), get_rex_vote_stake( bob ) ); + BOOST_REQUIRE_EQUAL( asset::from_string("10000.0000 REX"), get_buyrex_result( bob, core_sym::from_string("1.0000") ) ); // will also triggers sell matured REX + BOOST_REQUIRE_EQUAL( core_sym::from_string("1.0000"), get_rex_vote_stake( bob ) ); + + // 3. set `sell_matured_rex=false` and `buy_rex_to_savings=true` to test selling matured REX + BOOST_REQUIRE_EQUAL( success(), setrexmature( 21, false, true ) ); + BOOST_REQUIRE_EQUAL( asset::from_string("25000.0000 REX"), get_buyrex_result( charly, core_sym::from_string("2.5000") ) ); // when buying REX, it will automatically be moved to REX savings + BOOST_REQUIRE_EQUAL( success(), mvfrsavings( charly, asset::from_string("25000.0000 REX") ) ); // move REX from savings to initiate matured REX unstaking process + produce_blocks(2); + produce_block(fc::days(5)); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("insufficient available rex"), sellrex( charly, asset::from_string("25000.0000 REX") ) ); + produce_block(fc::days(16)); + BOOST_REQUIRE_EQUAL( success(), updaterex( charly ) ); // triggers sell matured REX (any REX action causes sell matured REX) + BOOST_REQUIRE_EQUAL( core_sym::from_string("2.5000"), get_sellrex_result( charly, asset::from_string("25000.0000 REX") ) ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_rex_vote_stake( charly ) ); + BOOST_REQUIRE_EQUAL( init_balance, get_rex_fund( charly ) ); + + // 4. legacy holders with matured REX + BOOST_REQUIRE_EQUAL( success(), setrexmature( 5, false, false ) ); + BOOST_REQUIRE_EQUAL( asset::from_string("25000.0000 REX"), get_buyrex_result( david, core_sym::from_string("2.5000") ) ); // legacy 5 days maturity + BOOST_REQUIRE_EQUAL( asset::from_string("25000.0000 REX"), get_buyrex_result( mark, core_sym::from_string("2.5000") ) ); + produce_blocks(2); + produce_block(fc::days(5)); + BOOST_REQUIRE_EQUAL( success(), setrexmature( 21, true, true ) ); + BOOST_REQUIRE_EQUAL( asset::from_string("10000.0000 REX"), get_buyrex_result( david, core_sym::from_string("1.0000") ) ); // new 21 days maturity & triggers sell matured REX + + // 4.1. Test selling less than all their matured rex, and having all of their already matured rex sold regardless + BOOST_REQUIRE_EQUAL( core_sym::from_string("2.5000"), get_sellrex_result( mark, asset::from_string("1.0000 REX") ) ); + + BOOST_REQUIRE_EQUAL( success(), mvfrsavings( david, asset::from_string("10000.0000 REX") ) ); // must move REX from savings to initiate matured REX unstaking process + BOOST_REQUIRE_EQUAL( wasm_assert_msg("insufficient available rex"), sellrex( david, asset::from_string("25000.0000 REX") ) ); // already sold when previously buying REX + produce_blocks(2); + produce_block(fc::days(5)); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("insufficient available rex"), sellrex( david, asset::from_string("10000.0000 REX") ) ); // 21 day REX not matured yet + produce_blocks(2); + produce_block(fc::days(21)); + BOOST_REQUIRE_EQUAL( core_sym::from_string("1.0000"), get_sellrex_result( david, asset::from_string("10000.0000 REX") ) ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_rex_vote_stake( david ) ); + BOOST_REQUIRE_EQUAL( init_balance, get_rex_fund( david ) ); + + + +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp new file mode 100644 index 00000000..05746db0 --- /dev/null +++ b/tests/eosio.system_schedules_tests.cpp @@ -0,0 +1,100 @@ +#include +#include "eosio.system_tester.hpp" + +using namespace eosio_system; + +BOOST_AUTO_TEST_SUITE(eosio_system_vesting_tests) + +BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { + + + auto check_schedule = [&](time_point_sec time, double rate){ + auto schedule = get_vesting_schedule(time.sec_since_epoch()); + REQUIRE_MATCHING_OBJECT( schedule, mvo() + ("start_time", time) + ("continuous_rate", rate) + ); + }; + + const std::vector accounts = { "alice"_n }; + create_accounts_with_resources( accounts ); + const account_name alice = accounts[0]; + + const uint32_t YEAR = 86400 * 365; + uint32_t initial_start_time = control->pending_block_time().sec_since_epoch(); // 1577836853 (2020-01-01T00:00:56.000Z) + time_point_sec start_time = time_point_sec(initial_start_time); + + + BOOST_REQUIRE_EQUAL( wasm_assert_msg("continuous_rate can't be over 100%"), setschedule(start_time, 1.00001) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("continuous_rate can't be negative"), setschedule(start_time, -1) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("schedule not found"), delschedule(start_time) ); + + // action validation + BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(0), 0.05) ); + BOOST_REQUIRE_EQUAL( success(), setschedule(start_time, 0.05) ); + check_schedule(start_time, 0.05); + + // allow override existing schedules + BOOST_REQUIRE_EQUAL( success(), setschedule(start_time, 0.02) ); + check_schedule(start_time, 0.02); + + + // Should be able to delete schedules, even in the past + BOOST_REQUIRE_EQUAL( success(), delschedule(start_time) ); + BOOST_REQUIRE_EQUAL( success(), delschedule(time_point_sec(0)) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("schedule not found"), delschedule(start_time) ); + + // Resetting timers to make math clean + initial_start_time = control->pending_block_time().sec_since_epoch(); + start_time = time_point_sec(initial_start_time); + + + // // set 4 future schedules + BOOST_REQUIRE_EQUAL( success(), setschedule(start_time, 0.02) ); + check_schedule(start_time, 0.02); + BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 4), 0.01) ); + check_schedule(time_point_sec(initial_start_time + YEAR * 4), 0.01); + BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 8), 0.005) ); + check_schedule(time_point_sec(initial_start_time + YEAR * 8), 0.005); + BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 12), 0.0005) ); + check_schedule(time_point_sec(initial_start_time + YEAR * 12), 0.0005); + BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 13), 0) ); + check_schedule(time_point_sec(initial_start_time + YEAR * 13), 0); + + // current state prior to first schedule change execution + const double before = get_global_state4()["continuous_rate"].as_double(); + BOOST_REQUIRE_EQUAL( before, 0.048790164169432007 ); // 5% continuous rate + + BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); + BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.02 ); + + // Cannot execute schedule before its time is due + // (we did 6 actions so we're late 3s late) + auto late = fc::seconds(3); + produce_block( fc::seconds(YEAR * 4) - fc::seconds(1) - late ); // advance to year 4 + BOOST_REQUIRE_EQUAL( wasm_assert_msg("no schedule to execute"), execschedule(alice) ); + + // Can execute this schedule 1 second after, as that is its time + produce_block( fc::seconds(1) ); // advance to year 4 + BOOST_REQUIRE_EQUAL( control->pending_block_time().sec_since_epoch(), initial_start_time + YEAR * 4 ); // 2024-01-01T00:00 + BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); + BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.01 ); // 1% continuous rate + + produce_block( fc::days(365 * 4) ); // advanced to year 8 + BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); + BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.005 ); // 0.5% continuous rate + + produce_block( fc::days(365 * 4) ); // advanced to year 12 + BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); + BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.0005 ); // 0.05% continuous rate + + produce_block( fc::days(365 * 1) ); // advanced to year 13 + BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); + BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.0 ); // 0% continuous rate + + // no more schedules + BOOST_REQUIRE_EQUAL( wasm_assert_msg("no schedule to execute"), execschedule(alice) ); + +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index aa9c9413..724c56ff 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -24,10 +24,11 @@ class eosio_system_tester : public validating_tester { produce_block(); create_accounts({ "eosio.token"_n, "eosio.ram"_n, "eosio.ramfee"_n, "eosio.stake"_n, - "eosio.bpay"_n, "eosio.vpay"_n, "eosio.saving"_n, "eosio.names"_n, "eosio.rex"_n }); + "eosio.bpay"_n, "eosio.vpay"_n, "eosio.saving"_n, "eosio.names"_n, "eosio.rex"_n, "eosio.fees"_n }); - produce_block(); + produce_blocks( 100 ); + set_code( "eosio.token"_n, contracts::token_wasm()); set_abi( "eosio.token"_n, contracts::token_abi().data() ); { @@ -36,6 +37,17 @@ class eosio_system_tester : public validating_tester { BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(accnt.abi, abi), true); token_abi_ser.set_abi(abi, abi_serializer::create_yield_function(abi_serializer_max_time)); } + + set_code( "eosio.fees"_n, contracts::fees_wasm()); + + set_code( "eosio.bpay"_n, contracts::bpay_wasm()); + set_abi( "eosio.bpay"_n, contracts::bpay_abi().data() ); + { + const auto& accnt = control->db().get( "eosio.bpay"_n ); + abi_def abi; + BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(accnt.abi, abi), true); + bpay_abi_ser.set_abi(abi, abi_serializer::create_yield_function(abi_serializer_max_time)); + } } void create_core_token( symbol core_symbol = symbol{CORE_SYM} ) { @@ -72,7 +84,7 @@ class eosio_system_tester : public validating_tester { create_account_with_resources( "bob111111111"_n, config::system_account_name, core_sym::from_string("0.4500"), false ); create_account_with_resources( "carol1111111"_n, config::system_account_name, core_sym::from_string("1.0000"), false ); - BOOST_REQUIRE_EQUAL( core_sym::from_string("1000000000.0000"), get_balance("eosio") + get_balance("eosio.ramfee") + get_balance("eosio.stake") + get_balance("eosio.ram") ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("1000000000.0000"), get_balance("eosio") + get_balance("eosio.ramfee") + get_balance("eosio.stake") + get_balance("eosio.ram") + get_balance("eosio.fees") ); } enum class setup_level { @@ -252,7 +264,11 @@ class eosio_system_tester : public validating_tester { { "name": "ram_bytes", "type": "int64" - } + }, + { + "name": "fee", + "type": "asset" + } ] }, { @@ -300,7 +316,11 @@ class eosio_system_tester : public validating_tester { { "name": "ram_bytes", "type": "int64" - } + }, + { + "name": "fee", + "type": "asset" + } ] }, ], @@ -421,6 +441,35 @@ class eosio_system_tester : public validating_tester { return ramburn(account_name(owner), bytes, memo); } + action_result buyramburn( const name& payer, const asset& quantity, const std::string& memo) + { + return push_action(payer, "buyramburn"_n, mvo()("payer", payer)("quantity", quantity)("memo", memo)); + } + + void validate_buyramburn_return(const name& payer, const asset& quantity, + const std::string& memo, const type_name& type, const std::string& json) { + // create hex return from provided json + std::string expected_hex = convert_json_to_hex(type, json); + // initialize string that will hold actual return + std::string actual_hex; + + // execute transaction and get traces must use base_tester + auto trace = base_tester::push_action(config::system_account_name, "buyramburn"_n, payer, + mvo()("payer",payer)("quantity",quantity)("memo", memo)); + produce_block(); + + // confirm we have trances and find the right one (should be trace idx == 0) + BOOST_REQUIRE_EQUAL(true, chain_has_transaction(trace->id)); + + // the first trace always has the return value + int i = 0; + std::string copy_trace = std::string(trace->action_traces[i].return_value.begin(), trace->action_traces[i].return_value.end()); + actual_hex = convert_ordinals_to_hex(copy_trace); + + // test fails here actual_hex is + BOOST_REQUIRE_EQUAL(expected_hex,actual_hex); + } + void validate_ramburn_return(const account_name& owner, uint32_t bytes, const std::string& memo, const type_name& type, const std::string& json) { // create hex return from provided json @@ -636,6 +685,13 @@ class eosio_system_tester : public validating_tester { ("unstake_cpu_quantity", cpu) ); } + action_result unvest( const account_name& account, const asset& net, const asset& cpu ) { + return push_action( "eosio"_n, "unvest"_n, mvo() + ("account", account) + ("unvest_net_quantity", net) + ("unvest_cpu_quantity", cpu) + ); + } action_result unstake( std::string_view from, std::string_view to, const asset& net, const asset& cpu ) { return unstake( account_name(from), account_name(to), net, cpu ); } @@ -733,13 +789,14 @@ class eosio_system_tester : public validating_tester { asset get_sellrex_result( const account_name& from, const asset& rex ) { auto trace = base_tester::push_action( config::system_account_name, "sellrex"_n, from, mvo()("from", from)("rex", rex) ); - asset proceeds; + asset proceeds = core_sym::from_string("0.0000"); for ( size_t i = 0; i < trace->action_traces.size(); ++i ) { if ( trace->action_traces[i].act.name == "sellresult"_n ) { + asset _action_proceeds; fc::raw::unpack( trace->action_traces[i].act.data.data(), trace->action_traces[i].act.data.size(), - proceeds ); - return proceeds; + _action_proceeds ); + proceeds += _action_proceeds; } } return proceeds; @@ -867,6 +924,18 @@ class eosio_system_tester : public validating_tester { return push_action( name(owner), "closerex"_n, mvo()("owner", owner) ); } + action_result setrexmature(const std::optional num_of_maturity_buckets, const std::optional sell_matured_rex, const std::optional buy_rex_to_savings ) { + return push_action( "eosio"_n, "setrexmature"_n, mvo()("num_of_maturity_buckets", num_of_maturity_buckets)("sell_matured_rex", sell_matured_rex)("buy_rex_to_savings", buy_rex_to_savings) ); + } + + action_result donatetorex( const account_name& payer, const asset& quantity, const std::string& memo ) { + return push_action( name(payer), "donatetorex"_n, mvo() + ("payer", payer) + ("quantity", quantity) + ("memo", memo) + ); + } + fc::variant get_last_loan(bool cpu) { vector data; const auto& db = control->db(); @@ -1150,14 +1219,36 @@ class eosio_system_tester : public validating_tester { base_tester::push_action(contract, "create"_n, contract, act ); } - void issue( const asset& amount, const name& manager = config::system_account_name ) { - base_tester::push_action( "eosio.token"_n, "issue"_n, manager, mutable_variant_object() - ("to", manager ) - ("quantity", amount ) + void issue( const asset& quantity, const name& to = config::system_account_name ) { + base_tester::push_action( "eosio.token"_n, "issue"_n, to, mutable_variant_object() + ("to", to ) + ("quantity", quantity ) + ("memo", "") + ); + } + + void retire( const asset& quantity, const name& issuer = config::system_account_name ) { + base_tester::push_action( "eosio.token"_n, "retire"_n, issuer, mutable_variant_object() + ("quantity", quantity ) ("memo", "") ); } + void issuefixed( const asset& supply, const name& to = config::system_account_name ) { + base_tester::push_action( "eosio.token"_n, "issuefixed"_n, to, mutable_variant_object() + ("to", to ) + ("supply", supply ) + ("memo", "") + ); + } + + void setmaxsupply( const asset& maximum_supply, const name& issuer = config::system_account_name) { + base_tester::push_action( "eosio.token"_n, "setmaxsupply"_n, issuer, mutable_variant_object() + ("issuer", issuer ) + ("maximum_supply", maximum_supply ) + ); + } + void transfer( const name& from, const name& to, const asset& amount, const name& manager = config::system_account_name ) { base_tester::push_action( "eosio.token"_n, "transfer"_n, manager, mutable_variant_object() ("from", from) @@ -1257,6 +1348,11 @@ class eosio_system_tester : public validating_tester { return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "eosio_global_state3", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); } + fc::variant get_global_state4() { + vector data = get_row_by_account( config::system_account_name, config::system_account_name, "global4"_n, "global4"_n ); + return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "eosio_global_state4", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + fc::variant get_refund_request( name account ) { vector data = get_row_by_account( config::system_account_name, account, "refunds"_n, account ); return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "refund_request", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); @@ -1393,8 +1489,54 @@ class eosio_system_tester : public validating_tester { ); } + action_result setpayfactor( int64_t inflation_pay_factor, int64_t votepay_factor ) { + return push_action( "eosio"_n, "setpayfactor"_n, mvo() + ("inflation_pay_factor", inflation_pay_factor) + ("votepay_factor", votepay_factor) + ); + } + + action_result setschedule( const time_point_sec start_time, double continuous_rate ) { + return push_action( "eosio"_n, "setschedule"_n, mvo() + ("start_time", start_time) + ("continuous_rate", continuous_rate) + ); + } + + action_result delschedule( const time_point_sec start_time ) { + return push_action( "eosio"_n, "delschedule"_n, mvo() + ("start_time", start_time) + ); + } + + action_result execschedule( const name executor ) { + return push_action( executor, "execschedule"_n, mvo()); + } + + fc::variant get_vesting_schedule( uint64_t time ) { + vector data = get_row_by_account( "eosio"_n, "eosio"_n, "schedules"_n, account_name(time) ); + return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "schedules_info", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + + + + action_result bpay_claimrewards( const account_name owner ) { + action act; + act.account = "eosio.bpay"_n; + act.name = "claimrewards"_n; + act.data = abi_ser.variant_to_binary( bpay_abi_ser.get_action_type("claimrewards"_n), mvo()("owner", owner), abi_serializer::create_yield_function(abi_serializer_max_time) ); + + return base_tester::push_action( std::move(act), owner.to_uint64_t() ); + } + + fc::variant get_bpay_rewards( account_name producer ) { + vector data = get_row_by_account( "eosio.bpay"_n, "eosio.bpay"_n, "rewards"_n, producer ); + return data.empty() ? fc::variant() : bpay_abi_ser.binary_to_variant( "rewards_row", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); + } + abi_serializer abi_ser; abi_serializer token_abi_ser; + abi_serializer bpay_abi_ser; }; inline fc::mutable_variant_object voter( account_name acct ) { diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index fdc6cd95..d576cdf1 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -44,11 +44,11 @@ BOOST_FIXTURE_TEST_CASE( buysell, eosio_system_tester ) try { auto init_bytes = total["ram_bytes"].as_uint64(); const asset initial_ram_balance = get_balance("eosio.ram"_n); - const asset initial_ramfee_balance = get_balance("eosio.ramfee"_n); + const asset initial_fees_balance = get_balance("eosio.fees"_n); BOOST_REQUIRE_EQUAL( success(), buyram( "alice1111111", "alice1111111", core_sym::from_string("200.0000") ) ); BOOST_REQUIRE_EQUAL( core_sym::from_string("800.0000"), get_balance( "alice1111111" ) ); BOOST_REQUIRE_EQUAL( initial_ram_balance + core_sym::from_string("199.0000"), get_balance("eosio.ram"_n) ); - BOOST_REQUIRE_EQUAL( initial_ramfee_balance + core_sym::from_string("1.0000"), get_balance("eosio.ramfee"_n) ); + BOOST_REQUIRE_EQUAL( initial_fees_balance + core_sym::from_string("1.0000"), get_balance("eosio.fees"_n) ); total = get_total_stake( "alice1111111" ); auto bytes = total["ram_bytes"].as_uint64(); @@ -1619,7 +1619,7 @@ BOOST_FIXTURE_TEST_CASE(producer_pay, eosio_system_tester, * boost::unit_test::t // defproducerb tries to claim rewards but he's not on the list { - BOOST_REQUIRE_EQUAL(wasm_assert_msg("unable to find key"), + BOOST_REQUIRE_EQUAL(wasm_assert_msg("producer not registered"), push_action("defproducerb"_n, "claimrewards"_n, mvo()("owner", "defproducerb"))); } @@ -1645,6 +1645,27 @@ BOOST_FIXTURE_TEST_CASE(producer_pay, eosio_system_tester, * boost::unit_test::t BOOST_REQUIRE(500 * 10000 > int64_t(double(initial_supply.get_amount()) * double(0.05)) - (supply.get_amount() - initial_supply.get_amount())); BOOST_REQUIRE(500 * 10000 > int64_t(double(initial_supply.get_amount()) * double(0.04)) - (savings - initial_savings)); } + + // test claimrewards when max supply is reached + { + produce_block(fc::hours(24)); + + const asset before_supply = get_token_supply(); + const asset before_system_balance = get_balance(config::system_account_name); + const asset before_producer_balance = get_balance("defproducera"_n); + + setmaxsupply( before_supply ); + BOOST_REQUIRE_EQUAL(success(), push_action("defproducera"_n, "claimrewards"_n, mvo()("owner", "defproducera"))); + + const asset after_supply = get_token_supply(); + const asset after_system_balance = get_balance(config::system_account_name); + const asset after_producer_balance = get_balance("defproducera"_n); + + BOOST_REQUIRE_EQUAL(after_supply.get_amount(), before_supply.get_amount()); + BOOST_REQUIRE_EQUAL(after_system_balance.get_amount() - before_system_balance.get_amount(), -1407793756); + BOOST_REQUIRE_EQUAL(after_producer_balance.get_amount() - before_producer_balance.get_amount(), 281558751); + } + } FC_LOG_AND_RETHROW() BOOST_AUTO_TEST_SUITE_END() @@ -1661,6 +1682,13 @@ BOOST_FIXTURE_TEST_CASE(change_inflation, eosio_system_tester) try { setinflation(1, 9999, 10000) ); BOOST_REQUIRE_EQUAL( wasm_assert_msg("votepay_factor must not be less than 10000"), setinflation(1, 10000, 9999) ); + + BOOST_REQUIRE_EQUAL( success(), + setpayfactor(10000, 10000) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("inflation_pay_factor must not be less than 10000"), + setpayfactor(9999, 10000) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("votepay_factor must not be less than 10000"), + setpayfactor(10000, 9999) ); } { @@ -1748,7 +1776,8 @@ BOOST_FIXTURE_TEST_CASE(change_inflation, eosio_system_tester) try { BOOST_AUTO_TEST_CASE(extreme_inflation) try { eosio_system_tester t(eosio_system_tester::setup_level::minimal); symbol core_symbol{CORE_SYM}; - t.create_currency( "eosio.token"_n, config::system_account_name, asset((1ll << 62) - 1, core_symbol) ); + const asset max_supply = asset((1ll << 62) - 1, core_symbol); + t.create_currency( "eosio.token"_n, config::system_account_name, max_supply ); t.issue( asset(10000000000000, core_symbol) ); t.deploy_contract(); t.produce_block(); @@ -1762,17 +1791,22 @@ BOOST_AUTO_TEST_CASE(extreme_inflation) try { BOOST_REQUIRE_EQUAL(t.success(), t.push_action("defproducera"_n, "claimrewards"_n, mvo()("owner", "defproducera"))); t.produce_block(); - asset current_supply; - { - vector data = t.get_row_by_account( "eosio.token"_n, name(core_symbol.to_symbol_code().value), "stat"_n, account_name(core_symbol.to_symbol_code().value) ); - current_supply = t.token_abi_ser.binary_to_variant("currency_stats", data, abi_serializer::create_yield_function(eosio_system_tester::abi_serializer_max_time))["supply"].template as(); - } - t.issue( asset((1ll << 62) - 1, core_symbol) - current_supply ); + const asset current_supply = t.get_token_supply(); + t.issue( max_supply - current_supply ); + + // empty system balance + // claimrewards operates by either `issue` new tokens or using the existing system balance + const asset system_balance = t.get_balance(config::system_account_name); + t.transfer( config::system_account_name, "eosio.null"_n, system_balance, config::system_account_name); + BOOST_REQUIRE_EQUAL(t.get_balance(config::system_account_name).get_amount(), 0); + BOOST_REQUIRE_EQUAL(t.get_token_supply().get_amount() - max_supply.get_amount(), 0); + + // set maximum inflation BOOST_REQUIRE_EQUAL(t.success(), t.setinflation(std::numeric_limits::max(), 50000, 40000)); t.produce_block(); t.produce_block(fc::hours(10*24)); - BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("quantity exceeds available supply"), t.push_action("defproducera"_n, "claimrewards"_n, mvo()("owner", "defproducera"))); + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("insufficient system token balance for claiming rewards"), t.push_action("defproducera"_n, "claimrewards"_n, mvo()("owner", "defproducera"))); t.produce_block(fc::hours(11*24)); BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("magnitude of asset amount must be less than 2^62"), t.push_action("defproducera"_n, "claimrewards"_n, mvo()("owner", "defproducera"))); @@ -1780,7 +1814,7 @@ BOOST_AUTO_TEST_CASE(extreme_inflation) try { t.produce_block(fc::hours(24)); BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("overflow in calculating new tokens to be issued; inflation rate is too high"), t.push_action("defproducera"_n, "claimrewards"_n, mvo()("owner", "defproducera"))); BOOST_REQUIRE_EQUAL(t.success(), t.setinflation(500, 50000, 40000)); - BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("quantity exceeds available supply"), t.push_action("defproducera"_n, "claimrewards"_n, mvo()("owner", "defproducera"))); + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("insufficient system token balance for claiming rewards"), t.push_action("defproducera"_n, "claimrewards"_n, mvo()("owner", "defproducera"))); } FC_LOG_AND_RETHROW() BOOST_AUTO_TEST_SUITE_END() @@ -3048,6 +3082,8 @@ BOOST_FIXTURE_TEST_CASE( voters_actions_affect_proxy_and_producers, eosio_system } FC_LOG_AND_RETHROW() +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(eosio_system_part4_tests) BOOST_FIXTURE_TEST_CASE( vote_both_proxy_and_producers, eosio_system_tester ) try { //alice1111111 becomes a proxy @@ -3734,6 +3770,9 @@ BOOST_FIXTURE_TEST_CASE( wasmcfg, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( active_params.max_table_elements, 8192 ); } FC_LOG_AND_RETHROW() +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(eosio_system_part5_tests) + BOOST_FIXTURE_TEST_CASE( setram_effect, eosio_system_tester ) try { const asset net = core_sym::from_string("8.0000"); @@ -3836,7 +3875,7 @@ BOOST_FIXTURE_TEST_CASE( eosioram_ramusage, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( success(), stake( "eosio", "alice1111111", core_sym::from_string("200.0000"), core_sym::from_string("100.0000") ) ); const asset initial_ram_balance = get_balance("eosio.ram"_n); - const asset initial_ramfee_balance = get_balance("eosio.ramfee"_n); + const asset initial_fees_balance = get_balance("eosio.fees"_n); BOOST_REQUIRE_EQUAL( success(), buyram( "alice1111111", "alice1111111", core_sym::from_string("1000.0000") ) ); BOOST_REQUIRE_EQUAL( false, get_row_by_account( "eosio.token"_n, "alice1111111"_n, "accounts"_n, account_name(symbol{CORE_SYM}.to_symbol_code()) ).empty() ); @@ -3906,7 +3945,7 @@ BOOST_FIXTURE_TEST_CASE( ram_gift, eosio_system_tester ) try { } FC_LOG_AND_RETHROW() BOOST_AUTO_TEST_SUITE_END() -BOOST_AUTO_TEST_SUITE(eosio_system_rex_tests) +BOOST_AUTO_TEST_SUITE(eosio_system_origin_rex_tests) BOOST_FIXTURE_TEST_CASE( rex_rounding_issue, eosio_system_tester ) try { const std::vector whales { "whale1"_n, "whale2"_n, "whale3"_n, "whale4"_n , "whale5"_n }; @@ -4047,6 +4086,8 @@ BOOST_FIXTURE_TEST_CASE( rex_auth, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( error(error_msg), push_action( bob, "mvtosavings"_n, mvo()("owner", alice)("rex", one_rex) ) ); BOOST_REQUIRE_EQUAL( error(error_msg), push_action( bob, "mvfrsavings"_n, mvo()("owner", alice)("rex", one_rex) ) ); BOOST_REQUIRE_EQUAL( error(error_msg), push_action( bob, "closerex"_n, mvo()("owner", alice) ) ); + BOOST_REQUIRE_EQUAL( error(error_msg), + push_action( bob, "donatetorex"_n, mvo()("payer", alice)("quantity", one_eos)("memo", "") ) ); BOOST_REQUIRE_EQUAL( error("missing authority of eosio"), push_action( alice, "setrex"_n, mvo()("balance", one_eos) ) ); @@ -4188,8 +4229,6 @@ BOOST_FIXTURE_TEST_CASE( unstake_buy_rex, eosio_system_tester, * boost::unit_tes BOOST_REQUIRE_EQUAL( get_net_limit( alice ), init_net_limit + net_stake.get_amount() ); BOOST_REQUIRE_EQUAL( success(), vote( alice, std::vector(producer_names.begin(), producer_names.begin() + 20) ) ); - BOOST_REQUIRE_EQUAL( wasm_assert_msg("must vote for at least 21 producers or for a proxy before buying REX"), - unstaketorex( alice, alice, net_stake, cpu_stake ) ); BOOST_REQUIRE_EQUAL( success(), vote( alice, std::vector(producer_names.begin(), producer_names.begin() + 21) ) ); const asset init_eosio_stake_balance = get_balance( "eosio.stake"_n ); @@ -4748,18 +4787,18 @@ BOOST_FIXTURE_TEST_CASE( ramfee_namebid_to_rex, eosio_system_tester ) try { account_name alice = accounts[0], bob = accounts[1], carol = accounts[2], emily = accounts[3], frank = accounts[4]; setup_rex_accounts( accounts, init_balance, core_sym::from_string("80.0000"), core_sym::from_string("80.0000"), false ); - asset cur_ramfee_balance = get_balance( "eosio.ramfee"_n ); + asset cur_fees_balance = get_balance( "eosio.fees"_n ); BOOST_REQUIRE_EQUAL( success(), buyram( alice, alice, core_sym::from_string("20.0000") ) ); - BOOST_REQUIRE_EQUAL( get_balance( "eosio.ramfee"_n ), core_sym::from_string("0.1000") + cur_ramfee_balance ); + BOOST_REQUIRE_EQUAL( get_balance( "eosio.fees"_n ), core_sym::from_string("0.1000") + cur_fees_balance ); BOOST_REQUIRE_EQUAL( wasm_assert_msg("must deposit to REX fund first"), buyrex( alice, core_sym::from_string("350.0000") ) ); BOOST_REQUIRE_EQUAL( success(), deposit( alice, core_sym::from_string("350.0000") ) ); BOOST_REQUIRE_EQUAL( success(), buyrex( alice, core_sym::from_string("350.0000") ) ); - cur_ramfee_balance = get_balance( "eosio.ramfee"_n ); + cur_fees_balance = get_balance( "eosio.fees"_n ); asset cur_rex_balance = get_balance( "eosio.rex"_n ); BOOST_REQUIRE_EQUAL( core_sym::from_string("350.0000"), cur_rex_balance ); BOOST_REQUIRE_EQUAL( success(), buyram( bob, carol, core_sym::from_string("70.0000") ) ); - BOOST_REQUIRE_EQUAL( cur_ramfee_balance, get_balance( "eosio.ramfee"_n ) ); + BOOST_REQUIRE_EQUAL( cur_fees_balance, get_balance( "eosio.fees"_n ) ); BOOST_REQUIRE_EQUAL( get_balance( "eosio.rex"_n ), cur_rex_balance + core_sym::from_string("0.3500") ); cur_rex_balance = get_balance( "eosio.rex"_n ); @@ -4793,7 +4832,7 @@ BOOST_FIXTURE_TEST_CASE( ramfee_namebid_to_rex, eosio_system_tester ) try { produce_block( fc::hours(24) ); produce_blocks( 2 ); - BOOST_REQUIRE_EQUAL( core_sym::from_string("29.3500"), get_rex_pool()["namebid_proceeds"].as() ); + BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_rex_pool()["namebid_proceeds"].as() ); BOOST_REQUIRE_EQUAL( success(), deposit( frank, core_sym::from_string("5.0000") ) ); BOOST_REQUIRE_EQUAL( success(), buyrex( frank, core_sym::from_string("5.0000") ) ); BOOST_REQUIRE_EQUAL( get_balance( "eosio.rex"_n ), cur_rex_balance + core_sym::from_string("34.3500") ); @@ -5211,8 +5250,6 @@ BOOST_FIXTURE_TEST_CASE( update_rex, eosio_system_tester, * boost::unit_test::to } } - BOOST_REQUIRE_EQUAL( wasm_assert_msg("voter holding REX tokens must vote for at least 21 producers or for a proxy"), - vote( alice, std::vector(producer_names.begin(), producer_names.begin() + 20) ) ); BOOST_REQUIRE_EQUAL( success(), vote( alice, std::vector(producer_names.begin(), producer_names.begin() + 21) ) ); @@ -5248,8 +5285,6 @@ BOOST_FIXTURE_TEST_CASE( update_rex, eosio_system_tester, * boost::unit_test::to BOOST_REQUIRE_EQUAL( success(), sellrex( alice, get_rex_balance( alice ) ) ); BOOST_REQUIRE_EQUAL( 0, get_rex_balance( alice ).get_amount() ); BOOST_REQUIRE_EQUAL( success(), vote( alice, { producer_names[0], producer_names[4] } ) ); - BOOST_REQUIRE_EQUAL( wasm_assert_msg("must vote for at least 21 producers or for a proxy before buying REX"), - buyrex( alice, core_sym::from_string("1.0000") ) ); } FC_LOG_AND_RETHROW() @@ -5469,6 +5504,33 @@ BOOST_FIXTURE_TEST_CASE( close_rex, eosio_system_tester ) try { } FC_LOG_AND_RETHROW() +BOOST_FIXTURE_TEST_CASE( donate_to_rex, eosio_system_tester ) try { + + const asset init_balance = core_sym::from_string("10000.0000"); + const std::vector accounts = { "aliceaccount"_n, "bobbyaccount"_n }; + account_name alice = accounts[0], bob = accounts[1]; + setup_rex_accounts( accounts, init_balance ); + issue_and_transfer( bob, core_sym::from_string("1000.0000"), config::system_account_name ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg("rex system is not initialized"), + donatetorex( bob, core_sym::from_string("500.0000"), "") ); + BOOST_REQUIRE_EQUAL( success(), buyrex( alice, init_balance ) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("quantity must be core token"), + donatetorex( bob, asset::from_string("100 TKN"), "") ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "quantity must be positive" ), + donatetorex( bob, core_sym::from_string("-100.0000"), "") ); + + BOOST_REQUIRE_EQUAL( success(), donatetorex( bob, core_sym::from_string("100.0000"), "") ); + + + for (int i = 0; i < 4; ++i) { + const asset rex_balance = get_balance("eosio.rex"_n); + const int64_t rex_proceeds = get_rex_return_pool()["proceeds"].as(); + BOOST_REQUIRE_EQUAL( success(), donatetorex( bob, core_sym::from_string("100.0000"), "") ); + BOOST_REQUIRE_EQUAL( rex_balance + core_sym::from_string("100.0000"), get_balance("eosio.rex"_n) ); + BOOST_REQUIRE_EQUAL( rex_proceeds + 1000000, get_rex_return_pool()["proceeds"].as() ); + } +} FC_LOG_AND_RETHROW() BOOST_FIXTURE_TEST_CASE( set_rex, eosio_system_tester ) try { @@ -5534,41 +5596,71 @@ BOOST_FIXTURE_TEST_CASE( b1_vesting, eosio_system_tester ) try { create_accounts_with_resources( { b1 }, alice ); const asset stake_amount = core_sym::from_string("50000000.0000"); - const asset final_amount = core_sym::from_string("17664825.5000"); - const asset small_amount = core_sym::from_string("1000.0000"); issue_and_transfer( b1, stake_amount + stake_amount + stake_amount, config::system_account_name ); stake( b1, b1, stake_amount, stake_amount ); BOOST_REQUIRE_EQUAL( 2 * stake_amount.get_amount(), get_voter_info( b1 )["staked"].as() ); - BOOST_REQUIRE_EQUAL( success(), unstake( b1, b1, small_amount, small_amount ) ); + // The code has changed since the tests were originally written, and B1's vesting is no longer based + // on the time of the block, but is a fixed amount instead. + // The total amount of possible vested is 35329651.2515, meaning there is 64670348.7485 + // left which will not be vested. + // These tests now reflect the new behavior. - produce_block( fc::days(4) ); + const asset vested = core_sym::from_string("35329651.2515"); + const asset unvestable = core_sym::from_string("64670348.7485"); + const asset oneToken = core_sym::from_string("1.0000"); + const asset zero = core_sym::from_string("0.0000"); - BOOST_REQUIRE_EQUAL( success(), push_action( b1, "refund"_n, mvo()("owner", b1) ) ); + BOOST_REQUIRE_EQUAL( error("missing authority of eosio"), vote( b1, { }, "proxyaccount"_n ) ); - BOOST_REQUIRE_EQUAL( 2 * ( stake_amount.get_amount() - small_amount.get_amount() ), + // Can't take what isn't vested + BOOST_REQUIRE_EQUAL( + wasm_assert_msg("b1 can only claim what has already vested"), + unstake( b1, b1, stake_amount, stake_amount ) + ); + BOOST_REQUIRE_EQUAL( + wasm_assert_msg("b1 can only claim what has already vested"), + unstake( b1, b1, stake_amount, zero ) + ); + + // Taking the vested amount - 1 token + BOOST_REQUIRE_EQUAL( success(), unstake( b1, b1, vested-oneToken, zero ) ); + produce_block( fc::days(4) ); + BOOST_REQUIRE_EQUAL( success(), push_action( b1, "refund"_n, mvo()("owner", b1) ) ); + BOOST_REQUIRE_EQUAL(unvestable.get_amount() + oneToken.get_amount(), get_voter_info( b1 )["staked"].as() ); - - BOOST_REQUIRE_EQUAL( wasm_assert_msg("b1 can only claim their tokens over 10 years"), - unstake( b1, b1, final_amount, final_amount ) ); - BOOST_REQUIRE_EQUAL( wasm_assert_msg("must vote for at least 21 producers or for a proxy before buying REX"), - unstaketorex( b1, b1, final_amount - small_amount, final_amount - small_amount ) ); + // Can't take 2 tokens, only 1 is vested + BOOST_REQUIRE_EQUAL( + wasm_assert_msg("b1 can only claim what has already vested"), + unstake( b1, b1, oneToken, oneToken ) + ); - BOOST_REQUIRE_EQUAL( error("missing authority of eosio"), vote( b1, { }, "proxyaccount"_n ) ); + // Can't unvest the 1 token, as it's already unvested + BOOST_REQUIRE_EQUAL( + wasm_assert_msg("can only unvest what is not already vested"), + unvest( b1, (stake_amount - vested) + oneToken, stake_amount ) + ); - BOOST_REQUIRE_EQUAL( success(), unstake( b1, b1, final_amount - small_amount, final_amount - small_amount ) ); - - produce_block( fc::days(4) ); + auto supply_before = get_token_supply(); - BOOST_REQUIRE_EQUAL( success(), push_action( b1, "refund"_n, mvo()("owner", b1) ) ); + // Unvesting the remaining unvested tokens + BOOST_REQUIRE_EQUAL( success(), unvest( b1, stake_amount - vested, stake_amount ) ); + BOOST_REQUIRE_EQUAL(oneToken.get_amount(), get_voter_info( b1 )["staked"].as() ); - produce_block( fc::days( 5 * 364 ) ); + // Should have retired the unvestable tokens + BOOST_REQUIRE_EQUAL( + get_token_supply(), + supply_before-unvestable + ); - BOOST_REQUIRE_EQUAL( wasm_assert_msg("b1 can only claim their tokens over 10 years"), - unstake( b1, b1, small_amount, small_amount ) ); + // B1 can take the last token, even after unvesting has occurred + BOOST_REQUIRE_EQUAL( success(), unstake( b1, b1, oneToken, zero ) ); + produce_block( fc::days(4) ); + BOOST_REQUIRE_EQUAL( success(), push_action( b1, "refund"_n, mvo()("owner", b1) ) ); + BOOST_REQUIRE_EQUAL(0, get_voter_info( b1 )["staked"].as() ); } FC_LOG_AND_RETHROW() diff --git a/tests/eosio.token_tests.cpp b/tests/eosio.token_tests.cpp index d469ce01..eb42aaec 100644 --- a/tests/eosio.token_tests.cpp +++ b/tests/eosio.token_tests.cpp @@ -78,6 +78,21 @@ class eosio_token_tester : public tester { ); } + action_result issuefixed( account_name to, asset supply, string memo ) { + return push_action( to, "issuefixed"_n, mvo() + ( "to", to) + ( "supply", supply) + ( "memo", memo) + ); + } + + action_result setmaxsupply( account_name issuer, asset maximum_supply ) { + return push_action( issuer, "setmaxsupply"_n, mvo() + ( "issuer", issuer) + ( "maximum_supply", maximum_supply) + ); + } + action_result retire( account_name issuer, asset quantity, string memo ) { return push_action( issuer, "retire"_n, mvo() ( "quantity", quantity) @@ -238,6 +253,78 @@ BOOST_FIXTURE_TEST_CASE( issue_tests, eosio_token_tester ) try { issue( "alice"_n, asset::from_string("1.000 TKN"), "hola" ) ); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( issuefixed_tests, eosio_token_tester ) try { + + auto token = create( "alice"_n, asset::from_string("1000.000 TKN")); + produce_blocks(1); + + issue( "alice"_n, asset::from_string("200.000 TKN"), "issue active supply" ); + + issuefixed( "alice"_n, asset::from_string("1000.000 TKN"), "issue max supply" ); + + auto stats = get_stats("3,TKN"); + REQUIRE_MATCHING_OBJECT( stats, mvo() + ("supply", "1000.000 TKN") + ("max_supply", "1000.000 TKN") + ("issuer", "alice") + ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "symbol precision mismatch" ), + issuefixed( "alice"_n, asset::from_string("1 TKN"), "" ) + ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "tokens can only be issued to issuer account" ), + issuefixed( "bob"_n, asset::from_string("1.000 TKN"), "" ) + ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "must issue positive quantity" ), + issuefixed( "alice"_n, asset::from_string("500.000 TKN"), "" ) + ); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( setmaxsupply_tests, eosio_token_tester ) try { + + auto token = create( "alice"_n, asset::from_string("1000.000 TKN")); + produce_blocks(1); + + issue( "alice"_n, asset::from_string("1000.000 TKN"), "issue active supply" ); + + auto stats = get_stats("3,TKN"); + REQUIRE_MATCHING_OBJECT( stats, mvo() + ("supply", "1000.000 TKN") + ("max_supply", "1000.000 TKN") + ("issuer", "alice") + ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "quantity exceeds available supply" ), + issue( "alice"_n, asset::from_string("1000.000 TKN"), "quantity exceeds available supply" ) + ); + + setmaxsupply( "alice"_n, asset::from_string("2000.000 TKN") ); + + issue( "alice"_n, asset::from_string("1000.000 TKN"), "issue active supply" ); + + stats = get_stats("3,TKN"); + REQUIRE_MATCHING_OBJECT( stats, mvo() + ("supply", "2000.000 TKN") + ("max_supply", "2000.000 TKN") + ("issuer", "alice") + ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "symbol precision mismatch" ), + setmaxsupply( "alice"_n, asset::from_string("3000 TKN") ) + ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "only issuer can set token maximum supply" ), + setmaxsupply( "bob"_n, asset::from_string("1000.000 TKN") ) + ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg( "max supply is less than available supply" ), + setmaxsupply( "alice"_n, asset::from_string("1000.000 TKN") ) + ); } FC_LOG_AND_RETHROW()