Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tokenomics - eosio.bpay #98

Merged
merged 3 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contracts/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ add_subdirectory(eosio.system)
add_subdirectory(eosio.token)
add_subdirectory(eosio.wrap)
add_subdirectory(eosio.fees)
add_subdirectory(eosio.bpay)

add_subdirectory(test_contracts)
14 changes: 14 additions & 0 deletions contracts/eosio.bpay/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
add_contract(eosio.bpay eosio.bpay ${CMAKE_CURRENT_SOURCE_DIR}/src/eosio.bpay.cpp)

target_include_directories(eosio.bpay PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/../eosio.system/include
${CMAKE_CURRENT_SOURCE_DIR}/../eosio.token/include)

set_target_properties(eosio.bpay
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")

configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/ricardian/eosio.bpay.contracts.md.in ${CMAKE_CURRENT_BINARY_DIR}/ricardian/eosio.bpay.contracts.md @ONLY )

target_compile_options( eosio.bpay PUBLIC -R${CMAKE_CURRENT_SOURCE_DIR}/ricardian -R${CMAKE_CURRENT_BINARY_DIR}/ricardian )
55 changes: 55 additions & 0 deletions contracts/eosio.bpay/include/eosio.bpay/eosio.bpay.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#pragma once

#include <eosio/eosio.hpp>
#include <eosio.system/eosio.system.hpp>
#include <eosio.token/eosio.token.hpp>

using namespace std;

namespace eosio {
/**
* The `eosio.bpay` contract handles system bpay distribution.
*/
class [[eosio::contract("eosio.bpay")]] bpay : public contract {
public:
using contract::contract;

/**
* ## TABLE `rewards`
*
* @param owner - block producer owner account
* @param quantity - reward quantity in EOS
*
* ### example
*
* ```json
* [
* {
* "owner": "alice",
* "quantity": "8.800 EOS"
* }
* ]
* ```
*/
struct [[eosio::table("rewards")]] rewards_row {
name owner;
asset quantity;

uint64_t primary_key() const { return owner.value; }
};
typedef eosio::multi_index< "rewards"_n, rewards_row > rewards_table;

/**
* Claim rewards for a block producer.
*
* @param owner - block producer owner account
*/
[[eosio::action]]
void claimrewards( const name owner);

[[eosio::on_notify("eosio.token::transfer")]]
void on_transfer( const name from, const name to, const asset quantity, const string memo );

private:
};
} /// namespace eosio
10 changes: 10 additions & 0 deletions contracts/eosio.bpay/ricardian/eosio.bpay.contracts.md.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<h1 class="contract">claimrewards</h1>

---
spec_version: "0.2.0"
title: Claim Rewards
summary: '{{nowrap owner}} claims block production rewards'
icon: @ICON_BASE_URL@/@MULTISIG_ICON_URI@
---

{{owner}} claims block production rewards accumulated through network fees.
73 changes: 73 additions & 0 deletions contracts/eosio.bpay/src/eosio.bpay.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#include <eosio.bpay/eosio.bpay.hpp>

