0

Ultimate Guide to EOS Smart Contract Security

Ultimate Guide to EOS Smart Contract Security. The crypto community became skeptical when the World’s biggest ICO, EOS launched in June 2018 and it got freezed out for 2 days due to a software bug. But fast forward 4 months and EOS today accounts for more than double the transactions that Ethereum does today. Through the promise of free and faster transactions, the topmost Dapp of EOS has about 13,000 daily active users compared to just 2,000 of Ethereum’s topmost Dapp.

BountyOne Blog: EOS Smart Contract Security

EOS Smart Contract Security

By  Rohan Agarwal

Some general smart contract vulnerabilities are applicable for almost all of the platforms. Like Ethereum, smart contracts written on EOS needs to be audited before going live on the mainnet. Fatal bugs in the contract can get exploited when the contracts are not battletested enough. In this guide, we will help you avoid the common pitfalls on your way to make the next killer dApp on EOS.

Before you read the guide, it is important to know about some prerequisite information regarding EOS development that will come handy while you read the guide. Knowledge of C++ is a must. The best place to start with the smart contract development is EOSIO’s own documentation

Dealing with the ABI Dispatcher

extern "C" {
void apply(uint64_t receiver, uint64_t code, uint64_t action) {
  class_name thiscontract(receiver);

  if ((code == N(eosio.token)) && (action == N(transfer))) {
      execute_action(&thiscontract, &class_name::transfer);
      return;
  }

  if (code != receiver) return;

  switch (action) {     EOSIO_API(class_name, (action_1)(action_n))};
    eosio_exit(0);
}
}

The above image is a sample code of a modified ABI dispatcher. A simpler ABI dispatcher as shown below is used for simpler action handling of the contract.

EOSIO_ABI( class_name, (action_1)(action_n) );

The ABI dispatcher/forwarder allows the contract to listen to incoming eosio.token transfer events, as well as normal interactions with the smart contract. It is important to bind each key action and code to meet the requirements, in order to avoid abnormal and illegal calls.

An example would be the hack that happened to dApp EOSBet Casino due to a bug in their ABI forwarding source code.

if( code == self || code == N(eosio.token) ) {
TYPE thiscontract( self );
switch( action ) {
EOSIO_API( TYPE, MEMBERS )
}
}

The above check in the apply action handler of the ABI forwarding source code allowed an attacker to bypass the eosio.token :: transfer() function completely, and directly call contract :: transfer() function without transferring EOS to the contract before placing the bet. For losses, he was paid nothing, but lost nothing. However, for wins he was paid out real EOS from the contract.

They fixed the bug above by adding a check of eosio.token contract transfer action before the incoming action requests to the contract.

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

It is important to use the statement require_auth(account); into actions that you want only the authorized account to execute. require_auth(_self); is used to authorize only the owner of the contract to sign the transaction

Authorization in Actions

void token::transfer( account_name from, account_name to, asset quantity)
{
   auto sym = quantity.symbol.name();
   require_recipient( from );
   require_recipient( to );
   auto payer = has_auth( to ) ? to : from;
   sub_balance( from, quantity );
   add_balance( to, quantity, payer );
}

The above sample code allows anyone to call the action. In order to resolve it, use require_auth(from); statement to authorize the payer to call the action.

Try to avoid modifying the eosio.token contract

A recent white hat hacker managed to claim 1 billion tokens of a dapp due to a poorly tested method call in their eosio.token contract. The Dapp Se7ens (now inactive) declared a new method inside the eosio.token contract for airdropping their tokens into user accounts. The contract did not call the issue or the transfer action of the eosio.token contract to reflect the changes and hence the funds magically appeared on the accounts of the users. Secondly, they forgot to verify the amount in the method before the transfer which allowed the hacker to claim 1 billion of their tokens in the process.

Apart from changing the maximum supply and the token symbol, it is advisable to avoid modifying it for custom functions as the bugs in the eosio.token contract can be fatal. In order to facilitate an airdrop securely, transfer the airdrop tokens to a separate account and distribute it from there.

