Skip to content

Commit

Permalink
Merge pull request #98 from AntelopeIO/eosio.bpay
Browse files Browse the repository at this point in the history
Tokenomics - eosio.bpay
  • Loading branch information
nsjames authored Jun 5, 2024
2 parents 917d722 + 9646c7e commit 47599f0
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 2 deletions.
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;

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

0 comments on commit 47599f0

Please sign in to comment.