Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

migrate reentrancy to new directory structure #83

Merged
merged 11 commits into from
Feb 1, 2023
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ These libraries contain helper functions, generalized standards, and other tools
- [Binary Merkle Proof](./libs/merkle_proof/) is used to verify Binary Merkle Trees computed off-chain.
- [Non-Fungible Token (NFT)](./libs/nft/) is a token library which provides unqiue collectibles, identified and differentiated by token IDs.
- [Ownership](./libs/ownership/) is used to apply restrictions on functions such that only a single user may call them.
- [Reentrancy](./libs/reentrancy) is used to detect and prevent reentrancy attacks.
- [String](./libs/string/) is an interface to implement dynamic length strings that are UTF-8 encoded.
- [Signed Integers](./libs/signed_integers/) is an interface to implement signed integers.
- [Unsigned Fixed Point Number](./libs/fixed_point/) is an interface to implement fixed-point numbers.
Expand Down
1 change: 1 addition & 0 deletions libs/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"merkle_proof",
"nft",
"ownership",
"reentrancy",
"signed_integers",
"storagemapvec",
"string",
Expand Down
5 changes: 5 additions & 0 deletions libs/reentrancy/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "lib.sw"
license = "Apache-2.0"
name = "reentrancy"
40 changes: 40 additions & 0 deletions libs/reentrancy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Overview

The Reentrancy library provides an API to check for and disallow reentrancy on a contract.

More information can be found in the [specification](./SPECIFICATION.md).

## Known Issues
jtriley-eth marked this conversation as resolved.
Show resolved Hide resolved

While this can protect against both single-function reentrancy and cross-function reentrancy
attacks, it WILL NOT PREVENT a cross-contract reentrancy attack.

## Using the Library

### Using the Reentrancy Guard

Once imported, using the Reentrancy Library can be done by calling one of the two functions. For
more information, see the [specification](./SPECIFICATION.md).

- `is_reentrant() -> bool`
- `reentrancy_guard()`
jtriley-eth marked this conversation as resolved.
Show resolved Hide resolved

The `reentrancy_guard` function asserts `is_reentrant()` returns false.

## Example

```rust
use reentrancy::reentrancy_guard;

abi MyContract {
fn my_non_reentrant_function();
}

impl MyContract for Contract {
fn my_non_reentrant_function() {
reentrancy_guard();

// my code here
}
}
```
25 changes: 25 additions & 0 deletions libs/reentrancy/SPECIFICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Overview

This document provides an overview of the Reentrancy library.

It outlines the use cases and specification.

## Use Cases

The reentrancy check is used to check if a contract ID has been called more than
once in the current call stack.

