[Note] — If you’re directly reading this blog without actually working on the challenge, It doesn’t help you. I request you to try your best and come back if you face any difficulties.
Introduction:
Hello Hackers🥰, Welcome back to our world of Web3 Security. Today We’ll discuss the first challenge from the “Math Section” — Token Sale. This Challenge is rated as Low-Medium in my perspective. However, It may differ from person to person.
What do you need to know before attempting this challenge?
My First and Foremost suggestion is to go through this video from “Smart-Contract-Programmer” who explained the “Overflow and Underflow” vulnerability in detail.
Now let’s jump into our Challenge :
The Question Says,
“This token contract allows you to buy and sell tokens at an even exchange rate of 1 token per ether.
The contract starts off with a balance of 1 ether. See if you can take some of that away. ”
To Explain in Simple Terms, This is an Exchange contract where we can sell and buy the tokens at some specific ratio(1:1).
- For Example, If I want to buy 10 Tokens, I need to pay 10 ETH to the Exchange. In the Same Way, If I’m holding 100 Tokens and on selling them, I get 100 ETH into my account.
- Initially the contract holds 1 ETH and our target is to make the contract balance less than 1 ETH.
Solidity ^0.8.0 Documentation
“If you clearly observe on the first line it says that, For uint32 — the maximum value it can hold upto is 2³² — 1.”
- Now What if we increase this value by 1? Simple, It reverts to Zero.
Contract Review:
pragma solidity ^0.4.21;
contract TokenSaleChallenge {
mapping(address => uint256) public balanceOf;
uint256 constant PRICE_PER_TOKEN = 1 ether;
function TokenSaleChallenge(address _player) public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance < 1 ether;
}
function buy(uint256 numTokens) public payable {
require(msg.value == numTokens * PRICE_PER_TOKEN);
balanceOf[msg.sender] += numTokens;
}
function sell(uint256 numTokens) public {
require(balanceOf[msg.sender] >= numTokens);
balanceOf[msg.sender] -= numTokens;
msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
}
}
First, There are 3 functions(excluding the Constructor) namely “isComplete(), buy(), sell()”. If you look into the state variables, there is a mapping which stores the number of tokens each address holds. And constant variable which is set to 1 ether.
If you’re good at Solidity, you can also see that “there are no traces of Reentrancy vulnerability”. You should remember that “All versions before 0.8.0 are vulnerable to Overflow/Underflow issues unless they use Math Library.”
In the buy() function, the input it takes “uint256″ which means the highest value it can hold is ( 2²⁵⁶ — 1 ). As you know, the contract works on a 1:1 ratio, we cannot send that large amount of ether.
[Note] — EVM(Ethereum Virtual Machine) converts any value to its low-level value which means “1 eth is converted to a low-level value of 10¹⁸”
- Now let’s get back to our statement. However, we cannot send uint256 value but what if we send this instead? [Explained in the End]
: 2²⁵⁶ — 1 / 10¹⁸ + 1 = 115792089237316195423570985008687907853269984665640564039458
On Passing this value on buy() function, it multiply again by 10¹⁸ which results in overflow issue. But here, We need to send the exact amount of eth in wei to make this successful which is 415992086870360064 wei(0.4 eth)
Time to Exploit:
- Call the buy() function with the value as 115792089237316195423570985008687907853269984665640564039459 and send msg.value as 415992086870360064 wei.
- Now if you check your balance, it turns out to be the largest number(uint256 value).
- To complete this section, sell 1 ETH which finally makes the contract hold less than 1 ETH.
Any specific reason?
You may ask why should we pass the exact value. How did you get that specific value? Why it won’t work even if you give a number that is less than 1? — Here is the answer
A uint256 variable has a maximum value of 2256–1. So to make msg.value overflow, we need to ensure that numTokens multiplied by 1018 is greater than 2256–1. To get that value, we divide 2256–1 by 10**18, giving us: 115792089237316195423570985008687907853269984665640564039458(round value)
We need to figure out the amount that msg.value overflows so we can send the correct amount of ether with the call. To calculate the amount of it overflows, subtract 2**256–1 from the incredibly large number we just calculated. Doing so leaves us with 415992086870360064 wei, the amount of wei we need to send when we call buy().
When can I see you again?
Ummm, Most probably I’ll try to be consistent but I can’t promise as my Board exams are really close enough. I’ll be waiting for your love and support and let’s learn and grow together 🙂
“Feel Free to Reach Me” ~
Instagram — @Gowtham_Ponnana
Twitter — @Gowtham_Ponnana
LinkedIn — Gowtham-Ponnana
Mail — gowtham.official45@gmail.com