Getting Started

The eos Developer Hub

Welcome to the eos developer hub. You'll find comprehensive guides and documentation to help you start working with eos as quickly as possible, as well as support if you get stuck. Let's jump right in!

Get Started    Discussions

2.8 Custom Dispatchers

Introduction: the EOSIO_DISPATCH Macro

Up until now, we've been using a nifty macro called EOSIO_DISPATCH to handle dispatching actions sent to the WASM API to our functions inside our contracts.

EOSIO_DISPATCH( myclass, (upsert)(notify)(erase) )

The EOSIO_DISPATCH macro abstracts the dispatcher with a common pattern.

  • The base class is set, myclass
  • The actions exposed are defined.
  • A dispatched action's arguments are positional.

Step 1: Understanding the EOSIO_DISPATCH macro

Every smart contract must provide an apply action handler, or "dispatcher". The dispatcher is a function that listens to all incoming actions and performs the desired behavior. In order to respond to a particular action, code is required to identify and respond to specific actions requests. apply uses the receiver, code, and action input parameters as filters to map to the desired functions that implement particular actions. The apply function can filter on the code parameter using something like the following

if (code == name("contract_name").value) {
   // your handler to respond to particular code
}

Within a given code, one can respond to a particular action by filtering on the action parameter. This is normally used in conjunction with the code filter.

if (action == name("action_name").value) {
    //your handler to respond to a particular action
}

To simplify the work for contract developers, the EOSIO_DISPATCH macro encapsulates the lower level action mapping details of the apply function, enabling developers to focus on their application implementation.

