Smart Contract Security Guide

How to audit a smart contract

To teach you how to audit, I'll audit a contract I wrote myself. This way, you can see a real-world audit that can be done on your own.

Now you may be asking: what exactly is auditing of smart contracts?

Smart contract auditing is the process of scrutinizing code, and in this case, finding bugs, bugs, and risks before a Solidity contract is deployed and used on the Ethereum main network; because once published, the code can no longer be modified. This definition is for discussion purposes only.

Note that an audit is not a legal document that verifies the security of your code. No one can be 100% sure that the code will not be buggy or buggy in the future. This is just to ensure that your code has been edited by experts and is basically safe.

Discuss possible improvements, primarily to identify risks and vulnerabilities that could compromise a user's ether.

Ok, now let's take a look at the structure of a smart contract audit report:

  1. Disclaimer:  Here you will say that an audit is not a legally binding document, it does not guarantee anything. This is just a discussion document.
  2. Audit overview and good features:  Quickly view smart contracts that will be audited and find good practices.
  3. Attacks on contracts:  In this section, you will discuss attacks on contracts and the consequences. This is just to verify that it is actually safe.
  4. Critical vulnerabilities found in contracts:  Critical issues that could seriously compromise the integrity of the contract. Those are serious problems that would allow attackers to steal ether.
  5. Moderate vulnerabilities found in contracts:  Those that could harm the contract but have limited harm. Like a bug that allows people to modify random variables.
  6. Low-severity vulnerabilities:  These issues do not actually harm the contract and may already exist in the deployed version of the contract.
  7. Line-by-Line Commentary:  In this section, you will analyze the most important lines of statements with potential improvements.
  8. Audit Summary:  Your opinion on the contract and the final conclusion about the audit.

Keeping this struct description in a safe place is all you need to do to securely audit a smart contract. It will really help you find those hard-to-find bugs.

I suggest you start with point 7 "Comment line by line" because when analyzing the contract line by line, you will find the most important problems, you will see what is missing and what should be changed or improved.

Later on, I'll show you a disclaimer that you can use as the first step in an audit. You can watch from point 1 until the end.

Next, I'll show you the results of an audit done using such a structure, which I did for a contract I wrote myself. You'll also see an introduction to the most important possible attacks on smart contracts in point 3.

Casino Contract Audit

You can see the audited code on my Github: https://github.com/merlox/casino-ethereum/blob/master/contracts/Casino.sol

The following is the audit report of my contract Casino.sol:


Preamble

The following will be included in this smart contract audit report:

  1. Disclaimer
  2. Audit overview and good features
  3. Attack on the contract
  4. Critical vulnerabilities found in contracts
  5. Moderate vulnerabilities found in contracts
  6. low severity vulnerabilities
  7. line-by-line commentary
  8. Audit summary

1. Disclaimer

The audit makes no representations or warranties about the usefulness of the code, the security of the code, the suitability of the business model, the regulatory regime of the business model, or any other statement about the suitability of the contract and the behavior of the contract in an error-free state. Audit documentation is for discussion purposes only.

2. Audit overview and good features

The project has only one file with 142 lines of Solidity code  Casino.sol . All functions and state variable comments are in the standard specification format .com/ethereum/wiki/wiki/Ethereum-Natural-Specification-Format ]), which can help us quickly understand how the program works.

The project implements the Oraclize API using a centralized service to generate truly random numbers on the blockchain.

