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()