In this lesson, you will learn the importance of validating the recipient address whenever a token is transferred to any address. The lack of address validation may result in lost or locked tokens if default addresses are used mistakenly on production chains.
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
In this lesson you will learn:
The consequences of not checking the recipient address against a list of known bad addresses;
How to attack a smart contract by using the lack of address validation;
How to mitigate the attack.
Exercise
Vulnerable Smart contract
A token contract that allows transfer to any addresses, without validation, is vulnerable. The following transfer function does not perform any address validations on the user-provided token recipient address and may result in lost or locked tokens if default addresses are used mistakenly on production chains.
#![cfg_attr(not(feature ="std"), no_std)]#[ink::contract]mod s0token {use ink::{storage::Mapping}; #[ink(storage)] pubstructS0token { total_supply:u32, balances:Mapping<AccountId, u32> }implS0token {/// Creates a token contract with the given initial supply belonging to the contract creator #[ink(constructor)]pubfnnew_token(supply:u32) -> Self {letmut balances =Mapping::default();let caller = Self::env().caller(); balances.insert(&caller,&supply); Self { total_supply: supply, balances } }/// Total Supply of s0token #[ink(message)]pubfntotal_supply(&self) ->u32 { self.total_supply }/// Current balance of the chosen account. #[ink(message)]pubfnbalance_of(&self, account:AccountId) ->u32 {match self.balances.get(&account) {Some(value) => value,None=>0, } }/// Transfers an amount of tokens to the chosen recipient. #[ink(message)]pubfntransfer(&mut self, recipient:AccountId, amount:u32) {let sender = self.env().caller();let sender_balance = self.balance_of(sender);if sender_balance < amount {return; } self.balances.insert(sender, &(sender_balance - amount));let recipient_balance = self.balance_of(recipient); self.balances.insert(recipient, &(recipient_balance + 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 the check of the function caller is done.
==- Answer The validation of the address is not done correctly.
Simulated Attack
==- Reveal Attack
Setup
When developing contracts in Ink, there are a number of addresses that are used for development purposes locally and on the testnet. However, use of these addresses on the production network can result in lost gas fees and unretrievable tokens and AZERO if accidentally used within that environment. A list of these accounts can be found in the ink_env test documentation.
Dry-running new_token (skip with --skip-dry-run)
Success! Gas required estimated at Weight(ref_time: 489377768, proof_size: 0)
Confirm transaction details: (skip with --skip-confirm)
Constructor new_token
Args 10000
Gas limit Weight(ref_time: 489377768, proof_size: 0)
Submit? (Y/n):
Code hash 0xeaae76bb62900ef5c5f17d0802b1c80a7346db0cbab00c729a74f6b1bb6e865c # Code Hash address will be different
Contract 5GJZow5yovTExpjBEMb8ECrJJ7sutR471gvVTbCETfjcAg6e # Contract address will be different
Events
....
export CONTRACT="<contract address from above>"export ACCOUNT="<account address that instantiated the contract>"
There is also a case where a contract is instantiated with an empty seed phrase.
Make sure that there is no environment variable for $SEED
echo"$SEED"
Should return an empty space.
Now instantiate a new contract. The environment variable $SEED can be used or just an empty string "", the reason to use the environment variable is to demonstrate a developer that has opened a new terminal without exporting their SEED and how easy it would be to deploy a contract unknowingly with that address. An attacker could send enough Azero to that address that would pay for the gas to instantiate a new contract and wait for more Azero to be sent to the address or transfer the newly created token.
Open a new terminal without an export environment variables
This token contract fixes the lack of address validation by checking that no default or zero address accounts are used as the recipient for receiving tokens.
Mitigation of the lack of address validation is accomplished by checking the recipient address against a list of known bad addresses. One concern we have in validating these addresses is that they are commonly used during development for debug purposes. The inability to use these addresses in the test environment may hamper development efforts. In order to account for this, address validation in the example only occurs in the release build. This is accomplished by checking the current contract configuration for debug_assertions, which are disabled in a release build.
Since the test environment is not available once deployed to the production chain, we must hard code the address values into an array that can be checked during the transfer process. The zero address and empty seed address are also included as they have been an issue on other chains. ==- Solution
These following transfer function now performs any address validations by checking the recipient against known default addresses that would result in lost or locked tokens if they are used mistakenly on production chains.
/// Transfers an amount of tokens to the chosen recipient. #[ink(message)]pubfntransfer(&mut self, recipient:AccountId, amount:u32) {let sender = self.env().caller();let sender_balance = self.balance_of(sender);if sender_balance < amount {return; }ifcfg!(not(debug_assertions)) {// Only check for default accounts if this is a release buildlet default_accounts =S0token::get_default_accounts();if default_accounts.contains(&recipient) {return; } } self.balances.insert(sender, &(sender_balance - amount));let recipient_balance = self.balance_of(recipient); self.balances.insert(recipient, &(recipient_balance + amount)); }
Verifying Secure Solution
When developing contracts in ink!, there are a number of addresses that are used for development purposes locally and on the testnet. However, use of these addresses on the production network can result in lost gas fees and unretrievable tokens and AZERO if accidentally used within that environment. A list of these accounts can be found in the ink_env test documentation.
Dry-running new_token (skip with --skip-dry-run)
Success! Gas required estimated at Weight(ref_time: 489377768, proof_size: 0)
Confirm transaction details: (skip with --skip-confirm)
Constructor new_token
Args 10000
Gas limit Weight(ref_time: 489377768, proof_size: 0)
Submit? (Y/n):
Code hash 0xeaae76bb62900ef5c5f17d0802b1c80a7346db0cbab00c729a74f6b1bb6e865c # Code Hash address will be different
Contract 5GJZow5yovTExpjBEMb8ECrJJ7sutR471gvVTbCETfjcAg6e # Contract address will be different
Events
....
export CONTRACT="<contract address from above>"export ACCOUNT="<account address that instantiated the contract>"
If you are up to the challenge, see if you know the answer to this question:
True or false: In the previous lessons of this security course, is there any lessen where the address validation checks should also have been performed?
==- Answer True. For example, in Lesson 4 the address validation should also have been performed.