From 47e44bd5a6eefa6731da3d7108aef61eb17f1554 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Thu, 2 May 2024 16:22:11 +0200 Subject: [PATCH 01/37] push fees upgrade changes --- contracts/CMakeLists.txt | 1 + contracts/eosio.fees/CMakeLists.txt | 11 +++ .../include/eosio.fees/eosio.fees.hpp | 35 +++++++++ contracts/eosio.fees/src/eosio.fees.cpp | 21 ++++++ .../include/eosio.system/eosio.system.hpp | 75 +++++++++++++++---- .../ricardian/eosio.system.contracts.md.in | 15 ++++ .../eosio.system/src/delegate_bandwidth.cpp | 42 ++++++++--- contracts/eosio.system/src/eosio.system.cpp | 11 ++- contracts/eosio.system/src/powerup.cpp | 6 +- contracts/eosio.system/src/producer_pay.cpp | 7 +- contracts/eosio.system/src/rex.cpp | 72 +++++++----------- tests/contracts.hpp.in | 1 + tests/eosio.fees_tests.cpp | 53 +++++++++++++ tests/eosio.msig_tests.cpp | 8 +- tests/eosio.powerup_tests.cpp | 3 - tests/eosio.system_ram_tests.cpp | 44 +++++++++-- tests/eosio.system_tester.hpp | 30 +++++++- tests/eosio.system_tests.cpp | 39 ++++++++-- 18 files changed, 373 insertions(+), 101 deletions(-) create mode 100644 contracts/eosio.fees/CMakeLists.txt create mode 100644 contracts/eosio.fees/include/eosio.fees/eosio.fees.hpp create mode 100644 contracts/eosio.fees/src/eosio.fees.cpp create mode 100644 tests/eosio.fees_tests.cpp diff --git a/contracts/CMakeLists.txt b/contracts/CMakeLists.txt index d48cae0e..86cd4775 100644 --- a/contracts/CMakeLists.txt +++ b/contracts/CMakeLists.txt @@ -51,5 +51,6 @@ add_subdirectory(eosio.msig) add_subdirectory(eosio.system) add_subdirectory(eosio.token) add_subdirectory(eosio.wrap) +add_subdirectory(eosio.fees) add_subdirectory(test_contracts) 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 d3dfc4b7..61b741dc 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -550,6 +550,7 @@ namespace eosiosystem { asset quantity; int64_t bytes_sold; int64_t ram_bytes; + asset fee; }; struct action_return_buyram { @@ -558,6 +559,7 @@ namespace eosiosystem { asset quantity; int64_t bytes_purchased; int64_t ram_bytes; + asset fee; }; struct action_return_ramtransfer { @@ -732,6 +734,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); @@ -745,8 +749,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: @@ -823,6 +840,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 /** @@ -1079,6 +1106,17 @@ namespace eosiosystem { [[eosio::action]] void closerex( const name& owner ); + /** + * 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 @@ -1152,9 +1190,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 @@ -1173,9 +1212,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. @@ -1198,6 +1238,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 * @@ -1482,6 +1533,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>; @@ -1502,6 +1554,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>; @@ -1510,6 +1563,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>; @@ -1534,19 +1588,12 @@ namespace eosiosystem { using powerup_action = eosio::action_wrapper<"powerup"_n, &system_contract::powerup>; 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 ); // defined in rex.cpp void runrex( uint16_t max ); @@ -1556,8 +1603,6 @@ namespace eosiosystem { 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 @@ -1567,8 +1612,6 @@ 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(); asset add_to_rex_balance( const name& owner, const asset& payment, const asset& rex_received ); asset add_to_rex_pool( const asset& payment ); diff --git a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in index d82a209b..3890f490 100644 --- a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in +++ b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in @@ -454,6 +454,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

--- diff --git a/contracts/eosio.system/src/delegate_bandwidth.cpp b/contracts/eosio.system/src/delegate_bandwidth.cpp index f9696edc..437a0f0e 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 ) { diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 527b0324..2506c51f 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -53,7 +53,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; } @@ -109,6 +109,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 @@ -390,6 +395,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 ); 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..3e4d0550 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; }); diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index 99542c13..804233fb 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -353,6 +353,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 * @@ -453,7 +473,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 +566,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 +766,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; } @@ -915,44 +931,6 @@ namespace eosiosystem { return rex_in_sell_order; } - /** - * @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 * of the day UTC diff --git a/tests/contracts.hpp.in b/tests/contracts.hpp.in index ba33a662..7280e795 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"); } 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 ef5c6396..9085f973 100644 --- a/tests/eosio.powerup_tests.cpp +++ b/tests/eosio.powerup_tests.cpp @@ -633,9 +633,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..106e364a 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,31 @@ 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; + + 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)); +} FC_LOG_AND_RETHROW() // buyramself BOOST_FIXTURE_TEST_CASE( buy_ram_self, eosio_system_tester ) try { @@ -180,7 +211,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_tester.hpp b/tests/eosio.system_tester.hpp index 93a8eb24..65e69344 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -32,11 +32,12 @@ class eosio_system_tester : public TESTER { produce_blocks( 2 ); 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_blocks( 100 ); set_code( "eosio.token"_n, contracts::token_wasm()); + set_code( "eosio.fees"_n, contracts::fees_wasm()); set_abi( "eosio.token"_n, contracts::token_abi().data() ); { const auto& accnt = control->db().get( "eosio.token"_n ); @@ -80,7 +81,7 @@ class eosio_system_tester : public 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 { @@ -259,7 +260,11 @@ class eosio_system_tester : public TESTER { { "name": "ram_bytes", "type": "int64" - } + }, + { + "name": "fee", + "type": "asset" + } ] }, { @@ -307,7 +312,11 @@ class eosio_system_tester : public TESTER { { "name": "ram_bytes", "type": "int64" - } + }, + { + "name": "fee", + "type": "asset" + } ] }, ], @@ -428,6 +437,11 @@ class eosio_system_tester : public 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_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 @@ -874,6 +888,14 @@ class eosio_system_tester : public TESTER { return push_action( name(owner), "closerex"_n, mvo()("owner", owner) ); } + 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(); diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 37d8e9cf..80099c00 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -40,11 +40,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(); @@ -3816,7 +3816,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() ); @@ -4024,6 +4024,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) ) ); @@ -4725,18 +4727,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 ); @@ -4770,7 +4772,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") ); @@ -5446,6 +5448,27 @@ 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"), "") ); + + const asset initial_eosio_rex_balance = get_balance("eosio.rex"_n); + BOOST_REQUIRE_EQUAL( success(), donatetorex( bob, core_sym::from_string("500.0000"), "") ); + BOOST_REQUIRE_EQUAL( initial_eosio_rex_balance + core_sym::from_string("500.0000"), get_balance("eosio.rex"_n) ); + +} FC_LOG_AND_RETHROW() BOOST_FIXTURE_TEST_CASE( set_rex, eosio_system_tester ) try { From e4c75b8eb7f175eb05010bae17bdc1c053fc6899 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Thu, 2 May 2024 16:29:34 +0200 Subject: [PATCH 02/37] push token upgrade --- contracts/eosio.system/src/producer_pay.cpp | 16 +++- .../include/eosio.token/eosio.token.hpp | 50 +++++++++--- .../ricardian/eosio.token.contracts.md.in | 32 ++++++++ contracts/eosio.token/src/eosio.token.cpp | 27 +++++++ tests/eosio.system_tester.hpp | 30 ++++++- tests/eosio.system_tests.cpp | 47 ++++++++--- tests/eosio.token_tests.cpp | 79 +++++++++++++++++++ 7 files changed, 255 insertions(+), 26 deletions(-) diff --git a/contracts/eosio.system/src/producer_pay.cpp b/contracts/eosio.system/src/producer_pay.cpp index e1a98dd0..9766ca7d 100644 --- a/contracts/eosio.system/src/producer_pay.cpp +++ b/contracts/eosio.system/src/producer_pay.cpp @@ -76,7 +76,7 @@ namespace eosiosystem { void system_contract::claimrewards( const name& owner ) { require_auth( owner ); - const auto& prod = _producers.get( owner.value ); + 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 +87,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,9 +103,17 @@ 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} } }; 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/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 93a8eb24..890818a1 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -1157,14 +1157,36 @@ class eosio_system_tester : public 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) diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 37d8e9cf..2502d56e 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -1612,7 +1612,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"))); } @@ -1638,6 +1638,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(), 0); + 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_FIXTURE_TEST_CASE(change_inflation, eosio_system_tester) try { @@ -1738,7 +1759,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(); @@ -1752,17 +1774,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"))); @@ -1770,7 +1797,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_FIXTURE_TEST_CASE(multiple_producer_pay, eosio_system_tester, * boost::unit_test::tolerance(1e-10)) try { diff --git a/tests/eosio.token_tests.cpp b/tests/eosio.token_tests.cpp index d469ce01..1968c7fd 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,70 @@ 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"), "hola" ) + ); + +} 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() From b694722bf2ab193fc53867b193717c4b3a6c9955 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Fri, 3 May 2024 13:58:37 +0200 Subject: [PATCH 03/37] Implement rex maturity changes ref: https://github.com/eosnetworkfoundation/eos-system-contracts/issues/132 --- .../include/eosio.system/eosio.system.hpp | 19 ++++++++++++++++++- contracts/eosio.system/src/eosio.system.cpp | 3 ++- contracts/eosio.system/src/rex.cpp | 19 +++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index d3dfc4b7..ddaf7384 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -539,6 +539,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; + + EOSLIB_SERIALIZE( rex_maturity, (num_of_maturity_buckets) ) + }; + + typedef eosio::singleton<"rexmaturity"_n, rex_maturity> rex_maturity_singleton; + struct rex_order_outcome { bool success; asset proceeds; @@ -720,6 +728,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}; @@ -1079,6 +1088,14 @@ 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. + */ + [[eosio::action]] + void rexmaturity(const uint32_t num_of_maturity_buckets); + /** * 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 @@ -1569,7 +1586,7 @@ namespace eosiosystem { 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 ); diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 527b0324..ab8c7183 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -30,7 +30,8 @@ namespace eosiosystem { _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{}; diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index 99542c13..d1198b0c 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -8,6 +8,20 @@ namespace eosiosystem { using eosio::token; using eosio::seconds; + void system_contract::rexmaturity(const uint32_t num_of_maturity_buckets) + { + require_auth(get_self()); + + 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"); + + auto state = _rexmaturity.get_or_default(); + if ( _rexmaturity.exists() ) check(state.num_of_maturity_buckets != num_of_maturity_buckets, "num_of_maturity_buckets is the same as the current value"); + + state.num_of_maturity_buckets = num_of_maturity_buckets; + _rexmaturity.set(state, get_self()); + } + void system_contract::deposit( const name& owner, const asset& amount ) { require_auth( owner ); @@ -959,9 +973,10 @@ namespace eosiosystem { * * @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 }; From a8afceebd5c39cfe6f52ea9fb3733ba7687c0a78 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Fri, 3 May 2024 14:09:36 +0200 Subject: [PATCH 04/37] Remove voting requirements for rex ref: https://github.com/eosnetworkfoundation/eos-system-contracts/issues/133 --- .../include/eosio.system/eosio.system.hpp | 8 -------- contracts/eosio.system/src/rex.cpp | 15 --------------- contracts/eosio.system/src/voting.cpp | 4 ---- tests/eosio.system_tests.cpp | 9 --------- 4 files changed, 36 deletions(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index ddaf7384..5e514e89 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -891,9 +891,6 @@ 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. @@ -910,9 +907,6 @@ 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. @@ -1569,8 +1563,6 @@ namespace eosiosystem { 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 ); diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index d1198b0c..46679c1c 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -57,7 +57,6 @@ 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 ); @@ -75,7 +74,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 ); @@ -425,19 +423,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 * diff --git a/contracts/eosio.system/src/voting.cpp b/contracts/eosio.system/src/voting.cpp index 9f47d23b..d360ef82 100644 --- a/contracts/eosio.system/src/voting.cpp +++ b/contracts/eosio.system/src/voting.cpp @@ -208,10 +208,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/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 37d8e9cf..90890aa2 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -4165,8 +4165,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 ); @@ -5188,8 +5186,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) ) ); @@ -5225,8 +5221,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() @@ -5531,9 +5525,6 @@ BOOST_FIXTURE_TEST_CASE( b1_vesting, eosio_system_tester ) try { 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 ) ); - BOOST_REQUIRE_EQUAL( error("missing authority of eosio"), vote( b1, { }, "proxyaccount"_n ) ); BOOST_REQUIRE_EQUAL( success(), unstake( b1, b1, final_amount - small_amount, final_amount - small_amount ) ); From 67c2429985dc0f8640089b026f9dee440381f7f9 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Fri, 3 May 2024 14:55:20 +0200 Subject: [PATCH 05/37] Move to savings when buying REX ref: https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 --- contracts/eosio.system/src/rex.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index 46679c1c..ab334094 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -62,6 +62,8 @@ namespace eosiosystem { 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 ); + mvtosavings( from, rex_received ); + // 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 ); From 6d791adc113c081894fe582d088d15c7175ab569 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Fri, 3 May 2024 16:18:03 +0200 Subject: [PATCH 06/37] Move to savings when buying REX & sell REX when matured ref: https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 & https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 --- .../include/eosio.system/eosio.system.hpp | 10 ++++++++++ contracts/eosio.system/src/rex.cpp | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index 5e514e89..930c95bb 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -24,6 +24,15 @@ // be set to 0. #define CHANNEL_RAM_AND_NAMEBID_FEES_TO_REX 1 +#ifdef MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS +#undef MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS +#endif +// MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS macro determines whether matured REX is sold immediately and buying REX is moved immediately to REX savings. +// In order to enable this behavior, the macro must be set to 1. +// https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 +// https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 +#define MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS 0 + namespace eosiosystem { using eosio::asset; @@ -1588,6 +1597,7 @@ namespace eosiosystem { 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 ); diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index ab334094..b1d883fa 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -62,7 +62,10 @@ namespace eosiosystem { 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 ); - mvtosavings( from, rex_received ); + + #if MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS + mvtosavings( from, rex_received ); + #endif // 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{ } ); @@ -103,6 +106,11 @@ 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 ); + + #if MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS + mvtosavings( owner, rex_received ); + #endif + // 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 ); @@ -111,7 +119,11 @@ namespace eosiosystem { void system_contract::sellrex( const name& from, const asset& rex ) { require_auth( from ); + sell_rex( from, rex ); + } + void system_contract::sell_rex( const name& from, const asset& rex ) + { runrex(2); auto bitr = _rexbalance.require_find( from.value, "user must first buyrex" ); @@ -983,6 +995,11 @@ namespace eosiosystem { rb.matured_rex += rb.rex_maturities.front().second; rb.rex_maturities.erase(rb.rex_maturities.begin()); } + #if MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS + if ( rb.matured_rex > 0 ) { + sell_rex(rb.owner, asset(rb.matured_rex, rex_symbol)); + } + #endif }); } From d664b5a365dfb2181f5162034cbffb73cd98f722 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Fri, 3 May 2024 23:09:41 +0200 Subject: [PATCH 07/37] implement unvest action ref: https://github.com/eosnetworkfoundation/eos-system-contracts/issues/131 --- .../include/eosio.system/eosio.system.hpp | 14 +- .../eosio.system/src/delegate_bandwidth.cpp | 182 ++++++++++-------- tests/eosio.system_tester.hpp | 7 + tests/eosio.system_tests.cpp | 8 + 4 files changed, 132 insertions(+), 79 deletions(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index d3dfc4b7..4cc766aa 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -1429,6 +1429,16 @@ namespace eosiosystem { [[eosio::action]] void setinflation( int64_t annual_rate, int64_t inflation_pay_factor, int64_t votepay_factor ); + /** + * 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. @@ -1588,10 +1598,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/src/delegate_bandwidth.cpp b/contracts/eosio.system/src/delegate_bandwidth.cpp index f9696edc..3c774bb4 100644 --- a/contracts/eosio.system/src/delegate_bandwidth.cpp +++ b/contracts/eosio.system/src/delegate_bandwidth.cpp @@ -264,77 +264,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 +337,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 ); + } + } + + 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_voting_power( const name& voter, const asset& total_update ) + 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 + } + + 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 +430,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 +462,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 +475,30 @@ 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() ); + + const asset stake_delta = unvest_net_quantity + unvest_cpu_quantity; + asset zero_asset( 0, core_symbol() ); + check( unvest_cpu_quantity >= zero_asset, "must unvest a positive amount" ); + check( unvest_net_quantity >= zero_asset, "must unvest a positive amount" ); + check( stake_delta.amount > 0, "must unvest a positive amount" ); + check( account == "b1"_n, "only b1 account can unvest"); + + // reduce staked from account + update_voting_power( account, -stake_delta ); + 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(), stake_delta, "unvest" ); + + // retire unvested tokens + token::retire_action retire_act{ token_account, { {"eosio"_n, active_permission} } }; + retire_act.send( stake_delta, "unvest" ); + } // unvest } //namespace eosiosystem diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 93a8eb24..691a6b11 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -643,6 +643,13 @@ class eosio_system_tester : public 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 ); } diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 37d8e9cf..c90f54ad 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -5547,6 +5547,14 @@ BOOST_FIXTURE_TEST_CASE( b1_vesting, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( wasm_assert_msg("b1 can only claim their tokens over 10 years"), unstake( b1, b1, small_amount, small_amount ) ); + const int64_t before = get_voter_info( b1 )["staked"].as(); + BOOST_REQUIRE_EQUAL( before, 646703490000 ); + + BOOST_REQUIRE_EQUAL( success(), unvest( b1, stake_amount - final_amount, stake_amount - final_amount ) ); + const int64_t after = get_voter_info( b1 )["staked"].as(); + + BOOST_REQUIRE_EQUAL( after, 0 ); + } FC_LOG_AND_RETHROW() From 7fb34dd6de2d75ace857106df47ddd1e330fc5ec Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Fri, 3 May 2024 23:16:50 +0200 Subject: [PATCH 08/37] add supply tests --- tests/eosio.system_tests.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index c90f54ad..5d35a278 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -5548,12 +5548,15 @@ BOOST_FIXTURE_TEST_CASE( b1_vesting, eosio_system_tester ) try { unstake( b1, b1, small_amount, small_amount ) ); const int64_t before = get_voter_info( b1 )["staked"].as(); + const asset before_supply = get_token_supply(); BOOST_REQUIRE_EQUAL( before, 646703490000 ); BOOST_REQUIRE_EQUAL( success(), unvest( b1, stake_amount - final_amount, stake_amount - final_amount ) ); const int64_t after = get_voter_info( b1 )["staked"].as(); + const asset after_supply = get_token_supply(); BOOST_REQUIRE_EQUAL( after, 0 ); + BOOST_REQUIRE_EQUAL( after_supply.get_amount() - before_supply.get_amount(), -646703490000 ); } FC_LOG_AND_RETHROW() From 812ffa6579f64738d03faa23a7dedcff10cbd88f Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Sat, 4 May 2024 16:24:00 +0200 Subject: [PATCH 09/37] Implement schedules ref: https://github.com/eosnetworkfoundation/eos-system-contracts/issues/136 --- .../include/eosio.system/eosio.system.hpp | 46 ++++++++++++++++ .../ricardian/eosio.system.contracts.md.in | 38 ++++++++++++- contracts/eosio.system/src/eosio.system.cpp | 47 ++++++++++++++++ contracts/eosio.system/src/producer_pay.cpp | 1 + tests/eosio.system_schedules_tests.cpp | 54 +++++++++++++++++++ tests/eosio.system_tester.hpp | 22 ++++++++ 6 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 tests/eosio.system_schedules_tests.cpp diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index 4cc766aa..dea99d85 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -197,6 +197,17 @@ 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; + int64_t annual_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)(annual_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}} }; } @@ -329,6 +340,7 @@ 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; @@ -713,6 +725,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; @@ -1429,6 +1442,35 @@ namespace eosiosystem { [[eosio::action]] void setinflation( int64_t annual_rate, 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 annual_rate - the annual inflation rate of the core token supply. + * (eg. For 5% Annual inflation => annual_rate=500 + * For 1.5% Annual inflation => annual_rate=150 + */ + [[eosio::action]] + void setschedule( const time_point_sec start_time, int64_t annual_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. * @@ -1542,6 +1584,9 @@ namespace eosiosystem { 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>; private: // Implementation details: @@ -1557,6 +1602,7 @@ namespace eosiosystem { static eosio_global_state4 get_default_inflation_parameters(); symbol core_symbol()const; void update_ram_supply(); + bool execute_next_schedule(); // defined in rex.cpp void runrex( uint16_t max ); diff --git a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in index d82a209b..a74ed9ec 100644 --- a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in +++ b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in @@ -752,4 +752,40 @@ 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}} +* Annual inflation rate (in units of a hundredth of a percent): {{annual_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. diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 527b0324..80a4cd65 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -24,6 +24,7 @@ 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), @@ -423,6 +424,52 @@ namespace eosiosystem { _global4.set( _gstate4, get_self() ); } + void system_contract::setschedule( const time_point_sec start_time, int64_t annual_rate ) + { + require_auth( get_self() ); + + check(annual_rate >= 0, "annual_rate can't be negative"); + + 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.annual_rate = annual_rate; + }); + } else { + _schedules.modify( itr, same_payer, [&]( auto& s ) { + check( annual_rate != s.annual_rate, "annual_rate was not modified"); + s.annual_rate = annual_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->annual_rate == 0 ) return false; // row is empty + if ( current_time_point().sec_since_epoch() >= itr->start_time.sec_since_epoch() ) { + _gstate4.continuous_rate = get_continuous_rate(itr->annual_rate); + _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/producer_pay.cpp b/contracts/eosio.system/src/producer_pay.cpp index e1a98dd0..0072472b 100644 --- a/contracts/eosio.system/src/producer_pay.cpp +++ b/contracts/eosio.system/src/producer_pay.cpp @@ -76,6 +76,7 @@ namespace eosiosystem { void system_contract::claimrewards( const name& owner ) { require_auth( owner ); + execute_next_schedule(); const auto& prod = _producers.get( owner.value ); check( prod.active(), "producer does not have an active key" ); diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp new file mode 100644 index 00000000..5d21ae90 --- /dev/null +++ b/tests/eosio.system_schedules_tests.cpp @@ -0,0 +1,54 @@ +#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 { + + const std::vector accounts = { "alice"_n }; + create_accounts_with_resources( accounts ); + const account_name alice = accounts[0]; + + const uint32_t YEAR = 86400 * 365; + const uint32_t initial_start_time = 1577836856; // 2020-01-01T00:00 + const time_point_sec start_time = time_point_sec(initial_start_time); + + // action validation + BOOST_REQUIRE_EQUAL( success(), setschedule(start_time, 500) ); // 5% annual rate + BOOST_REQUIRE_EQUAL( wasm_assert_msg("annual_rate was not modified"), setschedule(start_time, 500) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("annual_rate can't be negative"), setschedule(start_time, -1) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("schedule not found"), delschedule(time_point_sec(0)) ); + BOOST_REQUIRE_EQUAL( success(), setschedule(start_time, 200) ); // allow override existing schedules + BOOST_REQUIRE_EQUAL( success(), delschedule(start_time) ); + + // set 3 future schedules + BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(start_time), 200) ); + BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 4), 100) ); + BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 8), 50) ); + + // current state prior to first schedule + const double before = get_global_state4()["continuous_rate"].as_double(); + BOOST_REQUIRE_EQUAL( before, 0.048790164169432007 ); // 5% annual rate + const uint32_t current_time_sec = control->pending_block_time().sec_since_epoch(); + BOOST_REQUIRE_EQUAL( current_time_sec, initial_start_time ); // 2020-01-01T00:00 + + // advance to halvening schedules + produce_block( fc::days(365 * 4) ); // 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.019802627296179712 ); // 2% annual 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.0099503308531680833 ); // 1% annual 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.0049875415110390738 ); // 0.5% annual rate + +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 691a6b11..7a479ad2 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -1271,6 +1271,11 @@ class eosio_system_tester : public 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) ); @@ -1406,6 +1411,23 @@ class eosio_system_tester : public TESTER { ); } + action_result setschedule( const time_point_sec start_time, int64_t annual_rate ) { + return push_action( "eosio"_n, "setschedule"_n, mvo() + ("start_time", start_time) + ("annual_rate", annual_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()); + } + abi_serializer abi_ser; abi_serializer token_abi_ser; }; From 6e79c48ebc13eed612302ed35523540f6067a84c Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Sat, 4 May 2024 16:38:25 +0200 Subject: [PATCH 10/37] add schedule with 0% annual rate --- contracts/eosio.system/src/eosio.system.cpp | 3 ++- tests/eosio.system_schedules_tests.cpp | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 80a4cd65..edb07ecd 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -461,7 +461,8 @@ namespace eosiosystem { bool system_contract::execute_next_schedule() { auto itr = _schedules.begin(); - if ( itr->annual_rate == 0 ) return false; // row is empty + 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 = get_continuous_rate(itr->annual_rate); _schedules.erase( itr ); diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp index 5d21ae90..b617a525 100644 --- a/tests/eosio.system_schedules_tests.cpp +++ b/tests/eosio.system_schedules_tests.cpp @@ -24,10 +24,11 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { BOOST_REQUIRE_EQUAL( success(), setschedule(start_time, 200) ); // allow override existing schedules BOOST_REQUIRE_EQUAL( success(), delschedule(start_time) ); - // set 3 future schedules + // set 4 future schedules BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(start_time), 200) ); BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 4), 100) ); BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 8), 50) ); + BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 12), 0) ); // current state prior to first schedule const double before = get_global_state4()["continuous_rate"].as_double(); @@ -49,6 +50,10 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.0049875415110390738 ); // 0.5% annual rate + produce_block( fc::days(365 * 4) ); // advanced to year 16 + BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); + BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.0 ); // 0% annual rate + } FC_LOG_AND_RETHROW() BOOST_AUTO_TEST_SUITE_END() From 669d9664ba13b511a44bbf389ea2a064e3ec0687 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Sat, 4 May 2024 16:43:06 +0200 Subject: [PATCH 11/37] add no schedule to execute tests --- tests/eosio.system_schedules_tests.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp index b617a525..f3ec14e1 100644 --- a/tests/eosio.system_schedules_tests.cpp +++ b/tests/eosio.system_schedules_tests.cpp @@ -54,6 +54,9 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.0 ); // 0% annual 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() From 5c0256c806f8f343b588a24cd79d7bb27116a72a Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Wed, 8 May 2024 11:18:00 +0200 Subject: [PATCH 12/37] rename to "unallocated bucket" --- contracts/eosio.system/src/producer_pay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/eosio.system/src/producer_pay.cpp b/contracts/eosio.system/src/producer_pay.cpp index 3e4d0550..d2c972f3 100644 --- a/contracts/eosio.system/src/producer_pay.cpp +++ b/contracts/eosio.system/src/producer_pay.cpp @@ -113,7 +113,7 @@ namespace eosiosystem { { 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" ); From 32f62ab23b807ebe830db23c5af5bee2bbbf819b Mon Sep 17 00:00:00 2001 From: Nathan James Date: Wed, 8 May 2024 12:37:25 +0100 Subject: [PATCH 13/37] a few extra tests --- tests/eosio.system_tests.cpp | 2 +- tests/eosio.token_tests.cpp | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 2502d56e..68183f9a 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -1654,7 +1654,7 @@ BOOST_FIXTURE_TEST_CASE(producer_pay, eosio_system_tester, * boost::unit_test::t 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(), 0); + 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); } diff --git a/tests/eosio.token_tests.cpp b/tests/eosio.token_tests.cpp index 1968c7fd..eb42aaec 100644 --- a/tests/eosio.token_tests.cpp +++ b/tests/eosio.token_tests.cpp @@ -272,7 +272,15 @@ BOOST_FIXTURE_TEST_CASE( issuefixed_tests, eosio_token_tester ) try { ); BOOST_REQUIRE_EQUAL( wasm_assert_msg( "symbol precision mismatch" ), - issuefixed( "alice"_n, asset::from_string("1 TKN"), "hola" ) + 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() From 85ef99cc1d4f8f7fe12a0b84cbafc905313574b4 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Wed, 8 May 2024 15:59:34 +0200 Subject: [PATCH 14/37] make rex maturity settings as actions --- .../include/eosio.system/eosio.system.hpp | 20 +++--- contracts/eosio.system/src/rex.cpp | 66 ++++++++++++++----- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index 930c95bb..1906c1bc 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -24,15 +24,6 @@ // be set to 0. #define CHANNEL_RAM_AND_NAMEBID_FEES_TO_REX 1 -#ifdef MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS -#undef MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS -#endif -// MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS macro determines whether matured REX is sold immediately and buying REX is moved immediately to REX savings. -// In order to enable this behavior, the macro must be set to 1. -// https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 -// https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 -#define MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS 0 - namespace eosiosystem { using eosio::asset; @@ -550,8 +541,8 @@ namespace eosiosystem { struct [[eosio::table("rexmaturity"),eosio::contract("eosio.system")]] rex_maturity { uint32_t num_of_maturity_buckets = 5; - - EOSLIB_SERIALIZE( rex_maturity, (num_of_maturity_buckets) ) + bool sell_matured_rex = false; + bool buy_rex_to_savings = false; }; typedef eosio::singleton<"rexmaturity"_n, rex_maturity> rex_maturity_singleton; @@ -1095,9 +1086,13 @@ namespace eosiosystem { * 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 rexmaturity(const uint32_t num_of_maturity_buckets); + void rexmaturity(const std::optional num_of_maturity_buckets, const std::optional sell_matured_rex, const std::optional buy_rex_to_savings ); /** * Undelegate bandwidth action, decreases the total tokens delegated by `from` to `receiver` and/or @@ -1592,6 +1587,7 @@ namespace eosiosystem { 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 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 ); diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index b1d883fa..0c87fa31 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -8,17 +8,19 @@ namespace eosiosystem { using eosio::token; using eosio::seconds; - void system_contract::rexmaturity(const uint32_t num_of_maturity_buckets) + void system_contract::rexmaturity(const std::optional num_of_maturity_buckets, const std::optional sell_matured_rex, const std::optional buy_rex_to_savings ) { require_auth(get_self()); - 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"); - auto state = _rexmaturity.get_or_default(); - if ( _rexmaturity.exists() ) check(state.num_of_maturity_buckets != num_of_maturity_buckets, "num_of_maturity_buckets is the same as the current value"); - state.num_of_maturity_buckets = num_of_maturity_buckets; + 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 ( _rexmaturity.exists() && num_of_maturity_buckets ) check(state.num_of_maturity_buckets != *num_of_maturity_buckets, "num_of_maturity_buckets is the same as the current value"); + + 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()); } @@ -63,9 +65,18 @@ namespace eosiosystem { runrex(2); update_rex_account( from, asset( 0, core_symbol() ), delta_rex_stake ); - #if MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS + // buying REX immediately moves to REX savings + // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 + const auto rex_maturity_state = _rexmaturity.get_or_default(); + if ( rex_maturity_state.buy_rex_to_savings ) { mvtosavings( from, rex_received ); - #endif + } + + // sell any matured REX + // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 + if ( rex_maturity_state.sell_matured_rex ) { + 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{ } ); @@ -107,9 +118,18 @@ namespace eosiosystem { runrex(2); update_rex_account( owner, asset( 0, core_symbol() ), rex_stake_delta - payment, true ); - #if MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS + // unstake to REX immediately moves to REX savings + // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 + const auto rex_maturity_state = _rexmaturity.get_or_default(); + if ( rex_maturity_state.buy_rex_to_savings ) { mvtosavings( owner, rex_received ); - #endif + } + + // sell any matured REX + // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 + if ( rex_maturity_state.sell_matured_rex ) { + 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{ } ); @@ -120,6 +140,13 @@ namespace eosiosystem { { require_auth( from ); sell_rex( from, rex ); + + // sell any remaining matured REX + // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 + const auto rex_maturity_state = _rexmaturity.get_or_default(); + if ( rex_maturity_state.sell_matured_rex ) { + process_sell_matured_rex( from ); + } } void system_contract::sell_rex( const name& from, const asset& rex ) @@ -995,14 +1022,23 @@ namespace eosiosystem { rb.matured_rex += rb.rex_maturities.front().second; rb.rex_maturities.erase(rb.rex_maturities.begin()); } - #if MATURED_REX_SOLD_AND_BUY_REX_TO_SAVINGS - if ( rb.matured_rex > 0 ) { - sell_rex(rb.owner, asset(rb.matured_rex, rex_symbol)); - } - #endif }); } + /** + * @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 itr = _rexbalance.find( owner.value ); + if ( itr->matured_rex > 0 ) { + sell_rex(owner, asset(itr->matured_rex, rex_symbol)); + } + } + /** * @brief Consolidates REX maturity buckets into one * From 4358d70ecd7267ab99a1ef624ba97f9f9b1056d8 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Wed, 8 May 2024 16:28:49 +0200 Subject: [PATCH 15/37] refactor process_sell_matured_rex --- contracts/eosio.system/src/rex.cpp | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index 0c87fa31..4a43474f 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -71,12 +71,7 @@ namespace eosiosystem { if ( rex_maturity_state.buy_rex_to_savings ) { mvtosavings( from, rex_received ); } - - // sell any matured REX - // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 - if ( rex_maturity_state.sell_matured_rex ) { - process_sell_matured_rex( from ); - } + 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{ } ); @@ -124,12 +119,7 @@ namespace eosiosystem { if ( rex_maturity_state.buy_rex_to_savings ) { mvtosavings( owner, rex_received ); } - - // sell any matured REX - // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 - if ( rex_maturity_state.sell_matured_rex ) { - process_sell_matured_rex( owner ); - } + 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{ } ); @@ -140,13 +130,7 @@ namespace eosiosystem { { require_auth( from ); sell_rex( from, rex ); - - // sell any remaining matured REX - // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/134 - const auto rex_maturity_state = _rexmaturity.get_or_default(); - if ( rex_maturity_state.sell_matured_rex ) { - process_sell_matured_rex( from ); - } + process_sell_matured_rex( from ); } void system_contract::sell_rex( const name& from, const asset& rex ) @@ -1033,6 +1017,9 @@ namespace eosiosystem { */ 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)); From a9a79e17d132cb40741a4eda2ecbc4eb29c2f93f Mon Sep 17 00:00:00 2001 From: Nathan James Date: Wed, 8 May 2024 16:21:53 +0100 Subject: [PATCH 16/37] direct continuous rate modification, extending tests --- .../include/eosio.system/eosio.system.hpp | 7 +- .../ricardian/eosio.system.contracts.md.in | 2 +- contracts/eosio.system/src/eosio.system.cpp | 13 +-- tests/eosio.system_schedules_tests.cpp | 83 +++++++++++++------ tests/eosio.system_tester.hpp | 9 +- 5 files changed, 77 insertions(+), 37 deletions(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index dea99d85..ad3e8f6c 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -200,12 +200,12 @@ namespace eosiosystem { // Defines the schedule for pre-determined annual rate changes. struct [[eosio::table, eosio::contract("eosio.system")]] schedules_info { time_point_sec start_time; - int64_t annual_rate; + 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)(annual_rate) ) + EOSLIB_SERIALIZE( schedules_info, (start_time)(continuous_rate) ) }; inline eosio::block_signing_authority convert_to_block_signing_authority( const eosio::public_key& producer_key ) { @@ -1451,7 +1451,7 @@ namespace eosiosystem { * For 1.5% Annual inflation => annual_rate=150 */ [[eosio::action]] - void setschedule( const time_point_sec start_time, int64_t annual_rate ); + void setschedule( const time_point_sec start_time, double continuous_rate ); /** * Delete the schedule for pre-determined annual rate changes. @@ -1587,6 +1587,7 @@ namespace eosiosystem { 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: diff --git a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in index a74ed9ec..56ff0434 100644 --- a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in +++ b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in @@ -766,7 +766,7 @@ 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}} -* Annual inflation rate (in units of a hundredth of a percent): {{annual_rate}} +* The continuous rate of inflation: {{continuous_rate}}

delschedule

diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index edb07ecd..4024d75f 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -424,23 +424,24 @@ namespace eosiosystem { _global4.set( _gstate4, get_self() ); } - void system_contract::setschedule( const time_point_sec start_time, int64_t annual_rate ) + void system_contract::setschedule( const time_point_sec start_time, double continuous_rate ) { require_auth( get_self() ); - check(annual_rate >= 0, "annual_rate can't be negative"); + check(continuous_rate >= 0, "continuous_rate can't be negative"); + check(continuous_rate <= 1, "continuous_rate can't be over 100%"); + check(start_time.sec_since_epoch() >= current_time_point().sec_since_epoch(), "start_time cannot be in the past"); 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.annual_rate = annual_rate; + s.continuous_rate = continuous_rate; }); } else { _schedules.modify( itr, same_payer, [&]( auto& s ) { - check( annual_rate != s.annual_rate, "annual_rate was not modified"); - s.annual_rate = annual_rate; + s.continuous_rate = continuous_rate; }); } } @@ -464,7 +465,7 @@ namespace eosiosystem { 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 = get_continuous_rate(itr->annual_rate); + _gstate4.continuous_rate = itr->continuous_rate; _schedules.erase( itr ); return true; } diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp index f3ec14e1..49fd0109 100644 --- a/tests/eosio.system_schedules_tests.cpp +++ b/tests/eosio.system_schedules_tests.cpp @@ -8,51 +8,84 @@ 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; - const uint32_t initial_start_time = 1577836856; // 2020-01-01T00:00 - const time_point_sec start_time = time_point_sec(initial_start_time); + 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(time_point_sec(0), 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(start_time, 500) ); // 5% annual rate - BOOST_REQUIRE_EQUAL( wasm_assert_msg("annual_rate was not modified"), setschedule(start_time, 500) ); - BOOST_REQUIRE_EQUAL( wasm_assert_msg("annual_rate can't be negative"), setschedule(start_time, -1) ); - BOOST_REQUIRE_EQUAL( wasm_assert_msg("schedule not found"), delschedule(time_point_sec(0)) ); - BOOST_REQUIRE_EQUAL( success(), setschedule(start_time, 200) ); // allow override existing schedules + 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); + + // You should no longer be able to modify something that has passed the + // execution date, or add new schedules in the past + BOOST_REQUIRE_EQUAL( wasm_assert_msg("start_time cannot be in the past"), setschedule(start_time, 0.05) ); + + // Should be able to delete schedules, even in the past BOOST_REQUIRE_EQUAL( success(), delschedule(start_time) ); - - // set 4 future schedules - BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(start_time), 200) ); - BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 4), 100) ); - BOOST_REQUIRE_EQUAL( success(), setschedule(time_point_sec(initial_start_time + YEAR * 8), 50) ); + 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) ); + check_schedule(time_point_sec(initial_start_time + YEAR * 12), 0); - // current state prior to first schedule + // 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% annual rate - const uint32_t current_time_sec = control->pending_block_time().sec_since_epoch(); - BOOST_REQUIRE_EQUAL( current_time_sec, initial_start_time ); // 2020-01-01T00:00 + BOOST_REQUIRE_EQUAL( before, 0.048790164169432007 ); // 5% continuous rate - // advance to halvening schedules - produce_block( fc::days(365 * 4) ); // 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.019802627296179712 ); // 2% annual rate + BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); + BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.02 ); - produce_block( fc::days(365 * 4) ); // advanced to year 8 + // Cannot execute schedule before its time is due + // we did 5 actions so we're late 2.5s late (5 blocks / 2 blocks per second) + produce_block( fc::seconds(YEAR * 4) - fc::milliseconds(3500) ); // 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.0099503308531680833 ); // 1% annual rate + BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.01 ); // 1% 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.0049875415110390738 ); // 0.5% annual rate + 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 16 BOOST_REQUIRE_EQUAL( success(), execschedule(alice) ); - BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.0 ); // 0% annual rate + 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) ); diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 7a479ad2..ff22667e 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -1411,10 +1411,10 @@ class eosio_system_tester : public TESTER { ); } - action_result setschedule( const time_point_sec start_time, int64_t annual_rate ) { + action_result setschedule( const time_point_sec start_time, double continuous_rate ) { return push_action( "eosio"_n, "setschedule"_n, mvo() ("start_time", start_time) - ("annual_rate", annual_rate) + ("continuous_rate", continuous_rate) ); } @@ -1428,6 +1428,11 @@ class eosio_system_tester : public TESTER { 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) ); + } + abi_serializer abi_ser; abi_serializer token_abi_ser; }; From 5a9176dbb85485951b5d280ec0630e16782c3cf5 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Wed, 8 May 2024 16:23:55 +0100 Subject: [PATCH 17/37] year typo in versting schedule test --- tests/eosio.system_schedules_tests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp index 49fd0109..09fbccf1 100644 --- a/tests/eosio.system_schedules_tests.cpp +++ b/tests/eosio.system_schedules_tests.cpp @@ -74,7 +74,7 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { 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 + produce_block( fc::seconds(1) ); // advance to year 8 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 From c550e955ccffc5744fa311545286aa092326f986 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Wed, 8 May 2024 16:24:57 +0100 Subject: [PATCH 18/37] year typo in versting schedule test --- tests/eosio.system_schedules_tests.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp index 09fbccf1..7fdac096 100644 --- a/tests/eosio.system_schedules_tests.cpp +++ b/tests/eosio.system_schedules_tests.cpp @@ -74,16 +74,16 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { 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 8 + 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 12 + 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 16 + 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.0 ); // 0% continuous rate From ff27f8cb6d64d6e7d429a3326dea1978cc8e721e Mon Sep 17 00:00:00 2001 From: Nathan James Date: Thu, 9 May 2024 15:51:07 +0100 Subject: [PATCH 19/37] extend unvest tests, enforce only unvested amount, and allow claiming vested remainders --- .../eosio.system/src/delegate_bandwidth.cpp | 40 ++++++---- tests/eosio.system_schedules_tests.cpp | 15 +++- tests/eosio.system_tests.cpp | 78 ++++++++++++------- 3 files changed, 88 insertions(+), 45 deletions(-) diff --git a/contracts/eosio.system/src/delegate_bandwidth.cpp b/contracts/eosio.system/src/delegate_bandwidth.cpp index 3c774bb4..92c7f576 100644 --- a/contracts/eosio.system/src/delegate_bandwidth.cpp +++ b/contracts/eosio.system/src/delegate_bandwidth.cpp @@ -241,13 +241,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; + + auto hasAlreadyUnvested = new_stake < unvestable + && stake_change.amount < 0 + && new_stake + std::abs(stake_change.amount) < unvestable; + if(hasAlreadyUnvested) return; - check( max_claimable - claimable <= stake, "b1 can only claim their tokens over 10 years" ); + check( new_stake >= unvestable, "b1 can only claim what has already vested" ); } void system_contract::changebw( name from, const name& receiver, @@ -339,7 +351,7 @@ namespace eosiosystem { vote_stake_updater( from ); const int64_t staked = update_voting_power( from, stake_net_delta + stake_cpu_delta ); if ( from == "b1"_n ) { - validate_b1_vesting( staked ); + validate_b1_vesting( staked, stake_net_delta + stake_cpu_delta ); } } @@ -479,26 +491,28 @@ namespace eosiosystem { { require_auth( get_self() ); - const asset stake_delta = unvest_net_quantity + unvest_cpu_quantity; - asset zero_asset( 0, core_symbol() ); - check( unvest_cpu_quantity >= zero_asset, "must unvest a positive amount" ); - check( unvest_net_quantity >= zero_asset, "must unvest a positive amount" ); - check( stake_delta.amount > 0, "must unvest a positive amount" ); 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, -stake_delta ); + 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(), stake_delta, "unvest" ); + 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( stake_delta, "unvest" ); + retire_act.send( unvesting, "unvest" ); } // unvest } //namespace eosiosystem diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp index 7fdac096..eee8c491 100644 --- a/tests/eosio.system_schedules_tests.cpp +++ b/tests/eosio.system_schedules_tests.cpp @@ -58,8 +58,10 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { 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) ); - check_schedule(time_point_sec(initial_start_time + YEAR * 12), 0); + 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(); @@ -69,8 +71,9 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { BOOST_REQUIRE_EQUAL( get_global_state4()["continuous_rate"].as_double(), 0.02 ); // Cannot execute schedule before its time is due - // we did 5 actions so we're late 2.5s late (5 blocks / 2 blocks per second) - produce_block( fc::seconds(YEAR * 4) - fc::milliseconds(3500) ); // advance to year 4 + // (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 @@ -85,6 +88,10 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { 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 diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 7d52db53..bdd55cf1 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -5538,52 +5538,74 @@ 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) ); - - BOOST_REQUIRE_EQUAL( success(), push_action( b1, "refund"_n, mvo()("owner", b1) ) ); - - BOOST_REQUIRE_EQUAL( 2 * ( stake_amount.get_amount() - small_amount.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 ) ); + 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"); + // Can't use rex 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 ) ); - + unstaketorex( b1, b1, vested, zero ) ); BOOST_REQUIRE_EQUAL( error("missing authority of eosio"), vote( b1, { }, "proxyaccount"_n ) ); - BOOST_REQUIRE_EQUAL( success(), unstake( b1, b1, final_amount - small_amount, final_amount - small_amount ) ); - - produce_block( fc::days(4) ); + // 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() ); + + // 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 ) + ); - produce_block( fc::days( 5 * 364 ) ); + // 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( wasm_assert_msg("b1 can only claim their tokens over 10 years"), - unstake( b1, b1, small_amount, small_amount ) ); + auto supply_before = get_token_supply(); - const int64_t before = get_voter_info( b1 )["staked"].as(); - const asset before_supply = get_token_supply(); - BOOST_REQUIRE_EQUAL( before, 646703490000 ); + // 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() ); - BOOST_REQUIRE_EQUAL( success(), unvest( b1, stake_amount - final_amount, stake_amount - final_amount ) ); - const int64_t after = get_voter_info( b1 )["staked"].as(); - const asset after_supply = get_token_supply(); + // Should have retired the unvestable tokens + BOOST_REQUIRE_EQUAL( + get_token_supply(), + supply_before-unvestable + ); - BOOST_REQUIRE_EQUAL( after, 0 ); - BOOST_REQUIRE_EQUAL( after_supply.get_amount() - before_supply.get_amount(), -646703490000 ); + // 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() From 8028ff7c581179dcb05f786e888c2f6485b041f8 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Thu, 9 May 2024 22:25:55 +0200 Subject: [PATCH 20/37] add matured rex tests --- contracts/eosio.system/src/rex.cpp | 1 - tests/eosio.system_rex_matured_tests.cpp | 60 ++++++++++++++++++++++++ tests/eosio.system_tester.hpp | 4 ++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/eosio.system_rex_matured_tests.cpp diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index 4a43474f..325541d2 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -16,7 +16,6 @@ namespace eosiosystem { 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 ( _rexmaturity.exists() && num_of_maturity_buckets ) check(state.num_of_maturity_buckets != *num_of_maturity_buckets, "num_of_maturity_buckets is the same as the current value"); 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; diff --git a/tests/eosio.system_rex_matured_tests.cpp b/tests/eosio.system_rex_matured_tests.cpp new file mode 100644 index 00000000..1a753561 --- /dev/null +++ b/tests/eosio.system_rex_matured_tests.cpp @@ -0,0 +1,60 @@ +#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 + BOOST_REQUIRE_EQUAL( success(), rexmaturity( 21, true, true ) ); + + 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 }; + account_name alice = accounts[0], bob = accounts[1]; + setup_rex_accounts( accounts, init_balance ); + + BOOST_REQUIRE_EQUAL( asset::from_string("25000.0000 REX"), get_buyrex_result( alice, core_sym::from_string("2.5000") ) ); + BOOST_REQUIRE_EQUAL( success(), mvfrsavings( alice, asset::from_string("25000.0000 REX") ) ); + + 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() ); + + // set `buy_rex_to_savings=false` to test buying REX without moving it to REX savings + BOOST_REQUIRE_EQUAL( success(), rexmaturity( 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(21)); + BOOST_REQUIRE_EQUAL( core_sym::from_string("2.5000"), get_sellrex_result( bob, asset::from_string("25000.0000 REX") ) ); + +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 93a8eb24..ef36ef67 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -874,6 +874,10 @@ class eosio_system_tester : public TESTER { return push_action( name(owner), "closerex"_n, mvo()("owner", owner) ); } + action_result rexmaturity(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, "rexmaturity"_n, mvo()("num_of_maturity_buckets", num_of_maturity_buckets)("sell_matured_rex", sell_matured_rex)("buy_rex_to_savings", buy_rex_to_savings) ); + } + fc::variant get_last_loan(bool cpu) { vector data; const auto& db = control->db(); From 4239d2097b4881e126daec91069f09834f87b896 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Fri, 10 May 2024 12:53:30 +0100 Subject: [PATCH 21/37] allow setting a schedule in the past or msigs will break --- contracts/eosio.system/src/eosio.system.cpp | 1 - tests/eosio.system_schedules_tests.cpp | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 4024d75f..702b6dbc 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -430,7 +430,6 @@ namespace eosiosystem { check(continuous_rate >= 0, "continuous_rate can't be negative"); check(continuous_rate <= 1, "continuous_rate can't be over 100%"); - check(start_time.sec_since_epoch() >= current_time_point().sec_since_epoch(), "start_time cannot be in the past"); auto itr = _schedules.find( start_time.sec_since_epoch() ); diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp index eee8c491..17fd69ee 100644 --- a/tests/eosio.system_schedules_tests.cpp +++ b/tests/eosio.system_schedules_tests.cpp @@ -26,11 +26,12 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { 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(time_point_sec(0), 1.00001) ); + 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); @@ -38,12 +39,10 @@ BOOST_FIXTURE_TEST_CASE(set_schedules, eosio_system_tester) try { BOOST_REQUIRE_EQUAL( success(), setschedule(start_time, 0.02) ); check_schedule(start_time, 0.02); - // You should no longer be able to modify something that has passed the - // execution date, or add new schedules in the past - BOOST_REQUIRE_EQUAL( wasm_assert_msg("start_time cannot be in the past"), setschedule(start_time, 0.05) ); // 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 From 9e3ac02d3caa801ac8bef6e44e4e5fccfcb0c83a Mon Sep 17 00:00:00 2001 From: Nathan James Date: Fri, 10 May 2024 13:12:51 +0100 Subject: [PATCH 22/37] expose new action to allow setting pay factors without affecting continuous_rate --- .cspell/custom-dictionary.txt | 1 + .../include/eosio.system/eosio.system.hpp | 16 ++++++++++++++++ .../ricardian/eosio.system.contracts.md.in | 14 ++++++++++++++ contracts/eosio.system/src/eosio.system.cpp | 13 +++++++++++++ tests/eosio.system_tester.hpp | 7 +++++++ tests/eosio.system_tests.cpp | 5 +++++ 6 files changed, 56 insertions(+) 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/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index ad3e8f6c..04665412 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -1442,6 +1442,21 @@ 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. * @@ -1581,6 +1596,7 @@ 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>; diff --git a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in index 56ff0434..6fb0b223 100644 --- a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in +++ b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in @@ -626,6 +626,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

--- diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 702b6dbc..68d25617 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -424,6 +424,19 @@ 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() ); diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index fda0609b..28d6d94a 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -1433,6 +1433,13 @@ class eosio_system_tester : public 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) diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index bdd55cf1..491d8eb2 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -1672,6 +1672,11 @@ 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( wasm_assert_msg("inflation_pay_factor must not be less than 10000"), + setpayfactor(1, 9999, 10000) ); + BOOST_REQUIRE_EQUAL( wasm_assert_msg("votepay_factor must not be less than 10000"), + setpayfactor(1, 10000, 9999) ); } { From b188adc3e0534b2e4432c96ca9e19597eb1ee38c Mon Sep 17 00:00:00 2001 From: Nathan James Date: Fri, 10 May 2024 13:15:53 +0100 Subject: [PATCH 23/37] fixes for setpayfactor tests --- tests/eosio.system_tests.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 491d8eb2..3987f53b 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -1673,10 +1673,12 @@ BOOST_FIXTURE_TEST_CASE(change_inflation, eosio_system_tester) try { 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(1, 9999, 10000) ); + setpayfactor(9999, 10000) ); BOOST_REQUIRE_EQUAL( wasm_assert_msg("votepay_factor must not be less than 10000"), - setpayfactor(1, 10000, 9999) ); + setpayfactor(10000, 9999) ); } { From 981608a86e10a4eece39b69eb8dc11eb0a98f605 Mon Sep 17 00:00:00 2001 From: Denis Carriere Date: Sat, 11 May 2024 11:52:21 +0200 Subject: [PATCH 24/37] update rex tests --- .../include/eosio.system/eosio.system.hpp | 1 + contracts/eosio.system/src/rex.cpp | 29 ++++++----- tests/eosio.system_rex_matured_tests.cpp | 49 ++++++++++++++++--- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index 1906c1bc..b876bd10 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -1588,6 +1588,7 @@ namespace eosiosystem { 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 ); diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index 325541d2..3c89d1e7 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -64,12 +64,7 @@ namespace eosiosystem { runrex(2); update_rex_account( from, asset( 0, core_symbol() ), delta_rex_stake ); - // buying REX immediately moves to REX savings - // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 - const auto rex_maturity_state = _rexmaturity.get_or_default(); - if ( rex_maturity_state.buy_rex_to_savings ) { - mvtosavings( from, rex_received ); - } + 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 @@ -112,12 +107,7 @@ namespace eosiosystem { runrex(2); update_rex_account( owner, asset( 0, core_symbol() ), rex_stake_delta - payment, true ); - // unstake to REX immediately moves to REX savings - // https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 - const auto rex_maturity_state = _rexmaturity.get_or_default(); - if ( rex_maturity_state.buy_rex_to_savings ) { - mvtosavings( owner, rex_received ); - } + 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 @@ -1025,6 +1015,21 @@ namespace eosiosystem { } } + /** + * @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/tests/eosio.system_rex_matured_tests.cpp b/tests/eosio.system_rex_matured_tests.cpp index 1a753561..cc6320cc 100644 --- a/tests/eosio.system_rex_matured_tests.cpp +++ b/tests/eosio.system_rex_matured_tests.cpp @@ -17,18 +17,18 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_matured_rex, eosio_system_tester ) try { // 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 - BOOST_REQUIRE_EQUAL( success(), rexmaturity( 21, true, true ) ); + // 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 }; - account_name alice = accounts[0], bob = accounts[1]; + const std::vector accounts = { "alice"_n, "bob"_n, "charly"_n, "david"_n }; + account_name alice = accounts[0], bob = accounts[1], charly = accounts[2], david = accounts[3]; 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(), rexmaturity( 21, false, false ) ); BOOST_REQUIRE_EQUAL( asset::from_string("25000.0000 REX"), get_buyrex_result( alice, core_sym::from_string("2.5000") ) ); - BOOST_REQUIRE_EQUAL( success(), mvfrsavings( alice, asset::from_string("25000.0000 REX") ) ); - 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") ) ); @@ -48,12 +48,47 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_matured_rex, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( init_rent, rex_pool["total_rent"].as() ); BOOST_REQUIRE_EQUAL( get_rex_balance(alice), rex_pool["total_rex"].as() ); - // set `buy_rex_to_savings=false` to test buying REX without moving it to REX savings + // 2. set `buy_rex_to_savings=false` to test buying REX without moving it to REX savings BOOST_REQUIRE_EQUAL( success(), rexmaturity( 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` to test selling matured REX + BOOST_REQUIRE_EQUAL( success(), rexmaturity( 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(), rexmaturity( 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 + produce_blocks(2); + produce_block(fc::days(5)); + BOOST_REQUIRE_EQUAL( success(), rexmaturity( 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 + 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("2.5000"), get_sellrex_result( bob, asset::from_string("25000.0000 REX") ) ); + 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() From 6a175c051c83f1565b8752204396694a7e9c0047 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Mon, 13 May 2024 12:26:02 +0100 Subject: [PATCH 25/37] set global state --- contracts/eosio.system/src/eosio.system.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/eosio.system/src/eosio.system.cpp b/contracts/eosio.system/src/eosio.system.cpp index 68d25617..7637e5f5 100644 --- a/contracts/eosio.system/src/eosio.system.cpp +++ b/contracts/eosio.system/src/eosio.system.cpp @@ -478,6 +478,7 @@ namespace eosiosystem { 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; } From dbd2eb43a50f53409fc11fa0909cb7fcdbe5f9d3 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Mon, 13 May 2024 20:22:34 +0100 Subject: [PATCH 26/37] validate rex return in tests --- tests/eosio.system_tests.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 80099c00..c30d7ee3 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -5464,10 +5464,16 @@ BOOST_FIXTURE_TEST_CASE( donate_to_rex, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( wasm_assert_msg( "quantity must be positive" ), donatetorex( bob, core_sym::from_string("-100.0000"), "") ); - const asset initial_eosio_rex_balance = get_balance("eosio.rex"_n); - BOOST_REQUIRE_EQUAL( success(), donatetorex( bob, core_sym::from_string("500.0000"), "") ); - BOOST_REQUIRE_EQUAL( initial_eosio_rex_balance + core_sym::from_string("500.0000"), get_balance("eosio.rex"_n) ); - + 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 { From 33ab294eafc5fae6ae8d3fca4d8d5c40117c8d76 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Wed, 15 May 2024 19:54:30 +0100 Subject: [PATCH 27/37] fix for setschedule docs --- .../eosio.system/include/eosio.system/eosio.system.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index 1ed1a987..129b803b 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -1512,9 +1512,9 @@ namespace eosiosystem { * Set the schedule for pre-determined annual rate changes. * * @param start_time - the time to start the schedule. - * @param annual_rate - the annual inflation rate of the core token supply. - * (eg. For 5% Annual inflation => annual_rate=500 - * For 1.5% Annual inflation => annual_rate=150 + * @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 ); From ded2d7fbfacd3c9100dcb6d44b696e663e93bb33 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Fri, 17 May 2024 12:34:49 +0100 Subject: [PATCH 28/37] add ricardians for unvest and donatetorex --- .../ricardian/eosio.system.contracts.md.in | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in index 3761db0f..dd5c7101 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

--- @@ -818,3 +829,14 @@ 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 From 4dff31f5d63f00b27fb246038fe4c4e7d92ede25 Mon Sep 17 00:00:00 2001 From: Eric Passmore Date: Tue, 21 May 2024 08:03:55 -0700 Subject: [PATCH 29/37] interface test for buyramburn --- tests/eosio.system_ram_tests.cpp | 14 ++++++++++++++ tests/eosio.system_tester.hpp | 24 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/tests/eosio.system_ram_tests.cpp b/tests/eosio.system_ram_tests.cpp index 106e364a..e88fa5bb 100644 --- a/tests/eosio.system_ram_tests.cpp +++ b/tests/eosio.system_ram_tests.cpp @@ -173,6 +173,17 @@ BOOST_FIXTURE_TEST_CASE( buy_ram_burn, eosio_system_tester ) try { 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 ); @@ -191,6 +202,9 @@ BOOST_FIXTURE_TEST_CASE( buy_ram_burn, eosio_system_tester ) try { 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 diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index a62a8cc7..8efa57e6 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -442,6 +442,30 @@ class eosio_system_tester : public TESTER { 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 From 1741995ac8a3985e85fa11cc79ff56ff21af91b4 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Mon, 3 Jun 2024 17:18:18 +0100 Subject: [PATCH 30/37] preliminary tests and code for eosio.bpay --- contracts/CMakeLists.txt | 1 + contracts/eosio.bpay/CMakeLists.txt | 11 ++ .../include/eosio.bpay/eosio.bpay.hpp | 55 ++++++ .../include/eosio.system/eosio.system.hpp | 157 ++++++++++++++++++ .../include/eosio.token/eosio.token.hpp | 146 ++++++++++++++++ contracts/eosio.bpay/src/eosio.bpay.cpp | 83 +++++++++ tests/contracts.hpp.in | 2 + tests/eosio.bpay_tests.cpp | 93 +++++++++++ tests/eosio.system_schedules_tests.cpp | 1 - tests/eosio.system_tester.hpp | 30 +++- 10 files changed, 577 insertions(+), 2 deletions(-) create mode 100644 contracts/eosio.bpay/CMakeLists.txt create mode 100644 contracts/eosio.bpay/include/eosio.bpay/eosio.bpay.hpp create mode 100644 contracts/eosio.bpay/include/eosio.system/eosio.system.hpp create mode 100644 contracts/eosio.bpay/include/eosio.token/eosio.token.hpp create mode 100644 contracts/eosio.bpay/src/eosio.bpay.cpp create mode 100644 tests/eosio.bpay_tests.cpp diff --git a/contracts/CMakeLists.txt b/contracts/CMakeLists.txt index 86cd4775..f96b0e45 100644 --- a/contracts/CMakeLists.txt +++ b/contracts/CMakeLists.txt @@ -52,5 +52,6 @@ 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..840d9756 --- /dev/null +++ b/contracts/eosio.bpay/CMakeLists.txt @@ -0,0 +1,11 @@ +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) + +set_target_properties(eosio.bpay + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + +target_compile_options( eosio.bpay PUBLIC ) 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..5cf62904 --- /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("*::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/include/eosio.system/eosio.system.hpp b/contracts/eosio.bpay/include/eosio.system/eosio.system.hpp new file mode 100644 index 00000000..b91a1e4e --- /dev/null +++ b/contracts/eosio.bpay/include/eosio.system/eosio.system.hpp @@ -0,0 +1,157 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace eosio; + +namespace eosiosystem { + +class [[eosio::contract("eosio")]] system_contract : public contract +{ +public: + using contract::contract; + + static 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}} }; + } + + // Defines `producer_info` structure to be stored in `producer_info` table, added after version 1.0 + struct [[eosio::table, eosio::contract("eosio.system")]] producer_info { + name owner; + double total_votes = 0; + eosio::public_key producer_key; /// a packed public key object + bool is_active = true; + std::string url; + uint32_t unpaid_blocks = 0; + time_point last_claim_time; + uint16_t location = 0; + eosio::binary_extension producer_authority; // added in version 1.9.0 + + uint64_t primary_key()const { return owner.value; } + double by_votes()const { return is_active ? -total_votes : total_votes; } + bool active()const { return is_active; } + void deactivate() { producer_key = public_key(); producer_authority.reset(); is_active = false; } + + eosio::block_signing_authority get_producer_authority()const { + if( producer_authority.has_value() ) { + bool zero_threshold = std::visit( [](auto&& auth ) -> bool { + return (auth.threshold == 0); + }, *producer_authority ); + // zero_threshold could be true despite the validation done in regproducer2 because the v1.9.0 eosio.system + // contract has a bug which may have modified the producer table such that the producer_authority field + // contains a default constructed eosio::block_signing_authority (which has a 0 threshold and so is invalid). + if( !zero_threshold ) return *producer_authority; + } + return convert_to_block_signing_authority( producer_key ); + } + + // The unregprod and claimrewards actions modify unrelated fields of the producers table and under the default + // serialization behavior they would increase the size of the serialized table if the producer_authority field + // was not already present. This is acceptable (though not necessarily desired) because those two actions require + // the authority of the producer who pays for the table rows. + // However, the rmvproducer action and the onblock transaction would also modify the producer table in a similar + // way and increasing its serialized size is not acceptable in that context. + // So, a custom serialization is defined to handle the binary_extension producer_authority + // field in the desired way. (Note: v1.9.0 did not have this custom serialization behavior.) + + template + friend DataStream& operator << ( DataStream& ds, const producer_info& t ) { + ds << t.owner + << t.total_votes + << t.producer_key + << t.is_active + << t.url + << t.unpaid_blocks + << t.last_claim_time + << t.location; + + if( !t.producer_authority.has_value() ) return ds; + + return ds << t.producer_authority; + } + + template + friend DataStream& operator >> ( DataStream& ds, producer_info& t ) { + return ds >> t.owner + >> t.total_votes + >> t.producer_key + >> t.is_active + >> t.url + >> t.unpaid_blocks + >> t.last_claim_time + >> t.location + >> t.producer_authority; + } + }; + + typedef eosio::multi_index< "producers"_n, producer_info, + indexed_by<"prototalvote"_n, const_mem_fun> + > producers_table; + + + + // struct [[eosio::table, eosio::contract("eosio.system")]] + struct [[eosio::table("global"), eosio::contract("eosio.system")]] eosio_global_state : eosio::blockchain_parameters { + uint64_t free_ram()const { return max_ram_size - total_ram_bytes_reserved; } + + uint64_t max_ram_size = 64ll*1024 * 1024 * 1024; + uint64_t total_ram_bytes_reserved = 0; + int64_t total_ram_stake = 0; + + block_timestamp last_producer_schedule_update; + time_point last_pervote_bucket_fill; + int64_t pervote_bucket = 0; + int64_t perblock_bucket = 0; + uint32_t total_unpaid_blocks = 0; /// all blocks which have been produced but not paid + int64_t total_activated_stake = 0; + time_point thresh_activated_stake_time; + uint16_t last_producer_schedule_size = 0; + double total_producer_vote_weight = 0; /// the sum of all producer votes + block_timestamp last_name_close; + + // explicit serialization macro is not necessary, used here only to improve compilation time + EOSLIB_SERIALIZE_DERIVED( eosio_global_state, eosio::blockchain_parameters, + (max_ram_size)(total_ram_bytes_reserved)(total_ram_stake) + (last_producer_schedule_update)(last_pervote_bucket_fill) + (pervote_bucket)(perblock_bucket)(total_unpaid_blocks)(total_activated_stake)(thresh_activated_stake_time) + (last_producer_schedule_size)(total_producer_vote_weight)(last_name_close) ) + }; + + typedef eosio::singleton< "global"_n, eosio_global_state > global_state_singleton; + + + struct [[eosio::table, eosio::contract("eosio.system")]] exchange_state { + asset supply; + + struct connector { + asset balance; + double weight = .5; + + EOSLIB_SERIALIZE( connector, (balance)(weight) ) + }; + + connector base; + connector quote; + + uint64_t primary_key()const { return supply.symbol.raw(); } + + EOSLIB_SERIALIZE( exchange_state, (supply)(base)(quote) ) + }; + + typedef eosio::multi_index< "rammarket"_n, exchange_state > rammarket; + + static constexpr symbol ramcore_symbol = symbol(symbol_code("RAMCORE"), 4); + static symbol get_core_symbol( name system_account = "eosio"_n ) { + rammarket rm(system_account, system_account.value); + auto itr = rm.find(ramcore_symbol.raw()); + check(itr != rm.end(), "system contract must first be initialized"); + return itr->quote.balance.symbol; + } +}; +} \ No newline at end of file diff --git a/contracts/eosio.bpay/include/eosio.token/eosio.token.hpp b/contracts/eosio.bpay/include/eosio.token/eosio.token.hpp new file mode 100644 index 00000000..e4942df0 --- /dev/null +++ b/contracts/eosio.bpay/include/eosio.token/eosio.token.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include +#include + +#include + +namespace eosiosystem { + class system_contract; +} + +namespace eosio { + + using std::string; + + /** + * 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 { + public: + using contract::contract; + + /** + * Allows `issuer` account to create a token in supply of `maximum_supply`. If validation is successful a new entry in statstable for token symbol scope gets created. + * + * @param issuer - the account that creates the token, + * @param maximum_supply - the maximum supply set for the token created. + * + * @pre Token symbol has to be valid, + * @pre Token symbol must not be already created, + * @pre maximum_supply has to be smaller than the maximum supply allowed by the system: 1^62 - 1. + * @pre Maximum supply must be positive; + */ + [[eosio::action]] + void create( const name& issuer, + const asset& maximum_supply); + /** + * This action issues to `to` account a `quantity` of tokens. + * + * @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. + */ + [[eosio::action]] + void issue( const name& to, const asset& quantity, const string& memo ); + + /** + * The opposite for create action, if all validations succeed, + * it debits the statstable.supply amount. + * + * @param quantity - the quantity of tokens to retire, + * @param memo - the memo string to accompany the transaction. + */ + [[eosio::action]] + void retire( const asset& quantity, const string& memo ); + + /** + * Allows `from` account to transfer to `to` account the `quantity` tokens. + * One account is debited and the other is credited with quantity tokens. + * + * @param from - the account to transfer from, + * @param to - the account to be transferred to, + * @param quantity - the quantity of tokens to be transferred, + * @param memo - the memo string to accompany the transaction. + */ + [[eosio::action]] + void transfer( const name& from, + const name& to, + const asset& quantity, + const string& memo ); + /** + * Allows `ram_payer` to create an account `owner` with zero balance for + * token `symbol` at the expense of `ram_payer`. + * + * @param owner - the account to be created, + * @param symbol - the token to be payed with by `ram_payer`, + * @param ram_payer - the account that supports the cost of this action. + * + * More information can be read [here](https://github.com/EOSIO/eosio.contracts/issues/62) + * and [here](https://github.com/EOSIO/eosio.contracts/issues/61). + */ + [[eosio::action]] + void open( const name& owner, const symbol& symbol, const name& ram_payer ); + + /** + * This action is the opposite for open, it closes the account `owner` + * for token `symbol`. + * + * @param owner - the owner account to execute the close action for, + * @param symbol - the symbol of the token to execute the close action for. + * + * @pre The pair of owner plus symbol has to exist otherwise no action is executed, + * @pre If the pair of owner plus symbol exists, the balance has to be zero. + */ + [[eosio::action]] + void close( const name& owner, const symbol& symbol ); + + 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; + } + + 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; + } + + using create_action = eosio::action_wrapper<"create"_n, &token::create>; + using issue_action = eosio::action_wrapper<"issue"_n, &token::issue>; + using retire_action = eosio::action_wrapper<"retire"_n, &token::retire>; + 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: + struct [[eosio::table]] account { + asset balance; + + uint64_t primary_key()const { return balance.symbol.code().raw(); } + }; + + struct [[eosio::table]] currency_stats { + asset supply; + asset max_supply; + name issuer; + + uint64_t primary_key()const { return supply.symbol.code().raw(); } + }; + + typedef eosio::multi_index< "accounts"_n, account > accounts; + typedef eosio::multi_index< "stat"_n, currency_stats > stats; + + 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.bpay/src/eosio.bpay.cpp b/contracts/eosio.bpay/src/eosio.bpay.cpp new file mode 100644 index 00000000..b37bc8ff --- /dev/null +++ b/contracts/eosio.bpay/src/eosio.bpay.cpp @@ -0,0 +1,83 @@ +#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" ); + + // transfer rewards to owner + 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; + + check( get_first_receiver() == "eosio.token"_n, "only eosio.token allowed") ; + + 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::system_contract::producers_table _producers( "eosio"_n, "eosio"_n.value ); + + eosiosystem::system_contract::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; + + // calculate rewards equal share for top n producers + asset reward = quantity / producer_count; + + // 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 + std::vector top_producers; + while (true) { + if ( prod == idx.end() ) { + break; + } + if ( prod->is_active == false ) { + continue; + } + top_producers.push_back(prod->owner); + + // TODO: Remove + print("rank=", top_producers.size(), " producer=", prod->owner, " reward=", reward.to_string(), "\n"); + + if ( top_producers.size() == producer_count ) { + break; + } + prod++; + } + + // update rewards table + 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/tests/contracts.hpp.in b/tests/contracts.hpp.in index 7280e795..cce660dc 100644 --- a/tests/contracts.hpp.in +++ b/tests/contracts.hpp.in @@ -15,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..1d974009 --- /dev/null +++ b/tests/eosio.bpay_tests.cpp @@ -0,0 +1,93 @@ +#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( sanity_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 ) ); + } + +} FC_LOG_AND_RETHROW() + + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/tests/eosio.system_schedules_tests.cpp b/tests/eosio.system_schedules_tests.cpp index 17fd69ee..05746db0 100644 --- a/tests/eosio.system_schedules_tests.cpp +++ b/tests/eosio.system_schedules_tests.cpp @@ -1,5 +1,4 @@ #include - #include "eosio.system_tester.hpp" using namespace eosio_system; diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 8efa57e6..65142e61 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -36,8 +36,8 @@ class eosio_system_tester : public TESTER { produce_blocks( 100 ); + set_code( "eosio.token"_n, contracts::token_wasm()); - set_code( "eosio.fees"_n, contracts::fees_wasm()); set_abi( "eosio.token"_n, contracts::token_abi().data() ); { const auto& accnt = control->db().get( "eosio.token"_n ); @@ -45,6 +45,17 @@ class eosio_system_tester : public 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} ) { @@ -1508,8 +1519,25 @@ class eosio_system_tester : public TESTER { 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 ) { From 8e469fe8d74bf13c3c8802b7fbe54a29c3fc5942 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Tue, 4 Jun 2024 12:35:56 +0100 Subject: [PATCH 31/37] use headers from contracts instead of duplicating, extend tests --- contracts/eosio.bpay/CMakeLists.txt | 3 +- .../include/eosio.bpay/eosio.bpay.hpp | 2 +- .../include/eosio.system/eosio.system.hpp | 157 ------------------ .../include/eosio.token/eosio.token.hpp | 146 ---------------- .../ricardian/eosio.bpay.contracts.md.in | 10 ++ contracts/eosio.bpay/src/eosio.bpay.cpp | 30 ++-- tests/eosio.bpay_tests.cpp | 10 +- 7 files changed, 32 insertions(+), 326 deletions(-) delete mode 100644 contracts/eosio.bpay/include/eosio.system/eosio.system.hpp delete mode 100644 contracts/eosio.bpay/include/eosio.token/eosio.token.hpp create mode 100644 contracts/eosio.bpay/ricardian/eosio.bpay.contracts.md.in diff --git a/contracts/eosio.bpay/CMakeLists.txt b/contracts/eosio.bpay/CMakeLists.txt index 840d9756..1b5d85df 100644 --- a/contracts/eosio.bpay/CMakeLists.txt +++ b/contracts/eosio.bpay/CMakeLists.txt @@ -2,7 +2,8 @@ add_contract(eosio.bpay eosio.bpay ${CMAKE_CURRENT_SOURCE_DIR}/src/eosio.bpay.cp target_include_directories(eosio.bpay PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include - ${CMAKE_CURRENT_SOURCE_DIR}/../eosio.system/include) + ${CMAKE_CURRENT_SOURCE_DIR}/../eosio.system/include + ${CMAKE_CURRENT_SOURCE_DIR}/../eosio.token/include) set_target_properties(eosio.bpay PROPERTIES diff --git a/contracts/eosio.bpay/include/eosio.bpay/eosio.bpay.hpp b/contracts/eosio.bpay/include/eosio.bpay/eosio.bpay.hpp index 5cf62904..042ebd3b 100644 --- a/contracts/eosio.bpay/include/eosio.bpay/eosio.bpay.hpp +++ b/contracts/eosio.bpay/include/eosio.bpay/eosio.bpay.hpp @@ -47,7 +47,7 @@ namespace eosio { [[eosio::action]] void claimrewards( const name owner); - [[eosio::on_notify("*::transfer")]] + [[eosio::on_notify("eosio.token::transfer")]] void on_transfer( const name from, const name to, const asset quantity, const string memo ); private: diff --git a/contracts/eosio.bpay/include/eosio.system/eosio.system.hpp b/contracts/eosio.bpay/include/eosio.system/eosio.system.hpp deleted file mode 100644 index b91a1e4e..00000000 --- a/contracts/eosio.bpay/include/eosio.system/eosio.system.hpp +++ /dev/null @@ -1,157 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace eosio; - -namespace eosiosystem { - -class [[eosio::contract("eosio")]] system_contract : public contract -{ -public: - using contract::contract; - - static 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}} }; - } - - // Defines `producer_info` structure to be stored in `producer_info` table, added after version 1.0 - struct [[eosio::table, eosio::contract("eosio.system")]] producer_info { - name owner; - double total_votes = 0; - eosio::public_key producer_key; /// a packed public key object - bool is_active = true; - std::string url; - uint32_t unpaid_blocks = 0; - time_point last_claim_time; - uint16_t location = 0; - eosio::binary_extension producer_authority; // added in version 1.9.0 - - uint64_t primary_key()const { return owner.value; } - double by_votes()const { return is_active ? -total_votes : total_votes; } - bool active()const { return is_active; } - void deactivate() { producer_key = public_key(); producer_authority.reset(); is_active = false; } - - eosio::block_signing_authority get_producer_authority()const { - if( producer_authority.has_value() ) { - bool zero_threshold = std::visit( [](auto&& auth ) -> bool { - return (auth.threshold == 0); - }, *producer_authority ); - // zero_threshold could be true despite the validation done in regproducer2 because the v1.9.0 eosio.system - // contract has a bug which may have modified the producer table such that the producer_authority field - // contains a default constructed eosio::block_signing_authority (which has a 0 threshold and so is invalid). - if( !zero_threshold ) return *producer_authority; - } - return convert_to_block_signing_authority( producer_key ); - } - - // The unregprod and claimrewards actions modify unrelated fields of the producers table and under the default - // serialization behavior they would increase the size of the serialized table if the producer_authority field - // was not already present. This is acceptable (though not necessarily desired) because those two actions require - // the authority of the producer who pays for the table rows. - // However, the rmvproducer action and the onblock transaction would also modify the producer table in a similar - // way and increasing its serialized size is not acceptable in that context. - // So, a custom serialization is defined to handle the binary_extension producer_authority - // field in the desired way. (Note: v1.9.0 did not have this custom serialization behavior.) - - template - friend DataStream& operator << ( DataStream& ds, const producer_info& t ) { - ds << t.owner - << t.total_votes - << t.producer_key - << t.is_active - << t.url - << t.unpaid_blocks - << t.last_claim_time - << t.location; - - if( !t.producer_authority.has_value() ) return ds; - - return ds << t.producer_authority; - } - - template - friend DataStream& operator >> ( DataStream& ds, producer_info& t ) { - return ds >> t.owner - >> t.total_votes - >> t.producer_key - >> t.is_active - >> t.url - >> t.unpaid_blocks - >> t.last_claim_time - >> t.location - >> t.producer_authority; - } - }; - - typedef eosio::multi_index< "producers"_n, producer_info, - indexed_by<"prototalvote"_n, const_mem_fun> - > producers_table; - - - - // struct [[eosio::table, eosio::contract("eosio.system")]] - struct [[eosio::table("global"), eosio::contract("eosio.system")]] eosio_global_state : eosio::blockchain_parameters { - uint64_t free_ram()const { return max_ram_size - total_ram_bytes_reserved; } - - uint64_t max_ram_size = 64ll*1024 * 1024 * 1024; - uint64_t total_ram_bytes_reserved = 0; - int64_t total_ram_stake = 0; - - block_timestamp last_producer_schedule_update; - time_point last_pervote_bucket_fill; - int64_t pervote_bucket = 0; - int64_t perblock_bucket = 0; - uint32_t total_unpaid_blocks = 0; /// all blocks which have been produced but not paid - int64_t total_activated_stake = 0; - time_point thresh_activated_stake_time; - uint16_t last_producer_schedule_size = 0; - double total_producer_vote_weight = 0; /// the sum of all producer votes - block_timestamp last_name_close; - - // explicit serialization macro is not necessary, used here only to improve compilation time - EOSLIB_SERIALIZE_DERIVED( eosio_global_state, eosio::blockchain_parameters, - (max_ram_size)(total_ram_bytes_reserved)(total_ram_stake) - (last_producer_schedule_update)(last_pervote_bucket_fill) - (pervote_bucket)(perblock_bucket)(total_unpaid_blocks)(total_activated_stake)(thresh_activated_stake_time) - (last_producer_schedule_size)(total_producer_vote_weight)(last_name_close) ) - }; - - typedef eosio::singleton< "global"_n, eosio_global_state > global_state_singleton; - - - struct [[eosio::table, eosio::contract("eosio.system")]] exchange_state { - asset supply; - - struct connector { - asset balance; - double weight = .5; - - EOSLIB_SERIALIZE( connector, (balance)(weight) ) - }; - - connector base; - connector quote; - - uint64_t primary_key()const { return supply.symbol.raw(); } - - EOSLIB_SERIALIZE( exchange_state, (supply)(base)(quote) ) - }; - - typedef eosio::multi_index< "rammarket"_n, exchange_state > rammarket; - - static constexpr symbol ramcore_symbol = symbol(symbol_code("RAMCORE"), 4); - static symbol get_core_symbol( name system_account = "eosio"_n ) { - rammarket rm(system_account, system_account.value); - auto itr = rm.find(ramcore_symbol.raw()); - check(itr != rm.end(), "system contract must first be initialized"); - return itr->quote.balance.symbol; - } -}; -} \ No newline at end of file diff --git a/contracts/eosio.bpay/include/eosio.token/eosio.token.hpp b/contracts/eosio.bpay/include/eosio.token/eosio.token.hpp deleted file mode 100644 index e4942df0..00000000 --- a/contracts/eosio.bpay/include/eosio.token/eosio.token.hpp +++ /dev/null @@ -1,146 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace eosiosystem { - class system_contract; -} - -namespace eosio { - - using std::string; - - /** - * 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 { - public: - using contract::contract; - - /** - * Allows `issuer` account to create a token in supply of `maximum_supply`. If validation is successful a new entry in statstable for token symbol scope gets created. - * - * @param issuer - the account that creates the token, - * @param maximum_supply - the maximum supply set for the token created. - * - * @pre Token symbol has to be valid, - * @pre Token symbol must not be already created, - * @pre maximum_supply has to be smaller than the maximum supply allowed by the system: 1^62 - 1. - * @pre Maximum supply must be positive; - */ - [[eosio::action]] - void create( const name& issuer, - const asset& maximum_supply); - /** - * This action issues to `to` account a `quantity` of tokens. - * - * @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. - */ - [[eosio::action]] - void issue( const name& to, const asset& quantity, const string& memo ); - - /** - * The opposite for create action, if all validations succeed, - * it debits the statstable.supply amount. - * - * @param quantity - the quantity of tokens to retire, - * @param memo - the memo string to accompany the transaction. - */ - [[eosio::action]] - void retire( const asset& quantity, const string& memo ); - - /** - * Allows `from` account to transfer to `to` account the `quantity` tokens. - * One account is debited and the other is credited with quantity tokens. - * - * @param from - the account to transfer from, - * @param to - the account to be transferred to, - * @param quantity - the quantity of tokens to be transferred, - * @param memo - the memo string to accompany the transaction. - */ - [[eosio::action]] - void transfer( const name& from, - const name& to, - const asset& quantity, - const string& memo ); - /** - * Allows `ram_payer` to create an account `owner` with zero balance for - * token `symbol` at the expense of `ram_payer`. - * - * @param owner - the account to be created, - * @param symbol - the token to be payed with by `ram_payer`, - * @param ram_payer - the account that supports the cost of this action. - * - * More information can be read [here](https://github.com/EOSIO/eosio.contracts/issues/62) - * and [here](https://github.com/EOSIO/eosio.contracts/issues/61). - */ - [[eosio::action]] - void open( const name& owner, const symbol& symbol, const name& ram_payer ); - - /** - * This action is the opposite for open, it closes the account `owner` - * for token `symbol`. - * - * @param owner - the owner account to execute the close action for, - * @param symbol - the symbol of the token to execute the close action for. - * - * @pre The pair of owner plus symbol has to exist otherwise no action is executed, - * @pre If the pair of owner plus symbol exists, the balance has to be zero. - */ - [[eosio::action]] - void close( const name& owner, const symbol& symbol ); - - 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; - } - - 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; - } - - using create_action = eosio::action_wrapper<"create"_n, &token::create>; - using issue_action = eosio::action_wrapper<"issue"_n, &token::issue>; - using retire_action = eosio::action_wrapper<"retire"_n, &token::retire>; - 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: - struct [[eosio::table]] account { - asset balance; - - uint64_t primary_key()const { return balance.symbol.code().raw(); } - }; - - struct [[eosio::table]] currency_stats { - asset supply; - asset max_supply; - name issuer; - - uint64_t primary_key()const { return supply.symbol.code().raw(); } - }; - - typedef eosio::multi_index< "accounts"_n, account > accounts; - typedef eosio::multi_index< "stat"_n, currency_stats > stats; - - 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.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 index b37bc8ff..fee02ce1 100644 --- a/contracts/eosio.bpay/src/eosio.bpay.cpp +++ b/contracts/eosio.bpay/src/eosio.bpay.cpp @@ -9,7 +9,6 @@ void bpay::claimrewards( const name owner ) { const auto& row = _rewards.get( owner.value, "no rewards to claim" ); - // transfer rewards to owner eosio::token::transfer_action transfer( "eosio.token"_n, { get_self(), "active"_n }); transfer.send( get_self(), owner, row.quantity, "producer block pay" ); @@ -20,23 +19,21 @@ void bpay::on_transfer( const name from, const name to, const asset quantity, co 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; - check( get_first_receiver() == "eosio.token"_n, "only eosio.token allowed") ; - 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::system_contract::producers_table _producers( "eosio"_n, "eosio"_n.value ); + eosiosystem::producers_table _producers( "eosio"_n, "eosio"_n.value ); - eosiosystem::system_contract::global_state_singleton _global("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; - // calculate rewards equal share for top n producers asset reward = quantity / producer_count; // get producer with the most votes @@ -44,27 +41,20 @@ void bpay::on_transfer( const name from, const name to, const asset quantity, co auto idx = _producers.get_index<"prototalvote"_n>(); auto prod = idx.begin(); - // get top n producers by vote + // 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 (prod == idx.end()) break; + if (prod->is_active == false) continue; - // TODO: Remove - print("rank=", top_producers.size(), " producer=", prod->owner, " reward=", reward.to_string(), "\n"); + top_producers.push_back(prod->owner); - if ( top_producers.size() == producer_count ) { - break; - } + if (top_producers.size() == producer_count) break; + prod++; } - // update rewards table + // distribute rewards to top producers for (auto producer : top_producers) { auto row = _rewards.find( producer.value ); if (row == _rewards.end()) { diff --git a/tests/eosio.bpay_tests.cpp b/tests/eosio.bpay_tests.cpp index 1d974009..19beb4f7 100644 --- a/tests/eosio.bpay_tests.cpp +++ b/tests/eosio.bpay_tests.cpp @@ -10,7 +10,7 @@ account_name inactive = "bp.inactive"_n; account_name fees = "eosio.fees"_n; account_name bpay = "eosio.bpay"_n; -BOOST_FIXTURE_TEST_CASE( sanity_test, eosio_system_tester ) try { +BOOST_FIXTURE_TEST_CASE( bpay_test, eosio_system_tester ) try { // Transferring some tokens to the fees account @@ -87,6 +87,14 @@ BOOST_FIXTURE_TEST_CASE( sanity_test, eosio_system_tester ) try { 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() From 9646c7e5900a28e6bcef3110e459596ceadd719e Mon Sep 17 00:00:00 2001 From: Nathan James Date: Tue, 4 Jun 2024 12:37:51 +0100 Subject: [PATCH 32/37] add bpay ricardians to cmakelists --- contracts/eosio.bpay/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/eosio.bpay/CMakeLists.txt b/contracts/eosio.bpay/CMakeLists.txt index 1b5d85df..09fe613e 100644 --- a/contracts/eosio.bpay/CMakeLists.txt +++ b/contracts/eosio.bpay/CMakeLists.txt @@ -9,4 +9,6 @@ set_target_properties(eosio.bpay PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") -target_compile_options( eosio.bpay PUBLIC ) +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 ) From e21f721a9013cbfd734a57b731d09d698f843042 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Mon, 10 Jun 2024 10:32:32 -0400 Subject: [PATCH 33/37] Split system_tests into multiple suites such that each of them can finish CICD testing in time --- tests/eosio.system_tests.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index a3cad504..72c73395 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -24,11 +24,13 @@ FC_REFLECT( connector, (balance)(weight) ); using namespace eosio_system; -BOOST_AUTO_TEST_SUITE(eosio_system_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); } +// Split the tests into multiple suites so that they can be finished within CICD time limit. +// Each suite takes approximately same amount of time. +BOOST_AUTO_TEST_SUITE(eosio_system_part1_tests) + BOOST_FIXTURE_TEST_CASE( buysell, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_balance( "alice1111111" ) ); @@ -1640,6 +1642,9 @@ BOOST_FIXTURE_TEST_CASE(producer_pay, eosio_system_tester, * boost::unit_test::t } } FC_LOG_AND_RETHROW() +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(eosio_system_part2_tests) + BOOST_FIXTURE_TEST_CASE(change_inflation, eosio_system_tester) try { { @@ -1773,6 +1778,9 @@ BOOST_AUTO_TEST_CASE(extreme_inflation) try { BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("quantity exceeds available supply"), t.push_action("defproducera"_n, "claimrewards"_n, mvo()("owner", "defproducera"))); } FC_LOG_AND_RETHROW() +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(eosio_system_part3_tests) + BOOST_FIXTURE_TEST_CASE(multiple_producer_pay, eosio_system_tester, * boost::unit_test::tolerance(1e-10)) try { const int64_t secs_per_year = 52 * 7 * 24 * 3600; @@ -3035,6 +3043,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 @@ -3718,6 +3728,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"); From 2e865e5729a576c3df78f897e953849d5b2d657d Mon Sep 17 00:00:00 2001 From: Nathan James Date: Thu, 13 Jun 2024 12:39:03 +0100 Subject: [PATCH 34/37] rex2 docs update, ricardians, tests, rename maturity setter --- .../include/eosio.system/eosio.system.hpp | 12 +++--- .../ricardian/eosio.system.contracts.md.in | 31 +++++++++++++- contracts/eosio.system/src/rex.cpp | 4 +- tests/eosio.system_rex_matured_tests.cpp | 42 ++++++++++++------- tests/eosio.system_tester.hpp | 11 ++--- 5 files changed, 70 insertions(+), 30 deletions(-) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index e3c99d8d..52056b01 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -933,7 +933,7 @@ namespace eosiosystem { * * @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 ); @@ -949,7 +949,7 @@ namespace eosiosystem { * * @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 ); @@ -1078,7 +1078,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. @@ -1090,7 +1090,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. @@ -1100,7 +1100,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. @@ -1132,7 +1132,7 @@ namespace eosiosystem { * https://github.com/eosnetworkfoundation/eos-system-contracts/issues/135 */ [[eosio::action]] - void rexmaturity(const std::optional num_of_maturity_buckets, const std::optional sell_matured_rex, const std::optional buy_rex_to_savings ); + 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 diff --git a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in index dd5c7101..396f4be9 100644 --- a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in +++ b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in @@ -413,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

--- @@ -489,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. + +{{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. +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

diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index 79daea25..43bbac5f 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -8,7 +8,7 @@ namespace eosiosystem { using eosio::token; using eosio::seconds; - void system_contract::rexmaturity(const std::optional num_of_maturity_buckets, const std::optional sell_matured_rex, const std::optional buy_rex_to_savings ) + 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()); @@ -945,7 +945,7 @@ namespace eosiosystem { } /** - * @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 diff --git a/tests/eosio.system_rex_matured_tests.cpp b/tests/eosio.system_rex_matured_tests.cpp index cc6320cc..e9e5702b 100644 --- a/tests/eosio.system_rex_matured_tests.cpp +++ b/tests/eosio.system_rex_matured_tests.cpp @@ -22,12 +22,13 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_matured_rex, eosio_system_tester ) try { 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 }; - account_name alice = accounts[0], bob = accounts[1], charly = accounts[2], david = accounts[3]; + 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(), rexmaturity( 21, false, false ) ); + 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)); @@ -42,14 +43,14 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_matured_rex, eosio_system_tester ) try { 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 `buy_rex_to_savings=false` to test buying REX without moving it to REX savings - BOOST_REQUIRE_EQUAL( success(), rexmaturity( 21, true, false ) ); + 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)); @@ -59,8 +60,8 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_matured_rex, eosio_system_tester ) try { 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` to test selling matured REX - BOOST_REQUIRE_EQUAL( success(), rexmaturity( 21, false, true ) ); + // 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); @@ -73,12 +74,21 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_matured_rex, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( init_balance, get_rex_fund( charly ) ); // 4. legacy holders with matured REX - BOOST_REQUIRE_EQUAL( success(), rexmaturity( 5, false, false ) ); + 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(), rexmaturity( 21, true, true ) ); + 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( get_rex_balance_obj( mark )["vote_stake"].as(), core_sym::from_string("0.0000") ); + BOOST_REQUIRE_EQUAL( asset::from_string("10000.0000 REX"), get_buyrex_result( mark, core_sym::from_string("1.0000") ) ); + BOOST_REQUIRE_EQUAL( success(), mvfrsavings( mark, asset::from_string("10000.0000 REX") ) ); + // BOOST_REQUIRE_EQUAL( get_rex_balance_obj( mark )["rex_maturities"].get_array()[0].as(), get_rex_balance_obj( mark )["matured_rex"].as() ); + 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); @@ -90,6 +100,8 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_matured_rex, eosio_system_tester ) try { 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_tester.hpp b/tests/eosio.system_tester.hpp index afe27c72..7b6ca8df 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -796,13 +796,14 @@ class eosio_system_tester : public 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; @@ -930,8 +931,8 @@ class eosio_system_tester : public TESTER { return push_action( name(owner), "closerex"_n, mvo()("owner", owner) ); } - action_result rexmaturity(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, "rexmaturity"_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 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 ) { From 126d5f82caa05f8f54abe2f507befb473b467358 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Thu, 13 Jun 2024 12:41:44 +0100 Subject: [PATCH 35/37] remove unecessary tests --- tests/eosio.system_rex_matured_tests.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/eosio.system_rex_matured_tests.cpp b/tests/eosio.system_rex_matured_tests.cpp index e9e5702b..c7699bcd 100644 --- a/tests/eosio.system_rex_matured_tests.cpp +++ b/tests/eosio.system_rex_matured_tests.cpp @@ -84,10 +84,6 @@ BOOST_FIXTURE_TEST_CASE( buy_sell_matured_rex, eosio_system_tester ) try { // 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( get_rex_balance_obj( mark )["vote_stake"].as(), core_sym::from_string("0.0000") ); - BOOST_REQUIRE_EQUAL( asset::from_string("10000.0000 REX"), get_buyrex_result( mark, core_sym::from_string("1.0000") ) ); - BOOST_REQUIRE_EQUAL( success(), mvfrsavings( mark, asset::from_string("10000.0000 REX") ) ); - // BOOST_REQUIRE_EQUAL( get_rex_balance_obj( mark )["rex_maturities"].get_array()[0].as(), get_rex_balance_obj( mark )["matured_rex"].as() ); 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 From c2fac1a2f185647357dd191f22e1e486195dfbca Mon Sep 17 00:00:00 2001 From: Eric Passmore Date: Mon, 17 Jun 2024 15:21:22 -0700 Subject: [PATCH 36/37] resolve dup test name --- tests/eosio.system_tests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/eosio.system_tests.cpp b/tests/eosio.system_tests.cpp index 2f3db362..d576cdf1 100644 --- a/tests/eosio.system_tests.cpp +++ b/tests/eosio.system_tests.cpp @@ -3945,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 }; From 551fd9e7e14630ce97db9e396c525dfadc0f6dcc Mon Sep 17 00:00:00 2001 From: Nathan James Date: Tue, 25 Jun 2024 11:55:33 +0100 Subject: [PATCH 37/37] use actual producer list to split rewards instead of configured max --- contracts/eosio.bpay/src/eosio.bpay.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/eosio.bpay/src/eosio.bpay.cpp b/contracts/eosio.bpay/src/eosio.bpay.cpp index fee02ce1..42999b01 100644 --- a/contracts/eosio.bpay/src/eosio.bpay.cpp +++ b/contracts/eosio.bpay/src/eosio.bpay.cpp @@ -34,8 +34,6 @@ void bpay::on_transfer( const name from, const name to, const asset quantity, co check( _global.exists(), "global state does not exist"); uint16_t producer_count = _global.get().last_producer_schedule_size; - asset reward = quantity / producer_count; - // get producer with the most votes // using `by_votes` secondary index auto idx = _producers.get_index<"prototalvote"_n>(); @@ -54,6 +52,8 @@ void bpay::on_transfer( const name from, const name to, const asset quantity, co prod++; } + asset reward = quantity / top_producers.size(); + // distribute rewards to top producers for (auto producer : top_producers) { auto row = _rewards.find( producer.value );