The apply() method using in EOS smart contract

Why do I mention about this method?

As an DApp developer, the common task is to check payment and send token from users to contract and vice versa. To work with this inside EOS contract, contract owner should use apply() method to catch up the transfer action from token contract. But EOSIO doesn’t officially cover this topic. That is why most EOS contracts were hacked from this function: EOSBet, DEOS, EOS.WIN … I am going to explain in detail about it and conclude with some experiences from the hacked in EOSbet as well as other DApps.

How does apply() works?

In your contract, define apply() method to catch up transaction notification from eosio.token. Once someone transfers EOS token to your contract, it will catch up this transaction, then execute mytransfer() function with packed data. Look at the flowchart below:

Example for apply() method

To understand how this function works, let’s discuss an example: In my EOS contract , I want to accept EOS payments from users, then send my token back to them with a certain ratio. The detailed code is as below:

How they are hacked EOSDice twice.

EOSBEet has experienced being hacked two times and the hacker took more than 100.000 EOS. Although they have security audits from 2 of 3rd parties after the first hacked. The second attack is very smart and tricky.

Today, I will show you in detail, how the hacker did it step by step.Here is EOSBet contract code.

Please consider the following code functions of EOSBet before it was hacked:

The Hack Number 1:

In expected scenario, contract only accept transfer from eosio.token contract then process it. But what happen if we call transfer() function directly to contract?

Absolutely, this action would be rejected by node because the abi file doesn’t support this function as well as the data parameters. But what happen if we call it via inline function or changing node code to accept calling action directly.

I choose simple way — write a simple hack contract to call inline function as below:

Here is the result after I pushed an action to hackcontract with fake data parameters, then hackcontract would call transfer function to play bet.

$ cleos push action hackcontract hack '["hackcontract", "10000000.0000 SYS", "50"]' -p hackcontract $ cleos get table eosbettest12 eosbettest12 activebets{ "rows": [{ "id": "3931680559943168590", "bettor": "hackcontract", "referral": "eosbettest12", "bet_amt": "100000000000", "roll_under": 50, "seed": "6799ccac87b63a34dcec107a860c2463b84b687ac5a8c88dca6970b58acf6bf1", "bet_time": "2018-11-28T10:10:19" }]}

As you can see the bet order is already in queue.

Solution for the hack number 1:

To fix this bug, just add one line as below code. It means, contract only accept calling transfer function from eosio.token contract, other case would be rejected. Or you can add this check in dispatch function.

The Hack Number 2

The hacker exploited a bug in dispatch transfer function. In this function, contract get packed data from eosio.token but never check transfer from/to. Basically, the hacker has 2 accounts A and hacking contract B. A will send token to B, B catch up this action and forward to eosbet account via require_recipient() . EOSBet would misunderstand that this action is from eosio.token and process it.

Here is the detail code:

After transfer token to hackcontract, the bet order is already in queue.

$ cleos transfer myaccount hackcontract "1234567.0000 SYS" "50" -p myaccount $ cleos get table eosbettest12 eosbettest12 activebets{ "rows": [{ "id": "2817973050780582232", "bettor": "myaccount", "referral": "eosbettest12", "bet_amt": "12345670000", "roll_under": 50, "seed": "e2a32f55658ebfa76a9d335abe532253326b86910b52e1b8c81f319c3d71ab73", "bet_time": "2018-11-29T04:15:27" }]}

Solution for the hack number 2:

Remember to check where transfer to in dispatch transfer().

The common ways to prevent being hacked

Update last patch for “apply” function

Please refer here: https://medium.com/@eoscafeblock/contract-vulnerability-patch-57b948cacc3a

Use different permission in contract with require_auth2

At present, DApp developers usually use require_auth() method to get permission from action sender. In case the action require_auth(self), you should store the private key of contract owner on server if you want to do it automatically. But what happen if the hacker stole this private key, it’s absolutely a disaster…

To solve this issue, you should separate permission level in your contract.

Example: The permission active for important actions (set contract, transfer token …) and this action should be doing manually. The permission code for actions is less important, so it’s more likely to be done automatically, then you can store private key of permission code to server. If someone stole this key, there are nothing to lose.

Make sure that your contract has maintain mode to freeze contract when it notices a bug