Security smart contract - Chapter 2: Common vulnerability

When developing smart contracts, it’s important to be aware of common errors that can arise and potentially cause issues in the future. Some of these errors may seem minor or inconsequential at first but can have serious consequences over time if left unchecked.

Reentrancy

Just image, you are meeting a seashell collector. He offers you a lamp, a pen and a notebook for your seashell at price one seashell or each. You only have one seashell, so you have a strategy to has both of them that is: You show seashell to have seller give you lamp, them picking another two lefts. After has all three items, you hand over your seashell to him then run aways carrying all three items. Of course, you cannot do that in real life but not at all, still can do it in some situations. Blockchain reentrancy is work like that.

Here is the way it happens in Smart contract by vulnerability logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// This contract is vulnerable. Do not use in production

contract Victim {
mapping (address => uint256) public balances;

function deposit() external payable {
balances[msg.sender] += msg.value;
}

function withdraw() external {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call.value(amount)("");
require(success);
balances[msg.sender] = 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
 contract Attacker {
function beginAttack() external payable {
Victim(victim_address).deposit.value(1 ether)();
Victim(victim_address).withdraw();
}

function() external payable {
if (gasleft() > 40000) {
Victim(victim_address).withdraw();
}
}
}
1
2
3
4
5
6
7
8
9
10
- Attacker's EOA calls `Attacker.beginAttack()` with 1 ETH
- `Attacker.beginAttack()` deposits 1 ETH into `Victim`
- `Attacker` calls `withdraw() in `Victim`
- `Victim` checks `Attacker`’s balance (1 ETH)
- `Victim` sends 1 ETH to `Attacker` (which triggers the default function)
- `Attacker` calls `Victim.withdraw()` again (note that `Victim` hasn’t reduced `Attacker`’s balance from the first withdrawal)
- `Victim` checks `Attacker`’s balance (which is still 1 ETH because it hasn’t applied the effects of the first call)
- `Victim` sends 1 ETH to `Attacker` (which triggers the default function and allows `Attacker` to reenter the `withdraw` function)
- The process repeats until `Attacker` runs out of gas, at which point `msg.sender.call.value` returns without triggering additional withdrawals
- `Victim` finally applies the results of the first transaction (and subsequent ones) to its state, so `Attacker`’s balance is set to 0

How to avoid that, here is the solution

1
2
3
4
5
6
7
8
contract NoLongerAVictim {
function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(amount)("");
require(success);
}
}

Or using code block:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.7.0;

contract MutexPattern {
bool locked = false;
mapping(address => uint256) public balances;

modifier noReentrancy() {
require(!locked, "Blocked from reentrancy.");
locked = true;
_;
locked = false;
}
// This function is protected by a mutex, so reentrant calls from within `msg.sender.call` cannot call `withdraw` again.
// The `return` statement evaluates to `true` but still evaluates the `locked = false` statement in the modifier
function withdraw(uint _amount) public payable noReentrancy returns(bool) {
require(balances[msg.sender] >= _amount, "No balance to withdraw.");

balances[msg.sender] -= _amount;
bool (success, ) = msg.sender.call{value: _amount}("");
require(success);

return true;
}
}

Also, you can using full payment to avoid that.

Integer underflows and overflows

Details

How it happen?

An integer overflow occurs when the results of an arithmetic operation falls outside the acceptable range of values, causing it to “roll over” to the lowest representable value. For example, a uint8 can only store values up to 2^8-1=255. Arithmetic operations that result in values higher than 255 will overflow and reset uint to 0, similar to how the odometer on a car resets to 0 once it reaches the maximum mileage (999999).

Integer underflows happen for similar reasons: the the results of an arithmetic operation falls below the acceptable range. Say you tried decrementing 0 in a uint8, the result would simply roll over to the maximum representable value (255).

How to prevent integer underflows and overflows

As of version 0.8.0, the Solidity compiler rejects code that results in integer underflows and overflows. However, contracts compiled with a lower compiler version should either perform checks on functions involving arithmetic operations or use a library (e.g., SafeMath) that checks for underflow/overflow.

Oracle manipulation

Oracles is off-chain source data. It helps smart contract can gain data outside of chain, but also provide a hidden dangerous if data be control by hacker. We called it “oracle problem”.

For example: A DEX using oracle to create a trade system. If hacker know market price source and gain control of it, then lots of money can be drain by large number of moneys be buy in very cheap price thank to flash loans from another platform.

To avoid this situation, you need to make sure your oracle has a trusted mechanism to protect itself. One of the famous oracles is ChainLink that we will have another post to getting knowledge.