Check out our new ERC20 Verification Audit!

The Dispatcher – First Line Of Defense In Any EOS Smart Contract

Back in September 2018, an attacker managed to steal US$200 000 worth of EOS from the EOSBet contract. The attacker exploited a serious bug in a designated entry function of the contract, called the dispatcher. Moreover, the introduced fix only partially closed the security issue, leaving the contract vulnerable. This incident highlights the need for education about security critical details of the dispatcher function.

This blog post aims to help developers write secure dispatchers in their EOS contracts and avoid such bugs, which put the funds of the smart contract at risk.

The Dispatcher Function

A dispatcher is the function that is executed first whenever the code of the account is invoked. This function receives an action identifying the function to be invoked, and it is the dispatcher’s responsibility to invoke the intended function. Every EOS smart contract must provide an apply handler that defines the behavior of the dispatcher function.

To implement the dispatcher, the contract development toolkit (eosio.cdt) allows developers to generate the dispatcher automatically by executing the macros inside a contract. All functions which are marked by [[eosio::action]] will be callable by actions. However, for some projects this is insufficient and the developer may need to customize the dispatcher, e.g., to accept certain actions only upon a token transfer. While basic helper functions are available, these only cover the invocation of the functions, not the control logic enforcing the condition of when they may be invoked.

Recap – EOS’ communication model

Before we dive in, we need to get up to speed with EOS’ communication model.

To execute a smart contract, actions need to be pushed to its account. During the execution of a smart contract it may notify other accounts about this action. Notifying another account is done by calling require_recipient(). What happens is that after the execution of this smart contract’s code, the dispatcher of the notified account is invoked with a copy of this action.

Let us look at a simple dispatcher:

   void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
      if( code == receiver ) {
         switch( action ) {
            EOSIO_DISPATCH_HELPER( TYPE, MEMBERS )
         }
      }
   }

We see the implementation of apply(): If code is equal to receiver, the passed action is called, the helper function EOSIO_DISPATCH_HELPER does this for us. But what are these function parameters passed to apply() exactly? To write secure dispatchers, it is vital to fully understand these parameters.

The following parameters are passed to the apply function: uint64_t receiveruint64_t code and uint64_t action. Let us have a look at the EOS source code!

This is the function that invokes the dispatcher of the smart contract by calling the apply() function and passing the following parameters:

      void apply(apply_context& context) override {
         vector<Value> args = {Value(uint64_t(context.get_receiver())),
                                Value(uint64_t(context.get_action().account)),
                               Value(uint64_t(context.get_action().name))};

         call("apply", args, context);
      }

Source

  • receiver is context.get_receiver(), the account currently executing code
  • code is context.get_action().account, the account the action was sent to
  • action is context.get_action().name, the name of the action

But what exactly does that mean? It is important to understand the difference between code and receiver. In short, code is always the first receiver of the action, while receiver is the account currently executing the action.

code and receiver – aren’t they the same?

Not necessarily! They may be equal, e.g. when an action is sent to an account (= code) and this account’s code is currently executing (= receiver). In EOS’ communication model, contracts executing code can notify other accounts about this action by calling require_recipient(). This invokes the code of the recipient’s account (= receiver changes) but the code stays constant, thus code and receiver differ.

By using code == receiver the dispatcher knows that this contract has been called by the action directly, while in the case of code != receiver the action was originally destined for another account, but this account has been notified about it.

Most prominent use case – reacting to a token transfer

If your contract receives a transfer of EOS tokens for example, you may want to execute a certain action. This functionality allows you to do so because the eosio.token contract notifies the sender and the receiver about a successful token transfer and gives them the opportunity to execute code. Note that the transfer action, to transfer x funds from account a to b, was sent to the eosio.token account, so code is equal to eosio.token. After a successful transfer, the receiver is informed about it and its dispatcher is invoked. Here code is eosio.token and receiver is b, because b is currently executing.

Let us stop here for a moment. Assume we are within our dispatcher and code is equal to eosio.token (receiver is equal to our account), what exactly do we know at this point? Did we just receive a token transfer?

Not necessarily! At this point we just know that the original action was sent to the eosio.token contract (hence code is eosio.token), but we have absolutely no other information about the actual details of the token transfer! Most importantly we need to make sure that the transfer was to our account (ensuring parameter to of the action is our account) and that the amount is what we expect. If we don’t do this (most importantly checking the to field), attacks of the following kind are possible:

