最近的 EOS 可以说是史上最惨,应该没人会反驳吧。不是这个合约爆漏洞,就是那个合约爆漏洞,价格一跌再跌,已经丧失了人生意义的感觉了~~
小弟我去研究了下 EOS 这些漏洞的成因,无论是什么样的漏洞,最后终究会发展到:原本你希望某个动作/事务的发起者是一个正常的人/账号,可黑客就是不吃这一套,自己搞了一个合约,然后使用合约调合约/动作调动作的方式访问你
加上 区块链上无随机数 ,作为合约的开发者,我们真的是比惨还惨。
但,有一个好消息是,官方提供了 API,可以查到当前的某个动作的发起者是谁,发起的参数是什么 ?
看起来是不是很复杂,但这是目前唯一能够查到合约的发起者是不是一个正常人的机会了。
动作调动作/合约调合约
假设存在一个合约 hello
,它有两个动作 hi
和 who
,其中 hi
动作除了输出 Hello hi
之外,还会调用 who
这个动作。
相关的源码如下
#include <eosiolib/eosio.hpp> #include <eosiolib/transaction.hpp> #include <eosiolib/action.hpp> using namespace eosio; using namespace std; class hello : public contract { public: using contract::contract; [[eosio::action]] void hi() { print("Hello hi!"); action( permission_level(_self,"active"_n), _self, "who"_n, std::make_tuple(_self,"who"_n,std::string("i am from hi")) ).send(); } [[eosio::action]] void who(name from, name act, string memo) { print("Hello World"); } }; EOSIO_DISPATCH( hello, (hi)(who))
我们使用下面的命令编译并部署这个合约
eosio-cpp -o hello.wasm hello.cpp --abigen cleos set contract hello ../hello -p hello
然后,我们看一下 hello
这个账号是否有 eosio.code
权限
cleos get account hello created: 2018-12-01T00:39:54.500 permissions: owner 1: 1 EOS8FXCAGvW7pe5fFiehKMcFGZWBaAkQ3BpRndcJitQCJizNetk6R active 1: 1 EOS8FXCAGvW7pe5fFiehKMcFGZWBaAkQ3BpRndcJitQCJizNetk6R memory: quota: unlimited used: 77.28 KiB net bandwidth: used: unlimited available: unlimited limit: unlimited cpu bandwidth: used: unlimited available: unlimited limit: unlimited
没有,那么我们使用下面的命令给这个账号添加 eosio.code
权限。
注意: 你需要把公钥修改成你自己的
cleos set account permission hello active '{"threshold": 1,"keys": [{"key":"EOS8FXCAGvW7pe5fFiehKMcFGZWBaAkQ3BpRndcJitQCJizNetk6R","weight":1}],"accounts": [{"permission":{"actor":"hello","permission":"eosio.code"},"weight":1}]}' owner -p hello@owner
执行成功后,我们再调用 cleos get account hello
就可以在 active
里看到 eosio.code
权限了
cleos get account hello created: 2018-12-01T00:39:54.500 permissions: owner 1: 1 EOS8FXCAGvW7pe5fFiehKMcFGZWBaAkQ3BpRndcJitQCJizNetk6R active 1: 1 EOS8FXCAGvW7pe5fFiehKMcFGZWBaAkQ3BpRndcJitQCJizNetk6R, 1 hello@eosio.code memory: quota: unlimited used: 77.31 KiB net bandwidth: used: unlimited available: unlimited limit: unlimited cpu bandwidth: used: unlimited available: unlimited limit: unlimited
现在,我们来看一下对 who
这个动作的普通调用,也就是直接执行这个动作
cleos push action hello who '["hello","who","i am from director"]' -p hello executed transaction: 1a609c4e046c641a0c7f344ce3615cb246de9a6f2adf23bbc11c1be381768b2b 128 bytes 2294 us # hello <= hello::who {"from":"hello","act":"who","memo":"i am from director"} >> Hello World
从合约的输出结果中可以看到,普通的调用,只有一个动作被执行,就是
hello <= hello::who {"from":"hello","act":"who","memo":"i am from director"}
然后,我们执行 hi
这个动作,看一下输出结果
cleos push action hello hi '[]' -p hello executed transaction: 0d31853b68c111d822a05471de372dc3675788bb98d450b81434fd8c32801d55 96 bytes 303 us # hello <= hello::hi "" >> Hello hi! # hello <= hello::who {"from":"hello","act":"who","memo":"i am from hi"} >> Hello World
从合约的输出结果中可以看到,动作调动作/合约调合约,一定会有两个动作在执行,第一个是调用动作,第二个是被调用的动作
# hello <= hello::hi "" >> Hello hi! # hello <= hello::who {"from":"hello","act":"who","memo":"i am from hi"} >> Hello World
从上面的输出结果中可以看出,我们可以通过判断第一个动作是否是当前动作,得出当前的动作是否被合约调用
相关方法
<eosiolib/transaction.hpp>
头文件提供了 get_action()
这个函数用户获取整个事务的发起动作。
get_action()
函数的原型如下
/** * Retrieve the indicated action from the active transaction. * @param type - 0 for context free action, 1 for action * @param index - the index of the requested action * @return the indicated action */ inline action get_action( uint32_t type, uint32_t index );
从注释中可以看出,第一个参数,也就是 type
的意思是当前动作的类型,0 表示上下文无关动作 1 表示普通动作
第二个参数 index
表示动作的索引,就是要获取当前事务中的第几个动作。
因为我们要获取的是普通的动作的第一个动作,所以一般的使用方法如下
eosio::action act = eosio::get_action( 1, 0 );
get_action()
的返回值是一个 eosio::action
结构体,我们再看看这个结构体包含了哪些东西。
在 <eosiolib/action.hpp>
头文件中,有关 eosio::action
的描述在注释中
* A EOS.IO action has the following abstract structure: * * ``` * struct action { * capi_name account_name; // the contract defining the primary code to execute for code/type * capi_name action_name; // the action to be taken * permission_level[] authorization; // the accounts and permission levels provided * bytes data; // opaque data processed by code * }; * ``` * * This API enables your contract to inspect the fields on the current action and act accordingly. * * Example: * @code * // Assume this action is used for the following examples: * // { * // "code": "eos", * // "type": "transfer", * // "authorization": [{ "account": "inita", "permission": "active" }], * // "data": { * // "from": "inita", * // "to": "initb", * // "amount": 1000 * // } * // }
从这段描述中,我们可以看到,一个动作包含以下成员
struct action { capi_name account_name; // 合约所在的账号名 capi_name action_name; // 动作名 permission_level[] authorization; // 执行动作所传递的权限 bytes data; // 执行动作的所传递的参数 };
至于执行当前动作的方法,我们可以通过 get_action()
返回的 action
的实例的 as()
方法来获取
data_as()
方法的原型如下
/** * Retrieve the unpacked data as T * * @brief Retrieve the unpacked data as T * @tparam T expected type of data * @return the action data */ template<typename T> T data_as() { return unpack<T>( &data[0], data.size() ); }
其中的 T
类型是一个按照顺序包含所有动作参数的结构。
比如有一个动作如下
void transfer(name from, name to, asset quantity, string memo);
那么 T
类型的结构体就应该如下
struct transfer_args { name from, name to, asset quantity, string memo };
范例
好了,我们已经了解了如何获取某个动作的各种参数了,下面我们就来完善下开头写的范例
#include <eosiolib/eosio.hpp> #include <eosiolib/print.hpp> #include <eosiolib/asset.hpp> #include <eosiolib/transaction.hpp> #include <eosiolib/action.hpp> using namespace eosio; using namespace std; class hello : public contract { public: using contract::contract; [[eosio::action]] void hi() { action( permission_level(_self,"active"_n), _self, "who"_n, std::make_tuple(_self,"who"_n,std::string("i am from hi")) ).send(); } [[eosio::action]] void who(name from, name actname, string memo) { print("Hello World"); eosio::action act = eosio::get_action( 1, 0 ); print("==actor=>"); print(act.authorization.back().actor); print("==permission=>"); print(act.authorization.back().permission); print("==contract account===>"); print(act.account); print("==action name===>"); print(act.name); if ( act.name == "who"_n) { auto args = act.data_as<who_args>(); print("===args=====>"); print("===from=====>"); print(args.from); print("===act======>"); print(args.act); print("===memo=====>"); print(std::string(args.memo)); } } struct who_args { name from; name act; std::string memo; }; }; EOSIO_DISPATCH( hello, (hi)(who))
编译和部署合约后,我们使用普通的方式调用 who
动作
cleos push action hello who '["hello","who","i am from director"]' -p hello
输出的内容如下
# hello <= hello::who {"from":"hello","actname":"who","memo":"i am from director"} >> Hello World==actor=>hello==permission=>active==contract account===>hello==action name===>who===args=====>===from=====>hello===act======>who===memo=====>i am from director
我们在换一个账号,比如使用 hi
账号
cleos push action hello who '["hello","who","i am from director"]' -p hi
输出内容如下
# hello <= hello::who {"from":"hello","actname":"who","memo":"i am from director"} >> Hello World==actor=>hi==permission=>active==contract account===>hello==action name===>who===args=====>===from=====>hello===act======>who===memo=====>i am from director
可以看到,换了账号执行的区别在于 actor
,这个参数表示动作的执行人,而最主要的 action name
仍然是 who
接下来我们看看动作调动作
cleos push action hello hi '[]' -p hello
输出结果如下
# hello <= hello::hi "" # hello <= hello::who {"from":"hello","actname":"who","memo":"i am from hi"} >> Hello World==actor=>hello==permission=>active==contract account===>hello==action name===>hi
对比下刚刚的普通调用,发现 action name
已经是 hi
了,而不是 who
即使我们换了一个账号,那么 action name
仍然是 hi
cleos push action hello hi '[]' -p hi executed transaction: c83ef8cfd91e0dac11b9d99561eddeb66cf025d6abdd4beb7d49c432e1f5e813 96 bytes 317 us # hello <= hello::hi "" # hello <= hello::who {"from":"hello","actname":"who","memo":"i am from hi"} >> Hello World==actor=>hi==permission=>active==contract account===>hello==action name===>hi
哈哈,是不是很有趣
那么,我们要判断当前动作是否被直接调用,只要使用下面的方法即可
eosio::action act = eosio::get_action( 1, 0 ); eosio_assert(act.name == "action name"_n, "you are not allowed");
把
action name
换成你自己的想要判定的动作即可
#3 • 5 年, 11 月 前 •
从 eospark.com 上可以看到,目前 eos 合约大概有 3000 个,直接写一个合约用来判断是否是合约账号就可以了
接口地址如下
https://eospark.com/api/contracts?type=all&order_by=invoker_num&order=DESC&page=1&size=200&account=
#4 • 5 年, 11 月 前 •
目前不能,正确地说,没有提供 api 来判断一个地址是否是合约
#5 • 5 年, 11 月 前 •
不过,当A合约发起一笔延迟事务,由延迟事务触发B合约的action,文章的提到的方法并不能判断延迟事务是有A合约发起的。有什么方法可以知道是A合约发起的延迟交易吗?