namespace eosio {

void bpay::claimrewards( const name owner ) {
require_auth( owner );

rewards_table _rewards( get_self(), get_self().value );

const auto& row = _rewards.get( owner.value, "no rewards to claim" );

eosio::token::transfer_action transfer( "eosio.token"_n, { get_self(), "active"_n });
transfer.send( get_self(), owner, row.quantity, "producer block pay" );

_rewards.erase(row);
}

void bpay::on_transfer( const name from, const name to, const asset quantity, const string memo ) {
if (from == get_self() || to != get_self()) {
return;
}

// ignore eosio system incoming transfers (caused by bpay income transfers eosio => eosio.bpay => producer)
if ( from == "eosio"_n) return;

symbol system_symbol = eosiosystem::system_contract::get_core_symbol();

check( quantity.symbol == system_symbol, "only core token allowed" );

rewards_table _rewards( get_self(), get_self().value );
eosiosystem::producers_table _producers( "eosio"_n, "eosio"_n.value );

eosiosystem::global_state_singleton _global("eosio"_n, "eosio"_n.value);
check( _global.exists(), "global state does not exist");
uint16_t producer_count = _global.get().last_producer_schedule_size;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is handy, makes it more flexible 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not sure what savanna might do to prod count, so...


asset reward = quantity / producer_count;

// get producer with the most votes
// using `by_votes` secondary index
auto idx = _producers.get_index<"prototalvote"_n>();
auto prod = idx.begin();

// get top n producers by vote, excluding inactive
std::vector<name> top_producers;
while (true) {
if (prod == idx.end()) break;
if (prod->is_active == false) continue;

top_producers.push_back(prod->owner);

if (top_producers.size() == producer_count) break;

prod++;
}

// distribute rewards to top producers
for (auto producer : top_producers) {
auto row = _rewards.find( producer.value );
if (row == _rewards.end()) {
_rewards.emplace( get_self(), [&](auto& row) {
row.owner = producer;
row.quantity = reward;
});
} else {
_rewards.modify(row, get_self(), [&](auto& row) {
row.quantity += reward;
});
}
}
}

} /// namespace eosio
2 changes: 2 additions & 0 deletions tests/contracts.hpp.in
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct contracts {
static std::vector<char> wrap_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/eosio.wrap/eosio.wrap.abi"); }
static std::vector<uint8_t> bios_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/eosio.bios/eosio.bios.wasm"); }
static std::vector<char> bios_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/eosio.bios/eosio.bios.abi"); }
static std::vector<uint8_t> bpay_wasm() { return read_wasm("${CMAKE_BINARY_DIR}/contracts/eosio.bpay/eosio.bpay.wasm"); }
static std::vector<char> bpay_abi() { return read_abi("${CMAKE_BINARY_DIR}/contracts/eosio.bpay/eosio.bpay.abi"); }

struct util {
static std::vector<uint8_t> reject_all_wasm() { return read_wasm("${CMAKE_CURRENT_SOURCE_DIR}/test_contracts/reject_all.wasm"); }
Expand Down
101 changes: 101 additions & 0 deletions tests/eosio.bpay_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include "eosio.system_tester.hpp"

using namespace eosio_system;

BOOST_AUTO_TEST_SUITE(eosio_bpay_tests);

account_name voter = "alice1111111"_n;
account_name standby = "bp.standby"_n;
account_name inactive = "bp.inactive"_n;
account_name fees = "eosio.fees"_n;
account_name bpay = "eosio.bpay"_n;

BOOST_FIXTURE_TEST_CASE( bpay_test, eosio_system_tester ) try {


// Transferring some tokens to the fees account
// since tokens from eosio will not be directly accepted as contributions to
// the bpay contract
transfer( config::system_account_name, fees, core_sym::from_string("100000.0000"), config::system_account_name );


// Setting up the producers, standby and inactive producers, and voting them in
setup_producer_accounts({standby, inactive});
auto producer_names = active_and_vote_producers();

BOOST_REQUIRE_EQUAL( success(), regproducer(standby) );
BOOST_REQUIRE_EQUAL( success(), regproducer(inactive) );
vector<name> top_producers_and_inactive = {inactive};
top_producers_and_inactive.insert( top_producers_and_inactive.end(), producer_names.begin(), producer_names.begin()+21 );

BOOST_REQUIRE_EQUAL( success(), vote( voter, top_producers_and_inactive ) );
produce_blocks( 250 );


BOOST_REQUIRE_EQUAL( 0, get_producer_info( standby )["unpaid_blocks"].as<uint32_t>() );
BOOST_REQUIRE_EQUAL( get_producer_info( producer_names[0] )["unpaid_blocks"].as<uint32_t>() > 0, true );

// TODO: Check nothing happened here, no rewards since it comes from system account

asset rewards_sent = core_sym::from_string("1000.0000");
transfer( fees, bpay, rewards_sent, fees);

// rewards / 21
asset balance_per_producer = core_sym::from_string("47.6190");

auto rewards = get_bpay_rewards(producer_names[0]);

// bp.inactive is still active, so should be included in the rewards
BOOST_REQUIRE_EQUAL( get_bpay_rewards(inactive)["quantity"].as<asset>(), balance_per_producer );
// Random sample
BOOST_REQUIRE_EQUAL( get_bpay_rewards(producer_names[11])["quantity"].as<asset>(), balance_per_producer );


// Deactivating a producer
BOOST_REQUIRE_EQUAL( success(), push_action(config::system_account_name, "rmvproducer"_n, mvo()("producer", inactive) ) );
BOOST_REQUIRE_EQUAL( false, get_producer_info( inactive )["is_active"].as<bool>() );

transfer( fees, bpay, rewards_sent, fees);
BOOST_REQUIRE_EQUAL( get_bpay_rewards(inactive)["quantity"].as<asset>(), balance_per_producer );
BOOST_REQUIRE_EQUAL( get_bpay_rewards(producer_names[11])["quantity"].as<asset>(), core_sym::from_string("95.2380") );

// BP should be able to claim their rewards
{
auto prod = producer_names[11];
BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_balance( prod ) );
BOOST_REQUIRE_EQUAL( success(), bpay_claimrewards( prod ) );
BOOST_REQUIRE_EQUAL( core_sym::from_string("95.2380"), get_balance( prod ) );
BOOST_REQUIRE_EQUAL( true, get_bpay_rewards(prod).is_null() );

// should still have rewards for another producer
BOOST_REQUIRE_EQUAL( get_bpay_rewards(producer_names[10])["quantity"].as<asset>(), core_sym::from_string("95.2380") );
}

// Should be able to claim rewards from a producer that is no longer active
{
BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_balance( inactive ) );
BOOST_REQUIRE_EQUAL( success(), bpay_claimrewards( inactive ) );
BOOST_REQUIRE_EQUAL( core_sym::from_string("47.6190"), get_balance( inactive ) );
BOOST_REQUIRE_EQUAL( true, get_bpay_rewards(inactive).is_null() );
}

// Should not have rewards for a producer that was never active
{
BOOST_REQUIRE_EQUAL( true, get_bpay_rewards(standby).is_null() );
BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_balance( standby ) );
BOOST_REQUIRE_EQUAL( wasm_assert_msg("no rewards to claim"), bpay_claimrewards( standby ) );
BOOST_REQUIRE_EQUAL( core_sym::from_string("0.0000"), get_balance( standby ) );
}

