EOS 判断当前动作/事务的发起者是不是一个合约

yufei       2 月, 1 周 前       491

最近的 EOS 可以说是史上最惨,应该没人会反驳吧。不是这个合约爆漏洞,就是那个合约爆漏洞,价格一跌再跌,已经丧失了人生意义的感觉了~~

小弟我去研究了下 EOS 这些漏洞的成因,无论是什么样的漏洞,最后终究会发展到:原本你希望某个动作/事务的发起者是一个正常的人/账号,可黑客就是不吃这一套,自己搞了一个合约,然后使用合约调合约/动作调动作的方式访问你

加上 区块链上无随机数 ,作为合约的开发者,我们真的是比惨还惨。

但,有一个好消息是,官方提供了 API,可以查到当前的某个动作的发起者是谁,发起的参数是什么 ?

看起来是不是很复杂,但这是目前唯一能够查到合约的发起者是不是一个正常人的机会了。

动作调动作/合约调合约

假设存在一个合约 hello,它有两个动作 hiwho,其中 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 换成你自己的想要判定的动作即可

5 回复  |  直到 Feb 10, 2019

carl

#1   •   3 周, 4 天 前   •  

不过,当A合约发起一笔延迟事务,由延迟事务触发B合约的action,文章的提到的方法并不能判断延迟事务是有A合约发起的。有什么方法可以知道是A合约发起的延迟交易吗?

yufei

#2   •   3 周, 4 天 前   •  

目前不能,正确地说,没有提供 api 来判断一个地址是否是合约

yufei

#3   •   3 周, 4 天 前   •  

从 eospark.com 上可以看到,目前 eos 合约大概有 3000 个,直接写一个合约用来判断是否是合约账号就可以了

接口地址如下

https://eospark.com/api/contracts?type=all&order_by=invoker_num&order=DESC&page=1&size=200&account=

carl

#4   •   3 周, 4 天 前   •  

@yufei 但是A合约发起的延迟事务中permission_level 用的是非合约账户来触发B合约的话 好像没办法判断延迟事务是否是合约A账号发起的

yufei

#5   •   1 周, 1 天 前   •  

@carl 这个我还真没试过,如果是普通账号,应该没授权吧

简单教程 = 简单教程,简单编程
简单教程 是一个关于技术和学习的地方
现在注册
已注册用户请 登入
关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2018 简单教程 twle.cn All Rights Reserved.