-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
migrate reentrancy to new directory structure (#83)
## 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
1 parent
f33fc23
commit 4ca20a0
Showing
21 changed files
with
404 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
out | ||
target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
out | ||
target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
46
tests/src/reentrancy/reentrancy_attacker_contract/src/main.sw
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
out | ||
target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
out | ||
target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } |
Oops, something went wrong.