diff --git a/.gitignore b/.gitignore index 1f63cf480..c22535cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ build/* .DS_Store +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ef311a3b..39adfe9e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,7 +75,9 @@ string(REPLACE ";" "|" TEST_PREFIX_PATH "${CMAKE_PREFIX_PATH}") string(REPLACE ";" "|" TEST_FRAMEWORK_PATH "${CMAKE_FRAMEWORK_PATH}") string(REPLACE ";" "|" TEST_MODULE_PATH "${CMAKE_MODULE_PATH}") -set(BUILD_TESTS FALSE CACHE BOOL "Build unit tests") +set(BUILD_TESTS FALSE CACHE BOOL "Build unit tests, which should have been integration tests, + but are left here named unit tests, so that the newer correctly-named integration tests, + don't have a name conflict") if(BUILD_TESTS) message(STATUS "Building unit tests.") @@ -92,3 +94,21 @@ if(BUILD_TESTS) else() message(STATUS "Unit tests will not be built. To build unit tests, set BUILD_TESTS to true.") endif() + +set(BUILD_INTEGRATION_TESTS FALSE CACHE BOOL "Build integration tests") + +if(BUILD_INTEGRATION_TESTS) + ExternalProject_Add( + integration-tests + SOURCE_DIR ${CMAKE_SOURCE_DIR}/integration-tests + BINARY_DIR ${CMAKE_BINARY_DIR}/integration-tests + CMAKE_ARGS -DCMAKE_TOOLCHAIN_FILE=${EOSIO_CDT_ROOT}/lib/cmake/eosio.cdt/EosioWasmToolchain.cmake + UPDATE_COMMAND "" + PATCH_COMMAND "" + TEST_COMMAND "" + INSTALL_COMMAND "" + BUILD_ALWAYS 1 + ) +else() + message(STATUS "Integration tests will not be built. To build integration tests, set BUILD_INTEGRATION_TESTS to true.") +endif() diff --git a/contracts/eosio.token/include/eosio.token/eosio.token.hpp b/contracts/eosio.token/include/eosio.token/eosio.token.hpp index bb08ca409..3222b5708 100644 --- a/contracts/eosio.token/include/eosio.token/eosio.token.hpp +++ b/contracts/eosio.token/include/eosio.token/eosio.token.hpp @@ -153,7 +153,7 @@ 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: + struct [[eosio::table]] account { asset balance; diff --git a/integration-tests/.clang-format b/integration-tests/.clang-format new file mode 100644 index 000000000..072e21a7b --- /dev/null +++ b/integration-tests/.clang-format @@ -0,0 +1,78 @@ +BasedOnStyle: LLVM +IndentWidth: 3 +UseTab: Never +ColumnLimit: 120 + +--- +Language: Cpp +# always align * and & to the type +DerivePointerAlignment: false +PointerAlignment: Left + +# regroup includes to these classes +IncludeCategories: + - Regex: '(<|"(eosio)/)' + Priority: 4 + - Regex: '(<|"(boost)/)' + Priority: 3 + - Regex: '(<|"(llvm|llvm-c|clang|clang-c)/' + Priority: 3 + - Regex: '<[[:alnum:]]+>' + Priority: 2 + - Regex: '.*' + Priority: 1 + +#IncludeBlocks: Regroup + +# set indent for public, private and protected +#AccessModifierOffset: 3 + +# make line continuations twice the normal indent +ContinuationIndentWidth: 6 + +# add missing namespace comments +FixNamespaceComments: true + +# add spaces to braced list i.e. int* foo = { 0, 1, 2 }; instead of int* foo = {0,1,2}; +Cpp11BracedListStyle: false +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: true +AlignOperands: true +AlignTrailingComments: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortBlocksOnASingleLine: true +#AllowShortIfStatementsOnASingleLine: WithoutElse +#AllowShortIfStatementsOnASingleLine: true +#AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: true +AlwaysBreakTemplateDeclarations: true + +BinPackParameters: true +### use this with clang9 +BreakBeforeBraces: Custom +BraceWrapping: + #AfterCaseLabel: true + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + +BreakConstructorInitializers: BeforeColon +CompactNamespaces: true +IndentCaseLabels: true +IndentPPDirectives: AfterHash +NamespaceIndentation: Inner +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true + +ReflowComments: false +--- diff --git a/integration-tests/CMakeLists.txt b/integration-tests/CMakeLists.txt new file mode 100644 index 000000000..85bf4f6a9 --- /dev/null +++ b/integration-tests/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.10) + +project(integration-tests) + +set(EOSIO_WASM_OLD_BEHAVIOR "off") +find_package(eosio.cdt) + +function(add_test TARGET SRC) + add_executable(${TARGET} ${SRC}) + target_link_libraries(${TARGET} -ftester -stack-size=65536) + target_compile_options(${TARGET} PUBLIC -ftester -Os) + target_include_directories(${TARGET} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../contracts/eosio.bios/include + ${CMAKE_CURRENT_SOURCE_DIR}/../contracts/eosio.msig/include + ${CMAKE_CURRENT_SOURCE_DIR}/../contracts/eosio.system/include + ${CMAKE_CURRENT_SOURCE_DIR}/../contracts/eosio.token/include + ${CMAKE_CURRENT_SOURCE_DIR}/../contracts/eosio.wrap/include + ) + set_target_properties(${TARGET} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +endfunction() + +add_test(eosio.msig-tests eosio.msig-tests.cpp) +add_test(eosio.token-tests eosio.token-tests.cpp) diff --git a/integration-tests/eosio.msig-tests.cpp b/integration-tests/eosio.msig-tests.cpp new file mode 100644 index 000000000..48ff4fd03 --- /dev/null +++ b/integration-tests/eosio.msig-tests.cpp @@ -0,0 +1,83 @@ +#include +#include +#include +#include + +#define BOOST_TEST_MAIN +#include + +using namespace eosio; +using eosiobios::bios; + +namespace eosio { + +inline bool operator!=(const token::currency_stats& a, const token::currency_stats& b) { + return a.supply != b.supply || a.max_supply != b.max_supply || a.issuer != b.issuer; +} + +} // namespace eosio + +struct approve_args { + name proposer = {}; + name proposal_name = {}; + permission_level level = {}; + + EOSLIB_SERIALIZE(approve_args, (proposer)(proposal_name)(level)) +}; + +struct msig_tester { + test_chain chain; + + msig_tester() { + chain.set_code("eosio"_n, "contracts/eosio.bios/eosio.bios.wasm"); + chain.create_code_account("eosio.msig"_n, true); + chain.create_account("eosio.stake"_n); + chain.create_account("eosio.ram"_n); + chain.create_account("eosio.ramfee"_n); + chain.create_account("alice"_n); + chain.create_account("bob"_n); + chain.create_account("carol"_n); + chain.set_code("eosio.msig"_n, "contracts/eosio.msig/eosio.msig.wasm"); + } + + transaction_trace propose(name proposer, name proposal_name, std::vector requested, + transaction trx, const char* expected_except = nullptr) { + return chain.transact({ multisig::propose_action{ "eosio.msig"_n, { proposer, "active"_n } }.to_action( + proposer, proposal_name, std::move(requested), std::move(trx)) }, + expected_except); + } + + transaction_trace approve(name proposer, name proposal_name, permission_level level, + const char* expected_except = nullptr) { + return chain.transact({ { level, "eosio.msig"_n, "approve"_n, approve_args{ proposer, proposal_name, level } } }, + expected_except); + } + + transaction_trace exec(name proposer, name proposal_name, name executer, + const char* expected_except = nullptr) { + return chain.transact({ multisig::exec_action{ "eosio.msig"_n, { executer, "active"_n } }.to_action( + proposer, proposal_name, executer) }, + expected_except); + } + +}; // msig_tester + +BOOST_FIXTURE_TEST_CASE(propose_approve_execute, msig_tester) { + propose("alice"_n, "first"_n, { { "alice"_n, "active"_n } }, + chain.make_transaction( + { bios::reqauth_action{ "eosio"_n, { "alice"_n, "active"_n } }.to_action("alice"_n) })); + + // fail to execute before approval + exec("alice"_n, "first"_n, "alice"_n, "transaction authorization failed"); + + // approve and execute + approve("alice"_n, "first"_n, { "alice"_n, "active"_n }); + BOOST_TEST(!chain.exec_deferred()); + exec("alice"_n, "first"_n, "alice"_n); + + auto receipt = chain.exec_deferred(); + BOOST_TEST(!chain.exec_deferred()); + BOOST_TEST(receipt.has_value()); + expect(*receipt); + BOOST_TEST(receipt->action_traces.size() == 1); +} // propose_approve_execute diff --git a/integration-tests/eosio.token-tests.cpp b/integration-tests/eosio.token-tests.cpp new file mode 100644 index 000000000..f0ea84ca5 --- /dev/null +++ b/integration-tests/eosio.token-tests.cpp @@ -0,0 +1,269 @@ +#include +#include + +#define CATCH_CONFIG_MAIN +#include + +using namespace eosio; + +namespace eosio { + +inline bool operator!=(const token::currency_stats& a, const token::currency_stats& b) { + return a.supply != b.supply || a.max_supply != b.max_supply || a.issuer != b.issuer; +} +inline bool operator==(const token::currency_stats& a, const token::currency_stats& b) { + return !(a != b); +} + +} // namespace eosio + +struct token_tester { + test_chain chain; + + token_tester() { + chain.create_account("alice"_n); + chain.create_account("bob"_n); + chain.create_account("carol"_n); + chain.create_code_account("eosio.token"_n); + chain.set_code("eosio.token"_n, "contracts/eosio.token/eosio.token.wasm"); + } + + transaction_trace create(name issuer, const asset& maximum_supply, + const char* expected_except = nullptr) { + return chain.transact({ token::create_action{ "eosio.token"_n, { "eosio.token"_n, "active"_n } }.to_action( + issuer, maximum_supply) }, + expected_except); + } + + transaction_trace issue(name issuer, name to, const asset& quantity, const string& memo, + const char* expected_except = nullptr) { + return chain.transact( + { token::issue_action{ "eosio.token"_n, { issuer, "active"_n } }.to_action(to, quantity, memo) }, + expected_except); + } + + transaction_trace retire(name issuer, const asset& quantity, const string& memo, + const char* expected_except = nullptr) { + return chain.transact( + { token::retire_action{ "eosio.token"_n, { issuer, "active"_n } }.to_action(quantity, memo) }, + expected_except); + } + + transaction_trace transfer(name from, name to, const asset& quantity, const string& memo, + const char* expected_except = nullptr) { + return chain.transact( + { token::transfer_action{ "eosio.token"_n, { from, "active"_n } }.to_action(from, to, quantity, memo) }, + expected_except); + } + + transaction_trace open(name owner, const symbol& symbol, name ram_payer, + const char* expected_except = nullptr) { + return chain.transact( + { token::open_action{ "eosio.token"_n, { ram_payer, "active"_n } }.to_action(owner, symbol, ram_payer) }, + expected_except); + } + + transaction_trace close(name owner, const symbol& symbol, const char* expected_except = nullptr) { + return chain.transact({ token::close_action{ "eosio.token"_n, { owner, "active"_n } }.to_action(owner, symbol) }, + expected_except); + } + + auto get_stats(symbol_code sym_code) { + token::stats statstable("eosio.token"_n, sym_code.raw()); + return statstable.get(sym_code.raw()); + } + + std::optional get_account_optional(name owner, symbol_code sym_code) { + token::accounts accountstable("eosio.token"_n, owner.value); + auto it = accountstable.find(sym_code.raw()); + if (it != accountstable.end()) + return *it; + else + return {}; + } +}; // token_tester + +TEST_CASE("Create a token", "[create]") { + token_tester t; + + t.create("alice"_n, s2a("1000.000 TKN")); + REQUIRE(t.get_stats(symbol_code{ "TKN" }) == + (token::currency_stats{ + .supply = s2a("0.000 TKN"), + .max_supply = s2a("1000.000 TKN"), + .issuer = "alice"_n, + })); +} + +TEST_CASE("Create a token with negative max supply", "[create_negative_max_supply]") { + token_tester t; + t.create("alice"_n, s2a("-1000.000 TKN"), "max-supply must be positive"); +} + +TEST_CASE("Create token with a symbol that already exists", "[symbol_already_exists]") { + token_tester t; + + t.create("alice"_n, s2a("1000 TKN")); + REQUIRE(t.get_stats(symbol_code{ "TKN" }) == + (token::currency_stats{ + .supply = s2a("0 TKN"), + .max_supply = s2a("1000 TKN"), + .issuer = "alice"_n, + })); + t.create("alice"_n, s2a("100 TKN"), "token with symbol already exists"); +} + +TEST_CASE("Create a token whose max supply is to large", "[create_max_supply]") { + token_tester t; + + t.create("alice"_n, s2a("4611686018427387903 TKN")); + REQUIRE(t.get_stats(symbol_code{ "TKN" }) == + (token::currency_stats{ + .supply = s2a("0 TKN"), + .max_supply = s2a("4611686018427387903 TKN"), + .issuer = "alice"_n, + })); + + auto too_big = s2a("4611686018427387903 NKT"); + ++too_big.amount; + t.create("alice"_n, too_big, "invalid supply"); +} + +TEST_CASE("Create a token whose precision is too high", "[precision_too_high]") { + token_tester t; + + t.create("alice"_n, asset{ 1, symbol{ "TKN", 18 } }); + REQUIRE(t.get_stats(symbol_code{ "TKN" }) == // + (token::currency_stats{ + .supply = s2a("0.000000000000000000 TKN"), + .max_supply = s2a("0.000000000000000001 TKN"), + .issuer = "alice"_n, + })); + + // eosio.token fails to check precision. Verify this broken behavior is still present. + t.create("alice"_n, asset{ 1, symbol{ "NKT", 50 } }); +} + +TEST_CASE("Test issuing a token", "[issue_tests]") { + token_tester t; + + t.create("alice"_n, s2a("1000.000 TKN")); + t.issue("alice"_n, "alice"_n, s2a("500.000 TKN"), "hola"); + REQUIRE(t.get_stats(symbol_code{ "TKN" }) == // + (token::currency_stats{ + .supply = s2a("500.000 TKN"), + .max_supply = s2a("1000.000 TKN"), + .issuer = "alice"_n, + })); + REQUIRE(token::get_balance("eosio.token"_n, "alice"_n, symbol_code{ "TKN" }) == s2a("500.000 TKN")); + t.issue("alice"_n, "alice"_n, s2a("500.001 TKN"), "hola", "quantity exceeds available supply"); + t.issue("alice"_n, "alice"_n, s2a("-1.000 TKN"), "hola", "must issue positive quantity"); + t.issue("alice"_n, "alice"_n, s2a("1.000 TKN"), "hola"); +} + +TEST_CASE("Retire tokens", "[retire_tests]") { + token_tester t; + + t.create("alice"_n, s2a("1000.000 TKN")); + t.issue("alice"_n, "alice"_n, s2a("500.000 TKN"), "hola"); + REQUIRE(t.get_stats(symbol_code{ "TKN" }) == // + (token::currency_stats{ + .supply = s2a("500.000 TKN"), + .max_supply = s2a("1000.000 TKN"), + .issuer = "alice"_n, + })); + + REQUIRE(token::get_balance("eosio.token"_n, "alice"_n, symbol_code{ "TKN" }) == s2a("500.000 TKN")); + t.retire("alice"_n, s2a("200.000 TKN"), "hola"); + + REQUIRE(t.get_stats(symbol_code{ "TKN" }) == // + (token::currency_stats{ + .supply = s2a("300.000 TKN"), + .max_supply = s2a("1000.000 TKN"), + .issuer = "alice"_n, + })); + REQUIRE(token::get_balance("eosio.token"_n, "alice"_n, symbol_code{ "TKN" }) == s2a("300.000 TKN")); + + // should fail to retire more than current balance + t.retire("alice"_n, s2a("500.000 TKN"), "hola", "overdrawn balance"); + + t.transfer("alice"_n, "bob"_n, s2a("200.000 TKN"), "hola"); + // should fail to retire since tokens are not on the issuer's balance + t.retire("alice"_n, s2a("300.000 TKN"), "hola", "overdrawn balance"); + // transfer tokens back + t.transfer("bob"_n, "alice"_n, s2a("200.000 TKN"), "hola"); + + t.retire("alice"_n, s2a("300.000 TKN"), "hola"); + REQUIRE(t.get_stats(symbol_code{ "TKN" }) == // + (token::currency_stats{ + .supply = s2a("0.000 TKN"), + .max_supply = s2a("1000.000 TKN"), + .issuer = "alice"_n, + })); + + REQUIRE(token::get_balance("eosio.token"_n, "alice"_n, symbol_code{ "TKN" }) == s2a("0.000 TKN")); + + // trying to retire tokens with zero balance + t.retire("alice"_n, s2a("1.000 TKN"), "hola", "overdrawn balance"); +} + +TEST_CASE("Transfer tokens", "[transfer_tests]") { + token_tester t; + + t.create("alice"_n, s2a("1000 CERO")); + t.issue("alice"_n, "alice"_n, s2a("1000 CERO"), "hola"); + + REQUIRE(t.get_stats(symbol_code{ "CERO" }) == // + (token::currency_stats{ + .supply = s2a("1000 CERO"), + .max_supply = s2a("1000 CERO"), + .issuer = "alice"_n, + })); + + REQUIRE(token::get_balance("eosio.token"_n, "alice"_n, symbol_code{ "CERO" }) == s2a("1000 CERO")); + t.transfer("alice"_n, "bob"_n, s2a("300 CERO"), "hola"); + REQUIRE(token::get_balance("eosio.token"_n, "alice"_n, symbol_code{ "CERO" }) == s2a("700 CERO")); + REQUIRE(token::get_balance("eosio.token"_n, "bob"_n, symbol_code{ "CERO" }) == s2a("300 CERO")); + + t.transfer("alice"_n, "bob"_n, s2a("701 CERO"), "hola", "overdrawn balance"); + t.transfer("alice"_n, "bob"_n, s2a("-1000 CERO"), "hola", "must transfer positive quantity"); +} + +TEST_CASE("Open token balance", "[open_tests]") { + token_tester t; + + t.create("alice"_n, s2a("1000 CERO")); + + REQUIRE(t.get_account_optional("alice"_n, symbol_code("CERO")) == std::nullopt); + t.issue("alice"_n, "bob"_n, s2a("1000 CERO"), "", "tokens can only be issued to issuer account"); + t.issue("alice"_n, "alice"_n, s2a("1000 CERO"), "issue"); + + REQUIRE(token::get_balance("eosio.token"_n, "alice"_n, symbol_code{ "CERO" }) == s2a("1000 CERO")); + REQUIRE(t.get_account_optional("bob"_n, symbol_code("CERO")) == std::nullopt); + + t.open("nonexistent"_n, symbol{ "CERO", 0 }, "alice"_n, "owner account does not exist"); + t.open("bob"_n, symbol{ "CERO", 0 }, "alice"_n); + + REQUIRE(token::get_balance("eosio.token"_n, "bob"_n, symbol_code{ "CERO" }) == s2a("0 CERO")); + t.transfer("alice"_n, "bob"_n, s2a("200 CERO"), "hola"); + REQUIRE(token::get_balance("eosio.token"_n, "bob"_n, symbol_code{ "CERO" }) == s2a("200 CERO")); + + t.open("carol"_n, symbol{ "INVALID", 0 }, "alice"_n, "symbol does not exist"); + t.open("carol"_n, symbol{ "CERO", 1 }, "alice"_n, "symbol precision mismatch"); +} + +TEST_CASE("Close token balance", "[close_tests]") { + token_tester t; + + t.create("alice"_n, s2a("1000 CERO")); + REQUIRE(t.get_account_optional("alice"_n, symbol_code("CERO")) == std::nullopt); + + t.issue("alice"_n, "alice"_n, s2a("1000 CERO"), "hola"); + REQUIRE(token::get_balance("eosio.token"_n, "alice"_n, symbol_code{ "CERO" }) == s2a("1000 CERO")); + + t.transfer("alice"_n, "bob"_n, s2a("1000 CERO"), "hola"); + REQUIRE(token::get_balance("eosio.token"_n, "alice"_n, symbol_code{ "CERO" }) == s2a("0 CERO")); + + t.close("alice"_n, symbol{ "CERO", 0 }); + REQUIRE(t.get_account_optional("alice"_n, symbol_code("CERO")) == std::nullopt); +}