diff --git a/.cicd/defaults.json b/.cicd/defaults.json index 136b822d..6af973a2 100644 --- a/.cicd/defaults.json +++ b/.cicd/defaults.json @@ -4,7 +4,7 @@ "prerelease":false }, "cdt":{ - "target":"hotstuff_integration", + "target":"main", "prerelease":false } } 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..5159d1e5 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 { @@ -41,15 +41,34 @@ namespace eosio { void create( const name& issuer, const asset& maximum_supply); /** - * This action issues to `to` account a `quantity` of tokens. + * 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. + * @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..e0f73123 100644 --- a/contracts/eosio.token/src/eosio.token.cpp +++ b/contracts/eosio.token/src/eosio.token.cpp @@ -22,7 +22,6 @@ void token::create( const name& issuer, }); } - void token::issue( const name& to, const asset& quantity, const string& memo ) { auto sym = quantity.symbol; @@ -49,6 +48,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 89d144fe..4015889a 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 7bce7fa8..ff16cc60 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()