diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index 4c2574fa..d3dfc4b7 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -545,6 +545,29 @@ namespace eosiosystem { asset stake_change; }; + struct action_return_sellram { + name account; + asset quantity; + int64_t bytes_sold; + int64_t ram_bytes; + }; + + struct action_return_buyram { + name payer; + name receiver; + asset quantity; + int64_t bytes_purchased; + int64_t ram_bytes; + }; + + struct action_return_ramtransfer { + name from; + name to; + int64_t bytes; + int64_t from_ram_bytes; + int64_t to_ram_bytes; + }; + struct powerup_config_resource { std::optional current_weight_ratio; // Immediately set weight_ratio to this amount. 1x = 10^15. 0.01x = 10^13. // Do not specify to preserve the existing setting or use the default; @@ -1097,7 +1120,7 @@ namespace eosiosystem { * @param quant - the quantity of tokens to buy ram with. */ [[eosio::action]] - void buyram( const name& payer, const name& receiver, const asset& quant ); + action_return_buyram buyram( const name& payer, const name& receiver, const asset& quant ); /** * Buy a specific amount of ram bytes action. Increases receiver's ram in quantity of bytes provided. @@ -1108,7 +1131,30 @@ namespace eosiosystem { * @param bytes - the quantity of ram to buy specified in bytes. */ [[eosio::action]] - void buyrambytes( const name& payer, const name& receiver, uint32_t bytes ); + action_return_buyram buyrambytes( const name& payer, const name& receiver, uint32_t bytes ); + + /** + * The buyramself action is designed to enhance the permission security by allowing an account to purchase RAM exclusively for itself. + * This action prevents the potential risk associated with standard actions like buyram and buyrambytes, + * which can transfer EOS tokens out of the account, acting as a proxy for eosio.token::transfer. + * + * @param account - the ram buyer and receiver, + * @param quant - the quantity of tokens to buy ram with. + */ + [[eosio::action]] + action_return_buyram buyramself( const name& account, const asset& quant ); + + /** + * Logging for buyram & buyrambytes action + * + * @param payer - the ram buyer, + * @param receiver - the ram receiver, + * @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. + */ + [[eosio::action]] + void logbuyram( const name& payer, const name& receiver, const asset& quantity, int64_t bytes, int64_t ram_bytes ); /** * Sell ram action, reduces quota by bytes and then performs an inline transfer of tokens @@ -1118,7 +1164,49 @@ namespace eosiosystem { * @param bytes - the amount of ram to sell in bytes. */ [[eosio::action]] - void sellram( const name& account, int64_t bytes ); + action_return_sellram sellram( const name& account, int64_t bytes ); + + /** + * Logging for sellram action + * + * @param account - the ram seller, + * @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. + */ + [[eosio::action]] + void logsellram( const name& account, const asset& quantity, int64_t bytes, int64_t ram_bytes ); + + /** + * Transfer ram action, reduces sender's quota by bytes and increase receiver's quota by bytes. + * + * @param from - the ram sender account, + * @param to - the ram receiver account, + * @param bytes - the amount of ram to transfer in bytes, + * @param memo - the memo string to accompany the transaction. + */ + [[eosio::action]] + action_return_ramtransfer ramtransfer( const name& from, const name& to, int64_t bytes, const std::string& memo ); + + /** + * Burn ram action, reduces owner's quota by bytes. + * + * @param owner - the ram owner account, + * @param bytes - the amount of ram to be burned in bytes, + * @param memo - the memo string to accompany the transaction. + */ + [[eosio::action]] + action_return_ramtransfer ramburn( const name& owner, int64_t bytes, const std::string& memo ); + + /** + * Logging for ram changes + * + * @param owner - the ram owner account, + * @param bytes - the bytes balance change, + * @param ram_bytes - the ram bytes held by owner after the action. + */ + [[eosio::action]] + void logramchange( const name& owner, int64_t bytes, int64_t ram_bytes ); /** * Refund action, this action is called after the delegation-period to claim all pending @@ -1417,7 +1505,12 @@ namespace eosiosystem { 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>; + using logbuyram_action = eosio::action_wrapper<"logbuyram"_n, &system_contract::logbuyram>; using sellram_action = eosio::action_wrapper<"sellram"_n, &system_contract::sellram>; + 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 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>; using regproducer2_action = eosio::action_wrapper<"regproducer2"_n, &system_contract::regproducer2>; @@ -1496,6 +1589,9 @@ namespace eosiosystem { 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 ); + 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 ); // defined in voting.cpp void register_producer( const name& producer, const eosio::block_signing_authority& producer_authority, const std::string& url, uint16_t location ); diff --git a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in index bce8880a..d82a209b 100644 --- a/contracts/eosio.system/ricardian/eosio.system.contracts.md.in +++ b/contracts/eosio.system/ricardian/eosio.system.contracts.md.in @@ -59,6 +59,17 @@ icon: @ICON_BASE_URL@/@RESOURCE_ICON_URI@ {{payer}} buys RAM on behalf of {{receiver}} by paying {{quant}}. This transaction will incur a 0.5% fee out of {{quant}} and the amount of RAM delivered will depend on market rates. +

buyramself

+ +--- +spec_version: "0.2.0" +title: Buy RAM self +summary: '{{nowrap account}} buys RAM to self by paying {{nowrap quant}}' +icon: @ICON_BASE_URL@/@RESOURCE_ICON_URI@ +--- + +{{account}} buys RAM to self by paying {{quant}}. This transaction will incur a 0.5% fee out of {{quant}} and the amount of RAM delivered will depend on market rates. +

buyrambytes

--- @@ -413,6 +424,36 @@ icon: @ICON_BASE_URL@/@RESOURCE_ICON_URI@ Sell {{bytes}} bytes of unused RAM from account {{account}} at market price. This transaction will incur a 0.5% fee on the proceeds which depend on market rates. +

ramtransfer

+ +--- +spec_version: "0.2.0" +title: Transfer RAM from Account +summary: 'Transfer unused RAM from {{nowrap from}} to {{nowrap to}}' +icon: @ICON_BASE_URL@/@RESOURCE_ICON_URI@ +--- + +Transfer {{bytes}} bytes of unused RAM from account {{from}} to account {{to}}. + +{{#if memo}}There is a memo attached to the transfer stating: +{{memo}} +{{/if}} + +

ramburn

+ +--- +spec_version: "0.2.0" +title: Burn RAM from Account +summary: 'Burn unused RAM from {{nowrap owner}}' +icon: @ICON_BASE_URL@/@RESOURCE_ICON_URI@ +--- + +Burn {{bytes}} bytes of unused RAM from account {{owner}}. + +{{#if memo}}There is a memo attached to the burn stating: +{{memo}} +{{/if}} +

sellrex

--- diff --git a/contracts/eosio.system/src/delegate_bandwidth.cpp b/contracts/eosio.system/src/delegate_bandwidth.cpp index f8fc6f67..f9696edc 100644 --- a/contracts/eosio.system/src/delegate_bandwidth.cpp +++ b/contracts/eosio.system/src/delegate_bandwidth.cpp @@ -22,15 +22,21 @@ namespace eosiosystem { /** * This action will buy an exact amount of ram and bill the payer the current market price. */ - void system_contract::buyrambytes( const name& payer, const name& receiver, uint32_t bytes ) { + action_return_buyram system_contract::buyrambytes( const name& payer, const name& receiver, uint32_t bytes ) { auto itr = _rammarket.find(ramcore_symbol.raw()); const int64_t ram_reserve = itr->base.balance.amount; const int64_t eos_reserve = itr->quote.balance.amount; const int64_t cost = exchange_state::get_bancor_input( ram_reserve, eos_reserve, bytes ); const int64_t cost_plus_fee = cost / double(0.995); - buyram( payer, receiver, asset{ cost_plus_fee, core_symbol() } ); + return buyram( payer, receiver, asset{ cost_plus_fee, core_symbol() } ); } + /** + * Buy self ram action, ram can only be purchased to itself. + */ + action_return_buyram system_contract::buyramself( const name& account, const asset& quant ) { + return buyram( account, account, quant ); + } /** * When buying ram the payer irreversibly transfers quant to system contract and only @@ -40,10 +46,12 @@ namespace eosiosystem { * RAM is a scarce resource whose supply is defined by global properties max_ram_size. RAM is * priced using the bancor algorithm such that price-per-byte with a constant reserve ratio of 100:1. */ - void system_contract::buyram( const name& payer, const name& receiver, const asset& quant ) + action_return_buyram system_contract::buyram( const name& payer, const name& receiver, const asset& quant ) { require_auth( payer ); update_ram_supply(); + require_recipient(payer); + require_recipient(receiver); check( quant.symbol == core_symbol(), "must buy ram with core token" ); check( quant.amount > 0, "must purchase a positive amount" ); @@ -79,27 +87,20 @@ namespace eosiosystem { _gstate.total_ram_bytes_reserved += uint64_t(bytes_out); _gstate.total_ram_stake += quant_after_fee.amount; - user_resources_table userres( get_self(), receiver.value ); - auto res_itr = userres.find( receiver.value ); - if( res_itr == userres.end() ) { - res_itr = userres.emplace( receiver, [&]( auto& res ) { - res.owner = receiver; - res.net_weight = asset( 0, core_symbol() ); - res.cpu_weight = asset( 0, core_symbol() ); - res.ram_bytes = bytes_out; - }); - } else { - userres.modify( res_itr, receiver, [&]( auto& res ) { - res.ram_bytes += bytes_out; - }); - } + const int64_t ram_bytes = add_ram( receiver, bytes_out ); - auto voter_itr = _voters.find( res_itr->owner.value ); - if( voter_itr == _voters.end() || !has_field( voter_itr->flags1, voter_info::flags1_fields::ram_managed ) ) { - int64_t ram_bytes, net, cpu; - get_resource_limits( res_itr->owner, ram_bytes, net, cpu ); - set_resource_limits( res_itr->owner, res_itr->ram_bytes + ram_gift_bytes, net, cpu ); - } + // logging + system_contract::logbuyram_action logbuyram_act{ get_self(), { {get_self(), active_permission} } }; + logbuyram_act.send( payer, receiver, quant, bytes_out, ram_bytes ); + + // action return value + return action_return_buyram{ payer, receiver, quant, bytes_out, ram_bytes }; + } + + void system_contract::logbuyram( const name& payer, const name& receiver, const asset& quantity, int64_t bytes, int64_t ram_bytes ) { + require_auth( get_self() ); + require_recipient(payer); + require_recipient(receiver); } /** @@ -108,16 +109,11 @@ namespace eosiosystem { * tomorrow. Overall this will result in the market balancing the supply and demand * for RAM over time. */ - void system_contract::sellram( const name& account, int64_t bytes ) { + action_return_sellram system_contract::sellram( const name& account, int64_t bytes ) { require_auth( account ); update_ram_supply(); - - check( bytes > 0, "cannot sell negative byte" ); - - user_resources_table userres( get_self(), account.value ); - auto res_itr = userres.find( account.value ); - check( res_itr != userres.end(), "no resource row" ); - check( res_itr->ram_bytes >= bytes, "insufficient quota" ); + require_recipient(account); + const int64_t ram_bytes = reduce_ram(account, bytes); asset tokens_out; auto itr = _rammarket.find(ramcore_symbol.raw()); @@ -134,17 +130,6 @@ namespace eosiosystem { //// this shouldn't happen, but just in case it does we should prevent it check( _gstate.total_ram_stake >= 0, "error, attempt to unstake more tokens than previously staked" ); - userres.modify( res_itr, account, [&]( auto& res ) { - res.ram_bytes -= bytes; - }); - - auto voter_itr = _voters.find( res_itr->owner.value ); - if( voter_itr == _voters.end() || !has_field( voter_itr->flags1, voter_info::flags1_fields::ram_managed ) ) { - int64_t ram_bytes, net, cpu; - get_resource_limits( res_itr->owner, ram_bytes, net, cpu ); - set_resource_limits( res_itr->owner, res_itr->ram_bytes + ram_gift_bytes, net, cpu ); - } - { 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" ); @@ -156,6 +141,104 @@ namespace eosiosystem { transfer_act.send( account, ramfee_account, asset(fee, core_symbol()), "sell ram fee" ); channel_to_rex( 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 ); + + // action return value + return action_return_sellram{ account, tokens_out, bytes, ram_bytes }; + } + + void system_contract::logsellram( const name& account, const asset& quantity, int64_t bytes, int64_t ram_bytes ) { + require_auth( get_self() ); + require_recipient(account); + } + + /** + * This action will transfer RAM bytes from one account to another. + */ + action_return_ramtransfer system_contract::ramtransfer( const name& from, const name& to, int64_t bytes, const std::string& memo ) { + require_auth( from ); + update_ram_supply(); + check( memo.size() <= 256, "memo has more than 256 bytes" ); + const int64_t from_ram_bytes = reduce_ram( from, bytes ); + const int64_t to_ram_bytes = add_ram( to, bytes ); + require_recipient( from ); + require_recipient( to ); + + // action return value + return action_return_ramtransfer{ from, to, bytes, from_ram_bytes, to_ram_bytes }; + } + + /** + * This action will burn RAM bytes from owner account. + */ + action_return_ramtransfer system_contract::ramburn( const name& owner, int64_t bytes, const std::string& memo ) { + require_auth( owner ); + return ramtransfer( owner, null_account, bytes, memo ); + } + + [[eosio::action]] + void system_contract::logramchange( const name& owner, int64_t bytes, int64_t ram_bytes ) + { + require_auth( get_self() ); + require_recipient( owner ); + } + + int64_t system_contract::reduce_ram( const name& owner, int64_t bytes ) { + check( bytes > 0, "cannot reduce negative byte" ); + user_resources_table userres( get_self(), owner.value ); + auto res_itr = userres.find( owner.value ); + check( res_itr != userres.end(), "no resource row" ); + check( res_itr->ram_bytes >= bytes, "insufficient quota" ); + + userres.modify( res_itr, same_payer, [&]( auto& res ) { + res.ram_bytes -= bytes; + }); + set_resource_ram_bytes_limits( owner ); + + // logging + system_contract::logramchange_action logramchange_act{ get_self(), { {get_self(), active_permission} }}; + logramchange_act.send( owner, -bytes, res_itr->ram_bytes ); + return res_itr->ram_bytes; + } + + int64_t system_contract::add_ram( const name& owner, int64_t bytes ) { + check( bytes > 0, "cannot add negative byte" ); + check( is_account(owner), "owner=" + owner.to_string() + " account does not exist"); + user_resources_table userres( get_self(), owner.value ); + auto res_itr = userres.find( owner.value ); + if ( res_itr == userres.end() ) { + userres.emplace( owner, [&]( auto& res ) { + res.owner = owner; + res.net_weight = asset( 0, core_symbol() ); + res.cpu_weight = asset( 0, core_symbol() ); + res.ram_bytes = bytes; + }); + } else { + userres.modify( res_itr, same_payer, [&]( auto& res ) { + res.ram_bytes += bytes; + }); + } + set_resource_ram_bytes_limits( owner ); + + // logging + system_contract::logramchange_action logramchange_act{ get_self(), { {get_self(), active_permission} } }; + logramchange_act.send( owner, bytes, res_itr->ram_bytes ); + return res_itr->ram_bytes; + } + + void system_contract::set_resource_ram_bytes_limits( const name& owner ) { + user_resources_table userres( get_self(), owner.value ); + auto res_itr = userres.find( owner.value ); + + auto voter_itr = _voters.find( owner.value ); + if ( voter_itr == _voters.end() || !has_field( voter_itr->flags1, voter_info::flags1_fields::ram_managed ) ) { + int64_t ram_bytes, net, cpu; + get_resource_limits( owner, ram_bytes, net, cpu ); + set_resource_limits( owner, res_itr->ram_bytes + ram_gift_bytes, net, cpu ); + } } void validate_b1_vesting( int64_t stake ) { diff --git a/tests/eosio.system_ram_tests.cpp b/tests/eosio.system_ram_tests.cpp new file mode 100644 index 00000000..b5b3e298 --- /dev/null +++ b/tests/eosio.system_ram_tests.cpp @@ -0,0 +1,192 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "eosio.system_tester.hpp" + +using namespace eosio_system; + +BOOST_AUTO_TEST_SUITE(eosio_system_ram_tests); + +// ramtransfer +BOOST_FIXTURE_TEST_CASE( ram_transfer, eosio_system_tester ) try { + const std::vector accounts = { "alice"_n, "bob"_n }; + create_accounts_with_resources( accounts ); + const account_name alice = accounts[0]; + const account_name bob = accounts[1]; + + transfer( config::system_account_name, alice, core_sym::from_string("100.0000"), config::system_account_name ); + transfer( config::system_account_name, bob, core_sym::from_string("100.0000"), config::system_account_name ); + BOOST_REQUIRE_EQUAL( success(), buyrambytes( alice, alice, 10000 ) ); + BOOST_REQUIRE_EQUAL( success(), buyrambytes( bob, bob, 10000 ) ); + + const uint64_t alice_before = get_total_stake( alice )["ram_bytes"].as_uint64(); + const uint64_t bob_before = get_total_stake( bob )["ram_bytes"].as_uint64(); + + ramtransfer( alice, bob, 1000, "" ); + + const uint64_t alice_after = get_total_stake( alice )["ram_bytes"].as_uint64(); + const uint64_t bob_after = get_total_stake( bob )["ram_bytes"].as_uint64(); + + BOOST_REQUIRE_EQUAL( alice_before - 1000, alice_after ); + BOOST_REQUIRE_EQUAL( bob_before + 1000, bob_after ); + + /* + * The from_ram_bytes is alice's ram byte total + * The to_ram_bytes is bob's ram byte total + * Accounts start with 8,000 ram bytes + * this is via create_accounts_with_resources() + * Next buyram on each account purchases 10,000 additional ram bytes + * minus fees of 17 ram bytes + * + * Before ram transfer the totals for each account are 17,983 ram bytes + * After transfer of 1,000 bytes + * bob and alice respective totals are 18,983 and 16,983 + * After the validate ram transfer below of 1 ram byte + * bob and alices respective totals are 18,984 and 16,982 + */ + const char* expected_return_data = R"=====( +{ + "from": "alice", + "to": "bob", + "bytes": 1, + "from_ram_bytes": 16982, + "to_ram_bytes": 18984 +} +)====="; + validate_ramtransfer_return(alice, bob, 1, "", + "action_return_ramtransfer", expected_return_data ); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( buy_sell_ram_validate, eosio_system_tester ) try { + const std::vector accounts = { "alice"_n, "bob"_n }; + create_accounts_with_resources( accounts ); + const account_name alice = accounts[0]; + const account_name bob = accounts[1]; + + transfer( config::system_account_name, alice, core_sym::from_string("100.0000"), config::system_account_name ); + transfer( config::system_account_name, bob, core_sym::from_string("100.0000"), config::system_account_name ); + BOOST_REQUIRE_EQUAL( success(), buyrambytes( bob, bob, 10000 ) ); + + const char* expected_buyrambytes_return_data = R"=====( +{ + "payer": "alice", + "receiver": "alice", + "quantity": "0.1462 TST", + "bytes_purchased": 9991, + "ram_bytes": 17983 +} +)====="; + validate_buyrambytes_return(alice, alice, 10000, + "action_return_buyram", expected_buyrambytes_return_data ); + + const char* expected_sellram_return_data = R"=====( +{ + "account": "alice", + "quantity": "0.1455 TST", + "bytes_sold": 10000, + "ram_bytes": 7983 +} +)====="; + validate_sellram_return(alice, 10000, + "action_return_sellram", expected_sellram_return_data ); + + const char* expected_buyram_return_data = R"=====( +{ + "payer": "bob", + "receiver": "alice", + "quantity": "2.0000 TST", + "bytes_purchased": 136750, + "ram_bytes": 144733 +} +)====="; + validate_buyram_return(bob, alice, core_sym::from_string("2.0000"), + "action_return_buyram", expected_buyram_return_data ); +} FC_LOG_AND_RETHROW() + +// ramburn +BOOST_FIXTURE_TEST_CASE( ram_burn, eosio_system_tester ) try { + const std::vector accounts = { "alice"_n, "bob"_n }; + create_accounts_with_resources( accounts ); + const account_name alice = accounts[0]; + const account_name bob = accounts[1]; + const account_name null_account = "eosio.null"_n; + + transfer( config::system_account_name, alice, core_sym::from_string("100.0000"), config::system_account_name ); + transfer( config::system_account_name, bob, 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 char* expected_buyramself_return_data = R"=====( +{ + "payer": "bob", + "receiver": "bob", + "quantity": "10.0000 TST", + "bytes_purchased": 683747, + "ram_bytes": 691739 +} +)====="; + validate_buyramself_return(bob, core_sym::from_string("10.0000"), + "action_return_buyram", expected_buyramself_return_data ) ; + + const uint64_t null_before_burn = get_total_stake( null_account )["ram_bytes"].as_uint64(); + const uint64_t alice_before_burn = get_total_stake( alice )["ram_bytes"].as_uint64(); + + // burn action + BOOST_REQUIRE_EQUAL( success(), ramburn( alice, 3000, "burn RAM memo" ) ); + const uint64_t alice_after_burn = get_total_stake( alice )["ram_bytes"].as_uint64(); + const uint64_t null_after_burn = get_total_stake( null_account )["ram_bytes"].as_uint64(); + BOOST_REQUIRE_EQUAL( alice_before_burn - 3000, alice_after_burn ); + BOOST_REQUIRE_EQUAL( null_before_burn + 3000, null_after_burn ); + + const char* expected_ramburn_return_data = R"=====( +{ + "from": "bob", + "to": "eosio.null", + "bytes": 1, + "from_ram_bytes": 691738, + "to_ram_bytes": 12992 +} +)====="; + validate_ramburn_return(bob, 1, "burn RAM memo", + "action_return_ramtransfer", expected_ramburn_return_data ); + +} FC_LOG_AND_RETHROW() + + +// buyramself +BOOST_FIXTURE_TEST_CASE( buy_ram_self, eosio_system_tester ) try { + const std::vector accounts = { "alice"_n }; + create_accounts_with_resources( accounts ); + const account_name alice = accounts[0]; + + transfer( config::system_account_name, alice, core_sym::from_string("100.0000"), config::system_account_name ); + const uint64_t alice_before = get_total_stake( alice )["ram_bytes"].as_uint64(); + BOOST_REQUIRE_EQUAL( success(), buyramself( alice, core_sym::from_string("1.0000")) ); + const uint64_t alice_after = get_total_stake( alice )["ram_bytes"].as_uint64(); + BOOST_REQUIRE_EQUAL( alice_before + 68375, alice_after ); + + const char* expected_buyramself_return_data = R"=====( +{ + "payer": "alice", + "receiver": "alice", + "quantity": "2.0000 TST", + "bytes_purchased": 136750, + "ram_bytes": 213117 +} +)====="; + + validate_buyramself_return(alice, core_sym::from_string("2.0000"), + "action_return_buyram", expected_buyramself_return_data ); +} FC_LOG_AND_RETHROW() + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/eosio.system_tester.hpp b/tests/eosio.system_tester.hpp index 6eae96d2..93a8eb24 100644 --- a/tests/eosio.system_tester.hpp +++ b/tests/eosio.system_tester.hpp @@ -1,10 +1,10 @@ #pragma once -#include -#include -#include #include "contracts.hpp" #include "test_symbol.hpp" +#include +#include +#include #include #include @@ -25,7 +25,6 @@ using mvo = fc::mutable_variant_object; namespace eosio_system { - class eosio_system_tester : public TESTER { public: @@ -203,6 +202,345 @@ class eosio_system_tester : public TESTER { return push_transaction( trx ); } + /* + * Doing an idump((trace->action_traces[i].return_value)) will show the full hex + * Converting return_value to a string will transform it into a series of ordianl values represented by chars + * fc::to_hex() did not work. + * Couldn't find a method to convert so wrote my own. + */ + std::string convert_ordinals_to_hex(const std::string& ordinals) { + // helper to convert to hex, 2 chars for hex, 3 char null terminator + char hex_temp[3]; + // return string + std::string hex_as_chars; + + for (unsigned char c : ordinals) { + // convert to hex from ordinal + sprintf(hex_temp, "%x", (int)c); + if (hex_temp[1] == '\0') { + hex_temp[1] = hex_temp[0]; + hex_temp[0] = '0'; + // null terminate + hex_temp[2] = '\0'; + } + hex_as_chars += hex_temp[0]; + hex_as_chars += hex_temp[1]; + } + return hex_as_chars; + } + + std::string convert_json_to_hex(const type_name& type, const std::string& json) { + // ABI for our return struct + const char* ramtransfer_return_abi = R"=====( + { + "version": "eosio::abi/1.2", + "types": [], + "structs": [ + { + "name": "action_return_buyram", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "bytes_purchased", + "type": "int64" + }, + { + "name": "ram_bytes", + "type": "int64" + } + ] + }, + { + "name": "action_return_ramtransfer", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "to", + "type": "name" + }, + { + "name": "bytes", + "type": "int64" + }, + { + "name": "from_ram_bytes", + "type": "int64" + }, + { + "name": "to_ram_bytes", + "type": "int64" + } + ] + }, + { + "name": "action_return_sellram", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "bytes_sold", + "type": "int64" + }, + { + "name": "ram_bytes", + "type": "int64" + } + ] + }, + ], + "actions": [], + "tables": [], + "ricardian_clauses": [], + "variants": [], + "action_results": [ + { + "name": "buyram", + "result_type": "action_return_buyram" + }, + { + "name": "buyrambytes", + "result_type": "action_return_buyram" + }, + { + "name": "buyramself", + "result_type": "action_return_buyram" + }, + { + "name": "ramburn", + "result_type": "action_return_ramtransfer" + }, + { + "name": "ramtransfer", + "result_type": "action_return_ramtransfer" + }, + { + "name": "sellram", + "result_type": "action_return_sellram" + } + ] + } + )====="; + + // create abi to parse return values + auto abi = fc::json::from_string(ramtransfer_return_abi).as(); + abi_serializer ramtransfer_return_serializer = abi_serializer{std::move(abi), abi_serializer::create_yield_function( abi_serializer_max_time )}; + + auto return_json = fc::json::from_string(json); + auto serialized_bytes = ramtransfer_return_serializer.variant_to_binary(type, return_json, abi_serializer::create_yield_function( abi_serializer_max_time )); + return fc::to_hex(serialized_bytes); + } + + action_result buyram( const account_name& payer, account_name receiver, const asset& eosin ) { + return push_action( payer, "buyram"_n, mvo()( "payer",payer)("receiver",receiver)("quant",eosin) ); + } + action_result buyram( std::string_view payer, std::string_view receiver, const asset& eosin ) { + return buyram( account_name(payer), account_name(receiver), eosin ); + } + + void validate_buyram_return(const account_name& payer, account_name receiver, const asset& eosin, + 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, "buyram"_n, payer, + mvo()("payer",payer)("receiver",receiver)("quant",eosin)); + 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)); + + /* + * This is assignment is giving me grief + * Doing an idump((trace->action_traces[i].return_value)) will show the full hex + * Inspecting trace->action_traces[i].return_value shows the hex have been converted to ordinals + * Couldn't find a method to convert so wrote my own. + */ + // 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); + } + + action_result ramtransfer(const account_name& from, const account_name& to, uint32_t bytes, const std::string& memo) { + return push_action(from, "ramtransfer"_n, + mvo()("from", from)("to", to)("bytes", bytes)("memo", memo)); + } + + void validate_ramtransfer_return(const account_name& from, const account_name& to, uint32_t bytes, 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, "ramtransfer"_n, from, + mvo()("from", from)("to", to)("bytes", bytes)("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); + } + + action_result ramburn(const account_name& owner, uint32_t bytes, const std::string& memo) + { + return push_action(owner, "ramburn"_n, mvo()("owner", owner)("bytes", bytes)("memo", memo)); + } + action_result ramburn(std::string_view owner, uint32_t bytes, const std::string& memo) + { + return ramburn(account_name(owner), bytes, 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 + std::string expected_hex = convert_json_to_hex(type, json); + // initialize string that will hold actual return + std::string actual_hex; + + auto trace = base_tester::push_action(config::system_account_name, "ramburn"_n, owner, mvo()("owner", owner)("bytes", bytes)("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()); + // convert from string of ordinals to char representation of hex + actual_hex = convert_ordinals_to_hex(copy_trace); + + // test fails here actual_hex is + BOOST_REQUIRE_EQUAL(expected_hex,actual_hex); + } + + action_result buyrambytes(const account_name& payer, account_name receiver, uint32_t numbytes) + { + return push_action(payer, "buyrambytes"_n, mvo()("payer", payer)("receiver", receiver)("bytes", numbytes)); + } + action_result buyrambytes(std::string_view payer, std::string_view receiver, uint32_t numbytes) + { + return buyrambytes(account_name(payer), account_name(receiver), numbytes); + } + + void validate_buyrambytes_return(const account_name& payer, account_name receiver, uint32_t numbytes, + 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; + + auto trace = base_tester::push_action(config::system_account_name, "buyrambytes"_n, payer, + mvo()("payer", payer)("receiver", receiver)("bytes", numbytes)); + 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()); + // convert from string of ordinals to char representation of hex + actual_hex = convert_ordinals_to_hex(copy_trace); + + // test fails here actual_hex is + BOOST_REQUIRE_EQUAL(expected_hex,actual_hex); + } + + action_result sellram(const account_name& account, uint64_t numbytes) + { + return push_action(account, "sellram"_n, mvo()("account", account)("bytes", numbytes)); + } + action_result sellram(std::string_view account, uint64_t numbytes) { return sellram(account_name(account), numbytes); } + + void validate_sellram_return(const account_name& account, uint32_t numbytes, + 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; + + auto trace = base_tester::push_action(config::system_account_name, "sellram"_n, account, mvo()("account", account)("bytes", numbytes)); + 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()); + // convert from string of ordinals to char representation of hex + actual_hex = convert_ordinals_to_hex(copy_trace); + + // test fails here actual_hex is + BOOST_REQUIRE_EQUAL(expected_hex,actual_hex); + } + + action_result buyramself(const account_name& account, const asset& quant) + { + return push_action(account, "buyramself"_n, mvo()("account", account)("quant", quant)); + } + + void validate_buyramself_return(const account_name& account, const asset& quant, + 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; + + auto trace = base_tester::push_action(config::system_account_name, "buyramself"_n, account, mvo()("account", account)("quant", quant)); + 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()); + // convert from string of ordinals to char representation of hex + actual_hex = convert_ordinals_to_hex(copy_trace); + + // test fails here actual_hex is + BOOST_REQUIRE_EQUAL(expected_hex,actual_hex); + } + transaction_trace_ptr setup_producer_accounts( const std::vector& accounts, asset ram = core_sym::from_string("1.0000"), asset cpu = core_sym::from_string("80.0000"), @@ -246,27 +584,6 @@ class eosio_system_tester : public TESTER { return push_transaction( trx ); } - action_result buyram( const account_name& payer, account_name receiver, const asset& eosin ) { - return push_action( payer, "buyram"_n, mvo()( "payer",payer)("receiver",receiver)("quant",eosin) ); - } - action_result buyram( std::string_view payer, std::string_view receiver, const asset& eosin ) { - return buyram( account_name(payer), account_name(receiver), eosin ); - } - - action_result buyrambytes( const account_name& payer, account_name receiver, uint32_t numbytes ) { - return push_action( payer, "buyrambytes"_n, mvo()( "payer",payer)("receiver",receiver)("bytes",numbytes) ); - } - action_result buyrambytes( std::string_view payer, std::string_view receiver, uint32_t numbytes ) { - return buyrambytes( account_name(payer), account_name(receiver), numbytes ); - } - - action_result sellram( const account_name& account, uint64_t numbytes ) { - return push_action( account, "sellram"_n, mvo()( "account", account)("bytes",numbytes) ); - } - action_result sellram( std::string_view account, uint64_t numbytes ) { - return sellram( account_name(account), numbytes ); - } - action_result push_action( const account_name& signer, const action_name &name, const variant_object &data, bool auth = true ) { string action_type_name = abi_ser.get_action_type(name); @@ -707,7 +1024,7 @@ class eosio_system_tester : public TESTER { memcpy( data.data(), itr->value.data(), data.size() ); return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "rex_return_buckets", data, abi_serializer::create_yield_function(abi_serializer_max_time) ); } - + void setup_rex_accounts( const std::vector& accounts, const asset& init_balance, const asset& net = core_sym::from_string("80.0000"),