A reentrancy, or "recursive call" attack
([example here](https://swcregistry.io/docs/SWC-107) can cause some functions to
behave in unexpected ways. This can be prevented by asserting a contract has not
yet been called in the current transaction.

## Public Functions

### `reentrancy_guard()`

Reverts if the current call is reentrant.

### `is_reentrant()`

Returns true if the current contract ID is found in any prior contract calls.
46 changes: 46 additions & 0 deletions libs/reentrancy/src/lib.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! A reentrancy check for use in Sway contracts.
//! Note that this only works in internal contexts.
//! to prevent reentrancy: `assert(!is_reentrant());`
library reentrancy;

use std::call_frames::*;
use std::registers::frame_ptr;

pub enum ReentrancyError {
NonReentrant: (),
}
/// Reverts if the reentrancy pattern is detected in the contract in which this is called.
/// Not needed if the Checks-Effects-Interactions (CEI) pattern is followed (as prompted by the
/// compiler).
/// > Caution: While this can protect against both single-function reentrancy and cross-function
jtriley-eth marked this conversation as resolved.
Show resolved Hide resolved
/// reentrancy attacks, it WILL NOT PREVENT a cross-contract reentrancy attack.
pub fn reentrancy_guard() {
require(!is_reentrant(), ReentrancyError::NonReentrant);
}

/// Returns `true` if the reentrancy pattern is detected, and `false` otherwise.
///
/// Detects reentrancy by iteratively checking previous calls in the current call stack for a
/// contract ID equal to the current contract ID. If a match is found, it returns true, else false.
pub fn is_reentrant() -> bool {
// Get our current contract ID
let this_id = contract_id();

// Reentrancy cannot occur in an external context. If not detected by the time we get to the
// bottom of the call_frame stack, then no reentrancy has occured.
let mut call_frame_pointer = frame_ptr();
if !call_frame_pointer.is_null() {
call_frame_pointer = get_previous_frame_pointer(call_frame_pointer);
};
while !call_frame_pointer.is_null() {
// get the ContractId value from the previous call frame
let previous_contract_id = get_contract_id_from_call_frame(call_frame_pointer);
if previous_contract_id == this_id {
return true;
};
call_frame_pointer = get_previous_frame_pointer(call_frame_pointer);
}

// The current contract ID wasn't found in any contract calls prior to here.
false
}
4 changes: 4 additions & 0 deletions tests/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ members = [
"./src/unsigned_numbers/ufp64_test",
"./src/unsigned_numbers/ufp128_div_test",
"./src/unsigned_numbers/ufp128_test",
"./src/reentrancy/reentrancy_attacker_abi",
"./src/reentrancy/reentrancy_attacker_contract",
"./src/reentrancy/reentrancy_target_abi",
"./src/reentrancy/reentrancy_target_contract",
]
1 change: 1 addition & 0 deletions tests/src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
mod merkle_proof;
mod nft;
mod ownership;
mod reentrancy;
mod signed_integers;
mod storagemapvec;
mod string;
Expand Down
121 changes: 121 additions & 0 deletions tests/src/reentrancy/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use fuels::{prelude::*, tx::ContractId};

abigen!(
AttackerContract,
"src/reentrancy/reentrancy_attacker_contract/out/debug/reentrancy_attacker_contract-abi.json",
);

abigen!(
TargetContract,
"src/reentrancy/reentrancy_target_contract/out/debug/reentrancy_target_contract-abi.json",
);

const REENTRANCY_ATTACKER_BIN: &str =
"src/reentrancy/reentrancy_attacker_contract/out/debug/reentrancy_attacker_contract.bin";
const REENTRANCY_ATTACKER_STORAGE: &str = "src/reentrancy/reentrancy_attacker_contract/out/debug/reentrancy_attacker_contract-storage_slots.json";

const REENTRANCY_TARGET_BIN: &str =
"src/reentrancy/reentrancy_target_contract/out/debug/reentrancy_target_contract.bin";
const REENTRANCY_TARGET_STORAGE: &str = "src/reentrancy/reentrancy_target_contract/out/debug/reentrancy_target_contract-storage_slots.json";

pub async fn get_attacker_instance(wallet: WalletUnlocked) -> (AttackerContract, ContractId) {
let id = Contract::deploy(
REENTRANCY_ATTACKER_BIN,
&wallet,
TxParameters::default(),
StorageConfiguration::with_storage_path(Some(REENTRANCY_ATTACKER_STORAGE.to_string())),
)
.await
.unwrap();

let instance = AttackerContract::new(id.clone(), wallet);

(instance, id.into())
}

pub async fn get_target_instance(wallet: WalletUnlocked) -> (TargetContract, ContractId) {
let id = Contract::deploy(
REENTRANCY_TARGET_BIN,
&wallet,
TxParameters::default(),
StorageConfiguration::with_storage_path(Some(REENTRANCY_TARGET_STORAGE.to_string())),
)
.await
.unwrap();

let instance = TargetContract::new(id.clone(), wallet);

(instance, id.into())
}

mod success {
use super::*;

#[tokio::test]
async fn can_detect_reentrancy() {
let wallet = launch_provider_and_get_wallet().await;
let (attacker_instance, _) = get_attacker_instance(wallet.clone()).await;
let (_, target_id) = get_target_instance(wallet).await;

let result = attacker_instance
.methods()
.launch_attack(target_id)
.set_contracts(&[target_id.into()])
.call()
.await
.unwrap();

assert_eq!(result.value, true);
}

#[tokio::test]
async fn can_call_guarded_function() {
let wallet = launch_provider_and_get_wallet().await;
let (attacker_instance, _) = get_attacker_instance(wallet.clone()).await;
let (_, target_id) = get_target_instance(wallet).await;

attacker_instance
.methods()
.innocent_call(target_id)
.set_contracts(&[target_id.into()])
.call()
.await
.unwrap();
}
}

mod revert {
use super::*;

#[tokio::test]
#[should_panic]
bitzoic marked this conversation as resolved.
Show resolved Hide resolved
async fn can_block_reentrancy() {
let wallet = launch_provider_and_get_wallet().await;
let (attacker_instance, _) = get_attacker_instance(wallet.clone()).await;
let (_, target_id) = get_target_instance(wallet).await;

attacker_instance
.methods()
.launch_thwarted_attack_1(target_id)
.set_contracts(&[target_id.into()])
.call()
.await
.unwrap();
}

#[tokio::test]
#[should_panic]
async fn can_block_cross_function_reentrancy() {
let wallet = launch_provider_and_get_wallet().await;
let (attacker_instance, _) = get_attacker_instance(wallet.clone()).await;
let (_, target_id) = get_target_instance(wallet).await;

attacker_instance
.methods()
.launch_thwarted_attack_2(target_id)
.set_contracts(&[target_id.into()])
.call()
.await
.unwrap();
}
}
2 changes: 2 additions & 0 deletions tests/src/reentrancy/reentrancy_attacker_abi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
7 changes: 7 additions & 0 deletions tests/src/reentrancy/reentrancy_attacker_abi/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "reentrancy_attacker_abi"

[dependencies]
12 changes: 12 additions & 0 deletions tests/src/reentrancy/reentrancy_attacker_abi/src/main.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
library reentrancy_attacker_abi;

abi Attacker {
fn launch_attack(target: ContractId) -> bool;
fn launch_thwarted_attack_1(target: ContractId);
fn launch_thwarted_attack_2(target: ContractId);
fn innocent_call(target: ContractId);
fn evil_callback_1();
fn evil_callback_2();
fn evil_callback_3();
fn innocent_callback();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
9 changes: 9 additions & 0 deletions tests/src/reentrancy/reentrancy_attacker_contract/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "reentrancy_attacker_contract"

[dependencies]
reentrancy_attacker_abi = { path = "../reentrancy_attacker_abi" }
reentrancy_target_abi = { path = "../reentrancy_target_abi" }
46 changes: 46 additions & 0 deletions tests/src/reentrancy/reentrancy_attacker_contract/src/main.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
contract;

use std::{auth::*, call_frames::contract_id};

use reentrancy_target_abi::Target;
use reentrancy_attacker_abi::Attacker;

// Return the sender as a ContractId or panic:
fn get_msg_sender_id_or_panic() -> ContractId {
match msg_sender().unwrap() {
Identity::ContractId(v) => v,
_ => revert(0),
}
}

impl Attacker for Contract {
fn launch_attack(target: ContractId) -> bool {
abi(Target, target.value).reentrancy_detected()
}

fn launch_thwarted_attack_1(target: ContractId) {
abi(Target, target.value).intra_contract_call();
}

fn launch_thwarted_attack_2(target: ContractId) {
abi(Target, target.value).cross_function_reentrance_denied();
}

fn innocent_call(target: ContractId) {
abi(Target, target.value).guarded_function_is_callable();
}

fn evil_callback_1() {
assert(abi(Attacker, contract_id().value).launch_attack(get_msg_sender_id_or_panic()));
}

fn evil_callback_2() {
abi(Attacker, contract_id().value).launch_thwarted_attack_1(get_msg_sender_id_or_panic());
}

fn evil_callback_3() {
abi(Attacker, contract_id().value).launch_thwarted_attack_1(get_msg_sender_id_or_panic());
}

fn innocent_callback() {}
}
2 changes: 2 additions & 0 deletions tests/src/reentrancy/reentrancy_target_abi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
7 changes: 7 additions & 0 deletions tests/src/reentrancy/reentrancy_target_abi/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "reentrancy_target_abi"

[dependencies]
9 changes: 9 additions & 0 deletions tests/src/reentrancy/reentrancy_target_abi/src/main.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
library reentrancy_target_abi;

abi Target {
fn reentrancy_detected() -> bool;
fn reentrance_denied();
fn cross_function_reentrance_denied();
fn intra_contract_call();
fn guarded_function_is_callable();
}
2 changes: 2 additions & 0 deletions tests/src/reentrancy/reentrancy_target_contract/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
10 changes: 10 additions & 0 deletions tests/src/reentrancy/reentrancy_target_contract/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "reentrancy_target_contract"

[dependencies]
reentrancy_attacker_abi = { path = "../reentrancy_attacker_abi" }
reentrancy_target_abi = { path = "../reentrancy_target_abi" }
reentrancy = { path = "../../../../libs/reentrancy" }
Loading