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)]pubstructPrice { total_supply:u32, price:u32, owner:AccountId, balances:Mapping<AccountId, u32>, }implPrice { #[ink(constructor)]pubfnnew(supply:u32, price:u32) -> Self {letmut balances =Mapping::default();let caller = Self::env().caller(); balances.insert(caller, &supply); Self{ total_supply:supply, price : price, balances, owner: caller, } } #[ink(message)]pubfntotal_supply(&self) ->u32 { self.total_supply } #[ink(message)]pubfnset_price(&mut self, price:u32) { self.price = price } #[ink(message)]pubfnget_price(&self) ->u32 { self.price } #[ink(message)]pubfnget_owner(&self) ->AccountId { self.owner } #[ink(message)]pubfnset_owner(&mut self, new_owner:AccountId) { self.owner = new_owner }/// Simply returns the current value of our `bool`. #[ink(message)]pubfnbalance_of(&self, account:AccountId) ->u32 {match self.balances.get(&account) {Some(value) => value,None=>0, } }pubfninflation(&mut self) { self.total_supply +=999_999 } #[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)); } } #[cfg(test)]mod tests {usecrate::price::Price;use ink::env::{test, DefaultEnvironment}; #[ink::test]fncontract_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]fnaccess_to_non_message_functions() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut 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]fnnon_owner_set_price() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut 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]fnnon_owner_change_owner() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut 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]fnowner_change_owner() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut price =Price::new(10000, 100); price.set_owner(accounts.django);assert_eq!(price.get_owner(), accounts.django); } #[ink::test]fnowner_change_price() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut 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.
cargoinstallcargo-contract--version2.0.0-beta.1
cargocontractnewpricecdprice
then copy paste the code above into the lib.rs file
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
# SecureSmartContract#![cfg_attr(not(feature ="std"), no_std)]#[ink::contract]mod price {use ink::storage::Mapping; #[ink(storage)] #[derive(Default)]pubstructPrice { total_supply:u32, price:u32, owner:AccountId, balances:Mapping<AccountId, u32>, }implPrice { #[ink(constructor)]pubfnnew(supply:u32, price:u32) -> Self {letmut balances =Mapping::default();let caller = Self::env().caller(); balances.insert(caller, &supply); Self{ total_supply:supply, price : price, balances, owner: caller, } } #[ink(message)]pubfntotal_supply(&self) ->u32 { self.total_supply } #[ink(message)]pubfnset_price(&mut self, price:u32) {if self.owner == self.env().caller() { self.price = price } } #[ink(message)]pubfnget_price(&self) ->u32 { self.price } #[ink(message)]pubfnget_owner(&self) ->AccountId { self.owner } #[ink(message)]pubfnset_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)]pubfnbalance_of(&self, account:AccountId) ->u32 {match self.balances.get(&account) {Some(value) => value,None=>0, } }pubfninflation(&mut self) { self.total_supply +=999_999 } #[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)); } } #[cfg(test)]mod tests {usecrate::price::Price;use ink::env::{test, DefaultEnvironment}; #[ink::test]fncontract_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]fnaccess_to_non_message_functions() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut 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]fnnon_owner_set_price() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut 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]fnnon_owner_change_owner() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut 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]fnowner_change_owner() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut price =Price::new(10000, 100); price.set_owner(accounts.django);assert_eq!(price.get_owner(), accounts.django); } #[ink::test]fnowner_change_price() {let accounts = test::default_accounts::<DefaultEnvironment>(); test::set_caller::<DefaultEnvironment>(accounts.alice);letmut 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. ==-