Skip to content

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 => uint64) public balanceReceived;

    function receiveMoney() public payable {
        balanceReceived[msg.sender] += uint64(msg.value);
    }

    function withdrawMoney(address payable _to, uint64 _amount) public {
        require(_amount <= balanceReceived[msg.sender], "Not Enough Funds, aborting");

        balanceReceived[msg.sender] -= _amount;
        _to.transfer(_amount);
    }
}
  1. Deploy a new Contract Instance
  2. Enter 19 Ether into the Value field
  3. Hit "receiveMoney"

The Transaction goes through. Let's check our balance, we should have 19000000000000000000 Wei, or?

That's only 553255926290448384 Wei, or around 0.553 Ether. Where is the rest? What happened?

We are storing the balance in an uint64. Unsigned integers go from 0 to 2^n-1, so that's 2^64-1 or 18446744073709551615. So, it can store a max of 18.4467... Ether. We sent 19 Ether to the contract. It automatically rolls over to 0. So, we end up with 19000000000000000000 - 18446744073709551615 -1 (the 0 value) = 553255926290448384.

How can we fix it?

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 => uint64) public balanceReceived;

    function receiveMoney() public payable {
        assert(msg.value == uint64(msg.value));
        balanceReceived[msg.sender] += uint64(msg.value);
        assert(balanceReceived[msg.sender] >= uint64(msg.value));
    }

    function withdrawMoney(address payable _to, uint64 _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 20 Ether. Or also try run two transactions with 10 Ether each, so it doesn't overflow for the transaction itself, but for the second assertion, where it checks if the balance is still valid.


Last update: March 28, 2022