From 1741995ac8a3985e85fa11cc79ff56ff21af91b4 Mon Sep 17 00:00:00 2001 From: Nathan James Date: Mon, 3 Jun 2024 17:18:18 +0100 Subject: [PATCH 1/3] 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 86cd477..f96b0e4 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 0000000..840d975 --- /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 0000000..5cf6290 --- /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 0000000..b91a1e4 --- /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 0000000..e4942df --- /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 0000000..b37bc8f --- /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 7280e79..cce660d 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 0000000..1d97400 --- /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 17fd69e..05746db 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 8efa57e..65142e6 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 2/3] 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 840d975..1b5d85d 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 5cf6290..042ebd3 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 b91a1e4..0000000 --- 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 e4942df..0000000 --- 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 0000000..85da666 --- /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 b37bc8f..fee02ce 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 1d97400..19beb4f 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 3/3] 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 1b5d85d..09fe613 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 )