Translator's Note:
Oraclize is an independent service that provides data for smart contracts and blockchain applications. Official website: [ http://www.oraclize.it ]. Because blockchain applications such as Bitcoin scripts or Ethereum smart contracts cannot directly obtain off-chain data, a service that can provide off-chain data and interact with the blockchain is required. Oraclize can provide price information similar to asset/financial applications, weather information that can be used for peer-to-peer insurance, or random number information required for wagering contracts.
This refers to the introduction of an open source Solidity codebase that implements the Oraclize API in the source code of this project.

Generating random numbers on a blockchain is a rather difficult subject because one of Ethereum's core values ​​is predictability, and the goal is to ensure that there are no undefined values.

Translator's Note:
The reason why it is said that it is difficult to generate random numbers on the blockchain is that, no matter what algorithm is used, it is necessary to use a timestamp as the "seed" for generating random numbers (because the timestamp is the only one in the computer field. It can theoretically guarantee a "non-repeating" value); and obtaining a timestamp in a smart contract can only rely on a node (miner) to do it. That is to say, the timestamp obtained in the contract is determined by the local time of the computer of the node (miner) running its code; so the trustworthiness of this node (miner) becomes the biggest issue. In theory, this local time can be forged by malicious programs, so this method is considered "unsafe". The common practice is to use an off-chain third-party service, such as Oraclize used here, to obtain random numbers. Because Oraclize is a public basic service and does not "fake" against a specific contract, this can be considered "relatively safe".

Because random numbers can be generated off-chain using Oraclize, it is considered good practice to use it to generate trusted numbers. It implements modifiers and a callback function to verify that the information comes from a trusted entity.

The purpose of this smart contract is to participate in random draws where people place bets between 1 and 9. When 10 people bet, the prize is automatically distributed to the winner. Each user has a minimum wagering amount.

Each player can only place one bet per game, and a winner number will only be generated when the required number of participants is reached.

Excellent features

This contract provides a nice set of functional code:

  • Use Oraclize to generate secure random numbers and validate in callbacks.
  • Modifiers check for end game conditions, blocking key functions until rewards are distributed.
  • More checks are made to verify that the use of the betting function is appropriate.
  • The winning number is safely generated only when the maximum number of bets is reached.

3. Attacks on contracts

To check the security of the contract, we tested various attacks to ensure that the contract is secure and follows best practices.

Reentrancy attack

This attack extracts the ether in the contract by recursively calling the call.value() method in the ERC20 token. If the user updates the sender's balance (ie account balance, translator's note) after sending the ether, the attack will take effect.

When you call a function to send ether to the contract, you can use the fallback function to execute the function again until the ether is withdrawn from the contract.

Since the contract uses  transfer() not  call.value() , there is no risk of a reentrancy attack; because the transfer function is only allowed to use 2300 gas, which is only enough to generate event log data and throw an exception on failure. This makes it impossible to recursively call the sender function, avoiding reentrancy attacks.

Since the transfer function is only called once at the end of each game to distribute the reward to the winner, reentrancy attacks won't cause any problems here.

Please note that the condition for calling this function is that the number of bets is greater than or equal to 10 times, but this number of bets  distributePrizes() will only be reset to 0 at the end of the function, which is risky; because theoretically it can be cleared after the number of bets The function is called before zero and performs all logic.

So my suggestion is to update the condition at the beginning of the function, set the number of bets to 0, to make sure that  distributePrizes()it doesn't have any real effect when called more times than expected.

Over and under flows