Modification of the multi-index table properties

EOS currently stores data onto a shared memory database for sharing across actions.

struct [[eosio::table]] person {
     account_name key;
     std::string first_name;
     std::string last_name;
     std::string street;
     std::string city;
     std::string state;

     uint64_t primary_key() const { return key; }
   };
typedef eosio::multi_index<N(people), person> address_index;

The sample code above creates a multi_index table named people that is based on data structure of a single row of that table using the struct person. EOS currently does not allow modification of the table properties once it gets deployed. eosio_assert_message assertion failure will be the error that will be thrown. Therefore properties need to be completely thought out before deployment of the table. Else, a new table with a different name needs to be created and extreme care needs to be taken when migrating from old table to the new one. Failing to do may result in loss of data.

Numerical Overflow Check

When doing arithmetic operations, values may overflow if the boundary conditions are not checked responsibly enough, causing loss of users assets.

void transfer(symbol_name symbol, account_name from, account_names to, uint64_t balance) {
require_auth(from);
account fromaccount;
eosio_assert(is_balance_within_range(balance), "invalid balance");
eosio_assert(balance > 0, "must transfer positive balance");   uint64_t amount = balance * 4; //Multiplication overflow
}

In the sample code above, using uint64_t to denote user balance can cause overflow when the value gets multiplied. Hence avoid using uint64_t to denote balances and performing arithmetic operations on it as far as possible. Use the asset structure defined in the eosiolib for operations rather than the exact balance which takes care of the overflow conditions.

Taking care of the Assumptions in the Contract

There are going to be assumptions which will require assertions while execution of the contract. Using eosio_assert will take care of the conditions beforehand and stop the execution of the specific action if the assertions fails. As an example –

void assert_roll_under(const uint8_t& roll_under) {
      eosio_assert(roll_under >= 2 && roll_under <= 96,
                   "roll under overflow, must be greater than 2 and less than 96");
  }

 

The assert statement above makes an assumption that roll_under integer is greater than 2 & less than 96. But if it doesn’t, throw the above message and stop the execution. Failing to discover corner cases like the above could become catastrophic for the house setting the rules.

Generating True Random numbers

Generating True Random numbers on the EOS Blockchain is still a risk if not done accurately. Failing to do so correctly will result in an adversary predicting the outcomes, gaming the whole system in the process. Services like Oracalize.it exists to provide random numbers from an external source but they are expensive and a single point of failure. People have used the Blockchain’s contextual variables (block number, block stamp etc.) in the past to generate the random number in Ethereum smart contract, but it has been gamed before. To do the generation correctly, the program has to provide a kind of combined randomness that no single party could control alone. One of the best way possible currently is a method suggested by Dan Larimar himself when generating a random number between two parties.

BountyOne Blog: EOS Smart Contract Security

string sha256_to_hex(const checksum256& sha256) {
  return to_hex((char*)sha256.hash, sizeof(sha256.hash));
}

