Beginners Guide to Smart Contract Auditing: Part 1

Beginners Guide to Smart Contract Auditing: Part 1

Table of Contents

Read Time: 9 minutes

Welcome to the Beginners guide to smart contract auditing! One of the best ways to get started with smart contract auditing is to jump in and look at a few common types of vulnerabilities in smart contracts.

It would be helpful if you already have a basic understanding of Ethereum’s Solidity programming language. As we will look at some of the codes written by Noob solidity programmers.

Re-Entrancy Attack

Real-World Scenario:

Imagine you have 50 chocolates. You have a naughty little sister whom you have allowed to take only 2 chocolates from you at any single time. You also don’t want to give more than 10 chocolates to her in a day, fearing she’ll get tooth decay. To ensure this, every evening you count how many chocolates are remaining with you. Do you think this would work? Or would you get hacked by your little sister?

Unfortunately, it won’t work! Your sister figures out that you are unaware of how many chocolates you have until it is evening. So the very next day, your little sister visits you 6 times before evening and takes 2 chocolates each time! This is what we call a Re-entrancy attack.

Here you are updating the count of chocolates you have at evening instead of updating the count every time your sister takes 2 chocolates from you. This is also what happens with smart contract. The smart contract assumes a particular balance while the attacker is actually busy withdrawing some amount of crypto from the contract multiple times.

Real-World Code Example:

This code belongs to a smart contract called Unbanked. Anyone can withdraw ether from an Unbanked contract as long the balances of the msg.sender (i.e. the caller of withdraw function ) is greater than or equal to the amount asked to withdraw.

function withdraw(uint _amount) {
	require(balances[msg.sender] >= _amount);
	msg.sender.call.value(_amount)();
	balances[msg.sender] -= _amount;
}

Notice that there is a call keyword used to send the required amount of ether to the msg.sender. An attacker can exploit this by creating a contract called Thief in which he calls the withdraw function in a fallback() function. A fallback() function in Solidity is a special function that gets executed when ether is sent to the smart contract.

This means that an attacker is able to recursively call the withdraw function. Thus, before the smart contract updates, the balances of msg.sender at the last line of code, the attacker has already withdrawn ether multiple times. This could be avoided if the balances are updated before using the call keyword, thus following a checks-effects-interactions pattern.

Impact:

The first-ever Reentrancy Attack happened in 2016 on a DAO (Decentralized Autonomous Organization) that resulted in an approximately $50 Million hacks. To reverse this hack, the Ethereum community split the Ethereum blockchain which gave rise to ETC (Ethereum Classic) and ETH (Ethereum).

Arithmetic Overflow and Underflow

Real-World Scenario:

Let’s play a thought game. It consists of a Spin-the wheel, and the winner is decided based on the largest number he is able to get on spinning the wheel. The wheel is marked all over from 256 to -256.

The rules for the game is that the pointer for all the players rests on 0 at the beginning of each spin. And a player is allowed to spin only in the direction of negative numbers. How will you win this game?

A good strategy to win this game every time would be to spin the wheel with such power that the wheel spins upto -256 and then turns to 256 in one go. This is possible because 256 comes just after -256 on the wheel. This is what we call an arithmetic underflow. And arithmetic overflow is just vice versa of this.

Real-World Code Example:

An underflow or overflow happens when an arithmetic operation reaches its minimum or maximum.

function withdraw(uint _amount) public {
        require(balances[msg.sender] - _amount > 0);
        address payable to = payable(msg.sender);
        to.transfer(_amount);
        balances[msg.sender] -= _amount;
}

The _amount parameter of the withdraw function is an unsigned integer. The value of the balances mapping (which is like a dictionary in python or a key-value pair in C++ or Java) is also an unsigned integer.


mapping(address => uint256) public balances

The required statement checks whether the balances of msg.sender is positive or not. But this statement will always be true even if the amount is greater than the balances of msg.sender. This is because both the balances and _amount variables are of type unsigned integer and their arithmetic result (after underflow) will also be an unsigned integer!

And as you may recall, an unsigned integer is always positive. This means that an attacker is able to withdraw an unlimited amount of Ether from the smart contract! You can find a detailed example and implementation code for this vulnerability here.

