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] 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