Lesson 3 - Integer Overflow

Introduction

In this lesson you will learn how improperly handled integer overflows (or underflows) can yield vulnerabilities in your smart contract. Integer typed variables in ink! can overflow or underflow. This means that if they are assigned values outside of their bounds, they will wrap around. If this is not addressed carefully, this may allow an attacker to set values outside the bounds of what is expected by the smart contract author.

Prerequisites

To be able to understand and complete this lesson, we believe that the minimum requirement should be the completion of Lesson 1 - Getting Started. If you desire to be even more prepared please have a look at the official website of ink!.

Objectives and Outcomes

At the end of this lesson, you will be identify integer overflow/underflow vulnerabilities in smart contract code. You will also understand a few ways to prevent these attacks beyond explicit checks. These include: appropriate compiler configuration, safe math, and wrapping methods.

Exercise

Vulnerable Smart contract

Bob develops a smart contract for as a simple token bank. The bank can be initialized with a specific quantity of tokens. Alice can then withdraw tokens into her personal account. His smart contract code can be found below.

#![cfg_attr(not(feature = "std"), no_std)]


#[ink::contract]
mod bank {
    use ink::storage::Mapping;
    
    #[ink(storage)]
    #[derive(Default)]
    pub struct Bank {
        bank: u8,
        balances: Mapping<AccountId, u8>,
    }

    impl Bank {
        /// Constructor that initializes the supply of the token bank
        #[ink(constructor)]
        pub fn new(supply: u8) -> Self {
            let balances = Mapping::default();
            Self{
                balances,
                bank: supply,
            }
        
        }

        /// report the totaly supply of token
        #[ink(message)]
        pub fn bank(&self) -> u8 {
            self.bank
        }
  
        /// Simply returns the current balance of token in balances
        #[ink(message)]
        pub fn balance_of(&self, account: AccountId) -> u8 {
            match self.balances.get(&account) {
                Some(value) => value,
                None => 0,
            }
        }

        #[ink(message)]
        pub fn withdraw(&mut self, amount: u8) {
            let sender = self.env().caller();
            let sender_balance = self.balance_of(sender);
            if sender_balance + amount < sender_balance {
                return;
            }
            self.balances.insert(sender, &(sender_balance + amount));
            self.bank -= amount;
        }

    }
}

Unfortunately, Bob did not follow the security guidelines and this has for consequences that his contract is vulnerable. Do you see where the security vulnerability occurs?

==- Hint First, identify lines where mathematical operations occur. Then check for a possible overflow or underflow. ==- Answer The underflow vulnerability is on line 50.

self.bank -= amount;

==-

Simulated Attack

Can you think about a way to exploit this vulnerability? You may download the contract here, deploy it on Aleph Zero testnet, and try to attack it.

Did you succeed? Congratulations! If not, see the exploit below.

==- Reveal Exploit This test demonstrates an exploit that allows Alice to withdraw more tokens than Bob has made available.

#[cfg(test)]
mod tests {
    use crate::bank::Bank;
    use ink::env::{test, DefaultEnvironment};

    #[ink::test]
    fn tokens_for_free() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut bank = Bank::new(10);

        // Initially, the bank has 10 tokens and Alice has 0 tokens
        assert_eq!(bank.balance_of(accounts.alice), 0);
        assert_eq!(bank.bank(), 10);

        // Alice attempts to withdraw 111 tokens, which is more than the bank has
        bank.withdraw(111);

        // Now, the bank has 155 tokens and Alice has 111 tokens due to underflow
        assert_eq!(bank.balance_of(accounts.alice), 111);
        assert_eq!(bank.bank(), 155);
    }
}

==-

Secure Solution

Luckily, there exist a secure way to implement the smart contract! Before revealing the solution, should take some time and try to secure the smart contract. Any success? Let’s reveal two possible solutions. ==- Solution 1 How can we fix the code so that we catch the underflow? We can use safe math that catches underflows.

        #[ink(message)]
        pub fn withdraw(&mut self, amount: u8) {
            let sender = self.env().caller();
            let sender_balance = self.balance_of(sender);
            if sender_balance + amount < sender_balance {
                return;
            }

            let check = self.bank.checked_sub(amount);
            assert_eq!(check, None, "Underflow");

            self.balances.insert(sender, &(sender_balance + amount));
            self.bank -= amount;
        }

==- Solution 2 Set the compiler flags debug-assertion and overflow-checks to true. These can be set in Cargo.toml for profile.release. This will ensure there will be a panic during runtime for overflows and underflows.

Keep in mind, this costs additional computations throughout the code. ==-

If you want, you can verify that the solution is secure by replaying the attack described above.

Question

If you are up to the challenge, see if you know the answer to this question: ==- Reveal Question True or false: Exceeding the maximum value for an integer type will always panic. ==- Answers False. By default, debug builds will panic on overflow while release builds will not. This can be modified in Cargo.profile or by providing appropriate compiler flags. Even with overflow checking on, panics can be averted with saturating_, wrapping_, checked_, and overflowing_ methods. See integer overflow in the the rust docs. ==-

Last updated