#define EOSIO_DISPATCH( TYPE, MEMBERS ) \
extern "C" { \
   void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
      if( code == receiver ) { \
         switch( action ) { \
            EOSIO_DISPATCH_HELPER( TYPE, MEMBERS ) \
         } \
         /* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
      } \
   } \
} \

A developer needs only to specify the code and action names from the contract in the macro, and all of the underlying C code mapping logic is generated by the macro. An example of use of the macro can be seen above, i.e., EOSIO_DISPATCH( hello, (hi) ) where hello and hi are values from the contract.

In this example you can see there is one function, apply. All it does is log the actions delivered and makes no other checks. Anyone can deliver any action at any time provided the block producers allow it. Absent any required signatures, the contract will be billed for the bandwidth consumed.

apply

apply is the action handler, it listens to all incoming actions and reacts according to the specifications within the function. The apply function requires two input parameters, code and action.

code filter

In order to respond to a particular action, structure the apply function as follows. You may also construct a response to general actions by omitting the code filter.

if (code == name("contract_name").value ) {
    // your handler to respond to particular code
}

You can also define responses to respective actions in the code block.

action filter

To respond to a particular action, structure your apply function as follows. This is normally used in conjuction with the code filter.

if (action == name("action_name").value) {
    //your handler to respond to a particular action
}

Step 2: Converting EOSIO_DISPATCH to various styles of dispatcher

The addressbook contract from previous tutorials, we used the EOSIO_DISPATCH macro as our dispatcher. We'll now take the functionality of this macro and apply it to various possible dispatcher patterns.

EOSIO_DISPATCH( addressbook, (upsert)(notify)(erase) )

Expanding the EOSIO_DISPATCH Macro

Below, the EOSIO_DISPATCH macro has been expanded to show its underlying logic. Since this dispatcher uses execute_action(), our actions will receive positional arguments so eosio-cpp's ABI generator can be utilized. As seen below the EOSIO_DISPATCH macro is suitable for contracts that handle local actions, but provides no way to handle actions from an external contract.

extern "C" {
    void apply(uint64_t receiver, uint64_t code, uint64_t action) {
        auto self = receiver;
        if( code == self ) {
          addressbook _addressbook(name(receiver));
          switch(action) {
            case name("upsert").value: 
              execute_action(name(receiver), name(code), &addressbook::upsert); 
              break;
            case name("notify").value: 
              execute_action(name(receiver), name(code), &addressbook::notify); 
              break;
            case name("erase").value: 
              execute_action(name(receiver), name(code), &addressbook::erase); 
              break;
          }
        }
    }
};
  1. Check that code == self before dispatching an action.
  2. Instantiate addressbook
  3. Throw the action into a switch()
  4. Pass a reference to our addressbook instance, and a caller reference to the public action of the addressbook class to the execute_action function

The above pattern is simply an exploded view of the EOSIO_DISPATCH and EOSIO_DISPATCH macros without actually using them, hence the "Exploded EOSIO_DISPATCH Dispatcher."

The exploded dispatcher pattern above is really only suitable when handling internal actions. If your contract needs to handle actions sent by another contract in a specific way, using a switch() introduces logical ambiguity which may introduce vulnerabilities. If you like the tradeoffs of this pattern, but wish to handle income actions from another contract, see the next example.

Flexible/Compatible Dispatcher

This pattern provides more control over security at the expense of maintainability. Utilizing if...else if statements as opposed to a switch inherently provides more granularity.

This dispatcher adds a transfer action from eosio.token in the last case.

This pattern is a good mix between convention and configuration. While the dispatcher itself requires some meaty configuration, we utilize the benefits of execute_action to automatically set _self and to unpack the arguments of the given action and call the action (class method) with these unpacked parameters. Because of this, our actions (class methods) will receive positional arguments and thus, eosio-cpp's ABI generator can be used. It's flexible in the dispatcher and compatible with ABIs.

The addressbook contract previously authored does not include a transfer action, it's included for demonstrative purposes only.

extern "C" {
  void apply(uint64_t receiver, uint64_t code, uint64_t action) {
    addressbook _addressbook(receiver);
    if(code==receiver && action==name("upsert").value) {
      execute_action(name(receiver), name(code), &addressbook::upsert );
    }
    else if(code==receiver && action==name("notify").value) {
      execute_action(name(receiver), name(code), &addressbook::notify );
    }
    else if(code==receiver && action==name("erase").value) {
      execute_action(name(receiver), name(code), &addressbook::erase );
    }
    else if(code==name("eosio.token").value && action==name("transfer").value) {
      execute_action(name(receiver), name(code), &addressbook::transfer );
    }
  }
};

The above pattern is the same as the "Exploded ABI Macro" pattern, but utilizes more verbose conditions that are appropriate for handling actions .

  1. Instantiate addressbook
  2. Have an individual if statement for each code and action pair.
  3. Pass a reference of the addressbook instance, and a caller reference to the public action of the addressbook class to the execute_action function.

Fleming's Dispatcher (Flexible)

This dispatcher places most of the security logic and control inside the action handler, however, cannot use eosio-cpp's ABI generator. This dispatcher requires almost a complete rewrite of the addressbook contract that we've already completed. We won't rewrite the entire contract, but will rewrite one of the actions to demonstrate how this pattern could be used.

extern "C" void apply(uint64_t receiver, uint64_t code, uint64_t action) { 
  switch(action) {
      case name("upsert").value: return upsert(receiver, code);
      case name("notify").value: return notify(receiver, code);
      case name("erase").value: return erase(receiver, code);
  }
}

To handle requests such as these move security logic that was otherwise in the dispatcher into the action. Going this route might provide more control in some situations. However, it may also introduce redundancy, particularly for larger contracts. In the end, it accomplishes all the same points as the above patterns while allowing more information into the scope (namely code). Having access to code inside your action may be useful in some situations. This pattern provides the most flexibility in both the dispatcher and the action, hence "Fully Flexible."

void notify(uint64_t self, uint64_t code) {
  eosio_assert(code == self, "These are not the droids you are looking for");
  auto data = unpack_action_data<notify>();
  require_auth(get_self());
  require_recipient(data.user);
}
  1. Checks that code == self for security.
  2. Unpacks action request into a variable where arguments can be accessed, for example data.user
  3. This contract or self can be called whatever we want. When using execute_action or the Macros that utilize execute_action, "self" is automatically set to _self.

This pattern is not compatible with eosio-cpp's ABI generator, contracts written with this pattern will need their ABI files written by hand.

Please note, you will need the following declared somewhere in your contract for unpack_action_data to be accessible in your contract.

using eosio::unpack_action_data;

Custom EOSIO_DISPATCH macro

The above dispatchers would likely not be suitable for complex contracts with many actions. Using a custom dispatcher could introduce redundant code that's difficult to maintain, and as a unintended consequence, may introduce vulnerabilities.

The final option is to write a custom macro that takes your unique needs into consideration. You should really only ever have to do this if your contract needs to handle actions from an external contract and your contract has more actions than is practical for the above patterns.

Below is a custom macro for safely handling transfer actions.

#define EOSIO_ABI_CUSTOM(TYPE, MEMBERS) \
  extern "C" { \
  void apply(uint64_t receiver, uint64_t code, uint64_t action) { \
    auto self = receiver; \
    bool self_action = code == self && action != N(transfer); \
    bool transfer = code == N(eosio.token) && action == N(transfer); \
    if (self_action || transfer) { \
      TYPE thiscontract(self); \
      switch (action) { EOSIO_API(TYPE, MEMBERS) } \
    } \
  }
  1. Checks if code==receiverand that the action is not transfer. If this condition is omitted, you may introduce a vulnerability into your contract. It allows the action through if the code is eosio.token and the action is transfer. This check prevents another contract with a transfer function from exploiting your contract.
  2. Instantiates the TYPE (this would be your standard C++11 class)
  3. Add a switch for action
  4. Use the EOSIO_DISPATCH_HELPER macro, which inserts a case for each MEMBER into your switch. Inside this case, it calls execute_action using a pattern very similar to the one demonstrated in the Flexible/Compatible Dispatcher defined above.
    You can then use this macro the same way you would with the provided EOSIO_DISPATCH macro. The below includes a fictional transfer action to make this demonstration relevant.
EOSIO_DISPATCH_CUSTOM( addressbook, (upsert)(notify)(erase)(transfer) )

The drawback to this approach is that it's not as immediately obvious what this macro does, if you need to handle different transfers from different token contracts, this code can quickly become difficult to maintain, and makes it easier to introduce logical errors. The benefit here is that your dispatcher is a bit more clean for more complex contracts with many actions.

The addressbook contract previously authored did not include a transfer action, it was included above to demonstrate a hypothetical case.

Step 3: Security, Security, Security...

Your contract's first line of security begins at your dispatcher. Understanding how the dispatching of actions to your contracts is handled is imperative to limiting exposure to logic inside your actions. Always take great caution when writing a custom dispatcher, and be aware of the security implications of each individual implementation method.

Conclusion

All the examples provided achieve the appproximately same result, but with different tradeoffs regarding maintenance and explicit security. Which implementation method employed is a contract depends on the use case and some personal preferences.

For simple contracts that only execute internal public actions, the EOSIO_DISPATCH is more than suitable, eliminates cruft and greatly decrease the chance of introducing logical errors.


2.8 Custom Dispatchers


Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.