diff --git a/contracts/eosio.system/delegate_bandwidth.cpp b/contracts/eosio.system/delegate_bandwidth.cpp index a0eafaf4661..a1bf330ca66 100644 --- a/contracts/eosio.system/delegate_bandwidth.cpp +++ b/contracts/eosio.system/delegate_bandwidth.cpp @@ -207,6 +207,9 @@ namespace eosiosystem { { require_auth( from ); eosio_assert( stake_net_delta != asset(0) || stake_cpu_delta != asset(0), "should stake non-zero amount" ); + eosio_assert( std::abs( (stake_net_delta + stake_cpu_delta).amount ) + >= std::max( std::abs( stake_net_delta.amount ), std::abs( stake_cpu_delta.amount ) ), + "net and cpu deltas cannot be opposite signs" ); account_name source_stake_from = from; if ( transfer ) { @@ -273,8 +276,16 @@ namespace eosiosystem { auto net_balance = stake_net_delta; auto cpu_balance = stake_cpu_delta; bool need_deferred_trx = false; - if ( req != refunds_tbl.end() ) { //need to update refund - refunds_tbl.modify( req, 0, [&]( refund_request& r ) { + + + // net and cpu are same sign by assertions in delegatebw and undelegatebw + // redundant assertion also at start of changebw to protect against misuse of changebw + bool is_undelegating = (net_balance.amount + cpu_balance.amount ) < 0; + bool is_delegating_to_self = (!transfer && from == receiver); + + if( is_delegating_to_self || is_undelegating ) { + if ( req != refunds_tbl.end() ) { //need to update refund + refunds_tbl.modify( req, 0, [&]( refund_request& r ) { if ( net_balance < asset(0) || cpu_balance < asset(0) ) { r.request_time = now(); } @@ -293,17 +304,19 @@ namespace eosiosystem { cpu_balance = asset(0); } }); - eosio_assert( asset(0) <= req->net_amount, "negative net refund amount" ); //should never happen - eosio_assert( asset(0) <= req->cpu_amount, "negative cpu refund amount" ); //should never happen - if ( req->net_amount == asset(0) && req->cpu_amount == asset(0) ) { - refunds_tbl.erase( req ); - need_deferred_trx = false; - } else { - need_deferred_trx = true; - } - } else if ( net_balance < asset(0) || cpu_balance < asset(0) ) { //need to create refund - refunds_tbl.emplace( from, [&]( refund_request& r ) { + eosio_assert( asset(0) <= req->net_amount, "negative net refund amount" ); //should never happen + eosio_assert( asset(0) <= req->cpu_amount, "negative cpu refund amount" ); //should never happen + + if ( req->net_amount == asset(0) && req->cpu_amount == asset(0) ) { + refunds_tbl.erase( req ); + need_deferred_trx = false; + } else { + need_deferred_trx = true; + } + + } else if ( net_balance < asset(0) || cpu_balance < asset(0) ) { //need to create refund + refunds_tbl.emplace( from, [&]( refund_request& r ) { r.owner = from; if ( net_balance < asset(0) ) { r.net_amount = -net_balance; @@ -315,8 +328,9 @@ namespace eosiosystem { } // else r.cpu_amount = 0 by default constructor r.request_time = now(); }); - need_deferred_trx = true; - } // else stake increase requested with no existing row in refunds_tbl -> nothing to do with refunds_tbl + need_deferred_trx = true; + } // else stake increase requested with no existing row in refunds_tbl -> nothing to do with refunds_tbl + } /// end if is_delegating_to_self || is_undelegating if ( need_deferred_trx ) { eosio::transaction out; @@ -367,6 +381,7 @@ namespace eosiosystem { eosio_assert( stake_cpu_quantity >= asset(0), "must stake a positive amount" ); eosio_assert( stake_net_quantity >= asset(0), "must stake a positive amount" ); eosio_assert( stake_net_quantity + stake_cpu_quantity > asset(0), "must stake a positive amount" ); + eosio_assert( !transfer || from != receiver, "cannot use transfer flag if delegating to self" ); changebw( from, receiver, stake_net_quantity, stake_cpu_quantity, transfer); } // delegatebw diff --git a/unittests/eosio.system_tests.cpp b/unittests/eosio.system_tests.cpp index a619339d556..5942a3fd6a3 100644 --- a/unittests/eosio.system_tests.cpp +++ b/unittests/eosio.system_tests.cpp @@ -212,7 +212,72 @@ BOOST_FIXTURE_TEST_CASE( stake_unstake_with_transfer, eosio_system_tester ) try BOOST_REQUIRE_EQUAL( success(), unstake( "alice1111111", "alice1111111", core_from_string("400.0000"), core_from_string("200.0000") ) ); BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); - edump((get_balance( "eosio.stake" ))); + produce_block( fc::hours(3*24-1) ); + produce_blocks(1); + BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); + //after 3 days funds should be released + + produce_block( fc::hours(1) ); + produce_blocks(1); + + BOOST_REQUIRE_EQUAL( core_from_string("1300.0000"), get_balance( "alice1111111" ) ); + + //stake should be equal to what was staked in constructor, voting power should be 0 + total = get_total_stake("alice1111111"); + BOOST_REQUIRE_EQUAL( core_from_string("10.0000"), total["net_weight"].as()); + BOOST_REQUIRE_EQUAL( core_from_string("10.0000"), total["cpu_weight"].as()); + REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("0.0000")), get_voter_info( "alice1111111" ) ); + + // Now alice stakes to bob with transfer flag + BOOST_REQUIRE_EQUAL( success(), stake_with_transfer( "alice1111111", "bob111111111", core_from_string("100.0000"), core_from_string("100.0000") ) ); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( stake_to_self_with_transfer, eosio_system_tester ) try { + cross_15_percent_threshold(); + + BOOST_REQUIRE_EQUAL( core_from_string("0.0000"), get_balance( "alice1111111" ) ); + transfer( "eosio", "alice1111111", core_from_string("1000.0000"), "eosio" ); + + BOOST_REQUIRE_EQUAL( wasm_assert_msg("cannot use transfer flag if delegating to self"), + stake_with_transfer( "alice1111111", "alice1111111", core_from_string("200.0000"), core_from_string("100.0000") ) + ); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( stake_while_pending_refund, eosio_system_tester ) try { + cross_15_percent_threshold(); + + issue( "eosio", core_from_string("1000.0000"), config::system_account_name ); + issue( "eosio.stake", core_from_string("1000.0000"), config::system_account_name ); + BOOST_REQUIRE_EQUAL( core_from_string("0.0000"), get_balance( "alice1111111" ) ); + + //eosio stakes for alice with transfer flag + + transfer( "eosio", "bob111111111", core_from_string("1000.0000"), "eosio" ); + BOOST_REQUIRE_EQUAL( success(), stake_with_transfer( "bob111111111", "alice1111111", core_from_string("200.0000"), core_from_string("100.0000") ) ); + + //check that alice has both bandwidth and voting power + auto total = get_total_stake("alice1111111"); + BOOST_REQUIRE_EQUAL( core_from_string("210.0000"), total["net_weight"].as()); + BOOST_REQUIRE_EQUAL( core_from_string("110.0000"), total["cpu_weight"].as()); + REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("300.0000")), get_voter_info( "alice1111111" ) ); + + BOOST_REQUIRE_EQUAL( core_from_string("0.0000"), get_balance( "alice1111111" ) ); + + //alice stakes for herself + transfer( "eosio", "alice1111111", core_from_string("1000.0000"), "eosio" ); + BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "alice1111111", core_from_string("200.0000"), core_from_string("100.0000") ) ); + //now alice's stake should be equal to transfered from eosio + own stake + total = get_total_stake("alice1111111"); + BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); + BOOST_REQUIRE_EQUAL( core_from_string("410.0000"), total["net_weight"].as()); + BOOST_REQUIRE_EQUAL( core_from_string("210.0000"), total["cpu_weight"].as()); + REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("600.0000")), get_voter_info( "alice1111111" ) ); + + //alice can unstake everything (including what was transfered) + BOOST_REQUIRE_EQUAL( success(), unstake( "alice1111111", "alice1111111", core_from_string("400.0000"), core_from_string("200.0000") ) ); + BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); produce_block( fc::hours(3*24-1) ); produce_blocks(1); @@ -224,7 +289,7 @@ BOOST_FIXTURE_TEST_CASE( stake_unstake_with_transfer, eosio_system_tester ) try BOOST_REQUIRE_EQUAL( core_from_string("1300.0000"), get_balance( "alice1111111" ) ); - //stake should be equal to what was staked in constructor, votring power should be 0 + //stake should be equal to what was staked in constructor, voting power should be 0 total = get_total_stake("alice1111111"); BOOST_REQUIRE_EQUAL( core_from_string("10.0000"), total["net_weight"].as()); BOOST_REQUIRE_EQUAL( core_from_string("10.0000"), total["cpu_weight"].as()); @@ -492,30 +557,47 @@ BOOST_FIXTURE_TEST_CASE( stake_from_refund, eosio_system_tester ) try { cross_15_percent_threshold(); issue( "alice1111111", core_from_string("1000.0000"), config::system_account_name ); - BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "bob111111111", core_from_string("200.0000"), core_from_string("100.0000") ) ); + BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "alice1111111", core_from_string("200.0000"), core_from_string("100.0000") ) ); - auto total = get_total_stake( "bob111111111" ); + auto total = get_total_stake( "alice1111111" ); BOOST_REQUIRE_EQUAL( core_from_string("210.0000"), total["net_weight"].as()); BOOST_REQUIRE_EQUAL( core_from_string("110.0000"), total["cpu_weight"].as()); - REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("300.0000") ), get_voter_info( "alice1111111" ) ); - BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); + BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "bob111111111", core_from_string("50.0000"), core_from_string("50.0000") ) ); - //unstake a share - BOOST_REQUIRE_EQUAL( success(), unstake( "alice1111111", "bob111111111", core_from_string("100.0000"), core_from_string("50.0000") ) ); total = get_total_stake( "bob111111111" ); + BOOST_REQUIRE_EQUAL( core_from_string("60.0000"), total["net_weight"].as()); + BOOST_REQUIRE_EQUAL( core_from_string("60.0000"), total["cpu_weight"].as()); + + REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("400.0000") ), get_voter_info( "alice1111111" ) ); + BOOST_REQUIRE_EQUAL( core_from_string("600.0000"), get_balance( "alice1111111" ) ); + + //unstake a share + BOOST_REQUIRE_EQUAL( success(), unstake( "alice1111111", "alice1111111", core_from_string("100.0000"), core_from_string("50.0000") ) ); + total = get_total_stake( "alice1111111" ); BOOST_REQUIRE_EQUAL( core_from_string("110.0000"), total["net_weight"].as()); BOOST_REQUIRE_EQUAL( core_from_string("60.0000"), total["cpu_weight"].as()); - REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("150.0000") ), get_voter_info( "alice1111111" ) ); - BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); + REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("250.0000") ), get_voter_info( "alice1111111" ) ); + BOOST_REQUIRE_EQUAL( core_from_string("600.0000"), get_balance( "alice1111111" ) ); auto refund = get_refund_request( "alice1111111" ); BOOST_REQUIRE_EQUAL( core_from_string("100.0000"), refund["net_amount"].as() ); BOOST_REQUIRE_EQUAL( core_from_string( "50.0000"), refund["cpu_amount"].as() ); //XXX auto request_time = refund["request_time"].as_int64(); - //stake less than pending refund, entire amount should be traken from refund - BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "bob111111111", core_from_string("50.0000"), core_from_string("25.0000") ) ); - total = get_total_stake( "bob111111111" ); + //alice delegates to bob, should pull from liquid balance not refund + BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "bob111111111", core_from_string("50.0000"), core_from_string("50.0000") ) ); + total = get_total_stake( "alice1111111" ); + BOOST_REQUIRE_EQUAL( core_from_string("110.0000"), total["net_weight"].as()); + BOOST_REQUIRE_EQUAL( core_from_string("60.0000"), total["cpu_weight"].as()); + REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("350.0000") ), get_voter_info( "alice1111111" ) ); + BOOST_REQUIRE_EQUAL( core_from_string("500.0000"), get_balance( "alice1111111" ) ); + refund = get_refund_request( "alice1111111" ); + BOOST_REQUIRE_EQUAL( core_from_string("100.0000"), refund["net_amount"].as() ); + BOOST_REQUIRE_EQUAL( core_from_string( "50.0000"), refund["cpu_amount"].as() ); + + //stake less than pending refund, entire amount should be taken from refund + BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "alice1111111", core_from_string("50.0000"), core_from_string("25.0000") ) ); + total = get_total_stake( "alice1111111" ); BOOST_REQUIRE_EQUAL( core_from_string("160.0000"), total["net_weight"].as()); BOOST_REQUIRE_EQUAL( core_from_string("85.0000"), total["cpu_weight"].as()); refund = get_refund_request( "alice1111111" ); @@ -523,42 +605,77 @@ BOOST_FIXTURE_TEST_CASE( stake_from_refund, eosio_system_tester ) try { BOOST_REQUIRE_EQUAL( core_from_string("25.0000"), refund["cpu_amount"].as() ); //request time should stay the same //BOOST_REQUIRE_EQUAL( request_time, refund["request_time"].as_int64() ); - //balance shoud stay the same - BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); + //balance should stay the same + BOOST_REQUIRE_EQUAL( core_from_string("500.0000"), get_balance( "alice1111111" ) ); //stake exactly pending refund amount - BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "bob111111111", core_from_string("50.0000"), core_from_string("25.0000") ) ); - total = get_total_stake( "bob111111111" ); + BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "alice1111111", core_from_string("50.0000"), core_from_string("25.0000") ) ); + total = get_total_stake( "alice1111111" ); BOOST_REQUIRE_EQUAL( core_from_string("210.0000"), total["net_weight"].as()); BOOST_REQUIRE_EQUAL( core_from_string("110.0000"), total["cpu_weight"].as()); //pending refund should be removed refund = get_refund_request( "alice1111111" ); BOOST_TEST_REQUIRE( refund.is_null() ); - //balance shoud stay the same - BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); + //balance should stay the same + BOOST_REQUIRE_EQUAL( core_from_string("500.0000"), get_balance( "alice1111111" ) ); //create pending refund again - BOOST_REQUIRE_EQUAL( success(), unstake( "alice1111111", "bob111111111", core_from_string("200.0000"), core_from_string("100.0000") ) ); - total = get_total_stake( "bob111111111" ); + BOOST_REQUIRE_EQUAL( success(), unstake( "alice1111111", "alice1111111", core_from_string("200.0000"), core_from_string("100.0000") ) ); + total = get_total_stake( "alice1111111" ); BOOST_REQUIRE_EQUAL( core_from_string("10.0000"), total["net_weight"].as()); BOOST_REQUIRE_EQUAL( core_from_string("10.0000"), total["cpu_weight"].as()); - BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); + BOOST_REQUIRE_EQUAL( core_from_string("500.0000"), get_balance( "alice1111111" ) ); refund = get_refund_request( "alice1111111" ); BOOST_REQUIRE_EQUAL( core_from_string("200.0000"), refund["net_amount"].as() ); BOOST_REQUIRE_EQUAL( core_from_string("100.0000"), refund["cpu_amount"].as() ); //stake more than pending refund - BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "bob111111111", core_from_string("300.0000"), core_from_string("200.0000") ) ); - total = get_total_stake( "bob111111111" ); + BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "alice1111111", core_from_string("300.0000"), core_from_string("200.0000") ) ); + total = get_total_stake( "alice1111111" ); BOOST_REQUIRE_EQUAL( core_from_string("310.0000"), total["net_weight"].as()); BOOST_REQUIRE_EQUAL( core_from_string("210.0000"), total["cpu_weight"].as()); + REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("700.0000") ), get_voter_info( "alice1111111" ) ); refund = get_refund_request( "alice1111111" ); BOOST_TEST_REQUIRE( refund.is_null() ); - //200 EOS should be taken from alice's account - BOOST_REQUIRE_EQUAL( core_from_string("500.0000"), get_balance( "alice1111111" ) ); + //200 core tokens should be taken from alice's account + BOOST_REQUIRE_EQUAL( core_from_string("300.0000"), get_balance( "alice1111111" ) ); } FC_LOG_AND_RETHROW() +BOOST_FIXTURE_TEST_CASE( stake_to_another_user_not_from_refund, eosio_system_tester ) try { + cross_15_percent_threshold(); + + issue( "alice1111111", core_from_string("1000.0000"), config::system_account_name ); + BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", core_from_string("200.0000"), core_from_string("100.0000") ) ); + + auto total = get_total_stake( "alice1111111" ); + BOOST_REQUIRE_EQUAL( core_from_string("210.0000"), total["net_weight"].as()); + BOOST_REQUIRE_EQUAL( core_from_string("110.0000"), total["cpu_weight"].as()); + BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); + + REQUIRE_MATCHING_OBJECT( voter( "alice1111111", core_from_string("300.0000") ), get_voter_info( "alice1111111" ) ); + BOOST_REQUIRE_EQUAL( core_from_string("700.0000"), get_balance( "alice1111111" ) ); + + //unstake + BOOST_REQUIRE_EQUAL( success(), unstake( "alice1111111", core_from_string("200.0000"), core_from_string("100.0000") ) ); + auto refund = get_refund_request( "alice1111111" ); + BOOST_REQUIRE_EQUAL( core_from_string("200.0000"), refund["net_amount"].as() ); + BOOST_REQUIRE_EQUAL( core_from_string("100.0000"), refund["cpu_amount"].as() ); + //auto orig_request_time = refund["request_time"].as_int64(); + + //stake to another user + BOOST_REQUIRE_EQUAL( success(), stake( "alice1111111", "bob111111111", core_from_string("200.0000"), core_from_string("100.0000") ) ); + total = get_total_stake( "bob111111111" ); + BOOST_REQUIRE_EQUAL( core_from_string("210.0000"), total["net_weight"].as()); + BOOST_REQUIRE_EQUAL( core_from_string("110.0000"), total["cpu_weight"].as()); + //stake should be taken from alices' balance, and refund request should stay the same + BOOST_REQUIRE_EQUAL( core_from_string("400.0000"), get_balance( "alice1111111" ) ); + refund = get_refund_request( "alice1111111" ); + BOOST_REQUIRE_EQUAL( core_from_string("200.0000"), refund["net_amount"].as() ); + BOOST_REQUIRE_EQUAL( core_from_string("100.0000"), refund["cpu_amount"].as() ); + //BOOST_REQUIRE_EQUAL( orig_request_time, refund["request_time"].as_int64() ); + +} FC_LOG_AND_RETHROW() // Tests for voting BOOST_FIXTURE_TEST_CASE( producer_register_unregister, eosio_system_tester ) try {