Assert to check Invariants¶
Assert is used to check invariants. Those are states our contract or variables should never reach, ever. For example, if we decrease a value then it should never get bigger, only smaller.
Let's change a few things in our Smart Contract to add an integer roll-over bug that we can easily trigger.
Bug
This contract has an intentional limitation, which we will use to trigger a bug. To subsequently fix it.
//SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
contract ExceptionExample {
mapping(address => uint8) public balanceReceived;
function receiveMoney() public payable {
balanceReceived[msg.sender] += uint8(msg.value);
}
function withdrawMoney(address payable _to, uint8 _amount) public {
require(_amount <= balanceReceived[msg.sender], "Not Enough Funds, aborting");
balanceReceived[msg.sender] -= _amount;
_to.transfer(_amount);
}
}
- Deploy a new Contract Instance
- Enter 257 Wei into the Value field
- Hit "receiveMoney"
The Transaction goes through. Let's check our balance, we should have 257 Wei, or?
That's only 1 Wei. Where is the rest? What happened? 🤔
We are storing the balance in an uint8. Unsigned integers go from 0 to 2^n-1, so that's 2^8-1 or 255. So, it can store a max of 255 Wei. We sent 257 Wei to the contract. It automatically rolls over to 0. So, we end up with 257 - 255 - 1 (the 0 value) = 1.
How can we fix it?
Well, the obvious way here is to increase the uint8 to a uint256. But that is only half the story, because even with large numbers it can roll over. Okay okay, the obvious way is to use Solidity 0.8. But bear with me, there are many internal states a contract should never reach. And the easiest to showcase here is with integers and rollovers.
Add an Assert to check invariants¶
Asserts are here to check states of your Smart Contract that should never be violated. For example: a balance can only get bigger if we add values or get smaller if we reduce values.
//SPDX-License-Identifier: MIT
pragma solidity 0.6.12;
contract ExceptionExample {
mapping(address => uint8) public balanceReceived;
function receiveMoney() public payable {
assert(msg.value == uint8(msg.value));
balanceReceived[msg.sender] += uint8(msg.value);
assert(balanceReceived[msg.sender] >= uint8(msg.value));
}
function withdrawMoney(address payable _to, uint8 _amount) public {
require(_amount <= balanceReceived[msg.sender], "Not Enough Funds, aborting");
assert(balanceReceived[msg.sender] >= balanceReceived[msg.sender] - _amount);
balanceReceived[msg.sender] -= _amount;
_to.transfer(_amount);
}
}
Run it again. Try to input 257 Wei. Or also try run two transactions with 200 Wei each, so it doesn't overflow for the transaction itself, but for the second assertion, where it checks if the balance is still valid.
Now let's have a look at one last concept, the concept of try/catch and named Exceptions!