diff --git a/contract/include/evm_runtime/evm_contract.hpp b/contract/include/evm_runtime/evm_contract.hpp index f6955fba..b5281c3f 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -84,6 +84,10 @@ class [[eosio::contract]] evm_contract : public contract /// @return true if all garbage has been collected [[eosio::action]] bool gc(uint32_t max); + + [[eosio::action]] void call(eosio::name from, const bytes& to, uint128_t value, bytes& data, uint64_t gas_limit); + [[eosio::action]] void admincall(const bytes& from, const bytes& to, uint128_t value, bytes& data, uint64_t gas_limit); + #ifdef WITH_TEST_ACTIONS [[eosio::action]] void testtx(const std::optional& orlptx, const evm_runtime::test::block_info& bi); [[eosio::action]] void @@ -140,6 +144,8 @@ class [[eosio::contract]] evm_contract : public contract void handle_account_transfer(const eosio::asset& quantity, const std::string& memo); void handle_evm_transfer(eosio::asset quantity, const std::string& memo); + void call_(intx::uint256 s, const evmc::address& to, intx::uint256 value, bytes& data, uint64_t gas_limit, uint64_t nonce); + // to allow sending through a Bytes (basic_string) w/o copying over to a std::vector void pushtx_bytes(eosio::name miner, const std::basic_string& rlptx); using pushtx_action = eosio::action_wrapper<"pushtx"_n, &evm_contract::pushtx_bytes>; diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index f2cb43de..b2a0cf41 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -40,6 +40,7 @@ namespace silkworm { namespace evm_runtime { static constexpr uint32_t hundred_percent = 100'000; +static constexpr char err_msg_invalid_addr[] = "invalid address"; using namespace silkworm; @@ -219,30 +220,37 @@ Receipt evm_contract::execute_tx( eosio::name miner, Block& block, Transaction& inevm.emplace(get_self(), get_self().value); }; + bool is_special_signature = (tx.r == intx::uint256()); + tx.from.reset(); tx.recover_sender(); eosio::check(tx.from.has_value(), "unable to recover sender"); LOGTIME("EVM RECOVER SENDER"); + // type 1: normal signature (normal address recovered from normal signature), required !from_self + // type 2: special signature (r == 0), reserved address (stored in s), required from_self + reduce special balance + // type 3: special signature (r == 0), normal address (stored in s), required from_self if(from_self) { - check(is_reserved_address(*tx.from), "actions from self without a reserved from address are unexpected"); - const name ingress_account(*extract_reserved_address(*tx.from)); + check(is_special_signature, "actions from self without a special signature are unexpected"); + if (is_reserved_address(*tx.from)) { + const name ingress_account(*extract_reserved_address(*tx.from)); - const intx::uint512 max_gas_cost = intx::uint256(tx.gas_limit) * tx.max_fee_per_gas; - check(max_gas_cost + tx.value < std::numeric_limits::max(), "too much gas"); - const intx::uint256 value_with_max_gas = tx.value + (intx::uint256)max_gas_cost; + const intx::uint512 max_gas_cost = intx::uint256(tx.gas_limit) * tx.max_fee_per_gas; + check(max_gas_cost + tx.value < std::numeric_limits::max(), "too much gas"); + const intx::uint256 value_with_max_gas = tx.value + (intx::uint256)max_gas_cost; - populate_bridge_accessors(); - balance_table.modify(balance_table.get(ingress_account.value), eosio::same_payer, [&](balance& b){ - b.balance -= value_with_max_gas; - }); - inevm->set(inevm->get() += value_with_max_gas, eosio::same_payer); + populate_bridge_accessors(); + balance_table.modify(balance_table.get(ingress_account.value), eosio::same_payer, [&](balance& b){ + b.balance -= value_with_max_gas; + }); + inevm->set(inevm->get() += value_with_max_gas, eosio::same_payer); - ep.state().set_balance(*tx.from, value_with_max_gas); - ep.state().set_nonce(*tx.from, tx.nonce); + ep.state().set_balance(*tx.from, value_with_max_gas); + ep.state().set_nonce(*tx.from, tx.nonce); + } } - else if(is_reserved_address(*tx.from)) - check(from_self, "bridge signature used outside of bridge transaction"); + else if(is_special_signature) + check(false, "bridge signature used outside of bridge transaction"); if(enforce_chain_id && !from_self) { check(tx.chain_id.has_value(), "tx without chain-id"); @@ -452,7 +460,7 @@ void evm_contract::close(eosio::name owner) { uint64_t evm_contract::get_and_increment_nonce(const name owner) { nextnonces nextnonce_table(get_self(), get_self().value); - const nextnonce& nonce = nextnonce_table.get(owner.value); + const nextnonce& nonce = nextnonce_table.get(owner.value, "caller account has not been opened"); uint64_t ret = nonce.next_nonce; nextnonce_table.modify(nonce, eosio::same_payer, [](nextnonce& n){ ++n.next_nonce; @@ -556,6 +564,60 @@ bool evm_contract::gc(uint32_t max) { return state.gc(max); } +void evm_contract::call_(intx::uint256 s, const evmc::address& to, intx::uint256 value, bytes& data, uint64_t gas_limit, uint64_t nonce) { + const auto& current_config = _config.get(); + + const Transaction txn { + .type = Transaction::Type::kLegacy, + .nonce = nonce, + .max_priority_fee_per_gas = current_config.gas_price, + .max_fee_per_gas = current_config.gas_price, + .gas_limit = gas_limit, + .to = to, + .value = value, + .data = Bytes{(const uint8_t*)data.data(), data.size()}, + .r = 0u, // r == 0 is pseudo signature that resolves to reserved address range + .s = s + }; + + Bytes rlp; + rlp::encode(rlp, txn); + pushtx_action pushtx_act(get_self(), {{get_self(), "active"_n}}); + pushtx_act.send(get_self(), rlp); +} + +void evm_contract::call(eosio::name from, const bytes& to, uint128_t value, bytes& data, uint64_t gas_limit) { + assert_unfrozen(); + require_auth(from); + + ByteView bv_to{(const uint8_t*)to.data(), to.size()}; + + call_(from.value, to_evmc_address(bv_to), intx::uint256(value), data, gas_limit, get_and_increment_nonce(from)); +} + +void evm_contract::admincall(const bytes& from, const bytes& to, uint128_t value, bytes& data, uint64_t gas_limit) { + assert_unfrozen(); + require_auth(get_self()); + + // Prepare s + eosio::check(from.size() == kAddressLength, err_msg_invalid_addr); + intx::uint256 s = intx::be::unsafe::load((const uint8_t *)from.data()); + // load will put the data in higher bytes, shift them donw. + s >>= 256 - kAddressLength * 8; + // pad with '1's + s |= ((~intx::uint256(0)) << (kAddressLength * 8)); + + // Prepare to + ByteView bv_to{(const uint8_t*)to.data(), to.size()}; + + // Prepare nonce + evm_runtime::state state{get_self(), get_self(), true}; + auto account = state.read_account(to_address(from)); + check(!!account, err_msg_invalid_addr); + + call_(s, to_evmc_address(bv_to), intx::uint256(value), data, gas_limit, account->nonce); +} + #ifdef WITH_TEST_ACTIONS [[eosio::action]] void evm_contract::testtx( const std::optional& orlptx, const evm_runtime::test::block_info& bi ) { assert_unfrozen(); diff --git a/contract/tests/CMakeLists.txt b/contract/tests/CMakeLists.txt index ab15b757..dc3e11f2 100644 --- a/contract/tests/CMakeLists.txt +++ b/contract/tests/CMakeLists.txt @@ -33,6 +33,7 @@ add_eosio_test_executable( unit_test ${CMAKE_SOURCE_DIR}/gas_fee_tests.cpp ${CMAKE_SOURCE_DIR}/blockhash_tests.cpp ${CMAKE_SOURCE_DIR}/exec_tests.cpp + ${CMAKE_SOURCE_DIR}/call_tests.cpp ${CMAKE_SOURCE_DIR}/chainid_tests.cpp ${CMAKE_SOURCE_DIR}/main.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/rlp/encode.cpp diff --git a/contract/tests/basic_evm_tester.cpp b/contract/tests/basic_evm_tester.cpp index a9d7ece5..2e337e33 100644 --- a/contract/tests/basic_evm_tester.cpp +++ b/contract/tests/basic_evm_tester.cpp @@ -291,6 +291,34 @@ transaction_trace_ptr basic_evm_tester::exec(const exec_input& input, const std: return basic_evm_tester::push_action(evm_account_name, "exec"_n, evm_account_name, bytes{binary_data.begin(), binary_data.end()}); } +void basic_evm_tester::call(name from, const evmc::bytes& to, uint128_t value, evmc::bytes& data, uint64_t gas_limit, name actor) +{ + bytes to_bytes; + to_bytes.resize(to.size()); + memcpy(to_bytes.data(), to.data(), to.size()); + bytes data_bytes; + data_bytes.resize(data.size()); + memcpy(data_bytes.data(), data.data(), data.size()); + + push_action(evm_account_name, "call"_n, actor, mvo()("from", from)("to", to_bytes)("value", value)("data", data_bytes)("gas_limit", gas_limit)); +} + +void basic_evm_tester::admincall(const evmc::bytes& from, const evmc::bytes& to, uint128_t value, evmc::bytes& data, uint64_t gas_limit, name actor) +{ + bytes to_bytes; + to_bytes.resize(to.size()); + memcpy(to_bytes.data(), to.data(), to.size()); + bytes data_bytes; + data_bytes.resize(data.size()); + memcpy(data_bytes.data(), data.data(), data.size()); + + bytes from_bytes; + from_bytes.resize(to.size()); + memcpy(from_bytes.data(), from.data(), from.size()); + + push_action(evm_account_name, "admincall"_n, actor, mvo()("from", from_bytes)("to", to_bytes)("value", value)("data", data_bytes)("gas_limit", gas_limit)); +} + void basic_evm_tester::pushtx(const silkworm::Transaction& trx, name miner) { silkworm::Bytes rlp; diff --git a/contract/tests/basic_evm_tester.hpp b/contract/tests/basic_evm_tester.hpp index 56b79940..d2c3fa16 100644 --- a/contract/tests/basic_evm_tester.hpp +++ b/contract/tests/basic_evm_tester.hpp @@ -200,6 +200,8 @@ class basic_evm_tester : public testing::validating_tester transaction_trace_ptr exec(const exec_input& input, const std::optional& callback); void pushtx(const silkworm::Transaction& trx, name miner = evm_account_name); + void call(name from, const evmc::bytes& to, uint128_t value, evmc::bytes& data, uint64_t gas_limit, name actor); + void admincall(const evmc::bytes& from, const evmc::bytes& to, uint128_t value, evmc::bytes& data, uint64_t gas_limit, name actor); evmc::address deploy_contract(evm_eoa& eoa, evmc::bytes bytecode); void addegress(const std::vector& accounts); diff --git a/contract/tests/call_tests.cpp b/contract/tests/call_tests.cpp new file mode 100644 index 00000000..e93e7ca2 --- /dev/null +++ b/contract/tests/call_tests.cpp @@ -0,0 +1,252 @@ +#include "basic_evm_tester.hpp" +#include + +using intx::operator""_u256; + +using namespace evm_test; +using eosio::testing::eosio_assert_message_is; +struct exec_output_row { + uint64_t id; + exec_output output; +}; +FC_REFLECT(exec_output_row, (id)(output)) + +struct call_evm_tester : basic_evm_tester { + call_evm_tester() { + create_accounts({"alice"_n}); + transfer_token(faucet_account_name, "alice"_n, make_asset(10000'0000)); + create_accounts({"bob"_n}); + transfer_token(faucet_account_name, "bob"_n, make_asset(10000'0000)); + init(); + } + + evmc::address deploy_test_contract(evm_eoa& eoa) { + + /* + //SPDX-License-Identifier: lgplv3 + pragma solidity ^0.8.0; + + contract Test { + uint256 public count; + address public lastcaller; + + function test(uint256 input) public { + count += input; + lastcaller = msg.sender; + } + } + */ + const std::string token_bytecode = + "608060405234801561001057600080fd5b506102ad806100206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c806306661abd1461004657806329e99f0714610064578063d097e7a614610080575b600080fd5b61004e61009e565b60405161005b919061013f565b60405180910390f35b61007e6004803603810190610079919061018b565b6100a4565b005b610088610100565b60405161009591906101f9565b60405180910390f35b60005481565b806000808282546100b59190610243565b9250508190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000819050919050565b61013981610126565b82525050565b60006020820190506101546000830184610130565b92915050565b600080fd5b61016881610126565b811461017357600080fd5b50565b6000813590506101858161015f565b92915050565b6000602082840312156101a1576101a061015a565b5b60006101af84828501610176565b91505092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101e3826101b8565b9050919050565b6101f3816101d8565b82525050565b600060208201905061020e60008301846101ea565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061024e82610126565b915061025983610126565b925082820190508082111561027157610270610214565b5b9291505056fea2646970667358221220f1474068cf9aac836f8c5a31a9307e9bf225c1fccdedaf90eeaba493de1a9c9564736f6c63430008120033"; + // Deploy token contract + return deploy_contract(eoa, evmc::from_hex(token_bytecode).value()); + } + + void call_test(const evmc::address& contract_addr, uint64_t amount, name eos, name actor) { + + auto to = evmc::bytes{std::begin(contract_addr.bytes), std::end(contract_addr.bytes)}; + + silkworm::Bytes data; + data += evmc::from_hex("29e99f07").value(); // sha3(test(uint256))[:4] + data += evmc::bytes32{amount}; // value + + call(eos, to, 0, data, 500000, actor); + } + + void admincall_test(const evmc::address& contract_addr, uint64_t amount, evm_eoa& eoa, name actor) { + auto to = evmc::bytes{std::begin(contract_addr.bytes), std::end(contract_addr.bytes)}; + auto from = evmc::bytes{std::begin(eoa.address.bytes), std::end(eoa.address.bytes)}; + silkworm::Bytes data; + data += evmc::from_hex("29e99f07").value(); // sha3(test(uint256))[:4] + data += evmc::bytes32{amount}; // value + + admincall(from, to, 0, data, 500000, actor); + } + + intx::uint256 get_count(const evmc::address& contract_addr, std::optional callback={}, std::optional context={}) { + exec_input input; + input.context = context; + input.to = bytes{std::begin(contract_addr.bytes), std::end(contract_addr.bytes)}; + + silkworm::Bytes data; + data += evmc::from_hex("06661abd").value(); // sha3(count())[:4] + input.data = bytes{data.begin(), data.end()}; + + auto res = exec(input, callback); + + BOOST_REQUIRE(res); + BOOST_REQUIRE(res->action_traces.size() == 1); + + // Since callback information was not provided the result of the + // execution is returned in the action return_value + auto out = fc::raw::unpack(res->action_traces[0].return_value); + BOOST_REQUIRE(out.status == 0); + BOOST_REQUIRE(out.data.size() == 32); + + auto result = intx::be::unsafe::load(reinterpret_cast(out.data.data())); + return result; + } + + evmc::address get_lastcaller(const evmc::address& contract_addr, std::optional callback={}, std::optional context={}) { + exec_input input; + input.context = context; + input.to = bytes{std::begin(contract_addr.bytes), std::end(contract_addr.bytes)}; + + silkworm::Bytes data; + data += evmc::from_hex("d097e7a6").value(); // sha3(lastcaller())[:4] + input.data = bytes{data.begin(), data.end()}; + + auto res = exec(input, callback); + + BOOST_REQUIRE(res); + BOOST_REQUIRE(res->action_traces.size() == 1); + + // Since callback information was not provided the result of the + // execution is returned in the action return_value + auto out = fc::raw::unpack(res->action_traces[0].return_value); + BOOST_REQUIRE(out.status == 0); + + BOOST_REQUIRE(out.data.size() >= silkworm::kAddressLength); + + evmc::address result; + memcpy(result.bytes, out.data.data()+out.data.size() - silkworm::kAddressLength, silkworm::kAddressLength); + return result; + } + +}; + +BOOST_AUTO_TEST_SUITE(call_evm_tests) +BOOST_FIXTURE_TEST_CASE(call_test_function, call_evm_tester) try { + evm_eoa evm1; + auto total_fund = intx::uint256(vault_balance("evm"_n)); + // Fund evm1 address with 100 EOS + transfer_token("alice"_n, "evm"_n, make_asset(1000000), evm1.address_0x()); + auto evm1_balance = evm_balance(evm1); + BOOST_REQUIRE(!!evm1_balance); + BOOST_REQUIRE(*evm1_balance == intx::exp<256>(10, 18) * 100); + total_fund += *evm1_balance; + + // Deploy contract + auto token_addr = deploy_test_contract(evm1); + + // Deployment gas fee go to evm vault + BOOST_REQUIRE(*evm_balance(evm1) + intx::uint256(vault_balance("evm"_n)) == total_fund); + + // Missing authority + BOOST_REQUIRE_EXCEPTION(call_test(token_addr, 1234, "alice"_n, "bob"_n), + missing_auth_exception, eosio::testing::fc_exception_message_starts_with("missing authority")); + + // Account not opened + BOOST_REQUIRE_EXCEPTION(call_test(token_addr, 1234, "alice"_n, "alice"_n), + eosio_assert_message_exception, eosio_assert_message_is("caller account has not been opened")); + + // Open + open("alice"_n); + + // No sufficient funds in the account so decrementing of balance failed. + BOOST_REQUIRE_EXCEPTION(call_test(token_addr, 1234, "alice"_n, "alice"_n), + eosio_assert_message_exception, eosio_assert_message_is("decrementing more than available")); + + // Transfer enough funds + transfer_token("alice"_n, "evm"_n, make_asset(1000000), "alice"); + + BOOST_REQUIRE(intx::uint256(vault_balance("alice"_n)) == intx::exp<256>(10, 18) * 100); + auto total_fund2 = intx::uint256(vault_balance("alice"_n)) + intx::uint256(vault_balance("evm"_n)); + + // Call and check results + call_test(token_addr, 1234, "alice"_n, "alice"_n); + auto count = get_count(token_addr); + BOOST_REQUIRE(count == 1234); + + // Gas go from alice's vault to evm's vault + BOOST_REQUIRE(total_fund2 == intx::uint256(vault_balance("alice"_n)) + intx::uint256(vault_balance("evm"_n))); + + + // Advance block so we do not generate same transaction. + produce_block(); + + call_test(token_addr, 4321, "alice"_n, "alice"_n); + count = get_count(token_addr); + BOOST_REQUIRE(count == 5555); + + // Gas go from alice's vault to evm's vault + BOOST_REQUIRE(total_fund2 == intx::uint256(vault_balance("alice"_n)) + intx::uint256(vault_balance("evm"_n))); + + // Function being called on behalf of reserved address of eos account "alice" + auto caller = get_lastcaller(token_addr); + BOOST_REQUIRE(caller == make_reserved_address("alice"_n.to_uint64_t())); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(admincall_test_function, call_evm_tester) try { + evm_eoa evm1; + evm_eoa evm2; + + auto total_fund = intx::uint256(vault_balance("evm"_n)); + + // Fund evm1 address with 100 EOS + transfer_token("alice"_n, "evm"_n, make_asset(1000000), evm1.address_0x()); + + auto evm1_balance = evm_balance(evm1); + BOOST_REQUIRE(!!evm1_balance); + BOOST_REQUIRE(*evm1_balance == intx::exp<256>(10, 18) * 100); + total_fund += *evm1_balance; + + // Deploy contract + auto token_addr = deploy_test_contract(evm1); + + // Deployment gas fee go to evm vault + BOOST_REQUIRE(*evm_balance(evm1) + intx::uint256(vault_balance("evm"_n)) == total_fund); + + // Missing authority + BOOST_REQUIRE_EXCEPTION(admincall_test(token_addr, 1234, evm2, "alice"_n), + missing_auth_exception, eosio::testing::fc_exception_message_starts_with("missing authority")); + + // Account not created + BOOST_REQUIRE_EXCEPTION( admincall_test(token_addr, 1234, evm2, "evm"_n), + eosio_assert_message_exception, eosio_assert_message_is("invalid address")); + + // Transfer small amount to create account + transfer_token("alice"_n, "evm"_n, make_asset(100), evm2.address_0x()); + + auto evm2_balance = evm_balance(evm2); + BOOST_REQUIRE(!!evm2_balance); + BOOST_REQUIRE(*evm2_balance == intx::exp<256>(10, 18 - 4) * 100); + + // Insufficient funds + BOOST_REQUIRE_EXCEPTION( admincall_test(token_addr, 1234, evm2, "evm"_n), + eosio_assert_message_exception, eosio_assert_message_is("validate_transaction error: 23 Insufficient funds")); + + // Transfer enough funds + transfer_token("alice"_n, "evm"_n, make_asset(10000), evm2.address_0x()); + + BOOST_REQUIRE(evm_balance(evm2) == intx::exp<256>(10, 18 - 4) * 10100); + auto total_fund2 = intx::uint256(vault_balance("evm"_n)) + *evm_balance(evm2); + + // Call and check results + admincall_test(token_addr, 1234, evm2, "evm"_n); + + auto count = get_count(token_addr); + BOOST_REQUIRE(count == 1234); + + // Gas go from evm2 to evm vault + BOOST_REQUIRE(total_fund2 == *evm_balance(evm2) + intx::uint256(vault_balance("evm"_n))); + + // Advance block so we do not generate same transaction. + produce_block(); + + admincall_test(token_addr, 4321, evm2, "evm"_n); + count = get_count(token_addr); + BOOST_REQUIRE(count == 5555); + + // Gas go from evm2 to evm vault + BOOST_REQUIRE(total_fund2 == *evm_balance(evm2) + intx::uint256(vault_balance("evm"_n))); + + // Function being called on behalf of evm address "evm2" + auto caller = get_lastcaller(token_addr); + BOOST_REQUIRE(caller== evm2.address); + + +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file