A simple example: we have a trading contract and transfer some GoldTokens if we received some EOS tokens. Our contract, however, only checks if code == eosio.token and the amount of tokens, it does not check if it was actually the receiver of these tokens. Alice notices this and decides to get some GoldTokens without actually transfering EOS to our contract.

All Alice needs is an account where she can deploy a simple contract. The only thing this contract has to do is to implement a transfer action and inform our trading contract about the transfer action. The attack works as follows:

  1. Alice sends a transaction to the eosio.token contract to transfer EOS from her account to the attacking contract she controls (where later she can reclaim these tokens).
  2. Upon executing the action, the eosio.token contract notifies the sender and the receiver about the action.
  3. The receiver, Alice’s attacking contract, in turn just notifies our trading contract.
  4. Our trading contract gets a copy of the action, where code is equal to eosio.token.

Without doing any further checks our contract mistakenly thinks it just received some EOS tokens and transfers GoldTokens to Alice.

This example highlights the importance of checking everything properly.

How to ensure your contract actually received an (EOS-) token transfer

The following considerations are valid for the eosio.token contract, but likely also applicable to other token contracts. The first important thing to note is that you have to fully trust the token contract, especially that it only notifies you about the transfer if the transfer actually happened and succeeded. One of the token contracts you can trust to only notify you in case of a successful transfer is the system’s eosid.token contract.

First, a mandatory requirement is that code is equal to eosio.token. Why? The eosio.token contract allows token transfers only when the action to transfer is directly sent to its account. When the eosio.token contract notifies you about a token transfer, your dispatcher gets a copy of the action.

The name of the passed action must be equal to transfer. Furthermore, all parameters, especially the field to and the quantity, need to be checked. A check if(to != self) return; is often added at a later stage in the contract to enforce this. Why is this done outside of the dispatcher? Inside the dispatcher it is hard to access the fields of the function’s argument, once inside the function these parameters are easily accessible.

If you follow these guidelines you can be sure that your contract actually received an EOS token transfer.

Ensure that you only invoke functions of your smart contract which depend on a token transfer after these checks successfully passed. Please ensure that your custom dispatcher does not allow to bypass these checks on invoking these functions! Such mishaps happened in the past, let us walk through one well-known example.

What went wrong for EOSBet?

EOSBet allows you to bet EOS tokens. You transfer EOS tokens to their contract which places your bet. Later, if you win, you can claim and withdraw your prize. To state the obvious: one should only be able to place a bet after having transferred EOS. What went wrong in the first version of their dispatcher?


void apply( uint64_t receiver, uint64_t code, uint64_t action ) { 
    if( action == N(onerror)) {
        eosio_assert(code == N(eosio), "onerror action's are only valid from the \"eosio\" system account"); 
    }
    if( code == receiver || code == N(eosio.token) || action == N(onerror) ) {
        switch( action ) {
            EOSIO_API( TYPE, MEMBERS )
        } 
    }
}

(slightly simplified for readability)

This dispatcher allows any action (if it exists) sent to the contract directly to execute, including the transfer function which should only be invoked after an actual transfer of EOS tokens to this contract. Thus an attacker was able to place bets for free and cash out the winnings. All  US$200 000 worth of funds were drained from the contract.

EOSBet went ahead and fixed their code after the attack. The fixed code can be found in their Gitlab repository.

void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
    if( code == self || code == N(eosio.token)) {
        if( action == N(transfer)) {
            eosio_assert( code == N(eosio.token), "Must transfer EOS");
        }
        switch( action ) {
            EOSIO_API( TYPE, MEMBERS )
        }
    }
}

(slightly simplified for readability)

The fix introduced checks verifying that the original recipient of the action is eosio.token in case of a call to the transfer action. As described above, this is insufficient to ensure that the EOSBet contract really received EOS tokens. While this fix now ensures that the action transfer was directed to the eosio.token contract, no checks have been added to the transfer() function, which handles this action, to ensure that the transfer.to receiver of the EOS tokens was the EOSBet smart contract. Please note that EOSBet is aware of this and all their deployed contracts are not subject to this issue anymore.

This blog post focuses on the most common use case, which is reacting to a token transfer. Please note that the same or similar problems exist during other interactions of smart contracts in EOS.

We hope this article raised your awareness, improved your understanding of the dispatcher of EOS contracts and will help you write secure smart contracts!

Need a security audit of your EOS contracts?

For an expert security review of your EOS contracts, reach out to us at contact@chainsecurity.com.