Another crucial thing to note here is that the arithmetic operation between say two unsigned integers is also an unsigned integer. It can be dangerous if this is overlooked in smart contracts, as it can result in unwanted security breaches!

function votes(uint postId, uint upvote, uint downvotes) {
	if (upvote - downvote < 0) {
		deletePost(postId)
	}
}

As you might have noticed in the above example, the if statement is quite pointless as upvote - downvote is always going to be positive. And the post will get deleted even if downvotes is greater than upvotes. To avoid such attacks, it is recommended to use a Solidity compiler version greater than 0.8.0.

Impact:

A coin called PoWH coin was launched in 2017. Although it was a Ponzi game, it itself got hacked due to an arithmetic overflow bug that resulted in a loss of about 866 ETH or $950,000 at that time. You can read about this in detail here.

Must Read: Lessons From The Attack On Tinyman, Largest DEX On Algorand

Denial of Service Attack

Real-World Scenario:

Imagine you are in a Bitcoin Tech University. Everything seems fine except that there is a common dining table for everyone. And unfortunately, there are few people from another class who always manage to occupy the dining table before anyone from your class.

In the practical scenario, they are denying the essential service to everyone resulting in precious time’s loss. This is what we call a ‘Denial of Service attack’.

Real-World Code Example:

In the game called King-of-Ether, anyone can become a king. But the rule to become king is that a person should deposit more ether than the current king. This can be done by calling the claimThrone() function of the King of Ether contract in which the person sends ether directly to the previous king and becomes the new king.

function claimThrone() external payable {
        require(msg.value > balance, "Need to pay more to become the king");

        (bool sent, ) = king.call{value: balance}("");
        require(sent, "Failed to send Ether");

        balance = msg.value;
        king = msg.sender;
    }

As you might have guessed, this code is vulnerable to a DoS attack, but how? For this, you’ll need to understand that there are two types of addresses in Ethereum- first is the address of an externally owned account or simply the address of a wallet, and the second is the contract address. Now the ether can be sent from either of these address types.

If this, ether is sent by the contract address, then the contract will become the king. But let’s assume that this new contract does not have a fallback() function that is necessary if the contract wants to accept ether. Then if a new person comes along and tries to call the claimThrone() function, it will always fail!

Notice that this also happens partly because the claimThrone() function explicitly checks whether the transfer of ether was successful or not in the second required statement. You can find the complete code and do a DoS attack on it here.

It is also possible for a code to be vulnerable to a DoS attack if the code has a loop over an array of large sizes. This happens because the gas limit can be exceeded in such cases. You can read about it here.

Impact:

A game called GovernMental, which was apparently a Ponzi scheme, got stuck with 1100 ether because a large amount of gas was needed to process the payout.

Insecure Randomness

Real-World Scenario:

Once there was a man named Hesky who was always accompanied by his monkey Pesky. Hesky conducted lottery games and made good profits. One day Alice noticed Hesky staring intently at his monkey Pesky. Then she saw him writing something on a piece of paper and sealed it in an envelope. Curious, she decided to investigate further.

Later that evening, Alice saw that the winner of the lottery was decided by publicly opening the sealed envelope. After watching him for a few days, Alice figured out that the Hesky decided the winning lottery number by looking at Pesky’s gestures(for example if the monkey scratched his head, Hesky wrote down 10)! Now Alice had the formula to win each lottery and just had to buy the lottery ticket with the right number!

Hesky had assumed that his “random” way of deciding the winner of the lottery can never be figured out, but he was indeed incorrect.

Real-World Code Example:

In this example, a random number is generated based on the hash of the combination of a block’s number and its block timestamp. this hash is then assigned to the answer variable. Now anyone who guesses this (seemingly) random number, is rewarded 1 Ether. Do you think this is unhackable?

function guess(uint _guess) public {
        uint answer = uint(
            keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
        );

        if (_guess == answer) {
            (bool sent, ) = msg.sender.call{value: 1 ether}("");
            require(sent, "Failed to send Ether");
        }
    }

Nope! An attacker can still guess this random number by simply copy-pasting the code to generate the value assigned to the answer variable, and pass the same answer variable to the guess() function!


