Skip to content

Commit

Permalink
migrate reentrancy to new directory structure (#83)
Browse files Browse the repository at this point in the history
## Reentrancy Library Migration

## Changes

The following changes have been made:

- Reentrancy library has been migrated from the std library to sway-libs
- Reentrancy tests have been migrated
- A new directory, `test_artifacts` has been introduced for
multi-contract tests
- The build script (`.github/scripts/build.sh`) has been modified to
build `test_artifacts`

## Notes

- It may be worth reviewing the rest of the ci pipeline to ensure the
new directory is handled properly

## Related Issues

Part 1 of [Issue 3217 in
Fuellabs/sway](FuelLabs/sway#3217)

---------

Co-authored-by: Nick Furfaro <[email protected]>
Co-authored-by: Cameron Carstens <[email protected]>
  • Loading branch information
3 people authored Feb 1, 2023
1 parent f33fc23 commit 4ca20a0
Show file tree
Hide file tree
Showing 21 changed files with 404 additions and 0 deletions.
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

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()`

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
/// 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]
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();
}
2 changes: 2 additions & 0 deletions tests/src/reentrancy/reentrancy_attacker_contract/.gitignore
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

0 comments on commit 4ca20a0

Please sign in to comment.