From 6950de5aaf3d31be9962beb62666eda80040fca4 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Wed, 25 Jan 2023 11:02:27 -0600 Subject: [PATCH 1/5] GH-273 Add garbage collection of account storage --- contract/include/evm_runtime/evm_contract.hpp | 4 +++ contract/include/evm_runtime/state.hpp | 3 ++ contract/include/evm_runtime/tables.hpp | 11 ++++++++ contract/src/actions.cpp | 5 ++++ contract/src/state.cpp | 28 +++++++++++++++---- 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/contract/include/evm_runtime/evm_contract.hpp b/contract/include/evm_runtime/evm_contract.hpp index c5155628..087734bf 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -22,6 +22,10 @@ CONTRACT evm_contract : public contract { [[eosio::action]] void pushtx(eosio::name ram_payer, const bytes& rlptx); + /// @return true if all garbage has been collected + [[eosio::action]] + bool gc(uint32_t max); + #ifdef WITH_TEST_ACTIONS ACTION testtx( const bytes& rlptx, const evm_runtime::test::block_info& bi ); ACTION updatecode( const bytes& address, uint64_t incarnation, const bytes& code_hash, const bytes& code); diff --git a/contract/include/evm_runtime/state.hpp b/contract/include/evm_runtime/state.hpp index 4f360cb5..6455d380 100644 --- a/contract/include/evm_runtime/state.hpp +++ b/contract/include/evm_runtime/state.hpp @@ -69,6 +69,9 @@ struct state : State { void update_account(const evmc::address& address, std::optional initial, std::optional current) override; + /// @return true if all garbage has been collected + bool gc(uint32_t max); + void update_account_code(const evmc::address& address, uint64_t incarnation, const evmc::bytes32& code_hash, ByteView code) override; diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index 967a06ff..501b6307 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -70,4 +70,15 @@ typedef multi_index< "storage"_n, storage, indexed_by<"by.key"_n, const_mem_fun> > storage_table; +struct [[eosio::table]] [[eosio::contract("evm_contract")]] gcstore { + uint64_t id; + uint64_t storage_id; + + uint64_t primary_key()const { return id; } + + EOSLIB_SERIALIZE(gcstore, (id)(storage_id)); +}; + +typedef multi_index< "gcstore"_n, gcstore> gc_store_table; + } //namespace evm_runtime \ No newline at end of file diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 86b9a4c8..cab00652 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -69,6 +69,11 @@ void evm_contract::pushtx( eosio::name ram_payer, const bytes& rlptx ) { LOGTIME("EVM EXECUTE"); } +bool evm_contract::gc(uint32_t max) { + evm_runtime::state state{get_self(), eosio::same_payer}; + return state.gc(max); +} + #ifdef WITH_TEST_ACTIONS ACTION evm_contract::testtx( const bytes& rlptx, const evm_runtime::test::block_info& bi ) { assert_inited(); diff --git a/contract/src/state.cpp b/contract/src/state.cpp index e4d8744b..8e38992d 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -111,17 +111,35 @@ void state::update_account(const evmc::address& address, std::optional } else { if(itr != inx.end()) { storage_table db(_self, itr->id); - auto sitr = db.begin(); - while( sitr != db.end() ) { - sitr = db.erase(sitr); - ++stats.storage.remove; - } + // add to garbage collection table for later removal + gc_store_table gc(_self, _self.value); + gc.emplace(_ram_payer, [&](auto& row){ + row.id = gc.available_primary_key(); + row.storage_id = itr->id; + }); accounts.erase(*itr); ++stats.account.remove; } } } +bool state::gc(uint32_t max) { + gc_store_table gc(_self, _self.value); + auto i = gc.begin(); + while( max && i != gc.end() ) { + storage_table db(_self, i->storage_id); + auto sitr = db.begin(); + while( max && sitr != db.end() ) { + sitr = db.erase(sitr); + --max; + } + if( !max ) break; + i = gc.erase(i); + --max; + } + return gc.begin() == gc.end(); +} + void state::update_account_code(const evmc::address& address, uint64_t, const evmc::bytes32& code_hash, ByteView code) { account_table accounts(_self, _self.value); auto inx = accounts.get_index<"by.address"_n>(); From 0f1bf600ab8dc12a62ae3d2ae6dacd690ef66983 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Fri, 27 Jan 2023 10:51:26 -0600 Subject: [PATCH 2/5] GH-273 Add testing for gc --- contract/src/actions.cpp | 32 ++++++++++++++++--- contract/tests/evm_runtime_tests.cpp | 48 ++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index cab00652..e85196d3 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -141,6 +141,14 @@ ACTION evm_contract::dumpall() { eosio::require_auth(get_self()); + auto print_store = [](auto sitr) { + eosio::print(" "); + eosio::printhex(sitr->key.data(), sitr->key.size()); + eosio::print(":"); + eosio::printhex(sitr->value.data(), sitr->value.size()); + eosio::print("\n"); + }; + account_table accounts(_self, _self.value); auto itr = accounts.begin(); eosio::print("DUMPALL start\n"); @@ -151,16 +159,29 @@ ACTION evm_contract::dumpall() { storage_table db(_self, itr->id); auto sitr = db.begin(); while( sitr != db.end() ) { - eosio::print(" "); - eosio::printhex(sitr->key.data(), sitr->key.size()); - eosio::print(":"); - eosio::printhex(sitr->value.data(), sitr->value.size()); - eosio::print("\n"); + print_store( sitr ); sitr++; } itr++; } + eosio::print(" gc:"); + gc_store_table gc(_self, _self.value); + auto i = gc.begin(); + while( i != gc.end() ) { + eosio::print(" storage_id:"); + eosio::print(i->storage_id); + eosio::print("\n"); + storage_table db(_self, i->storage_id); + auto sitr = db.begin(); + while( sitr != db.end() ) { + print_store( sitr ); + ++sitr; + } + + ++i; + } + eosio::print("DUMPALL end\n"); } @@ -192,6 +213,7 @@ ACTION evm_contract::clearall() { eosio::print("db size:", uint64_t(db_size), "\n"); itr = accounts.erase(itr); } + gc(std::numeric_limits::max()); auto account_size = std::distance(accounts.cbegin(), accounts.cend()); eosio::print("accounts size:", uint64_t(account_size), "\n"); diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index 25991fcd..a4a4c451 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -364,6 +364,21 @@ struct storage { }; FC_REFLECT(storage, (id)(key)(value)); +struct gcstore { + uint64_t id; + uint64_t storage_id; + + static name table_name() { return "gcstore"_n; } + static name index_name(const name& n) { + BOOST_REQUIRE(false); + return name{0}; + } + + static name index_name(uint64_t n) { + return index_name(name{n}); + } +}; +FC_REFLECT(gcstore, (id)(storage_id)); struct assert_message_check { string _expected; @@ -496,6 +511,12 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { ); } + action_result gc( uint32_t max, name signer=ME ) { + return call(signer, "gc"_n, mvo() + ("max", max) + ); + } + action_result dumpstorage(const bytes& address, name signer=ME ) { return call(signer, "dumpstorage"_n, mvo() ("addy", address) @@ -725,6 +746,26 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { return count; } + size_t gc_size() { + auto& db = const_cast(control->db()); + const auto* tid = db.find( + boost::make_tuple("evm"_n, "evm"_n, gcstore::table_name()) + ); + + if(tid == nullptr) { + return 0; + } + + const auto& idx = db.get_index(); + auto itr = idx.lower_bound( boost::make_tuple(tid->id) ); + size_t count=0; + while ( itr != idx.end() && itr->t_id == tid->id ) { + ++itr; + ++count; + } + return count; + } + //---- std::vector excluded_tests; std::vector included_tests; @@ -914,6 +955,11 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { } } + if( gc_size() != 0 ) { + std::cout << "gcstore is not empty: " << gc_size() << std::endl; + return false; + } + return true; } @@ -935,6 +981,8 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { return Status::kPassed; } + gc(std::numeric_limits::max()); + if (post_check(json_test["postState"])) { return Status::kPassed; } else { From ed2b6e80514f001068f28c4afc13dbad5f3cfb74 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Sat, 28 Jan 2023 14:02:57 -0500 Subject: [PATCH 3/5] do not upgrade wasm limits to "high" for non-exhasutive EVM tests --- contract/tests/basic_evm_tester.hpp | 56 +---------------------------- 1 file changed, 1 insertion(+), 55 deletions(-) diff --git a/contract/tests/basic_evm_tester.hpp b/contract/tests/basic_evm_tester.hpp index e3aea82a..6b4538b2 100644 --- a/contract/tests/basic_evm_tester.hpp +++ b/contract/tests/basic_evm_tester.hpp @@ -11,64 +11,10 @@ using namespace eosio; using namespace eosio::chain; using mvo = fc::mutable_variant_object; -static const char wasm_level_up_wast[] = R"=====( -(module - (import "env" "set_wasm_parameters_packed" (func $set_wasm_parameters_packed (param i32 i32))) - (memory $0 1) - (export "apply" (func $apply)) - (func $apply (param $0 i64) (param $1 i64) (param $2 i64) - (call $set_wasm_parameters_packed - (i32.const 0) - (i32.const 48) - ) - ) - ;; this is intended to be the same settings as used by eosio.system's 3.1.1 "high" setting - (data (i32.const 0) "\00\00\00\00") ;; version - (data (i32.const 4) "\00\20\00\00") ;; max_mutable_global_bytes - (data (i32.const 8) "\00\20\00\00") ;; max_table_elements - (data (i32.const 12) "\00\20\00\00") ;; max_section_elements - (data (i32.const 16) "\00\00\10\00") ;; max_linear_memory_init - (data (i32.const 20) "\00\20\00\00") ;; max_func_local_bytes - (data (i32.const 24) "\00\04\00\00") ;; max_nested_structures - (data (i32.const 28) "\00\20\00\00") ;; max_symbol_bytes - (data (i32.const 32) "\00\00\40\01") ;; max_module_bytes - (data (i32.const 36) "\00\00\40\01") ;; max_code_bytes - (data (i32.const 40) "\10\02\00\00") ;; max_pages - (data (i32.const 44) "\00\04\00\00") ;; max_call_depth -) -)====="; - -static const char wasm_level_up_abi[] = R"=====( -{ - "version": "eosio::abi/1.2", - "types": [], - "structs": [ - { - "name": "dothedew", - "base": "", - "fields": [] - } - ], - "actions": [ - { - "name": "dothedew", - "type": "dothedew" - } - ] -} -)====="; - class basic_evm_tester : public testing::validating_tester { public: basic_evm_tester() { - create_accounts({"wasmlevelup"_n, "evm"_n}); - push_action(config::system_account_name, "setpriv"_n, config::system_account_name, mvo()("account", "wasmlevelup"_n)("is_priv", 1)); - produce_blocks(2); - - set_code("wasmlevelup"_n, wasm_level_up_wast); - set_abi("wasmlevelup"_n, wasm_level_up_abi); - push_action("wasmlevelup"_n, "dothedew"_n, "wasmlevelup"_n, mvo()); - produce_blocks(2); + create_accounts({"evm"_n}); set_code("evm"_n, testing::contracts::evm_runtime_wasm()); set_abi("evm"_n, testing::contracts::evm_runtime_abi().data()); From 4cb5e8f4d88e0fcf79175808b93a17cdd3f5f1eb Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Sat, 28 Jan 2023 16:40:34 -0600 Subject: [PATCH 4/5] GH-273 Add assert_inited to gc action --- contract/src/actions.cpp | 2 ++ contract/tests/init_tests.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index e85196d3..8f475914 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -70,6 +70,8 @@ void evm_contract::pushtx( eosio::name ram_payer, const bytes& rlptx ) { } bool evm_contract::gc(uint32_t max) { + assert_inited(); + evm_runtime::state state{get_self(), eosio::same_payer}; return state.gc(max); } diff --git a/contract/tests/init_tests.cpp b/contract/tests/init_tests.cpp index da2f7b15..5a816227 100644 --- a/contract/tests/init_tests.cpp +++ b/contract/tests/init_tests.cpp @@ -32,6 +32,9 @@ BOOST_FIXTURE_TEST_CASE(check_init, basic_evm_tester) try { BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "setbal"_n, "evm"_n, mvo()("addy", bytes())("bal", bytes())), eosio_assert_message_exception, [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "gc"_n, "evm"_n, mvo()("max", 42)), + eosio_assert_message_exception, + [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); BOOST_REQUIRE_EXCEPTION(init(42), eosio_assert_message_exception, From 2dfc9cc5ccccca2734109d972aaa623f19786df8 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Sun, 29 Jan 2023 19:46:51 -0500 Subject: [PATCH 5/5] basic deposit & withdraw for native token --- contract/include/evm_runtime/eosio.token.hpp | 45 ++++ contract/include/evm_runtime/evm_contract.hpp | 22 ++ contract/src/actions.cpp | 62 +++++ contract/tests/CMakeLists.txt | 3 +- contract/tests/init_tests.cpp | 11 + contract/tests/native_token_tests.cpp | 218 ++++++++++++++++++ 6 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 contract/include/evm_runtime/eosio.token.hpp create mode 100644 contract/tests/native_token_tests.cpp diff --git a/contract/include/evm_runtime/eosio.token.hpp b/contract/include/evm_runtime/eosio.token.hpp new file mode 100644 index 00000000..295946dd --- /dev/null +++ b/contract/include/evm_runtime/eosio.token.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include + +namespace eosio { + + using std::string; + + class [[eosio::contract("eosio.token")]] token : public contract { + public: + using contract::contract; + + [[eosio::action]] + void create( const name& issuer, + const asset& maximum_supply); + + [[eosio::action]] + void issue( const name& to, const asset& quantity, const string& memo ); + + [[eosio::action]] + void retire( const asset& quantity, const string& memo ); + + [[eosio::action]] + void transfer( const name& from, + const name& to, + const asset& quantity, + const string& memo ); + + [[eosio::action]] + void open( const name& owner, const symbol& symbol, const name& ram_payer ); + + [[eosio::action]] + void close( const name& owner, const symbol& symbol ); + + 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>; + }; +} diff --git a/contract/include/evm_runtime/evm_contract.hpp b/contract/include/evm_runtime/evm_contract.hpp index c5155628..728929e1 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -22,6 +22,18 @@ CONTRACT evm_contract : public contract { [[eosio::action]] void pushtx(eosio::name ram_payer, const bytes& rlptx); + [[eosio::action]] + void open(eosio::name owner, eosio::name ram_payer); + + [[eosio::action]] + void close(eosio::name owner); + + [[eosio::on_notify("eosio.token::transfer")]] + void transfer(eosio::name from, eosio::name to, eosio::asset quantity, std::string memo); + + [[eosio::action]] + void withdraw(eosio::name owner, eosio::asset quantity); + #ifdef WITH_TEST_ACTIONS ACTION testtx( const bytes& rlptx, const evm_runtime::test::block_info& bi ); ACTION updatecode( const bytes& address, uint64_t incarnation, const bytes& code_hash, const bytes& code); @@ -33,6 +45,16 @@ CONTRACT evm_contract : public contract { ACTION setbal(const bytes& addy, const bytes& bal); #endif private: + struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { + name owner; + asset balance; + uint64_t dust = 0; + + uint64_t primary_key() const { return owner.value; } + }; + + typedef eosio::multi_index<"accounts"_n, account> accounts; + struct [[eosio::table]] [[eosio::contract("evm_contract")]] config { eosio::unsigned_int version; //placeholder for future variant index uint64_t chainid = 0; diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 86b9a4c8..a73a2614 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #ifdef WITH_TEST_ACTIONS #include @@ -17,6 +18,9 @@ #define LOGTIME(MSG) #endif +static constexpr eosio::name token_account("eosio.token"_n); +static constexpr eosio::symbol token_symbol("EOS", 4u); + namespace evm_runtime { using namespace silkworm; @@ -69,6 +73,64 @@ void evm_contract::pushtx( eosio::name ram_payer, const bytes& rlptx ) { LOGTIME("EVM EXECUTE"); } +void evm_contract::open(eosio::name owner, eosio::name ram_payer) { + assert_inited(); + require_auth(ram_payer); + check(is_account(owner), "owner account does not exist"); + + accounts account_table(get_self(), get_self().value); + if(account_table.find(owner.value) == account_table.end()) + account_table.emplace(ram_payer, [&](account& a) { + a.owner = owner; + a.balance = asset(0, token_symbol); + }); +} + +void evm_contract::close(eosio::name owner) { + assert_inited(); + require_auth(owner); + + accounts account_table(get_self(), get_self().value); + const account& owner_account = account_table.get(owner.value, "account is not open"); + + eosio::check(owner_account.balance.amount == 0 && owner_account.dust == 0, "cannot close because balance is not zero"); + account_table.erase(owner_account); +} + +void evm_contract::transfer(eosio::name from, eosio::name to, eosio::asset quantity, std::string memo) { + assert_inited(); + + if(to != get_self() || from == get_self()) + return; + + eosio::check(!memo.empty(), "memo must be already opened account name to credit deposit to"); + + eosio::name receiver(memo); + + accounts account_table(get_self(), get_self().value); + const account& receiver_account = account_table.get(receiver.value, "receiving account has not been opened"); + + account_table.modify(receiver_account, eosio::same_payer, [&](account& a) { + a.balance += quantity; + }); +} + +void evm_contract::withdraw(eosio::name owner, eosio::asset quantity) { + assert_inited(); + require_auth(owner); + + accounts account_table(get_self(), get_self().value); + const account& owner_account = account_table.get(owner.value, "account is not open"); + + check(owner_account.balance.amount >= quantity.amount, "overdrawn balance"); + account_table.modify(owner_account, eosio::same_payer, [&](account& a) { + a.balance -= quantity; + }); + + token::transfer_action transfer_act(token_account, {{get_self(), "active"_n}}); + transfer_act.send(get_self(), owner, quantity, std::string("Withdraw from EVM balance")); +} + #ifdef WITH_TEST_ACTIONS ACTION evm_contract::testtx( const bytes& rlptx, const evm_runtime::test::block_info& bi ) { assert_inited(); diff --git a/contract/tests/CMakeLists.txt b/contract/tests/CMakeLists.txt index 774b71ea..3308a050 100644 --- a/contract/tests/CMakeLists.txt +++ b/contract/tests/CMakeLists.txt @@ -21,13 +21,12 @@ include_directories( ${CMAKE_SOURCE_DIR}/../../silkworm/third_party/silkpre/third_party/secp256k1/include ) -#file(GLOB UNIT_TESTS "*.cpp" "*.hpp") - set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -Wno-deprecated-declarations") add_eosio_test( unit_test ${CMAKE_SOURCE_DIR}/evm_runtime_tests.cpp ${CMAKE_SOURCE_DIR}/init_tests.cpp + ${CMAKE_SOURCE_DIR}/native_token_tests.cpp ${CMAKE_SOURCE_DIR}/main.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/rlp/encode.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/rlp/decode.cpp diff --git a/contract/tests/init_tests.cpp b/contract/tests/init_tests.cpp index da2f7b15..1a71feaa 100644 --- a/contract/tests/init_tests.cpp +++ b/contract/tests/init_tests.cpp @@ -8,6 +8,17 @@ BOOST_FIXTURE_TEST_CASE(check_init, basic_evm_tester) try { eosio_assert_message_exception, [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "open"_n, "evm"_n, mvo()("owner", "evm"_n)("ram_payer", "evm"_n)), + eosio_assert_message_exception, + [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "close"_n, "evm"_n, mvo()("owner", "evm"_n)), + eosio_assert_message_exception, + [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "withdraw"_n, "evm"_n, mvo()("owner", "evm"_n)("quantity", asset())), + eosio_assert_message_exception, + [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); + // Test of transfer notification w/o init is handled in native_token_evm_tests/transfer_notifier_without_init test as it requires additional setup + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "testtx"_n, "evm"_n, mvo()("rlptx", bytes())("bi", mvo()("coinbase", bytes())("difficulty", 0)("gasLimit", 0)("number", 0)("timestamp", 0))), eosio_assert_message_exception, [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); diff --git a/contract/tests/native_token_tests.cpp b/contract/tests/native_token_tests.cpp new file mode 100644 index 00000000..5d2a23be --- /dev/null +++ b/contract/tests/native_token_tests.cpp @@ -0,0 +1,218 @@ +#include "basic_evm_tester.hpp" + +#include + +using namespace eosio::testing; + +struct native_token_evm_tester : basic_evm_tester { + native_token_evm_tester(std::string native_smybol_str, bool doinit) : native_symbol(symbol::from_string(native_smybol_str)) { + if(doinit) + init(15555); + create_accounts({"eosio.token"_n, "alice"_n, "bob"_n, "carol"_n}); + produce_block(); + + set_code("eosio.token"_n, contracts::eosio_token_wasm()); + set_abi("eosio.token"_n, contracts::eosio_token_abi().data()); + + push_action("eosio.token"_n, "create"_n, "eosio.token"_n, mvo()("issuer", "eosio.token"_n) + ("maximum_supply", asset(1'000'000'0000, native_symbol))); + for(const name& n : {"alice"_n, "bob"_n, "carol"_n}) + push_action("eosio.token"_n, "issue"_n, "eosio.token"_n, mvo()("to", n) + ("quantity", asset(100'0000, native_symbol)) + ("memo", "")); + } + + transaction_trace_ptr transfer_token(name from, name to, asset quantity, std::string memo) { + return push_action("eosio.token"_n, "transfer"_n, from, mvo()("from", from) + ("to", to) + ("quantity", quantity) + ("memo", memo)); + } + + int64_t native_balance(name owner) const { + return get_currency_balance("eosio.token"_n, native_symbol, owner).get_amount(); + } + + std::tuple evm_balance(name owner) const { + const vector d = get_row_by_account("evm"_n, "evm"_n, "accounts"_n, owner); + FC_ASSERT(d.size(), "EVM not open"); + auto [_, amount, dust] = fc::raw::unpack(d); + return std::make_tuple(amount, dust); + } + int64_t evm_balance_token(name owner) const { + return std::get<0>(evm_balance(owner)).get_amount(); + } + int64_t evm_balance_dust(name owner) const { + return std::get<1>(evm_balance(owner)); + } + + transaction_trace_ptr open(name owner, name ram_payer) { + return push_action("evm"_n, "open"_n, ram_payer, mvo()("owner", owner)("ram_payer", ram_payer)); + } + transaction_trace_ptr close(name owner) { + return push_action("evm"_n, "close"_n, owner, mvo()("owner", owner)); + } + transaction_trace_ptr withdraw(name owner, asset quantity) { + return push_action("evm"_n, "withdraw"_n, owner, mvo()("owner", owner)("quantity", quantity)); + } + + symbol native_symbol; + asset make_asset(int64_t amount) { + return asset(amount, native_symbol); + } + + struct evm_account_row { + name owner; + asset balance; + uint64_t dust = 0; + }; +}; +FC_REFLECT(native_token_evm_tester::evm_account_row, (owner)(balance)(dust)) + +struct native_token_evm_tester_EOS : native_token_evm_tester { + native_token_evm_tester_EOS() : native_token_evm_tester("4,EOS", true) {} +}; +struct native_token_evm_tester_SPOON : native_token_evm_tester { + native_token_evm_tester_SPOON() : native_token_evm_tester("4,SPOON", true) {} +}; +struct native_token_evm_tester_noinit : native_token_evm_tester { + native_token_evm_tester_noinit() : native_token_evm_tester("4,EOS", false) {} +}; + +BOOST_AUTO_TEST_SUITE(native_token_evm_tests) + +BOOST_FIXTURE_TEST_CASE(basic_deposit_withdraw, native_token_evm_tester_EOS) try { + //can't transfer to alice's balance because it isn't open + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), "alice"), + eosio_assert_message_exception, eosio_assert_message_is("receiving account has not been opened")); + + + open("alice"_n, "alice"_n); + + //alice sends her own tokens in to her EVM balance + { + const int64_t to_transfer = 1'0000; + const int64_t alice_native_before = native_balance("alice"_n); + const int64_t alice_evm_before = evm_balance_token("alice"_n); + transfer_token("alice"_n, "evm"_n, make_asset(to_transfer), "alice"); + + BOOST_REQUIRE_EQUAL(alice_native_before - native_balance("alice"_n), to_transfer); + BOOST_REQUIRE_EQUAL(evm_balance_token("alice"_n), to_transfer); + } + + //bob sends his tokens in to alice's EVM balance + { + const int64_t to_transfer = 1'0000; + const int64_t bob_native_before = native_balance("bob"_n); + const int64_t alice_evm_before = evm_balance_token("alice"_n); + transfer_token("bob"_n, "evm"_n, make_asset(to_transfer), "alice"); + + BOOST_REQUIRE_EQUAL(bob_native_before - native_balance("bob"_n), to_transfer); + BOOST_REQUIRE_EQUAL(evm_balance_token("alice"_n) - alice_evm_before, to_transfer); + } + + //carol can't send tokens to bob's balance because bob isn't open + BOOST_REQUIRE_EXCEPTION(transfer_token("carol"_n, "evm"_n, make_asset(1'0000), "bob"), + eosio_assert_message_exception, eosio_assert_message_is("receiving account has not been opened")); + + //alice can't close her account because of outstanding balance + BOOST_REQUIRE_EXCEPTION(close("alice"_n), + eosio_assert_message_exception, eosio_assert_message_is("cannot close because balance is not zero")); + //bob can't close either because he never opened + BOOST_REQUIRE_EXCEPTION(close("bob"_n), + eosio_assert_message_exception, eosio_assert_message_is("account is not open")); + + //withdraw a little bit of Alice's balance + { + const int64_t to_withdraw = 5000; + const int64_t alice_native_before = native_balance("alice"_n); + const int64_t alice_evm_before = evm_balance_token("alice"_n); + withdraw("alice"_n, make_asset(to_withdraw)); + + BOOST_REQUIRE_EQUAL(native_balance("alice"_n) - alice_native_before, to_withdraw); + BOOST_REQUIRE_EQUAL(alice_evm_before - evm_balance_token("alice"_n), to_withdraw); + } + + //try and withdraw more than alice has + { + const int64_t to_withdraw = 2'0000; + BOOST_REQUIRE_GT(to_withdraw, evm_balance_token("alice"_n)); + BOOST_REQUIRE_EXCEPTION(withdraw("alice"_n, make_asset(to_withdraw)), + eosio_assert_message_exception, eosio_assert_message_is("overdrawn balance")); + } + + //bob can't withdraw anything, since he isn't opened + { + const int64_t to_withdraw = 2'0000; + BOOST_REQUIRE_EXCEPTION(withdraw("bob"_n, make_asset(to_withdraw)), + eosio_assert_message_exception, eosio_assert_message_is("account is not open")); + } + + //withdraw the remaining amount that alice has + { + const int64_t to_withdraw = 1'5000; + const int64_t alice_native_before = native_balance("alice"_n); + const int64_t alice_evm_before = evm_balance_token("alice"_n); + withdraw("alice"_n, make_asset(to_withdraw)); + + BOOST_REQUIRE_EQUAL(native_balance("alice"_n) - alice_native_before, to_withdraw); + BOOST_REQUIRE_EQUAL(alice_evm_before - evm_balance_token("alice"_n), to_withdraw); + } + + produce_block(); + + //now alice can close out + close("alice"_n); + BOOST_REQUIRE_EXCEPTION(evm_balance_token("alice"_n), + fc::assert_exception, fc_assert_exception_message_is("EVM not open")); + + //make sure alice can't deposit any more + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), "alice"), + eosio_assert_message_exception, eosio_assert_message_is("receiving account has not been opened")); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(weird_names, native_token_evm_tester_EOS) try { + //just try some weird account names as memos + + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), "BANANA"), + eosio_assert_message_exception, eosio_assert_message_is("character is not in allowed character set for names")); + + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), "loooooooooooooooooong"), + eosio_assert_message_exception, eosio_assert_message_is("string is too long to be a valid name")); + + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), " "), + eosio_assert_message_exception, eosio_assert_message_is("character is not in allowed character set for names")); + + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), ""), + eosio_assert_message_exception, eosio_assert_message_is("memo must be already opened account name to credit deposit to")); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(non_existing_account, native_token_evm_tester_EOS) try { + //can only open for accounts that exist + + BOOST_REQUIRE_EXCEPTION(open("spoon"_n, "alice"_n), + eosio_assert_message_exception, eosio_assert_message_is("owner account does not exist")); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(non_standard_native_symbol, native_token_evm_tester_SPOON) try { + //the symbol 4,EOS is fixed as the expected native symbol. try transfering in a different symbol from eosio.token + + open("alice"_n, "alice"_n); + + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), "alice"), + eosio_assert_message_exception, eosio_assert_message_is("attempt to add asset with different symbol")); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(transfer_notifier_without_init, native_token_evm_tester_noinit) try { + //make sure to not accept transfer notifications unless contract has been inited + + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), "alice"), + eosio_assert_message_exception, eosio_assert_message_is("contract not initialized")); + +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file