diff --git a/contracts/CMakeLists.txt b/contracts/CMakeLists.txt index c00a31f3..18b6b592 100644 --- a/contracts/CMakeLists.txt +++ b/contracts/CMakeLists.txt @@ -15,3 +15,4 @@ add_subdirectory(fio.fee) add_subdirectory(fio.request.obt) add_subdirectory(fio.tpid) add_subdirectory(fio.treasury) +add_subdirectory(fio.staking) diff --git a/contracts/fio.address/fio.address.cpp b/contracts/fio.address/fio.address.cpp index 95a5b682..94b1dc7d 100644 --- a/contracts/fio.address/fio.address.cpp +++ b/contracts/fio.address/fio.address.cpp @@ -1694,9 +1694,9 @@ namespace fioio { void decrcounter(const string &fio_address, const int32_t &step) { check(step > 0, "step must be greater than 0"); - check((has_auth(AddressContract) || has_auth(TokenContract) || has_auth(TREASURYACCOUNT) || + check((has_auth(AddressContract) || has_auth(TokenContract) || has_auth(TREASURYACCOUNT) || has_auth(STAKINGACCOUNT) || has_auth(REQOBTACCOUNT) || has_auth(SYSTEMACCOUNT) || has_auth(FeeContract)), - "missing required authority of fio.address, fio.token, fio.fee, fio.treasury, fio.reqobt, fio.system"); + "missing required authority of fio.address, fio.token, fio.fee, fio.treasury, fio.reqobt, fio.system, fio.staking "); auto namesbyname = fionames.get_index<"byname"_n>(); auto fioname_iter = namesbyname.find(string_to_uint128_hash(fio_address.c_str())); diff --git a/contracts/fio.common/fio.accounts.hpp b/contracts/fio.common/fio.accounts.hpp index 32fd4093..c56579c1 100644 --- a/contracts/fio.common/fio.accounts.hpp +++ b/contracts/fio.common/fio.accounts.hpp @@ -30,11 +30,13 @@ namespace fioio { static const name REQOBTACCOUNT = name("fio.reqobt"); static const name FeeContract = name("fio.fee"); + static const name StakingContract = name("fio.staking"); static const name AddressContract = name("fio.address"); static const name TPIDContract = name("fio.tpid"); static const name TokenContract = name("fio.token"); static const name FOUNDATIONACCOUNT = name("tw4tjkmo4eyd"); static const name TREASURYACCOUNT = name("fio.treasury"); + static const name STAKINGACCOUNT = name("fio.staking"); static const name FIOSYSTEMACCOUNT= name("fio.system"); static const name FIOACCOUNT = name("fio"); diff --git a/contracts/fio.common/fio.common.hpp b/contracts/fio.common/fio.common.hpp index 673fabe9..70a78c4f 100644 --- a/contracts/fio.common/fio.common.hpp +++ b/contracts/fio.common/fio.common.hpp @@ -38,7 +38,13 @@ #define MAXBPS 42 #define MAXACTIVEBPS 21 #define DEFAULTBUNDLEAMT 100 +//staking +#define COMBINEDTOKENPOOLMINIMUM 1000000000000000 // 1M FIO SUFS +#define STAKINGREWARDSRESERVEMAXIMUM 25000000000000000 // 25M FIO SUFS. +#define DAILYSTAKINGMINTTHRESHOLD 25000000000000 //25k FIO threshold for MINTING staking rewards. +#define STAKE_FIO_TOKENS_ENDPOINT "stake_fio_tokens" +#define UNSTAKE_FIO_TOKENS_ENDPOINT "unstake_fio_tokens" #define REGISTER_ADDRESS_ENDPOINT "register_fio_address" #define REGISTER_DOMAIN_ENDPOINT "register_fio_domain" #define RENEW_ADDRESS_ENDPOINT "renew_fio_address" @@ -235,6 +241,25 @@ namespace fioio { typedef singleton<"bounties"_n, bounty> bounties_table; + //this will call update tpid in the tpid contract, + //add the info to the tpid table for this TPID and also set up the auto proxy if needed. + void set_auto_proxy(const string &tpid, const uint64_t &amount, const name &auth, const name &actor){ + fionames_table fionames(AddressContract, AddressContract.value); + uint128_t fioaddhash = string_to_uint128_hash(tpid.c_str()); + + auto namesbyname = fionames.get_index<"byname"_n>(); + auto fionamefound = namesbyname.find(fioaddhash); + + if (fionamefound != namesbyname.end()) { + action( + permission_level{auth, "active"_n}, + TPIDContract, + "updatetpid"_n, + std::make_tuple(tpid, actor, amount) + ).send(); + } + } + void process_rewards(const string &tpid, const uint64_t &amount, const name &auth, const name &actor) { action( @@ -282,7 +307,13 @@ namespace fioio { permission_level{auth, "active"_n}, TREASURYACCOUNT, "bprewdupdate"_n, - std::make_tuple((uint64_t)(static_cast(amount) * .85)) + std::make_tuple((uint64_t)(static_cast(amount) * .60)) + ).send(); + action( + permission_level{auth, "active"_n}, + STAKINGACCOUNT, + "incgrewards"_n, + std::make_tuple((uint64_t)(static_cast(amount) * .25)) ).send(); } else { @@ -290,7 +321,13 @@ namespace fioio { permission_level{auth, "active"_n}, TREASURYACCOUNT, "bprewdupdate"_n, - std::make_tuple((uint64_t)(static_cast(amount) * .95)) + std::make_tuple((uint64_t)(static_cast(amount) * .70)) + ).send(); + action( + permission_level{auth, "active"_n}, + STAKINGACCOUNT, + "incgrewards"_n, + std::make_tuple((uint64_t)(static_cast(amount) * .25)) ).send(); } } @@ -344,7 +381,13 @@ namespace fioio { permission_level{auth, "active"_n}, TREASURYACCOUNT, "bppoolupdate"_n, - std::make_tuple((uint64_t)(static_cast(amount) * .85)) + std::make_tuple((uint64_t)(static_cast(amount) * .60)) + ).send(); + action( + permission_level{auth, "active"_n}, + STAKINGACCOUNT, + "incgrewards"_n, + std::make_tuple((uint64_t)(static_cast(amount) * .25)) ).send(); } else { @@ -352,7 +395,13 @@ namespace fioio { permission_level{auth, "active"_n}, TREASURYACCOUNT, "bppoolupdate"_n, - std::make_tuple((uint64_t)(static_cast(amount) * .95)) + std::make_tuple((uint64_t)(static_cast(amount) * .70)) + ).send(); + action( + permission_level{auth, "active"_n}, + STAKINGACCOUNT, + "incgrewards"_n, + std::make_tuple((uint64_t)(static_cast(amount) * .25)) ).send(); } } @@ -366,7 +415,13 @@ namespace fioio { permission_level{actor, "active"_n}, TREASURYACCOUNT, "bprewdupdate"_n, - std::make_tuple((uint64_t)(static_cast(amount) * .95)) + std::make_tuple((uint64_t)(static_cast(amount) * .70)) + ).send(); + action( + permission_level{actor, "active"_n}, + STAKINGACCOUNT, + "incgrewards"_n, + std::make_tuple((uint64_t)(static_cast(amount) * .25)) ).send(); action( @@ -404,6 +459,8 @@ namespace fioio { static const uint64_t INITIALACCOUNTRAM = 25600; static const uint64_t ADDITIONALRAMBPDESCHEDULING = 25600; + static const uint64_t STAKEFIOTOKENSRAM = 1024; //integrated. + static const uint64_t UNSTAKEFIOTOKENSRAM = 1024; //integrated. static const uint64_t REGDOMAINRAM = 2560; //integrated. static const uint64_t REGADDRESSRAM = 2560; //integrated. static const uint64_t ADDADDRESSRAM = 512; //integrated. diff --git a/contracts/fio.staking/CMakeLists.txt b/contracts/fio.staking/CMakeLists.txt new file mode 100644 index 00000000..7a770788 --- /dev/null +++ b/contracts/fio.staking/CMakeLists.txt @@ -0,0 +1,14 @@ +add_contract(fio.staking fio.staking ${CMAKE_CURRENT_SOURCE_DIR}/fio.staking.cpp) + +target_include_directories(fio.staking + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../fio.system/include + ${CMAKE_CURRENT_SOURCE_DIR}/../ + ${CMAKE_CURRENT_SOURCE_DIR}/../fio.token/include + ) + + +set_target_properties(fio.staking + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/contracts/fio.staking/fio.staking.abi b/contracts/fio.staking/fio.staking.abi new file mode 100644 index 00000000..af59906a --- /dev/null +++ b/contracts/fio.staking/fio.staking.abi @@ -0,0 +1,224 @@ +{ + "____comment": "This file was generated with eosio-abigen. DO NOT EDIT Thu Jun 13 20:29:09 2019", + "version": "eosio::abi/1.1", + "structs": [ + + { + "name": "global_staking_state", + "base": "", + "fields": [ + { + "name": "staked_token_pool", + "type": "uint64" + }, + { + "name": "combined_token_pool", + "type": "uint64" + }, + { + "name": "rewards_token_pool", + "type": "uint64" + }, + { + "name": "global_srp_count", + "type": "uint64" + }, + { + "name": "daily_staking_rewards", + "type": "uint64" + }, + { + "name": "staking_rewards_reserves_minted", + "type": "uint64" + } + ] + },{ + "name": "account_staking_info", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "account", + "type": "name" + }, + { + "name": "total_srp", + "type": "uint64" + }, + { + "name": "total_staked_fio", + "type": "uint64" + } + ] + },{ + "name": "decgstake", + "base": "", + "fields": [ + { + "name": "fiostakedsufs", + "type": "int64" + }, + { + "name": "fiorewardedsufs", + "type": "int64" + }, + { + "name": "srpcountsus", + "type": "int64" + } + ] + },{ + "name": "incgstake", + "base": "", + "fields": [ + { + "name": "fiostakedsufs", + "type": "int64" + }, + { + "name": "srpcountsus", + "type": "int64" + } + ] + },{ + "name": "incacctstake", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "fiostakedsufs", + "type": "int64" + }, + { + "name": "srpaawardedsus", + "type": "int64" + } + ] + },{ + "name": "decacctstake", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "fiostakedsufs", + "type": "int64" + }, + { + "name": "srprewardedsus", + "type": "int64" + } + ] + },{ + "name": "stakefio", + "base": "", + "fields": [ + { + "name": "fio_address", + "type": "string" + }, + { + "name": "amount", + "type": "int64" + }, + { + "name": "max_fee", + "type": "int64" + }, + { + "name": "tpid", + "type": "string" + }, + { + "name": "actor", + "type": "name" + } + ] + },{ + "name": "unstakefio", + "base": "", + "fields": [ + { + "name": "fio_address", + "type": "string" + }, + { + "name": "amount", + "type": "int64" + }, + { + "name": "max_fee", + "type": "int64" + }, + { + "name": "tpid", + "type": "string" + }, + { + "name": "actor", + "type": "name" + } + ] + } + ], + "types": [], + "actions": [ + { + "name": "decgstake", + "type": "decgstake", + "ricardian_contract": "" + }, + { + "name": "incgstake", + "type": "incgstake", + "ricardian_contract": "" + }, + { + "name": "incacctstake", + "type": "incacctstake", + "ricardian_contract": "" + }, + { + "name": "decacctstake", + "type": "decacctstake", + "ricardian_contract": "" + }, + { + "name": "stakefio", + "type": "stakefio", + "ricardian_contract": "" + }, + { + "name": "unstakefio", + "type": "unstakefio", + "ricardian_contract": "" + } + ], + "tables": [ + { + "name": "staking", + "type": "global_staking_state", + "index_type": "i64", + "key_names": [], + "key_types": [] + }, + { + "name": "accountstake", + "type": "account_staking_info", + "index_type": "i64", + "key_names": [], + "key_types": [] + } + ], + "ricardian_clauses": [], + "variants": [], + "abi_extensions": [] +} diff --git a/contracts/fio.staking/fio.staking.cpp b/contracts/fio.staking/fio.staking.cpp new file mode 100644 index 00000000..501aaab0 --- /dev/null +++ b/contracts/fio.staking/fio.staking.cpp @@ -0,0 +1,597 @@ +/** Fio Staking implementation file + * Description: + * @author Ed Rotthoff + * @modifedby + * @file fio.staking.cpp + * @license FIO Foundation ( https://github.com/fioprotocol/fio/blob/master/LICENSE ) + */ + +#include +#include "fio.staking.hpp" +#include +#include +#include +#include + +namespace fioio { + +class [[eosio::contract("Staking")]] Staking: public eosio::contract { + +private: + + //these holds global staking state for fio + global_staking_singleton staking; + global_staking_state gstaking; + account_staking_table accountstaking; + //access to the voters table for voting info. + eosiosystem::voters_table voters; + //access to fionames for address info + fionames_table fionames; + //access to fio fees for computation of fees. + fiofee_table fiofees; + //access to general locks to adapt general locks on unstake + eosiosystem::general_locks_table_v2 generallocks; + //debug output flag + bool debugout = false; + +public: + using contract::contract; + + Staking(name s, name code, datastream ds) : + contract(s, code, ds), + staking(_self, _self.value), + accountstaking(_self,_self.value), + voters(SYSTEMACCOUNT,SYSTEMACCOUNT.value), + fiofees(FeeContract, FeeContract.value), + fionames(AddressContract, AddressContract.value), + generallocks(SYSTEMACCOUNT,SYSTEMACCOUNT.value){ + gstaking = staking.exists() ? staking.get() : global_staking_state{}; + } + + ~Staking() { + staking.set(gstaking, _self); + } + + + + //incgrewards performs the staking state increments when rewards are identified (including minted) during fee collection. + // params + // fioamountsufs, this is the amount of FIO being added to the rewards (from fees or when minted). units SUFs + [[eosio::action]] + void incgrewards(const int64_t &fioamountsufs ) { + eosio_assert((has_auth(AddressContract) || has_auth(TokenContract) || has_auth(TREASURYACCOUNT) || + has_auth(STAKINGACCOUNT) || has_auth(REQOBTACCOUNT) || has_auth(SYSTEMACCOUNT) || has_auth(FeeContract)), + "missing required authority of fio.address, fio.treasury, fio.fee, fio.token, fio.stakng, eosio or fio.reqobt"); + gstaking.rewards_token_pool += fioamountsufs; + gstaking.daily_staking_rewards += fioamountsufs; + gstaking.combined_token_pool += fioamountsufs; + } + + //recorddaily will perform the daily update of global state, when bps claim rewards. + [[eosio::action]] + void recorddaily(const int64_t &amounttomint ) { + eosio_assert( has_auth(TREASURYACCOUNT), + "missing required authority of fio.treasury"); + if (amounttomint > 0) { + gstaking.staking_rewards_reserves_minted += amounttomint; + gstaking.daily_staking_rewards += amounttomint; + } + gstaking.combined_token_pool += gstaking.daily_staking_rewards; + gstaking.daily_staking_rewards = 0; + } + + //this action performs staking of fio tokens + [[eosio::action]] + void stakefio(const string &fio_address, const int64_t &amount, const int64_t &max_fee, + const string &tpid, const name &actor) { + //signer not actor. + require_auth(actor); + + if(debugout) { + print(" calling stakefio fio address ", fio_address); + } + + uint64_t bundleeligiblecountdown = 0; + + //process the fio address specified + FioAddress fa; + getFioAddressStruct(fio_address, fa); + fio_400_assert(fio_address == "" || validateFioNameFormat(fa), "fio_address", fio_address, "Invalid FIO Address format", + ErrorDomainAlreadyRegistered); + + if (!fio_address.empty()) { + const uint128_t nameHash = string_to_uint128_hash(fa.fioaddress.c_str()); + auto namesbyname = fionames.get_index<"byname"_n>(); + auto fioname_iter = namesbyname.find(nameHash); + + fio_400_assert(fioname_iter != namesbyname.end(), "fio_address", fa.fioaddress, + "FIO Address not registered", ErrorFioNameAlreadyRegistered); + + fio_403_assert(fioname_iter->owner_account == actor.value, ErrorSignature); + + const uint32_t expiration = fioname_iter->expiration; + const uint32_t present_time = now(); + fio_400_assert(present_time <= expiration, "fio_address", fio_address, "FIO Address expired. Renew first.", + ErrorDomainExpired); + bundleeligiblecountdown = fioname_iter->bundleeligiblecountdown; + } + + + uint64_t paid_fee_amount = 0; + bool skipvotecheck = false; + //begin, bundle eligible fee logic for staking + const uint128_t endpoint_hash = string_to_uint128_hash(STAKE_FIO_TOKENS_ENDPOINT); + + auto fees_by_endpoint = fiofees.get_index<"byendpoint"_n>(); + auto fee_iter = fees_by_endpoint.find(endpoint_hash); + + //if the fee isnt found for the endpoint, then 400 error. + fio_400_assert(fee_iter != fees_by_endpoint.end(), "endpoint_name", STAKE_FIO_TOKENS_ENDPOINT, + "FIO fee not found for endpoint", ErrorNoEndpoint); + + const int64_t fee_amount = fee_iter->suf_amount; + const uint64_t fee_type = fee_iter->type; + + fio_400_assert(fee_type == 1, "fee_type", to_string(fee_type), + "unexpected fee type for endpoint stake_fio_tokens, expected 0", + ErrorNoEndpoint); + + if (bundleeligiblecountdown > 0) { + action{ + permission_level{_self, "active"_n}, + AddressContract, + "decrcounter"_n, + make_tuple(fio_address, 1) + }.send(); + + if (!tpid.empty()) { + if (debugout) { + print(" calling process auto proxy with ", tpid); + } + set_auto_proxy(tpid, 0,get_self(), actor); + + //when a tpid is used, if this is the first call for this account to use a tpid, + //then the auto proxy will be set in an inline action which executes outside of this + //execution stack. We check if the tpid is a proxy, and if it is then we know that + //the owner will be auto proxied in this transaction, but in an action outside of this one. + //so we set a local flag to skip the checks for the "has voted" requirement since we + //know the owner is auto proxied, this handles the edge condition if the staking is called + //very early by a new account integrated using tpid. + + + FioAddress fa1; + getFioAddressStruct(tpid, fa1); + + const uint128_t nameHash = string_to_uint128_hash(fa1.fioaddress.c_str()); + auto namesbyname = fionames.get_index<"byname"_n>(); + auto fioname_iter = namesbyname.find(nameHash); + fio_400_assert(fioname_iter != namesbyname.end(), "tpid", fa1.fioaddress, + "FIO Address not registered", ErrorFioNameAlreadyRegistered); + //now use the owner to find the voting record. + auto votersbyowner = voters.get_index<"byowner"_n>(); + const auto viter = votersbyowner.find(fioname_iter->owner_account); + if (viter != votersbyowner.end()) { + if (viter->is_proxy) { + skipvotecheck = true; + } + } + } + + } else { + paid_fee_amount = fee_iter->suf_amount; + fio_400_assert(max_fee >= (int64_t)paid_fee_amount, "max_fee", to_string(max_fee), "Fee exceeds supplied maximum.", + ErrorMaxFeeExceeded); + + fio_fees(actor, asset(fee_amount, FIOSYMBOL), STAKE_FIO_TOKENS_ENDPOINT); + process_rewards(tpid, fee_amount,get_self(), actor); + + if (fee_amount > 0) { + INLINE_ACTION_SENDER(eosiosystem::system_contract, updatepower) + ("eosio"_n, {{_self, "active"_n}}, + {actor, true} + ); + } + } + //End, bundle eligible fee logic for staking + + //if we are not auto proxying for the first time, check if the actor has voted. + if (!skipvotecheck) { + auto votersbyowner = voters.get_index<"byowner"_n>(); + auto voter = votersbyowner.find(actor.value); + fio_400_assert(voter != votersbyowner.end(), "actor", + actor.to_string(), "Account has not voted and has not proxied.",ErrorInvalidValue); + //if they are in the table check if they are is_auto_proxy, or if they have a proxy, or if they have producers not empty + fio_400_assert((((voter->proxy) || (voter->producers.size() > 0) || (voter->is_auto_proxy))), + "actor", actor.to_string(), "Account has not voted and has not proxied.", ErrorInvalidValue); + } + + + fio_400_assert(amount > 0, "amount", to_string(amount), "Invalid amount value",ErrorInvalidValue); + fio_400_assert(max_fee >= 0, "amount", to_string(max_fee), "Invalid fee value",ErrorInvalidValue); + fio_400_assert(validateTPIDFormat(tpid), "tpid", tpid,"TPID must be empty or valid FIO address",ErrorPubKeyValid); + + + + //get the usable balance for the account + auto stakeablebalance = eosio::token::computeusablebalance(actor,false); + fio_400_assert(stakeablebalance >= (paid_fee_amount + (uint64_t)amount), "max_fee", to_string(max_fee), "Insufficient balance.", + ErrorMaxFeeExceeded); + + //RAM bump + if (STAKEFIOTOKENSRAM > 0) { + action( + permission_level{SYSTEMACCOUNT, "active"_n}, + "eosio"_n, + "incram"_n, + std::make_tuple(actor, STAKEFIOTOKENSRAM) + ).send(); + } + + + //compute rate of exchange and SRPs + uint64_t rateofexchange = 1; + if (gstaking.combined_token_pool >= COMBINEDTOKENPOOLMINIMUM) { + if (debugout) { + print(" global srp count ", gstaking.global_srp_count); + print(" combined_token_pool ", gstaking.combined_token_pool); + } + rateofexchange = gstaking.combined_token_pool / gstaking.global_srp_count; + if(debugout) { + print(" rate of exchange set to ", rateofexchange); + } + if(rateofexchange < 1) { + if(debugout) { + print(" RATE OF EXCHANGE LESS THAN 1 ", rateofexchange); + } + rateofexchange = 1; + } + } + + uint64_t srptoaward = amount / rateofexchange; + + //update global staking state + gstaking.combined_token_pool += amount; + gstaking.global_srp_count += srptoaward; + gstaking.staked_token_pool += amount; + + + //update account staking info + auto astakebyaccount = accountstaking.get_index<"byaccount"_n>(); + auto astakeiter = astakebyaccount.find(actor.value); + if (astakeiter != astakebyaccount.end()) { + eosio_assert(astakeiter->account == actor,"incacctstake owner lookup error." ); + //update the existing record + astakebyaccount.modify(astakeiter, _self, [&](struct account_staking_info &a) { + a.total_staked_fio += amount; + a.total_srp += srptoaward; + }); + } else { + const uint64_t id = accountstaking.available_primary_key(); + accountstaking.emplace(get_self(), [&](struct account_staking_info &p) { + p.id = id; + p.account = actor; + p.total_staked_fio = amount; + p.total_srp = srptoaward; + }); + } + //end increment account staking info + + + + const string response_string = string("{\"status\": \"OK\",\"fee_collected\":") + + to_string(paid_fee_amount) + string("}"); + + fio_400_assert(transaction_size() <= MAX_TRX_SIZE, "transaction_size", std::to_string(transaction_size()), + "Transaction is too large", ErrorTransaction); + + send_response(response_string.c_str()); + } + + + //this action performs the unstaking of fio tokens. + [[eosio::action]] + void unstakefio(const string &fio_address,const int64_t &amount, const int64_t &max_fee, + const string &tpid, const name &actor) { + require_auth(actor); + + fio_400_assert(amount > 10000, "amount", to_string(amount), "Invalid amount value",ErrorInvalidValue); + fio_400_assert(max_fee >= 0, "amount", to_string(max_fee), "Invalid fee value",ErrorInvalidValue); + fio_400_assert(validateTPIDFormat(tpid), "tpid", tpid,"TPID must be empty or valid FIO address",ErrorPubKeyValid); + + + //process the fio address specified + FioAddress fa; + getFioAddressStruct(fio_address, fa); + + fio_400_assert(fio_address == "" || validateFioNameFormat(fa), "fio_address", fio_address, "Invalid FIO Address format", + ErrorDomainAlreadyRegistered); + + uint64_t bundleeligiblecountdown = 0; + + const uint32_t present_time = now(); + + if (!fio_address.empty()) { + const uint128_t nameHash = string_to_uint128_hash(fa.fioaddress.c_str()); + auto namesbyname = fionames.get_index<"byname"_n>(); + auto fioname_iter = namesbyname.find(nameHash); + fio_400_assert(fioname_iter != namesbyname.end(), "fio_address", fa.fioaddress, + "FIO Address not registered", ErrorFioNameAlreadyRegistered); + + fio_403_assert(fioname_iter->owner_account == actor.value, ErrorSignature); + + const uint32_t expiration = fioname_iter->expiration; + + fio_400_assert(present_time <= expiration, "fio_address", fio_address, "FIO Address expired. Renew first.", + ErrorDomainExpired); + bundleeligiblecountdown = fioname_iter->bundleeligiblecountdown; + } + + auto astakebyaccount = accountstaking.get_index<"byaccount"_n>(); + auto astakeiter = astakebyaccount.find(actor.value); + eosio_assert(astakeiter != astakebyaccount.end(),"incacctstake, actor has no accountstake record." ); + eosio_assert(astakeiter->account == actor,"incacctstake, actor accountstake lookup error." ); + fio_400_assert(astakeiter->total_staked_fio >= amount, "amount", to_string(amount), "Cannot unstake more than staked.", + ErrorInvalidValue); + + //get the usable balance for the account + auto stakeablebalance = eosio::token::computeusablebalance(actor,false); + + uint64_t paid_fee_amount = 0; + //begin, bundle eligible fee logic for staking + const uint128_t endpoint_hash = string_to_uint128_hash(UNSTAKE_FIO_TOKENS_ENDPOINT); + + auto fees_by_endpoint = fiofees.get_index<"byendpoint"_n>(); + auto fee_iter = fees_by_endpoint.find(endpoint_hash); + + //if the fee isnt found for the endpoint, then 400 error. + fio_400_assert(fee_iter != fees_by_endpoint.end(), "endpoint_name", UNSTAKE_FIO_TOKENS_ENDPOINT, + "FIO fee not found for endpoint", ErrorNoEndpoint); + + const int64_t fee_amount = fee_iter->suf_amount; + const uint64_t fee_type = fee_iter->type; + + fio_400_assert(fee_type == 1, "fee_type", to_string(fee_type), + "unexpected fee type for endpoint unstake_fio_tokens, expected 0", + ErrorNoEndpoint); + + + + if (bundleeligiblecountdown > 0) { + action{ + permission_level{_self, "active"_n}, + AddressContract, + "decrcounter"_n, + make_tuple(fio_address, 1) + }.send(); + } else { + paid_fee_amount = fee_iter->suf_amount; + fio_400_assert(max_fee >= (int64_t)paid_fee_amount, "max_fee", to_string(max_fee), "Fee exceeds supplied maximum.", + ErrorMaxFeeExceeded); + + fio_fees(actor, asset(fee_amount, FIOSYMBOL), UNSTAKE_FIO_TOKENS_ENDPOINT); + process_rewards(tpid, fee_amount,get_self(), actor); + + if (fee_amount > 0) { + INLINE_ACTION_SENDER(eosiosystem::system_contract, updatepower) + ("eosio"_n, {{_self, "active"_n}}, + {actor, true} + ); + } + } + + //fio_400_assert(stakeablebalance >= (paid_fee_amount + (uint64_t)amount), "max_fee", to_string(max_fee), "Insufficient balance.", + // ErrorMaxFeeExceeded); + //End, bundle eligible fee logic for staking + + //RAM bump + if (UNSTAKEFIOTOKENSRAM > 0) { + action( + permission_level{SYSTEMACCOUNT, "active"_n}, + "eosio"_n, + "incram"_n, + std::make_tuple(actor, UNSTAKEFIOTOKENSRAM) + ).send(); + } + //SRPs to Claim are computed: Staker's Account SRPs * (Unstaked amount / Total Tokens Staked in Staker's Account) + // this needs to be a floating point (double) operation + //round this to avoid issues with decimal representations + uint64_t srpstoclaim = (uint64_t)(((double)astakeiter->total_srp * (double)( (double)amount / (double)astakeiter->total_staked_fio))+0.5); + + if (debugout) { + print("srps to claim is ", to_string(srpstoclaim), "\n"); + } + //compute rate of exchange + uint64_t rateofexchange = 1; + if (gstaking.combined_token_pool >= COMBINEDTOKENPOOLMINIMUM) { + if(debugout) { + print(" global srp count ", gstaking.global_srp_count); + print(" combined_token_pool ", gstaking.combined_token_pool); + } + rateofexchange = gstaking.combined_token_pool / gstaking.global_srp_count; + if (debugout) { + print(" rate of exchange set to ", rateofexchange); + } + if(rateofexchange < 1) { + if(debugout) { + print(" RATE OF EXCHANGE LESS THAN 1 ", rateofexchange); + } + rateofexchange = 1; + } + } + + eosio_assert((srpstoclaim * rateofexchange) >= amount, "unstakefio, invalid calc in totalrewardamount, must be that (srpstoclaim * rateofexchange) > amount. "); + uint64_t totalrewardamount = ((srpstoclaim * rateofexchange) - amount); + if(debugout) { + print("total reward amount is ", totalrewardamount); + } + uint64_t tenpercent = totalrewardamount / 10; + //Staking Reward Amount is computed: ((SRPs to Claim * Rate of Exchnage) - Unstake amount) * 0.9 + uint64_t stakingrewardamount = tenpercent * 9; + if(debugout) { + print(" staking reward amount is ", totalrewardamount); + } + // TPID Reward Amount is computed: ((SRPs to Claim * Rate of Exchnage) - Unstake amount) * 0.1 + uint64_t tpidrewardamount = tenpercent; + + //decrement staking by account. + eosio_assert(astakeiter->total_srp >= srpstoclaim,"unstakefio, total srp for account must be greater than or equal srpstoclaim." ); + eosio_assert(astakeiter->total_staked_fio >= amount,"unstakefio, total staked fio for account must be greater than or equal fiostakedsufs." ); + + //update the existing record + astakebyaccount.modify(astakeiter, _self, [&](struct account_staking_info &a) { + a.total_staked_fio -= amount; + a.total_srp -= srpstoclaim; + }); + + //transfer the staking reward amount. + if (stakingrewardamount > 0) { + //Staking Reward Amount is transferred to Staker's Account. + // Memo: "Paying Staking Rewards" + action(permission_level{get_self(), "active"_n}, + TREASURYACCOUNT, "paystake"_n, + make_tuple(actor, stakingrewardamount) + ).send(); + } + + //decrement the global state + //avoid overflows due to negative results. + eosio_assert(gstaking.combined_token_pool >= (amount+stakingrewardamount),"unstakefio, combined token pool must be greater or equal to amount plus stakingrewardamount. " ); + eosio_assert(gstaking.staked_token_pool >= amount,"unstakefio, staked token pool must be greater or equal to staked amount. " ); + eosio_assert(gstaking.global_srp_count >= srpstoclaim,"unstakefio, global srp count must be greater or equal to srpstoclaim. " ); + + // decrement the combined_token_pool by fiostaked+fiorewarded. + gstaking.combined_token_pool -= (amount+stakingrewardamount); + // decrement the staked_token_pool by fiostaked. + gstaking.staked_token_pool -= amount; + // decrement the global_srp_count by srpcount. + gstaking.global_srp_count -= srpstoclaim; + + //pay the tpid. + if ((tpid.length() > 0)&&(tpidrewardamount>0)){ + //get the owner of the tpid and pay them. + const uint128_t tnameHash = string_to_uint128_hash(tpid.c_str()); + auto tnamesbyname = fionames.get_index<"byname"_n>(); + auto tfioname_iter = tnamesbyname.find(tnameHash); + fio_400_assert(tfioname_iter != tnamesbyname.end(), "fio_address", tpid, + "FIO Address not registered", ErrorFioNameAlreadyRegistered); + + const uint32_t expiration = tfioname_iter->expiration; + fio_400_assert(present_time <= expiration, "fio_address", fio_address, "FIO Address expired. Renew first.", + ErrorDomainExpired); + + //pay the tpid + action( + permission_level{get_self(), "active"_n}, + TPIDContract, + "updatetpid"_n, + std::make_tuple(tpid, actor, tpidrewardamount) + ).send(); + + //decrement the amount paid from combined token pool. + if(tpidrewardamount<= gstaking.combined_token_pool) { + gstaking.combined_token_pool -= tpidrewardamount; + } + } + + + //look and see if they have any general locks. + auto locks_by_owner = generallocks.get_index<"byowner"_n>(); + auto lockiter = locks_by_owner.find(actor.value); + if (lockiter != locks_by_owner.end()) { + //if they have general locks then adapt the locks. + //get the amount of the lock. + int64_t newlockamount = lockiter->lock_amount + (stakingrewardamount + amount); + //get the remaining unlocked of the lock. + int64_t newremaininglockamount = lockiter->remaining_lock_amount + (stakingrewardamount + amount); + //get the timestamp of the lock. + uint32_t insertperiod = (present_time - lockiter->timestamp) + 604800; + //the days since launch. + uint32_t insertday = (lockiter->timestamp + insertperiod) / SECONDSPERDAY; + //if your duration is less than this the period is in the past. + uint32_t expirednowduration = present_time - lockiter->timestamp; + uint32_t payouts = lockiter->payouts_performed; + + + vector newperiods; + + bool insertintoexisting = false; + uint32_t lastperiodduration = 0; + int insertindex = -1; + uint32_t daysforperiod = 0; + + for (int i = 0; i < lockiter->periods.size(); i++) { + daysforperiod = (lockiter->timestamp + lockiter->periods[i].duration)/SECONDSPERDAY; + uint64_t amountthisperiod = lockiter->periods[i].amount; + if (daysforperiod >= insertday) { + insertindex = newperiods.size(); + //always insert into the same day. + if (daysforperiod == insertday) { + insertintoexisting = true; + amountthisperiod += (stakingrewardamount + amount); + } + } + lastperiodduration = lockiter->periods[i].duration; + eosiosystem::lockperiodv2 tperiod; + tperiod.duration = lockiter->periods[i].duration; + tperiod.amount = amountthisperiod; + + //only those periods not in the past go into the list of periods. + //remove old periods. + if( tperiod.duration >= expirednowduration) { + newperiods.push_back(tperiod); + }else{ + eosio_assert(payouts > 0 ,"unstakefio, internal error decrementing payouts. " ); + newlockamount -= tperiod.amount; + eosio_assert(newlockamount >= newremaininglockamount,"unstakefio, inconsistent general lock state lock amount less than remaining lock amount. " ); + payouts --; + } + } + + + //add the period to the list. + if (!insertintoexisting) { + // print("EDEDEDEDEDEDEDEDED totalnewpercent ",totalnewpercent,"\n"); + eosiosystem::lockperiodv2 iperiod; + iperiod.duration = insertperiod; + iperiod.amount = amount; + newperiods.insert(newperiods.begin() + insertindex, iperiod); + } + + //update the locks table.. modgenlocked + action( + permission_level{get_self(), "active"_n}, + SYSTEMACCOUNT, + "modgenlocked"_n, + std::make_tuple(actor, newperiods, newlockamount, newremaininglockamount, payouts) + ).send(); + }else { + //else make new lock. + bool canvote = true; + int64_t lockamount = (int64_t)(stakingrewardamount + amount); + if(debugout) { + print(" creating general lock for amount ", lockamount, "\n"); + } + vector periods; + eosiosystem::lockperiodv2 period; + period.duration = 604800; + period.amount = lockamount; + periods.push_back(period); + INLINE_ACTION_SENDER(eosiosystem::system_contract, addgenlocked) + ("eosio"_n, {{_self, "active"_n}}, + {actor, periods, canvote, lockamount} + ); + } + + const string response_string = string("{\"status\": \"OK\",\"fee_collected\":") + + to_string(paid_fee_amount) + string("}"); + + fio_400_assert(transaction_size() <= MAX_TRX_SIZE, "transaction_size", std::to_string(transaction_size()), + "Transaction is too large", ErrorTransaction); + + send_response(response_string.c_str()); + } + +}; //class Staking + +EOSIO_DISPATCH(Staking, (stakefio)(unstakefio)(incgrewards)(recorddaily) ) +} diff --git a/contracts/fio.staking/fio.staking.hpp b/contracts/fio.staking/fio.staking.hpp new file mode 100644 index 00000000..4ad625fc --- /dev/null +++ b/contracts/fio.staking/fio.staking.hpp @@ -0,0 +1,59 @@ +/** Fio Staking implementation file + * Description: + * @author Ed Rotthoff + * @modifedby + * @file fio.staking.cpp + * @license FIO Foundation ( https://github.com/fioprotocol/fio/blob/master/LICENSE ) + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace fioio { + using namespace eosio; + + //staking info is a global state table used to track information relating to staking within the FIO protocol. + struct [[eosio::table("staking"), eosio::contract("fio.staking")]] global_staking_state { + global_staking_state(){} + uint64_t staked_token_pool = 0; //total FIO tokens staked for all accounts, units sufs. + uint64_t combined_token_pool = 1; //total fio tokens staked for all accounts plus fio rewards all accounts, units SUFs, + // incremented by the staked amount when user stakes, when tokens are earmarked as staking rewards, + // decremented by unstaked amount + reward amount when users unstake + uint64_t rewards_token_pool = 0; //total counter how much has come in from fees AND minting units SUFs + uint64_t global_srp_count = 1; // units SUS, total SRP for all FIO users, increment when users stake, decrement when users unstake. + uint64_t daily_staking_rewards = 0; //this is used to track the daily staking rewards collected from fees, + // its used only to determine if the protocol should mint FIO whe rewards are under the DAILYSTAKINGMINTTHRESHOLD + uint64_t staking_rewards_reserves_minted = 0; //the total amount of FIO used in minting rewards tokens, will not exceed STAKINGREWARDSRESERVEMAXIMUM + + EOSLIB_SERIALIZE( global_staking_state,(staked_token_pool) + (combined_token_pool)(rewards_token_pool)(global_srp_count) + (daily_staking_rewards)(staking_rewards_reserves_minted) + ) + }; + + //stake account table holds staking info used to compute staking rewards by FIO account + struct [[eosio::table, eosio::contract("fio.staking")]] account_staking_info { + uint64_t id = 0; //unique id for ease of maintenance. primary key + name account; //the account name associated with this staking info, secondary key, + uint64_t total_srp = 0; //the staking rewards points awarded to this account, units SUS, incremented on stake, decremented on unstake. + uint64_t total_staked_fio = 0; //total fio staked by this account, units SUFs, incremented on stake, decremented on unstake. + + uint64_t primary_key() const{return id;} + uint64_t by_account() const { return account.value; } + + EOSLIB_SERIALIZE( account_staking_info,(id) + (account)(total_srp)(total_staked_fio) + ) + }; + typedef eosio::multi_index<"accountstake"_n, account_staking_info, + indexed_by<"byaccount"_n, const_mem_fun> + > account_staking_table; + + + typedef eosio::singleton<"staking"_n, global_staking_state> global_staking_singleton; +} diff --git a/contracts/fio.system/include/fio.system/fio.system.hpp b/contracts/fio.system/include/fio.system/fio.system.hpp index e74d2662..fd2b599b 100755 --- a/contracts/fio.system/include/fio.system/fio.system.hpp +++ b/contracts/fio.system/include/fio.system/fio.system.hpp @@ -146,6 +146,8 @@ struct glockresult { uint64_t amount; //amount votable EOSLIB_SERIALIZE( glockresult, (lockfound)(amount)) }; + +/* TEMPORARY, COMMENT OUT ONLY FOR DEV PURPOSES struct lockperiods { int64_t duration = 0; //duration in seconds. each duration is seconds after grant creation. double percent; //this is the percent to be unlocked @@ -176,8 +178,48 @@ typedef eosio::multi_index<"locktokens"_n, locked_tokens_info, > general_locks_table; + */ +//end general locks + + +//begin general locks V2, these locks are used to hold tokens granted by any fio user +//to any other fio user. + +struct lockperiodv2 { + int64_t duration = 0; //duration in seconds. each duration is seconds after grant creation. + int64_t amount; //this is the amount in SUFs to be unlocked + EOSLIB_SERIALIZE( lockperiodv2, (duration)(amount)) +}; + +struct [[eosio::table, eosio::contract("fio.system")]] locked_tokens_info_v2 { + int64_t id; //this is the identifier of the lock, primary key + name owner_account; //this is the account that owns the lock, secondary key + int64_t lock_amount = 0; //this is the amount of the lock in FIO SUF + int32_t payouts_performed = 0; //this is the number of payouts performed thus far. + int32_t can_vote = 0; //this is the flag indicating if the lock is votable/proxy-able + std::vector periods;// this is the locking periods for the lock + int64_t remaining_lock_amount = 0; //this is the amount remaining in the lock in FIO SUF, get decremented as unlocking occurs. + uint32_t timestamp = 0; //this is the time of creation of the lock, locking periods are relative to this time. + + uint64_t primary_key() const { return id; } + uint64_t by_owner() const{return owner_account.value;} + + EOSLIB_SERIALIZE( locked_tokens_info_v2, (id)(owner_account) + (lock_amount)(payouts_performed)(can_vote)(periods)(remaining_lock_amount)(timestamp) + ) + +}; + +typedef eosio::multi_index<"locktokensv2"_n, locked_tokens_info_v2, + indexed_by<"byowner"_n, const_mem_fun < locked_tokens_info_v2, uint64_t, &locked_tokens_info_v2::by_owner> > + +> +general_locks_table_v2; //end general locks + +// + //Top producers that are calculated every block in update_elected_producers struct [[eosio::table, eosio::contract("fio.system")]] top_prod_info { name producer; @@ -283,12 +325,11 @@ indexed_by<"byowner"_n, const_mem_fun producers_table2; - typedef eosio::singleton<"global"_n, eosio_global_state> global_state_singleton; typedef eosio::singleton<"global2"_n, eosio_global_state2> global_state2_singleton; typedef eosio::singleton<"global3"_n, eosio_global_state3> global_state3_singleton; + static constexpr uint32_t seconds_per_day = 24 * 3600; @@ -300,7 +341,7 @@ class [[eosio::contract("fio.system")]] system_contract : public native { producers_table _producers; top_producers_table _topprods; locked_tokens_table _lockedtokens; - general_locks_table _generallockedtokens; + general_locks_table_v2 _generallockedtokens; //MAS-522 eliminate producers2 producers_table2 _producers2; global_state_singleton _global; global_state2_singleton _global2; @@ -340,7 +381,11 @@ class [[eosio::contract("fio.system")]] system_contract : public native { const int16_t &locktype); [[eosio::action]] - void addgenlocked(const name &owner, const vector &periods, const bool &canvote,const int64_t &amount); + void addgenlocked(const name &owner, const vector &periods, const bool &canvote,const int64_t &amount); + + [[eosio::action]] + void modgenlocked(const name &owner, const vector &periods, const int64_t &amount,const int64_t &rem_lock_amount, + const uint32_t &payouts); [[eosio::action]] void onblock(ignore header); diff --git a/contracts/fio.system/include/fio.system/native.hpp b/contracts/fio.system/include/fio.system/native.hpp index 16b51e84..c43a4b90 100755 --- a/contracts/fio.system/include/fio.system/native.hpp +++ b/contracts/fio.system/include/fio.system/native.hpp @@ -147,7 +147,8 @@ namespace eosiosystem { account == fioio::TokenContract || account == fioio::TREASURYACCOUNT || account == fioio::FIOSYSTEMACCOUNT || - account == fioio::FIOACCOUNT) + account == fioio::STAKINGACCOUNT || + account == fioio::FIOACCOUNT ) ) { //get the sizes of the tx. diff --git a/contracts/fio.system/src/delegate_bandwidth.cpp b/contracts/fio.system/src/delegate_bandwidth.cpp index ebf08b2c..20c12e41 100755 --- a/contracts/fio.system/src/delegate_bandwidth.cpp +++ b/contracts/fio.system/src/delegate_bandwidth.cpp @@ -60,9 +60,10 @@ namespace eosiosystem { has_auth(REQOBTACCOUNT) || has_auth(SYSTEMACCOUNT) || has_auth(FeeContract) || + has_auth(StakingContract) || has_auth(REQOBTACCOUNT) ), - "missing required authority of fio.address, fio.treasury, eosio, fio.fee, fio.token, or fio.reqobt"); + "missing required authority of fio.address, fio.treasury, eosio, fio.fee, fio.token, fio.staking, or fio.reqobt"); auto votersbyowner = _voters.get_index<"byowner"_n>(); auto voter_itr = votersbyowner.find(voter.value); diff --git a/contracts/fio.system/src/fio.system.cpp b/contracts/fio.system/src/fio.system.cpp index fa1087ec..451368cb 100755 --- a/contracts/fio.system/src/fio.system.cpp +++ b/contracts/fio.system/src/fio.system.cpp @@ -174,6 +174,7 @@ namespace eosiosystem { acnt == TPIDContract || acnt == TokenContract || acnt == TREASURYACCOUNT || + acnt == STAKINGACCOUNT || acnt == FIOSYSTEMACCOUNT || acnt == FIOACCOUNT),"set abi not permitted." ); @@ -215,17 +216,21 @@ namespace eosiosystem { a.remaining_locked_amount = amount; a.timestamp = now(); }); + //return status added for staking, to permit unit testing using typescript sdk. + const string response_string = string("{\"status\": \"OK\"}"); + send_response(response_string.c_str()); } - - void eosiosystem::system_contract::addgenlocked(const name &owner, const vector &periods, const bool &canvote, + void eosiosystem::system_contract::addgenlocked(const name &owner, const vector &periods, const bool &canvote, const int64_t &amount) { - require_auth(TokenContract); + + eosio_assert((has_auth(TokenContract) || has_auth(StakingContract)), + "missing required authority of fio.token or fio.staking"); check(is_account(owner),"account must pre exist"); check(amount > 0,"cannot add locked token amount less or equal 0."); - _generallockedtokens.emplace(owner, [&](struct locked_tokens_info &a) { + _generallockedtokens.emplace(owner, [&](struct locked_tokens_info_v2 &a) { a.id = _generallockedtokens.available_primary_key(); a.owner_account = owner; a.lock_amount = amount; @@ -237,6 +242,48 @@ namespace eosiosystem { }); } + void eosiosystem::system_contract::modgenlocked(const name &owner, const vector &periods, + const int64_t &amount, const int64_t &rem_lock_amount, + const uint32_t &payouts) { + + eosio_assert( has_auth(StakingContract), + "missing required authority of fio.staking"); + + check(is_account(owner),"account must pre exist"); + check(amount > 0,"cannot add locked token amount less or equal 0."); + check(rem_lock_amount > 0,"cannot add remaining locked token amount less or equal 0."); + check(payouts >= 0,"cannot add payouts less than 0."); + + uint64_t tota = 0; + + for(int i=0;i 0, "unlock_periods", "Invalid unlock periods", + "Invalid amount value in unlock periods", ErrorInvalidUnlockPeriods); + fio_400_assert(periods[i].duration > 0, "unlock_periods", "Invalid unlock periods", + "Invalid duration value in unlock periods", ErrorInvalidUnlockPeriods); + tota += periods[i].amount; + if (i>0){ + fio_400_assert(periods[i].duration > periods[i-1].duration, "unlock_periods", "Invalid unlock periods", + "Invalid duration value in unlock periods, must be sorted", ErrorInvalidUnlockPeriods); + } + } + + fio_400_assert(tota == amount, "unlock_periods", "Invalid unlock periods", + "Invalid total amount for unlock periods", ErrorInvalidUnlockPeriods); + + auto locks_by_owner = _generallockedtokens.get_index<"byowner"_n>(); + auto lockiter = locks_by_owner.find(owner.value); + check(lockiter != locks_by_owner.end(),"error looking up lock owner."); + //call the system contract and update the record. + locks_by_owner.modify(lockiter, get_self(), [&](auto &av) { + av.remaining_lock_amount = rem_lock_amount; + av.lock_amount = amount; + av.payouts_performed = payouts; + av.periods = periods; + + }); + } + } /// fio.system @@ -244,7 +291,7 @@ EOSIO_DISPATCH( eosiosystem::system_contract, // native.hpp (newaccount definition is actually in fio.system.cpp) (newaccount)(addaction)(remaction)(updateauth)(deleteauth)(linkauth)(unlinkauth)(canceldelay)(onerror)(setabi) // fio.system.cpp -(init)(addlocked)(addgenlocked)(setparams)(setpriv) +(init)(addlocked)(addgenlocked)(modgenlocked)(setparams)(setpriv) (rmvproducer)(updtrevision) // delegate_bandwidth.cpp (updatepower) diff --git a/contracts/fio.system/src/voting.cpp b/contracts/fio.system/src/voting.cpp index 4516b830..a99bb92d 100755 --- a/contracts/fio.system/src/voting.cpp +++ b/contracts/fio.system/src/voting.cpp @@ -790,10 +790,11 @@ namespace eosiosystem { amount = damount; } - }else{ + }//STAKING comment out to permit genesis and general locks to co-exist. + // else{ //amount is all the available tokens in the account. - return amount; - } + // return amount; + // } } //TEST TEST TEST LOCKED TOKENS //TEST TEST TEST LOCKED TOKENS uint32_t issueplus210 = lockiter->timestamp+(25*60); @@ -812,9 +813,26 @@ namespace eosiosystem { } } } + + //STAKING + //now add in the logic for the general locks. + auto locks_by_owner = _generallockedtokens.get_index<"byowner"_n>(); + auto glockiter = locks_by_owner.find(tokenowner.value); + if(glockiter != locks_by_owner.end()){ + //if can vote -- + if (glockiter->can_vote == 0){ + if (amount > glockiter->remaining_lock_amount) { + amount = amount - glockiter->remaining_lock_amount; + }else{ + amount = 0; + } + } + } + return amount; } + //STAKING -- this will be obsoleted, logic will be included in get_votable_balance. glockresult system_contract::get_general_votable_balance(const name &tokenowner){ glockresult res; @@ -868,12 +886,13 @@ namespace eosiosystem { //change to get_unlocked_balance() Ed 11/25/2019 uint64_t amount = 0; - glockresult res = get_general_votable_balance(voter->owner); - if(res.lockfound){ - amount = res.amount; - }else { + + // STAKING genesis locks and general can co exist. glockresult res = get_general_votable_balance(voter->owner); + // if(res.lockfound){ + // amount = res.amount; + // }else { amount = get_votable_balance(voter->owner); - } + // } //on the first vote we update the total voted fio. @@ -1274,12 +1293,13 @@ namespace eosiosystem { check(!voter.proxy || !voter.is_proxy, "account registered as a proxy is not allowed to use a proxy"); uint64_t amount = 0; - glockresult res = get_general_votable_balance(voter.owner); - if(res.lockfound){ - amount = res.amount; - }else { + // STAKING genesis and genersal locks can co exist + // glockresult res = get_general_votable_balance(voter.owner); + // if(res.lockfound){ + // amount = res.amount; + // }else { amount = get_votable_balance(voter.owner); - } + // } //instead of staked we use the voters current FIO balance MAS-522 eliminate stake from voting. auto new_weight = (double)amount; if (voter.is_proxy) { diff --git a/contracts/fio.token/include/fio.token/fio.token.hpp b/contracts/fio.token/include/fio.token/fio.token.hpp index d30f065b..aab77e87 100755 --- a/contracts/fio.token/include/fio.token/fio.token.hpp +++ b/contracts/fio.token/include/fio.token/fio.token.hpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace eosiosystem { class system_contract; @@ -31,7 +32,8 @@ namespace eosio { fioio::tpids_table tpids; fioio::fionames_table fionames; eosiosystem::locked_tokens_table lockedTokensTable; - eosiosystem::general_locks_table generalLockTokensTable; + eosiosystem::general_locks_table_v2 generalLockTokensTable; + fioio::account_staking_table accountstaking; public: token(name s, name code, datastream ds) : contract(s, code, ds), @@ -42,7 +44,8 @@ namespace eosio { fiofees(fioio::FeeContract, fioio::FeeContract.value), tpids(TPIDContract, TPIDContract.value), lockedTokensTable(SYSTEMACCOUNT, SYSTEMACCOUNT.value), - generalLockTokensTable(SYSTEMACCOUNT, SYSTEMACCOUNT.value){ + generalLockTokensTable(SYSTEMACCOUNT, SYSTEMACCOUNT.value), + accountstaking(STAKINGACCOUNT,STAKINGACCOUNT.value){ fioio::configs_singleton configsSingleton(fioio::FeeContract, fioio::FeeContract.value); appConfig = configsSingleton.get_or_default(fioio::config()); } @@ -75,7 +78,7 @@ namespace eosio { [[eosio::action]] void trnsloctoks(const string &payee_public_key, const int32_t &can_vote, - const vector periods, + const vector periods, const int64_t &amount, const int64_t &max_fee, const name &actor, @@ -151,6 +154,29 @@ namespace eosio { bool existing; }; + //This action will compute the number of unlocked tokens contained within an account. + // This considers + static uint64_t computeusablebalance(const name &owner,bool updatelocks){ + uint64_t genesislockedamount = computeremaininglockedtokens(owner,updatelocks); + uint64_t generallockedamount = computegenerallockedtokens(owner,updatelocks); + uint64_t stakedfio = 0; + + fioio::account_staking_table accountstaking(STAKINGACCOUNT, STAKINGACCOUNT.value); + auto astakebyaccount = accountstaking.get_index<"byaccount"_n>(); + auto astakeiter = astakebyaccount.find(owner.value); + if (astakeiter != astakebyaccount.end()) { + check(astakeiter->account == owner,"incacctstake owner lookup error." ); + stakedfio = astakeiter->total_staked_fio; + } + //apply a little QC. + const auto my_balance = eosio::token::get_balance("fio.token"_n, owner, FIOSYMBOL.code()); + check(my_balance.amount >= (generallockedamount + genesislockedamount + stakedfio), + "computeusablebalance, amount of locked fio plus staked is greater than balance!! for " + owner.to_string() ); + uint64_t amount = my_balance.amount - (generallockedamount + genesislockedamount + stakedfio); + return amount; + + } + //this will compute the present unlocked tokens for this user based on the @@ -167,14 +193,11 @@ namespace eosio { } if (lockiter->unlocked_period_count < 6) { //to shorten the vesting schedule adapt these variables. + // TESTING ONLY!!! comment out genesis locking periods..DO NOT DELIVER THIS uint32_t daysSinceGrant = (int) ((present_time - lockiter->timestamp) / SECONDSPERDAY); uint32_t firstPayPeriod = 90; uint32_t payoutTimePeriod = 180; - - //TEST LOCKED TOKENS uint32_t daysSinceGrant = (int)((present_time - lockiter->timestamp) / 60); - //TEST LOCKED TOKENS uint32_t firstPayPeriod = 15; - //TEST LOCKED TOKENS uint32_t payoutTimePeriod = 15; - + bool ninetyDaysSinceGrant = daysSinceGrant >= firstPayPeriod; uint64_t payoutsDue = 0; @@ -287,6 +310,8 @@ namespace eosio { numberVestingPayouts--; } + + //process the rest of the payout periods, other than the first period. if (payoutsDue > numberVestingPayouts) { remainingPayouts = payoutsDue - numberVestingPayouts; @@ -297,16 +322,22 @@ namespace eosio { //this logic assumes to have 3 decimal places in the specified percentage percentperblock = 18800; } else if (lockiter->grant_type == 4) { - //this is assumed to have 3 decimal places in the specified percentage return lockiter->remaining_locked_amount; } else { //unknown lock type, dont unlock return lockiter->remaining_locked_amount; } - //we eliminate the last 5 digits of the SUFs to avoid overflow in the calculations - //that follow. - uint64_t totalgrantsmaller = totalgrantamount/10000; - amountpay = ((remainingPayouts * (totalgrantsmaller * percentperblock)) / 100000) * 10000; + + if(payoutsDue >= 5){ + //always pay all the rest at the end of the locks life. + amountpay = lockiter->remaining_locked_amount; + } + else { + //we eliminate the last 5 digits of the SUFs to avoid overflow in the calculations + //that follow. + uint64_t totalgrantsmaller = totalgrantamount / 10000; + amountpay = ((remainingPayouts * (totalgrantsmaller * percentperblock)) / 100000) * 10000; + } if (newlockedamount > amountpay) { newlockedamount -= amountpay; @@ -316,6 +347,7 @@ namespace eosio { didsomething = true; } + if (didsomething && doupdate) { //get fio balance for this account, uint32_t present_time = now(); @@ -335,9 +367,7 @@ namespace eosio { av.unlocked_period_count += remainingPayouts + addone; }); } - return newlockedamount; - } else { return lockiter->remaining_locked_amount; } @@ -352,7 +382,7 @@ namespace eosio { static uint64_t computegenerallockedtokens(const name &actor, bool doupdate) { uint32_t present_time = now(); - eosiosystem::general_locks_table generalLockTokensTable(SYSTEMACCOUNT, SYSTEMACCOUNT.value); + eosiosystem::general_locks_table_v2 generalLockTokensTable(SYSTEMACCOUNT, SYSTEMACCOUNT.value); auto locks_by_owner = generalLockTokensTable.get_index<"byowner"_n>(); auto lockiter = locks_by_owner.find(actor.value); if (lockiter != locks_by_owner.end()) { @@ -361,6 +391,7 @@ namespace eosio { uint32_t secondsSinceGrant = (present_time - lockiter->timestamp); uint32_t payoutsDue = 0; + for (int i=0;iperiods.size(); i++){ if (lockiter->periods[i].duration <= secondsSinceGrant){ payoutsDue++; @@ -372,16 +403,17 @@ namespace eosio { bool didsomething = false; if (payoutsDue > lockiter->payouts_performed) { + if((lockiter->payouts_performed + payoutsDue) >= lockiter->periods.size()) + { + //payout the remaining lock amount. + amountpay = newlockedamount; + } + else { - uint64_t percentperblock = 0; - for (int i=lockiter->payouts_performed; iperiods[i].percent * 1000); - uint64_t lockamountsmaller = lockiter->lock_amount / 10000; - uint64_t amountadded = ((lockamountsmaller * percentperblock)/100000) * 10000; - amountpay += amountadded; - } + for (int i = lockiter->payouts_performed; i < payoutsDue; i++) { + amountpay += lockiter->periods[i].amount; + } + } if (newlockedamount > amountpay) { newlockedamount -= amountpay; diff --git a/contracts/fio.token/src/fio.token.cpp b/contracts/fio.token/src/fio.token.cpp index 4238e634..85c8783a 100755 --- a/contracts/fio.token/src/fio.token.cpp +++ b/contracts/fio.token/src/fio.token.cpp @@ -130,6 +130,7 @@ namespace eosio { ) { //recompute the remaining locked amount based on vesting. uint64_t lockedTokenAmount = computeremaininglockedtokens(tokenowner, false);//-feeamount; + //subtract the lock amount from the balance if (lockedTokenAmount < amount) { amount -= lockedTokenAmount; @@ -188,7 +189,8 @@ namespace eosio { const name &actor, const string &tpid, const int64_t &feeamount, - const bool &errorifaccountexists) { + const bool &errorifaccountexists) + { require_auth(actor); asset qty; @@ -240,6 +242,7 @@ namespace eosio { "Locked tokens can only be transferred to new account", ErrorPubKeyValid); } + auto other = eosionames.find(new_account_name.value); if (other == eosionames.end()) { //the name is not in the table. @@ -304,6 +307,7 @@ namespace eosio { "Insufficient balance", ErrorLowFunds); + //must do these three in this order!! can transfer can transfer computeusablebalance fio_400_assert(can_transfer(actor, feeamount, qty.amount, false), "amount", to_string(qty.amount), "Insufficient balance tokens locked", ErrorInsufficientUnlockedFunds); @@ -312,6 +316,13 @@ namespace eosio { "Funds locked", ErrorInsufficientUnlockedFunds); + + uint64_t uamount = computeusablebalance(actor,false); + fio_400_assert(uamount >= qty.amount, "actor", to_string(actor.value), + "Insufficient Funds.", + ErrorInsufficientUnlockedFunds); + + sub_balance(actor, qty); add_balance(new_account_name, qty, actor); @@ -380,6 +391,12 @@ namespace eosio { "Funds locked", ErrorInsufficientUnlockedFunds); + + int64_t amount = computeusablebalance(from,false); + fio_400_assert(amount >= quantity.amount, "actor", to_string(from.value), + "Insufficient Funds.", + ErrorInsufficientUnlockedFunds); + auto payer = has_auth(to) ? to : from; sub_balance(from, quantity); @@ -434,7 +451,7 @@ namespace eosio { void token::trnsloctoks(const string &payee_public_key, const int32_t &can_vote, - const vector periods, + const vector periods, const int64_t &amount, const int64_t &max_fee, const name &actor, @@ -442,24 +459,25 @@ namespace eosio { fio_400_assert(((periods.size()) >= 1 && (periods.size() <= 365)), "unlock_periods", "Invalid unlock periods", "Invalid number of unlock periods", ErrorTransactionTooLarge); - double totp = 0.0; + uint64_t tota = 0; double tv = 0.0; int64_t longestperiod = 0; for(int i=0;i 0.0, "unlock_periods", "Invalid unlock periods", - "Invalid percentage value in unlock periods", ErrorInvalidUnlockPeriods); - tv = periods[i].percent - (double(int(periods[i].percent * 1000.0)))/1000.0; - fio_400_assert(tv == 0.0, "unlock_periods", "Invalid unlock periods", - "Invalid precision for percentage in unlock periods", ErrorInvalidUnlockPeriods); + fio_400_assert(periods[i].amount > 0, "unlock_periods", "Invalid unlock periods", + "Invalid amount value in unlock periods", ErrorInvalidUnlockPeriods); fio_400_assert(periods[i].duration > 0, "unlock_periods", "Invalid unlock periods", "Invalid duration value in unlock periods", ErrorInvalidUnlockPeriods); - totp += periods[i].percent; + tota += periods[i].amount; + if (i>0){ + fio_400_assert(periods[i].duration > periods[i-1].duration, "unlock_periods", "Invalid unlock periods", + "Invalid duration value in unlock periods, must be sorted", ErrorInvalidUnlockPeriods); + } if (periods[i].duration > longestperiod){ longestperiod = periods[i].duration; } } - fio_400_assert(totp == 100.0, "unlock_periods", "Invalid unlock periods", - "Invalid total percentage for unlock periods", ErrorInvalidUnlockPeriods); + fio_400_assert(tota == amount, "unlock_periods", "Invalid unlock periods", + "Invalid total amount for unlock periods", ErrorInvalidUnlockPeriods); fio_400_assert(((can_vote == 0)||(can_vote == 1)), "can_vote", to_string(can_vote), "Invalid can_vote value", ErrorInvalidValue); @@ -492,6 +510,7 @@ namespace eosio { //check for pre existing account is done here. name owner = transfer_public_key(payee_public_key,amount,max_fee,actor,tpid,reg_amount,true); + //if no locked tokens in the account do this. bool canvote = (can_vote == 1); INLINE_ACTION_SENDER(eosiosystem::system_contract, addgenlocked) ("eosio"_n, {{_self, "active"_n}}, diff --git a/contracts/fio.tpid/fio.tpid.cpp b/contracts/fio.tpid/fio.tpid.cpp index 0df1b5ee..78ac0d69 100644 --- a/contracts/fio.tpid/fio.tpid.cpp +++ b/contracts/fio.tpid/fio.tpid.cpp @@ -95,8 +95,8 @@ class [[eosio::contract("TPIDController")]] TPIDController: public eosio::contr void updatetpid(const string &tpid, const name owner, const uint64_t &amount) { eosio_assert(has_auth(AddressContract) || has_auth(TokenContract) || has_auth(TREASURYACCOUNT) || - has_auth("fio.reqobt"_n) || has_auth("eosio"_n), - "missing required authority of fio.address, fio.treasury, fio.token, eosio or fio.reqobt"); + has_auth(STAKINGACCOUNT) || has_auth("fio.reqobt"_n) || has_auth("eosio"_n), + "missing required authority of fio.address, fio.treasury, fio.token, eosio or fio.reqobt or fio.staking"); if (debugout) { print("update tpid calling updatetpid with tpid ", tpid, " owner ", owner, "\n"); } diff --git a/contracts/fio.treasury/fio.treasury.abi b/contracts/fio.treasury/fio.treasury.abi index a8825c17..8084c0b3 100644 --- a/contracts/fio.treasury/fio.treasury.abi +++ b/contracts/fio.treasury/fio.treasury.abi @@ -31,6 +31,20 @@ } ] }, + { + "name": "paystake", + "base": "", + "fields": [ + { + "name": "actor", + "type": "name" + }, + { + "name": "amount", + "type": "uint64" + } + ] + }, { "name": "treasurystate", "base": "", @@ -151,6 +165,11 @@ "type": "bpclaim", "ricardian_contract": "" }, + { + "name": "paystake", + "type": "paystake", + "ricardian_contract": "" + }, { "name": "startclock", "type": "startclock", diff --git a/contracts/fio.treasury/fio.treasury.cpp b/contracts/fio.treasury/fio.treasury.cpp index c34e58ae..2cf5e34e 100644 --- a/contracts/fio.treasury/fio.treasury.cpp +++ b/contracts/fio.treasury/fio.treasury.cpp @@ -15,6 +15,7 @@ #define PAYABLETPIDS 100 #include "fio.treasury.hpp" +#include namespace fioio { @@ -31,6 +32,8 @@ class [[eosio::contract("FIOTreasury")]] FIOTreasury: public eosio::contract { voteshares_table voteshares; eosiosystem::eosio_global_state gstate; eosiosystem::global_state_singleton global; + fioio::global_staking_singleton staking; + fioio::global_staking_state gstaking; eosiosystem::producers_table producers; bool rewardspaid; uint64_t lasttpidpayout; @@ -47,7 +50,8 @@ class [[eosio::contract("FIOTreasury")]] FIOTreasury: public eosio::contract { producers(SYSTEMACCOUNT, SYSTEMACCOUNT.value), global(SYSTEMACCOUNT, SYSTEMACCOUNT.value), fdtnrewards(get_self(), get_self().value), - bucketrewards(get_self(), get_self().value) { + bucketrewards(get_self(), get_self().value), + staking(STAKINGACCOUNT, STAKINGACCOUNT.value){ state = clockstate.get_or_default(); } @@ -108,6 +112,22 @@ class [[eosio::contract("FIOTreasury")]] FIOTreasury: public eosio::contract { send_response(response_string.c_str()); } //tpid_claim + + // @abi action + [[eosio::action]] + void paystake( const name &actor, const uint64_t &amount) { + require_auth(STAKINGACCOUNT); + + action(permission_level{get_self(), "active"_n}, + TokenContract, "transfer"_n, + make_tuple(TREASURYACCOUNT, name(actor), + asset(amount, FIOSYMBOL), + string("Paying Staking Rewards")) + ).send(); + + } + + // @abi action [[eosio::action]] void bpclaim(const string &fio_address, const name &actor) { @@ -166,6 +186,44 @@ class [[eosio::contract("FIOTreasury")]] FIOTreasury: public eosio::contract { //*********** CREATE PAYSCHEDULE ************** // If there is no pay schedule then create a new one if (std::distance(voteshares.begin(), voteshares.end()) == 0) { //if new payschedule + + //process the staking rewards, once per day. + /* + If Daily Staking Rewards is less than 25,000 FIO Tokens and Staking Rewards Reserves Minted is less than Staking Rewards Reserves Maximum: + The difference between 25,000 FIO Tokens and Daily Staking Rewards + (or difference between Staking Rewards Reserves Minted and Staking Rewards Reserves Maximum, whichever is smaller) + is minted, transferred to treasury account and added to Staking Rewards Reserves Minted. + Daily Staking Rewards is incremented by the tokens minted. + Daily Staking Rewards amount is: + Added to Combined Token Pool, which modifies ROE + Set to 0 + */ + gstaking = staking.get(); + uint64_t amounttomint = 0; + if ((gstaking.daily_staking_rewards < DAILYSTAKINGMINTTHRESHOLD)&& + (gstaking.staking_rewards_reserves_minted < STAKINGREWARDSRESERVEMAXIMUM)){ + uint64_t twentyfivekminusdaily = DAILYSTAKINGMINTTHRESHOLD - gstaking.daily_staking_rewards; + uint64_t reservemaxminusminted = STAKINGREWARDSRESERVEMAXIMUM - gstaking.staking_rewards_reserves_minted; + amounttomint = reservemaxminusminted; + if (amounttomint > twentyfivekminusdaily){ + amounttomint = twentyfivekminusdaily; + } + //mint and update accounting. + action(permission_level{get_self(), "active"_n}, + TokenContract, "mintfio"_n, + make_tuple(TREASURYACCOUNT,amounttomint) + ).send(); + + } + + //update daily accounting, and zero daily staking + action(permission_level{get_self(), "active"_n}, + STAKINGACCOUNT, "recorddaily"_n, + make_tuple(amounttomint) + ).send(); + + //end process staking rewards. + //Create the payment schedule int64_t bpcounter = 0; uint64_t activecount = 0; @@ -221,7 +279,6 @@ class [[eosio::contract("FIOTreasury")]] FIOTreasury: public eosio::contract { print("Block producers reserve minting exhausted"); } //!!!rewards is now 0 in the bprewards table and can no longer be referred to. If needed use projectedpay - uint64_t fdtntomint = FDTNMAXTOMINT; const uint64_t fdtnremainingreserve = FDTNMAXRESERVE - state.fdtnreservetokensminted; @@ -304,8 +361,7 @@ class [[eosio::contract("FIOTreasury")]] FIOTreasury: public eosio::contract { make_tuple(producer) ).send(); } - - + // PAY FOUNDATION // auto fdtnstate = fdtnrewards.get(); if(fdtnstate.rewards > 0) { @@ -345,8 +401,8 @@ class [[eosio::contract("FIOTreasury")]] FIOTreasury: public eosio::contract { void bprewdupdate(const uint64_t &amount) { eosio_assert((has_auth(AddressContract) || has_auth(TokenContract) || has_auth(TREASURYACCOUNT) || - has_auth(REQOBTACCOUNT) || has_auth(SYSTEMACCOUNT) || has_auth(FeeContract)), - "missing required authority of fio.address, fio.treasury, fio.fee, fio.token, eosio or fio.reqobt"); + has_auth(STAKINGACCOUNT) || has_auth(REQOBTACCOUNT) || has_auth(SYSTEMACCOUNT) || has_auth(FeeContract)), + "missing required authority of fio.address, fio.treasury, fio.fee, fio.token, fio.stakng, eosio or fio.reqobt"); bprewards.set(bprewards.exists() ? bpreward{bprewards.get().rewards + amount} : bpreward{amount}, get_self()); } @@ -364,17 +420,15 @@ class [[eosio::contract("FIOTreasury")]] FIOTreasury: public eosio::contract { // @abi action [[eosio::action]] void fdtnrwdupdat(const uint64_t &amount) { - eosio_assert((has_auth(AddressContract) || has_auth(TokenContract) || has_auth(TREASURYACCOUNT) || - has_auth(REQOBTACCOUNT) || has_auth(SYSTEMACCOUNT) || has_auth(FeeContract)), - "missing required authority of fio.address, fio.token, fio.fee, fio.treasury or fio.reqobt"); + has_auth(STAKINGACCOUNT) || has_auth(REQOBTACCOUNT) || has_auth(SYSTEMACCOUNT) || has_auth(FeeContract)), + "missing required authority of fio.address, fio.token, fio.fee, fio.treasury, fio.staking, or fio.reqobt"); fdtnrewards.set(fdtnrewards.exists() ? fdtnreward{fdtnrewards.get().rewards + amount} : fdtnreward{amount}, get_self()); - } }; //class FIOTreasury EOSIO_DISPATCH(FIOTreasury, (tpidclaim)(startclock)(bprewdupdate)(fdtnrwdupdat)(bppoolupdate) - (bpclaim)) + (bpclaim)(paystake)) }