guessTheRandomNumber.guess(answer);

You can find the complete code here. To avoid this attack, it is recommended to use a Verifiable Random Function such as the Chainlink VRF.

Impact:

About 400 ETH was lost due to an attack on the Smart Billions lottery contract. Surprisingly, even the contract lottery itself was a Ponzi scheme (ouch!).

Time manipulation

Real-World Scenario:

Satoshi loves to eat cookies. He loves all types of cookies that his mother makes. But his mother is very strict and feels that eating too many cookies is not good for him. So his mother makes a rule that he will get the cookies only at 8 pm.

That very day at 7:45 pm, Satoshi runs to his mother and asks for cookies. His mother enquires- “What time is it?”

“It is 8’ O clock!” – he replies.

“Okay. Then take the cookies from my cupboard.”

And thus, Satoshi was able to manipulate time successfully by 15 minutes so that he could get his cookies! What a cookie-hungry chap!

Real-World Code Example:

The timestamp of a block can be manipulated by about 15 seconds by a miner. In this way, a miner can set a favorable timestamp and include his transaction in that same block he mines. The function play() belongs to a Game contract called G-Dot.

function play() public {
	require(now > 1640392200 && neverPlayed == true);
	neverPlayed = false;
	msg.sender.transfer(1500 ether);
}

This contract rewards 1500 ether to the player who is the first to call the play function. But as you can see, the play function can only be called if the now or block.timestamp of the transaction that contains the call to the play() function, is greater than the epoch time 1640392200.

A miner can easily manipulate this timestamp and include his transaction of calling the play() function in the same block such that he himself is the first player. In this way it is guaranteed that the miner will win the game!

Impact:

The block.timestamp was used to generate random numbers in the Governmental and was thus vulnerable to time manipulation attacks.

Follow QuillAudits for more updates.

Twitter | LinkedIn Facebook | Telegram

2,188 Views

Related Articles

Leave a Comment

Your email address will not be published.

#HashingBits | Week-25 📮

‣‣ @harmonyprotocol hacked leaving with $100M stolen funds

‣‣Yet another Flashloan attack on PandoraChain DAO for $128k

📧Subscribe to #HashingBits:
https://quillaudits.substack.com/p/the-harmony-heist-us-100m-stolen

#newsletter | #Web3 | #cybersecurity | #solidity | $BTC | $ETH

📉"Bull" & "Bear" are now commonly used in the #crypto world.

But these terms did not originate there.

These terms were first used in traditional #stockmarket.

If you’re still unable to figure out differences between the Bull & Bear market, check out our versus series below.

⚠️🚨⚠️

FOUR #Ethereum DeFi projects fell to #DNS attack—

‣‣@ConvexFinance

‣‣@ribbonfinance

‣‣@DeFiSaver, and

‣‣@Allbridge_io

🧵Front-end websites of #DeFi protocols are becoming primary targets of attackers.

𝗥𝗧 to spread the word!

More👇👇

🚀 What gave the yellow metal- Gold all the hype and financial value?

One obvious reason is its Rarity.

Experts determine the Stock to flow ratio to find the rarity of precious metals like Gold or Platinum.

🔽↓↓🔽

#Bitcoin $BTC #Cryptocurency $ETH

⚠️🚨⚠️

The $100M @harmonyprotocol exploit

‣The reason behind vulnerability is #Harmony’s multi-signature structure to approve transactions

#Harmony used 2-5 multi-sig wallet where only two signatures were required to approve the transfer from the #bridge

#HarmonyONE $ONE

Load More...

Inverse Finance hacked again for $1.2M⚠️In ‘Optimism’ Tokens 🚨

Inverse Finance’s Frontier money market was subject to an oracle price manipulation incident.It resulted in a net loss of $5.83 million in $DOLA, with the attacker earning a total of $1.2 million. 

Become a Quiffiliate!
Join our mission to safeguard web3

Sounds Interesting, Right? All you have to do is:

1

Refer QuillAudits to Web3 projects for audits.

2

Earn rewards as we conclude the audits.

3

Thereby help us Secure web3 ecosystem.

Total Rewards Shared Out: $150K+