string sha1_to_hex(const checksum160& sha1) {
  return to_hex((char*)sha1.hash, sizeof(sha1.hash));
}
template <class T>
Inline void hash_combine(std::size_t& seed, const T& v) {
  std::hash<T> hasher;
  seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

The sample code above gives an optimized random number generation between 1 to 100. seed1 is the house seed and seed2 is the user seed above. For reference, Dappub and EOSBetCasino have open sourced their complete contracts with random number generator implementation of a fair dice game between the player and the house (developer).

uint8_t compute_random_roll(const checksum256& seed1, const checksum160& seed2) {
      size_t hash = 0;
      hash_combine(hash, sha256_to_hex(seed1));
      hash_combine(hash, sha1_to_hex(seed2));
      return hash % 100 + 1;
  }

EOSBet recently got hacked again of 65,000 EOS when an adversary tricked their eosio.token contract to send EOS to his wallet whenever he transacted between his own wallets. The eosio.token contract code notifies both the sender and the receiver of EOS tokens that there are incoming tokens. To imitate the behavior & facilitate the hack, the adversary created two accounts, let’s assume A & B. A had a smart contract with an action having statement require_recipient(N(eosbetdice11)). When A facilitated the transaction from A to B through the action call, it notified the transfer function in the contract as if the call had come from eosio.token contract. Since there was no real transfer of EOS into the contract, whenever the hacker lost a bet, he lost nothing, but he was rewarded when won the bet. Hence, checking only the contract name and action name is not sufficient.


Checks on notifications from Contracts

To mitigate the issue, the function should check whether the contract is indeed the receiver of the tokens or not.

eosio_assert(transfer_data.from == _self || transfer_data.to == _self, "Must be incoming or outgoing transfer");

 

What are the best practices one should follow while developing a smart contract on EOS?

Bugs are inevitable part of any software. Its consequences gets amplified in a decentralised environment especially if it involves transaction of value. Apart from the EOS specific safeguards discussed above, here are some of the general precautions and best practices new smart contract developers should keep in mind –

  1. Always audit the contract independently from third party smart contract auditing firms before releasing on mainnet.
  2. Do the necessary Caveman debugging (only way to debug the contract currently) of the contract before releasing to the testnet. EOSIO documentation has a great guide for it.
  3. Set limit transfer rate on withdrawals to avoid excessive losses on initial days of mainnet launch. Have bug bounty program for responsible disclosure by white hat hackers.
  4. Have a killswitch to freeze the contract when a bug is detected.

To implement it, we persist a flag in the multi_index table. We set the flag using an action which can be called only by the owner of the contract. And then we check on every public action whether the flag is set to be frozen or not. A sample implementation of the function is given below.

struct st_frozen {
  uint64_t frozen;
};

typedef singleton<N(freeze), st_frozen> tb_frozen;
tb_frozen _frozen;

uint64_t getFreezeFlag() {
   st_frozen frozen_st{.frozen = 0};
   return _frozen.get_or_create(_self, frozen_st);
}

void setFreezeFlag(const uint64_t& pFrozen) {
  st_frozen frozen_st = getFreezeFlag();
  frozen_st.frozen = pFrozen;
  _frozen.set(frozen_st, _self);
}

// public Action
void freeze() {
   require_auth(_self);
   setFreezeFlag(1);
}

// public Action
void unfreeze() {
   require_auth(_self);
   setFreezeFlag(0);
}
// any public action
void action(...) {
   eosio_assert(getFreezeFlag().frozen == 1, "Contract is frozen!");
   ...
}
  1. Keep updated about the security enhancements in libraries or vulnerabilities disclosures on the platform. Update you libraries when necessary immediately.
  2. Open source the contract code at the least so that fairness is maintained in the game and indie developers could help spot bugs much quicker.

EOS Smart Contract Security: Conclusion

It has only been 5 months since EOS launch yet it has grown way past the expectation. The trade-offs that it has made – DPOS, mutable smart contracts, 21 mining nodes etc. have certainly faced heavy criticism from decentralization maximalists. Nevertheless, it has not stopped dApps based on Ethereum to shift to EOS given the scalability that the platform offers them today. Whether it is EOS or Ethereum that wins the war is yet to be decided, but EOS has certainly won the battle. And it is going to remain the same till Ethereum manages to reach the scalability the world needs to the run “The World Computer”.

_________________________________________________________________________________________

This article was written by Rohan Agarwal

Bio – #Android Dev # Entrepreneur # Blockchain Dev & Researcher Co-founder @ Cypherock.com – A secure hardware wallet for Smartphones.

Linkedin – https://www.linkedin.com/in/rohanagarwal94/
Github – https://github.com/rohanagarwal94
Twitter – https://twitter.com/rohanagarwal94

Like what you read? Give us one like or share it to your friends

2
0

Related Guides

Join Blockgeeks

Create an account to access our exclusive point system, get instant notifications for new courses, workshops, free webinars and start interacting with our enthusiastic blockchain community. Don’t miss out and join right now!

Already have an account? Sign In

Comments

There are no comments yet