关键字:REX,资源交易,资源租赁,系统费用,bancor,成熟期,EOS,eosio.system,voting
EOSIO 智能合约在v1.6.0版本增加了一个system合约使用的例子,可提供EOS资源交易。以供社区评估、调整和构建。REX只是智能合约层面提供的功能,而并没有相应的用户界面,部署选择等内容。
按照设计思路,REX是链上的主币持有者参与的一个CPU和网络资源租赁市场,参与者可以通过买卖REX池中的REX币来借出或收回他们的现有资源。下面有几个限制条件:
系统费用目前包含了内存资源的买卖,(网络cpu的抵押费用)以及账户的拍卖费用。在当前新版本的eosio.system合约中,默认设置将系统费用转由REX池收集,该设置生效以后,所有新产生的系统费用将由REX负责收集,但这并不影响之前作为管理内存买卖的eosio.ramfee账户以及收集账户拍卖费用的eosio.names账户的固有资金。这中设计的目的是为了保持系统的向前兼容。
同时,超级节点管理者仍然保留了是否切换REX的权利,只需要在system合约源代码中修改宏CHANNEL_RAM_AND_NAMEBID_FEES_TO_REX的定义。
// 默认是1,由REX收集系统费用,如果想保持原样不使用REX,则修改下面的值为0.
#define CHANNEL_RAM_AND_NAMEBID_FEES_TO_REX 1
REX功能对system系统合约的初始化有了新的要求。
eosio.rex账户必须加入原有的系统账户,在合约部署前要被创建成功,同时该账户不是一个特权账户。
目前构建用于测试的本地EOS链的常用方式是使用python脚本eos/tutorials/bios-boot-tutorial/bios-boot-tutorial.py。
关于链启动时序的内容请转到此处复习一下。
那么按照这个要求,把eosio.rex加入到bbt脚本中的系统账户集合中。
systemAccounts = [
'eosio.bpay',
'eosio.msig',
'eosio.names',
'eosio.ram',
'eosio.ramfee',
'eosio.saving',
'eosio.stake',
'eosio.token',
'eosio.vpay',
'eosio.rex'
]
system合约的eosio::init接口,最早于v1.4.0版本正式引入,只在system合约首次部署的时候被使用到。在当前版本,该接口被修改增加了一个内联调用eosio.token::open接口的操作,用来帮助eosio.rex账户开启一个主币余额为0的入口。
eosio.token::open接口最早于v1.3.0版本引入,所以建议先由eosio.token账户部署一个最近的版本(指超过v1.3.0的,本篇研究时的环境均为v1.6.0版本)eosio.token合约,然后再部署system合约。
如果是最新版本替换旧版本,则eosio::init动作是不必要的甚至不允许的。区块生产者可执行eosio.token::open动作来帮助eosio.rex账户开启一个主币余额为0的入口。所以在bbt脚本中无须针对此处做任何修改。
ABI文件rex.results.abi需要被账户eosio.rex部署,而相应的rex.results.wasm不能被部署。rex.results合约的接口 buyresult, sellresult, rentresult, 和 orderresult 都没有外部操作。他们都作为一种内联的操作集成进接口 rentnet, rentcpu, buyrex, unstaketorex, and sellrex。内联的操作不会造成任何影响,他们的数据包含在父接口的动作中,可被追踪。
按照这个要求,需要在bbt脚本中补充:
def stepSetSystemContract():
retry(args.cleos + 'set contract eosio.rex ' + args.contracts_dir + '/eosio.system/ eosio.system.wasm rex.results.abi ')
retry(args.cleos + 'set contract eosio ' + args.contracts_dir + '/eosio.system/')
...
本节通过以下12个方面介绍REX实现的详细逻辑。
要想得到REX的相关操作,用户需要首先创建一个REX基金,并且使用主币向该基金充值。
基金不仅用于burrex动作,还用于所有的涉及到修改用户余额的动作。它也方便退款以及延迟卖单,因为它可能会被另一个用户所执行。
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 get currency balance eosio.token useraaaaaaaa "SYS"
100000.0000 SYS
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex deposit useraaaaaaaa "10.0000 SYS"
executed transaction: 084717efbf446f25e58226e63ce71b1a17745f85a56cb4dbf663b6e410e2393d 120 bytes 465 us
# eosio <= eosio::deposit {"owner":"useraaaaaaaa","amount":"10.0000 SYS"}
# eosio.token <= eosio.token::transfer {"from":"useraaaaaaaa","to":"eosio.rex","quantity":"10.0000 SYS","memo":"deposit to REX fund"}
# useraaaaaaaa <= eosio.token::transfer {"from":"useraaaaaaaa","to":"eosio.rex","quantity":"10.0000 SYS","memo":"deposit to REX fund"}
# eosio.rex <= eosio.token::transfer {"from":"useraaaaaaaa","to":"eosio.rex","quantity":"10.0000 SYS","memo":"deposit to REX fund"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 get currency balance eosio.token useraaaaaaaa "SYS"
99990.0000 SYS
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system listbw useraaaaaaaa
Receiver Net bandwidth CPU bandwidth
useraaaaaaaa 193728833.4889 SYS 193728833.4889 SYS
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex unstaketorex useraaaaaaaa useraaaaaaaa "100.0000 SYS" "100.0000 SYS"
executed transaction: 45ea6749c406f496b5554e3729e8e53fbc848c964635298483a25ec8dc6a6e79 144 bytes 1042 us
# eosio <= eosio::unstaketorex {"owner":"useraaaaaaaa","receiver":"useraaaaaaaa","from_net":"100.0000 SYS","from_cpu":"100.0000 SYS...
# eosio.token <= eosio.token::transfer {"from":"eosio.stake","to":"eosio.rex","quantity":"200.0000 SYS","memo":"buy REX with staked tokens"...
# eosio.stake <= eosio.token::transfer {"from":"eosio.stake","to":"eosio.rex","quantity":"200.0000 SYS","memo":"buy REX with staked tokens"...
# eosio.rex <= eosio.token::transfer {"from":"eosio.stake","to":"eosio.rex","quantity":"200.0000 SYS","memo":"buy REX with staked tokens"...
# eosio.rex <= eosio.rex::buyresult {"rex_received":"2000000.0000 REX"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system listbw useraaaaaaaa
Receiver Net bandwidth CPU bandwidth
useraaaaaaaa 193728733.4889 SYS 193728733.4889 SYS
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex withdraw useraaaaaaaa "100.0000 SYS"
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: insufficient funds
pending console output:
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex withdraw useraaaaaaaa "1.0000 SYS"
executed transaction: 1d24ac5d5e1cce3eb13b108793ea31b8ee5cb73abbf68002d18d7a8196951c7d 120 bytes 644 us
# eosio <= eosio::withdraw {"owner":"useraaaaaaaa","amount":"1.0000 SYS"}
# eosio.token <= eosio.token::transfer {"from":"eosio.rex","to":"useraaaaaaaa","quantity":"1.0000 SYS","memo":"withdraw from REX fund"}
# eosio.rex <= eosio.token::transfer {"from":"eosio.rex","to":"useraaaaaaaa","quantity":"1.0000 SYS","memo":"withdraw from REX fund"}
# useraaaaaaaa <= eosio.token::transfer {"from":"eosio.rex","to":"useraaaaaaaa","quantity":"1.0000 SYS","memo":"withdraw from REX fund"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 get currency balance eosio.token useraaaaaaaa "SYS"
99991.0000 SYS
REX池代表了REX系统的全局状态,它由多种余额组成:
REX pool balances = total_lendable / total_rex
total_lendable = total_unlent + total_lent
total_rent是一个虚拟余额,它的初始化值必须是正数,基于对预期的主币能够在部署后不久就可用的评估得到这个初始化值,所以出租成本与其他市场类似。total_unlent和total_rent是Bancor算法中的两个连接器,决定了CPU和NET的出租价格。为了更好地理解这个算法在REX中的应用,请参照一篇文章。
区块生产者可按需通过setrex动作来重置REX池的total_rent余额。但这个行为在初始化REX系统时并不是必须的,也不推荐使用超过一次。这是一个备份机制,当初始设置有误或者不符合token借出的金额时,可让区块生产者能够平衡租借市场的价格。setrex动作不会使total_rent加入或删除某个真的token。
余额指的是用户的REX基金rex_fund的余额,单位是以主币计算,使用该余额来购买REX,可以通过buyrex动作。
payment: 是指用户通过提供一定数量的主币,来交易得到REX币。
payment将被添加到账户的投票抵押,相应的超级节点的投票数量也会更新,所以余额购买REX币的过程与CPU,NET资源抵押的过程非常相似。
该机制也正是为了decreasing voter apathy(减少选民冷漠)
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex buyrex useraaaaaaaa "4.0000 SYS"
executed transaction: c713e98584f971d71d21676a830ac15a8b557d48582677b334cdc34cd12e8860 120 bytes 894 us
# eosio <= eosio::buyrex {"from":"useraaaaaaaa","amount":"4.0000 SYS"}
# eosio.rex <= eosio.rex::buyresult {"rex_received":"40000.0000 REX"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
上面谈了使用在rex_fund基金中的主币余额购买REX的方法,用户也可以通过抵押币来购买REX,不需要先解除自己的抵押到余额,这个方法就是在上面已经演示过的 unstaketorex 动作。该动作会分别减少账户的CPU资源抵押额: from_cpu,和NET资源抵押额: from_net,购买REX的总量是from_net + from_cpu。同时也要更新对应超级节点的投票数量。
购买REX后的卖出限制是4天,也就是说4天以后才可以卖出你的REX币。根据不同的购买方式,token将被累计到不同的独立的位置(称作成熟桶)记录,通过账户的rex_balance的rex_maturities字段。这些成熟桶们分别记录着不同购买来源的REX距离可售卖的倒计时时间(称作成熟期),例如4天,3天,2天...。已成熟的REX将没有成熟期,可以随时被卖出,这部分REX被存储于账户的rex_balance的matured_rex字段,这种基于成熟期的延迟机制是为了给租赁市场反应时间。
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex consolidate useraaaaaaaa
executed transaction: 7cc631030d159e21d6327253a5261b04bbb1f5fa73c5455975e616bc9f70c29f 104 bytes 415 us
# eosio <= eosio::consolidate {"owner":"useraaaaaaaa"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
正如上面提到的成熟桶,一个REX持有者可以利用一个特殊的桶,称作储蓄桶。REX在这个桶中永远不能成熟,所以不可对外售出。
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex mvtosavings useraaaaaaaa "10.0000 REX"
executed transaction: a93fe2254b236d470de8c1366de8eab9aed5230a75bef8b2868daee1003ca889 120 bytes 409 us
# eosio <= eosio::mvtosavings {"owner":"useraaaaaaaa","rex":"10.0000 REX"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex mvfromsavings useraaaaaaaa "10.0000 REX"
executed transaction: 4357c4cac358b66b28e05953603228581f3689a7191bc987d9e26cef7e9b1edf 120 bytes 447 us
# eosio <= eosio::mvfrsavings {"owner":"useraaaaaaaa","rex":"10.0000 REX"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex sellrex useraaaaaaaa "500.0000 REX"
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: insufficient available rex
pending console output:
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex cancelrexorder useraaaaaaaa
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: no sellrex order is scheduled
pending console output:
当订单不能被填写时,会被加入到一个队列,就是创建一条rex_order记录,该数据结构的字段包括:
/**
* @brief 执行一笔sellrex订单并返回包含结果的对象
*
* 执行一笔刚进来或已在队列中的订单。如果REX池中已有足够的未冻结在资源租赁的主币,则成功填写该订单。
* 这种情况下,REX池总量,用户的rex_balance以及用户vote_stake字段都会被更新。然而,这个函数不更新
* 用户的投票权利。函数返回成功标志,订单收益,和投票抵押内容。这些将在不同的函数中使用到,用来完成
* 订单处理,收益转账到用户的REX基金并更新用户的投票权重。
*
* @param bitr - 迭代器,直系想rex_balance数据库记录
* @param rex - 要被卖出的rex数量
*
* @return 结构体rex_order_outcome,包含成功标志位,订单收益以及投票抵押更改
*/
rex_order_outcome system_contract::fill_rex_order( const rex_balance_table::const_iterator& bitr, const asset& rex )
{
auto rexitr = _rexpool.begin(); // rex池
const int64_t S0 = rexitr->total_lendable.amount; // total_lendable值
const int64_t R0 = rexitr->total_rex.amount; // total_rex值
const int64_t p = (uint128_t(rex.amount) * S0) / R0;
const int64_t R1 = R0 - rex.amount;
const int64_t S1 = S0 - p;
asset proceeds( p, core_symbol() ); // proceeds资产
asset stake_change( 0, core_symbol() ); // stake_change资产
bool success = false;
const int64_t unlent_lower_bound = ( uint128_t(2) * rexitr->total_lent.amount ) / 10;
const int64_t available_unlent = rexitr->total_unlent.amount - unlent_lower_bound; // available_unlent <= 0 is possible
if ( proceeds.amount <= available_unlent ) { // 余额充足
const int64_t init_vote_stake_amount = bitr->vote_stake.amount;
const int64_t current_stake_value = ( uint128_t(bitr->rex_balance.amount) * S0 ) / R0;
_rexpool.modify( rexitr, same_payer, [&]( auto& rt ) { // 修改rex池状态表
rt.total_rex.amount = R1;
rt.total_lendable.amount = S1;
rt.total_unlent.amount = rt.total_lendable.amount - rt.total_lent.amount;
});
_rexbalance.modify( bitr, same_payer, [&]( auto& rb ) { // 修改_rexbalance状态表
rb.vote_stake.amount = current_stake_value - proceeds.amount;
rb.rex_balance.amount -= rex.amount;
rb.matured_rex -= rex.amount;
});
stake_change.amount = bitr->vote_stake.amount - init_vote_stake_amount;
success = true;
} else {
proceeds.amount = 0;
}
return { success, proceeds, stake_change };
}
/**
* @brief 执行用户已填写的sellrex订单并更新投票权重
*
* 检查用户是否有在队列内的已填写sellrex订单,执行它然后删除它。执行时要将订单收益转账给
* 用户的REX基金以及更新用户的投票权重。
*
* @param owner - owner EOS账户
* @param proceeds - 额外收益,转给owner的REX基金
* @param delta_stake - 额外抵押,加到owner的投票权重
* @param force_vote_update - 设为true的时候,投票权重被更新即使没变化
*
* @return asset - 如果存在订单,owner未填写的卖单的REX数量
*/
asset system_contract::update_rex_account( const name& owner, const asset& proceeds, const asset& delta_stake, bool force_vote_update )
{
asset to_fund( proceeds );
asset to_stake( delta_stake );
asset rex_in_sell_order( 0, rex_symbol );
auto itr = _rexorders.find( owner.value );
if ( itr != _rexorders.end() ) { // 找到已存在的订单
if ( itr->is_open ) { //订单开放状态,修改卖单价格
rex_in_sell_order.amount = itr->rex_requested.amount;
} else { // 未开放则添加至已有订单
to_fund.amount += itr->proceeds.amount;
to_stake.amount += itr->stake_change.amount;
_rexorders.erase( itr );
}
}
if ( to_fund.amount > 0 )
transfer_to_fund( owner, to_fund );
if ( force_vote_update || to_stake.amount != 0 )
update_voting_power( owner, to_stake ); // 更新投票权重
return rex_in_sell_order;
}
一个用户可以只有一个开放的rex_order。如果该账户有执行了一个新的sellrex动作,这笔订单不会被立即填写,请求卖出的REX数量会被添加到已存在的订单的rex_requested字段中去,相当于在有开放订单的状态下,新订单会更新该开放订单,不必冗余生成新的,节约了订单量。
REX租赁就是通过REX来租赁资源,包括CPU,NET资源。
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex rentcpu useraaaaaaaa useraaaaaaaa "3.0000 SYS" "1.0000 SYS"
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: loan price does not favor renting
pending console output:
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex rentnet useraaaaaaaa useraaaaaaaa "3.0000 SYS" "1.0000 SYS"
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: loan price does not favor renting
pending console output:
这两个动作的调用都报出了“loan price does not favor renting”的错误,在源码中寻找该错误的解释。
int64_t rented_tokens = get_bancor_output( pool->total_rent.amount, pool->total_unlent.amount, payment.amount );
check( payment.amount < rented_tokens, "loan price does not favor renting" );
payment即第三个参数,是指用户支付的主币金额,从rex_fund余额中划出。而rented_tokens变量是通过bancor算法得到的一个已出租token的值,该值是由REX池中的是total_unlent和total_rent来决定,即bancor算法的两个连接器,或者说两个条件值是total_unlent和total_rent,一个是REX池中所有的未出借主币数量,一个是已出借主币数量。计算得到的total_staked的金额是从total_unlent转入到total_rent的数量,并且支付的主币也会被添加到total_rent。REX资源租赁被创建后,支付的主币会被添加到REX池的total_lendable,同时total_unlent因此被增加了REX币以及增加了可供租借的主币。资源贷款期限是30天,到期时会从receiver的资源中减去对应的抵押金额total_staked。total_staked会从total_lent迁回到total_unlent,total_lent会根据Bancor相应更新。
所以分析上面无法调通rentcpu以及rentnet的原因是rented_token在目前的环境下太低所致,bancor市场没有建立起来,也就是可租借额度很低,我们买不到资源,所以要提高REX池的可租借主币的额度。下面是get_bancor_output函数的计算方式。
/**
* 该函数通过给定的两个连接器余额,以及一个输入的金额,使用Bancor算法计算出结果。
*
* @param in - 输入的金额
* @param conin - 输入连接器的余额
* @param conout - 输出连接器的余额
*
* @return int64_t - 转换输出金额
*/
int64_t get_bancor_output( int64_t conin, int64_t conout, int64_t in )
{
const double F0 = double(conin);
const double T0 = double(conout);
const double I = double(in);
auto out = int64_t((I*T0) / (I+F0)); // 公式
if ( out < 0 ) out = 0;
return out;
}
在rentcpu和rentnet动作中,用户均可提供一个额外的主币金额增加到租赁balance字段,在到期日,如果有足够的基金该笔租赁单子可以被重新恢复,即balance >= payment,否则租赁关闭,退还用户所有仍在租赁balance中的主币。如果一笔租赁单子被重新恢复了,total_staked会使用当前市场价和receiver收到影响而更新的资源限制重新计算。REX池的余额也会被更新。一个租赁单子(由rentcpu和rentnet产生)的拥有者可以投资一笔租赁,以loan_num作为id鉴别,使用动作fundcpuloan和fundnetloan。owner也可以从loan余额中提取,使用动作defundcpuloan和defundnetloan。
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex fundcpuloan useraaaaaaaa 1 "1.0000 SYS"
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: loan not found
pending console output:
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex fundnetloan useraaaaaaaa 1 "1.0000 SYS"
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: loan not found
pending console output:
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex defundcpuloan useraaaaaaaa 1 "1.0000 SYS"
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: loan not found
pending console output:
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex defundnetloan useraaaaaaaa 1 "1.0000 SYS"
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: loan not found
pending console output:
由于前面未成功生成loan记录,所以无法得到有效的loan_num对应的记录,这4个动作都依赖该loan记录,所以无法成功执行。
查询loan租赁记录的方式:
cleos get table eosio eosio cpuloan --index 3 --key-type name -L strarteosfee -U strarteosfee
在大部分的REX动作中,都是调用runrex函数。它启动固定的2笔卖单的进程(默认为2),计算着他们的CPU以及NET租赁的过期时间,执行着上面描述的REX卖单成熟以及租赁到期的工作。
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex rexexec useraaaaaaaa 2
executed transaction: 5e85255c4dd3e6ed6c98b73eafbc26ed84d4cf05b0c91c2e47e2b447a5653edd 104 bytes 280 us
# eosio <= eosio::rexexec {"user":"useraaaaaaaa","max":2}
warning: transaction executed locally, but may not be confirmed by the network yet ]
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex updaterex useraaaaaaaa
executed transaction: e3dc3814dacb85313a76683217b023b010a7c19334f6e67489a2b8c0e94f616b 104 bytes 900 us
# eosio <= eosio::updaterex {"owner":"useraaaaaaaa"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
evsward@evsward-ThinkPad-E480:~$ cleos --wallet-url http://127.0.0.1:6666 system rex closerex useraaaaaaaa
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: account has remaining REX balance, must sell first
pending console output:
重申一下REX系统关于投票的硬性要求,就是所有持有REX的账户必须直接或者通过代理参与了为超级节点的投票。
本文详尽地介绍了REX系统的内容。REX是2019年以来EOS最新的重大功能发布,该项目由BM牵头提出核心算法分析,继而由blockone公司开发相关功能。本文从核心的bancor算法分析,到具体的命令,包括deposit,buyrex,sellrex,rentcpu,rentnet,closerex等一系列REX动作的分析与实践,及时同步了EOS的最新动作。