An overflow occurs when the value of a variable of type uint256 exceeds the upper limit of 2^256 (ie 2 to the 256th power, Translator's Note). The result is that the variable value becomes 0, not greater.

For example, if you try to assign a variable of type unit to a value greater than 2^256, it will simply become 0, which is dangerous.

On the other hand, when you subtract a number greater than 0 from the 0 value, underflow occurs. For example, if you subtract 1 from 0, the result will be 2^256, not -1.

This is very dangerous when dealing with ether; however there is no subtraction in this contract, so there is no risk of underflow.

The only time overflow can occur is when you call  bet() bet on a number, and  totalBet the value of the variable will increase accordingly:

 

totalBet += msg.value;

Someone could send a large amount of ether and cause the cumulative result to exceed 2**256, which would make totalBet 0. This is of course unlikely to happen, but the risk is there.

So I recommend using a library like  OpenZeppelin's SafeMath.sol  . It can make your computing processing safer, avoid the risk of overflow (overflow or underflow).

You can import it to use it, activate it for the uint256 type, and use  .mul() ,  .add() ,  .sub() and  .div() these functions. E.g:

import './SafeMath.sol';
contract Casino {
    using SafeMath for uint256;
    function example(uint256 _value) {
        uint number = msg.value.add(_value);
    }
}

Replay attack

A replay attack is an attack that initiates a transaction on a blockchain like Ethereum and then repeats the transaction on another chain like Ethereum Classic. (That is, after creating a transaction on the main chain, repeat the same transaction on the forked chain. Translator's Note.)

Ether will be transferred from one chain to another just like a normal transaction.

Based on EIP 155 proposed by Vitalik Buterin [ https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md ], starting from version 1.5.3 of Geth and version 1.4.4 of Parity, Protection against this attack has been added.

Translator's Note:
EIP, the Ethereum Improvement Proposal (Ethereum Improvement Proposal), the official address [ https://github.com/ethereum/EIPs ] is a standard specification document of the Ethereum platform jointly maintained by the Ethereum community, covering Basic protocol specification, client API and contract standard specification, etc.

Therefore, users who use the contract need to upgrade the client program themselves to ensure the security against this attack.

Reordering attack

This attack is when a miner or other party attempts to "compete" with smart contract participants by inserting their own information into a list or mapping, giving the attacker the opportunity to store their own information into the contract middle.

When a user uses the  bet() function to place a bet, since the actual data is stored on-chain, anyone can  playerBetsNumber see the bet number simply by calling the mapping of the public state variable.

This mapping is used to represent the numbers each person has chosen, so, combined with the transaction data, you can easily see how much ether they each bet. This can happen in a  distributePrizes() function because it's called in the callback of the random number generation process.

Because the conditions under which this function works are not reset until it ends, there is a risk of a reordering attack.

So my advice is as I said before:  distributePrizes() reset the bet count at the beginning of the function to avoid unintended behavior.

Short address attack

This attack was discovered by the Golem team against ERC20 tokens:

  • It is not difficult for a user to create an empty wallet, it is just a string of characters, for example: [0xiofa8d97756as7df5sd8f75g8675ds8gsdg0]
  • Then he uses the address with the last 0 in the address removed to buy tokens: that is, using [0xiofa8d97756as7df5sd8f75g8675ds8gsdg] as the receiving address to buy 1000 tokens.
  • If there is enough balance in the token contract and the function to buy tokens does not check the length of the sender address, the Ethereum virtual machine will add 0 to the transaction data until the packet length meets the requirements
  • The Ethereum Virtual Machine returns 256,000 tokens for every 1,000 token purchase. This is a virtual machine bug and still not fixed. So if you are a developer of a token contract, make sure to check the address length.

But because our contract is not an ERC20 token contract, this attack cannot be applied.

You can refer to this article [ http://vessenes.com/the-erc20-short-address-attack-explained/ ] for more information on this attack.

4. Serious vulnerabilities found in contracts

No serious vulnerabilities were found in the audit.

5. Moderate vulnerabilities found in contracts

checkPlayerExists() Should be a constant function, but in fact it is not. So this increases the gas consumption of calling this function, which can be a big problem when there are a lot of calls to this function.

It should be changed to a normal function to avoid expensive gas-consuming executions.

Translator's Note:
A constant function in the Solidity language refers to a function that does not change the contract state at runtime, that is, a function that does not change the value of a contract-level state variable. Because changes to state variables are saved to the chain, changes to state variables consume gas (to pay miners), which is very expensive. In this example, because the  checkPlayerExists() function accesses the state variable  playerBetsNumber to determine whether someone has placed a bet, although this is a contract-level variable, the function does not change its value, so the function should be declared  constant to save its gas consumption.

6. Low Severity Vulnerabilities

  • You  used  not   at the beginning  of __callback() functions and  functions .pay()assert()require()

assert() and  require() is basically the same, but the assert function is generally used to check after changing the contract state, and the require is usually used at the beginning of the function to check the input parameters.

  • You define a contract-level variable players, but you don't use it anywhere. Remove it if you don't plan to use it.

7. Line-by-line commentary

  • Line 1: You use a caret (^) in the version pragma version to specify a  0.4.11newer version of the compiler.

This is not a good practice. Because major version changes may destabilize your code, I recommend using a fixed version such as '0.4.11'.

  • Line 14: You define a  variable of uint type  totalBet , the variable name is inappropriate because it holds the total value of all bets. I recommend using  totalBets as variable name instead  totalBet .
  • Line 24: You define a constant variable in capital letters, which is a good practice to let people know that this is a fixed, immutable variable.
  • Line 30: Like I mentioned before, you define an unused array  player . Remove it if you don't plan to use it.
  • Line 60: The function  checkPlayerExists() should be declared as  constant . Since it doesn't change the contract state, declaring it to  constant save gas every time it runs.

Even though functions are public by default, it is still a good practice to explicitly assign the type to the function to avoid any confusion. Here you can add the public declaration exactly at the end of the function declaration.

  • Line 61: You didn't check that the input parameters  player were passed in normally and in the correct format. Make sure to use a  require(player != address(0)); statement at the beginning of the function to check if the incoming address is 0. Just in case, it is best to also check whether the length of the address meets the requirements to deal with short address attacks.
  • Line 69: It is also recommended to  bet() add the visibility keyword to the function  public to avoid any confusion as to how the function should be used.
  • Line 72: Use  require() to check for function input arguments, instead of  assert() .

Likewise, at the beginning of a function, it is generally used more often  require() . Please change all used at the beginning of the function  assert() to  require() .

  • Line 90: You use a  msg.value simple total of pairs, which can cause overflow when value is large. So I suggest that you check for overflow every time you operate on a value.
  • Line 98:  generateNumberWinner() Should be a  internal function, because you definitely don't want anyone to be able to execute it from outside the contract.

Translator's Note:
In the Solidity language,  internal the effect of the keyword is basically the same as the protected type in object-oriented languages ​​such as C++ and Java. The function or state variable defined by this keyword is only used in the current contract and the subcontracts of the current contract ( contacts deriving from this contract) can be accessed. private The keyword is the same as this keyword in other languages, and the function or state variable defined by it can only be accessed in the current contract.

  • Line 103: You  oraclize_newRandomDSQuery() store the result of the function in a variable of type bytes32. This is not required to call the callback function, and you are not using the variable elsewhere, so I recommend not using a variable to store the return value of this function.
  • Line 110: The  __callback() function should be declared as  external , since you only want it to be called from outside.

Translator's Note:
In Solidity, there is a difference between function keywords  public and  external gas consumption. Because  public the function can be called both outside the contract and inside the contract, the virtual machine allocates memory for it at runtime and copies all the variables it uses. However  external , the function can only be called from outside the contract, and its call will directly obtain parameters from calldata (that is, the binary bytecode data of the function call), and the virtual machine will not allocate memory for it and copy the variable value, so its gas consumption is higher than  public function is much lower.

  • Line 117:  assert() should be used  here require() , as I explained earlier.
  • Line 119: You use a  sha3() function, but it's not a good practice. The actual algorithm uses keccak256, not sha3. So I suggest here to use more explicitly instead  keccak256() .
  • Line 125: The  distributePrizes() function should be declared as  internal .

Translator's Note:
This function is the same as the function on line 98  , it is OK to generateNumberWinner() declare as  internal or  private . The only difference is that you don't want them to be available in subcontracts.

  • Line 129: Although you're using a variable-length array size here to control the number of loops, it's not too bad because the number of winners is limited to less than 100.

8. Audit Summary

Overall, the code for this contract is well commented, clearly explaining the purpose of each function.

The mechanics of staking and distributing rewards are very simple and don't pose a big problem.

My final suggestion is that you need to pay more attention to the visibility declaration of the function, as this is very important for the question of who should execute the function. Then there are best practices that need to be considered in coding  assert ,  require and  keccak used.

This is a secure contract that keeps funds safe for the duration of its operation.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324970855&siteId=291194637