// Tokens transferred from the eosio account should be ignored
{
transfer( config::system_account_name, bpay, rewards_sent, config::system_account_name );
BOOST_REQUIRE_EQUAL( get_bpay_rewards(producer_names[10])["quantity"].as<asset>(), core_sym::from_string("95.2380") );
}



} FC_LOG_AND_RETHROW()


BOOST_AUTO_TEST_SUITE_END()
1 change: 0 additions & 1 deletion tests/eosio.system_schedules_tests.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#include <boost/test/unit_test.hpp>

#include "eosio.system_tester.hpp"

using namespace eosio_system;
Expand Down
30 changes: 29 additions & 1 deletion tests/eosio.system_tester.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,26 @@ class eosio_system_tester : public TESTER {


produce_blocks( 100 );

set_code( "eosio.token"_n, contracts::token_wasm());
set_code( "eosio.fees"_n, contracts::fees_wasm());
set_abi( "eosio.token"_n, contracts::token_abi().data() );
{
const auto& accnt = control->db().get<account_object,by_name>( "eosio.token"_n );
abi_def abi;
BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(accnt.abi, abi), true);
token_abi_ser.set_abi(abi, abi_serializer::create_yield_function(abi_serializer_max_time));
}

set_code( "eosio.fees"_n, contracts::fees_wasm());

set_code( "eosio.bpay"_n, contracts::bpay_wasm());
set_abi( "eosio.bpay"_n, contracts::bpay_abi().data() );
{
const auto& accnt = control->db().get<account_object,by_name>( "eosio.bpay"_n );
abi_def abi;
BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(accnt.abi, abi), true);
bpay_abi_ser.set_abi(abi, abi_serializer::create_yield_function(abi_serializer_max_time));
}
}

void create_core_token( symbol core_symbol = symbol{CORE_SYM} ) {
Expand Down Expand Up @@ -1508,8 +1519,25 @@ class eosio_system_tester : public TESTER {
return data.empty() ? fc::variant() : abi_ser.binary_to_variant( "schedules_info", data, abi_serializer::create_yield_function(abi_serializer_max_time) );
}



action_result bpay_claimrewards( const account_name owner ) {
action act;
act.account = "eosio.bpay"_n;
act.name = "claimrewards"_n;
act.data = abi_ser.variant_to_binary( bpay_abi_ser.get_action_type("claimrewards"_n), mvo()("owner", owner), abi_serializer::create_yield_function(abi_serializer_max_time) );

return base_tester::push_action( std::move(act), owner.to_uint64_t() );
}

fc::variant get_bpay_rewards( account_name producer ) {
vector<char> data = get_row_by_account( "eosio.bpay"_n, "eosio.bpay"_n, "rewards"_n, producer );
return data.empty() ? fc::variant() : bpay_abi_ser.binary_to_variant( "rewards_row", data, abi_serializer::create_yield_function(abi_serializer_max_time) );
}

abi_serializer abi_ser;
abi_serializer token_abi_ser;
abi_serializer bpay_abi_ser;
};

inline fc::mutable_variant_object voter( account_name acct ) {
Expand Down
Loading