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

test: add unit test for reentrancy attack #221

Merged
merged 5 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hip-pans-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fuel-bridge/fungible-token': patch
---

Add reentrancy unit test for l2 proxy-bridge
9 changes: 9 additions & 0 deletions Forc.lock
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ dependencies = [
"std",
]

[[package]]
name = "reentrancy-attacker"
source = "member"
dependencies = [
"contract_message_receiver",
"standards git+https://github.com/FuelLabs/sway-standards?tag=v0.5.0#348f7175df4c012b23c86cdb18aab79025ca1f18",
"std",
]

[[package]]
name = "standards"
source = "git+https://github.com/FuelLabs/sway-standards?tag=v0.4.3#6f63eb7dff2458a7d976184e565b5cbf26f61da2"
Expand Down
1 change: 1 addition & 0 deletions Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"packages/fungible-token/bridge-fungible-token/interface",
"packages/fungible-token/bridge-fungible-token/proxy",
"packages/fungible-token/bridge-fungible-token/implementation",
"packages/fungible-token/bridge-fungible-token/reentrancy-attacker",
"packages/message-predicates/contract-message-predicate",
"packages/message-predicates/contract-message-receiver",
"packages/base-asset",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "reentrancy-attacker.sw"
license = "Apache-2.0"
name = "reentrancy-attacker"

[dependencies]
contract_message_receiver = { path = "../../../message-predicates/contract-message-receiver" }
standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.5.0" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
contract;

use std::execution::run_external;
use standards::{src14::SRC14, src5::{AccessError, State}};
use contract_message_receiver::MessageReceiver;

abi ReentrancyAttacker {
#[storage(read, write)]
fn process_message(msg_idx: u64);

#[storage(read)]
fn get_success() -> bool;
}

pub enum AttackStage {
Attacking: (),
Success: (),
Finished: (),
}

configurable {
TARGET: ContractId = ContractId::zero(),
}

#[namespace(SRC14)]
storage {
attacking: bool = false,
success: bool = false,
}

impl ReentrancyAttacker for Contract {
#[storage(read, write)]
fn process_message(msg_idx: u64) {
if storage.success.read() {
log(AttackStage::Finished);
return;
}

log(AttackStage::Attacking);
storage.attacking.write(true);

let target = abi(MessageReceiver, TARGET.into());
target.process_message(msg_idx);

storage.success.write(true);
log(AttackStage::Success);
}

#[storage(read)]
fn get_success() -> bool {
storage.success.read()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1101,9 +1101,16 @@ mod success {
}

mod revert {
use fuels::types::tx_status::TxStatus;
use fuels::{
accounts::wallet::WalletUnlocked,
programs::contract::SettableContract,
types::{tx_status::TxStatus, U256},
};

use crate::utils::setup::get_contract_ids;
use crate::utils::setup::{
create_reentrancy_attacker_contract, get_contract_ids, precalculate_reentrant_attacker_id,
AttackStage, ReentrancyAttacker,
};

use super::*;

Expand Down Expand Up @@ -1159,4 +1166,74 @@ mod revert {
}
}
}

#[tokio::test]
async fn rejects_reentrancy_attempts() {
let mut wallet = create_wallet();
let configurables: Option<BridgeFungibleTokenContractConfigurables> = None;
let (proxy_id, _implementation_contract_id) =
get_contract_ids(&wallet, configurables.clone());
let deposit_contract_id = precalculate_reentrant_attacker_id(proxy_id.clone()).await;
let amount = u64::MAX;

let (message, coin, deposit_contract) = create_deposit_message(
BRIDGED_TOKEN,
BRIDGED_TOKEN_ID,
FROM,
*deposit_contract_id,
U256::from(amount),
BRIDGED_TOKEN_DECIMALS,
proxy_id,
true,
Some(vec![11u8, 42u8, 69u8]),
)
.await;

let (_, _, utxo_inputs) = setup_environment(
&mut wallet,
vec![coin],
vec![message],
deposit_contract,
None,
configurables,
)
.await;

let provider = wallet.provider().expect("Needs provider");

let reentrant_attacker: ReentrancyAttacker<WalletUnlocked> =
create_reentrancy_attacker_contract(wallet.clone(), proxy_id.clone()).await;

// Relay the test message to the bridge contract
let tx_id = relay_message_to_contract(
&wallet,
utxo_inputs.message[0].clone(),
utxo_inputs.contract,
)
.await;

let tx_status = provider.tx_status(&tx_id).await.unwrap();
assert!(matches!(tx_status, TxStatus::Revert { .. }));

let receipts: Vec<fuels::tx::Receipt> =
provider.tx_status(&tx_id).await.unwrap().take_receipts();

let attack_stages = reentrant_attacker
.log_decoder()
.decode_logs_with_type::<AttackStage>(&receipts)
.unwrap();

assert_eq!(attack_stages.len(), 1);
assert_eq!(attack_stages[0], AttackStage::Attacking);

let attack_successful = reentrant_attacker
.methods()
.get_success()
.call()
.await
.unwrap()
.value;

assert!(!attack_successful);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub(crate) const BRIDGE_FUNGIBLE_TOKEN_CONTRACT_BINARY: &str =
pub(crate) const DEPOSIT_RECIPIENT_CONTRACT_BINARY: &str =
"../test-deposit-recipient-contract/out/release/test_deposit_recipient_contract.bin";
pub(crate) const BRIDGE_PROXY_BINARY: &str = "../bridge-fungible-token/proxy/out/release/proxy.bin";
pub(crate) const REENTRANCY_ATTACKER_BINARY: &str =
"../bridge-fungible-token/reentrancy-attacker/out/release/reentrancy-attacker.bin";

pub(crate) const BRIDGED_TOKEN: &str =
"0x00000000000000000000000000000000000000000000000000000000deadbeef";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use std::{mem::size_of, num::ParseIntError, result::Result as StdResult, str::Fr
use super::constants::{
BRIDGED_TOKEN, BRIDGED_TOKEN_ID, BRIDGE_PROXY_BINARY, DEPOSIT_TO_ADDRESS_FLAG,
DEPOSIT_TO_CONTRACT_FLAG, DEPOSIT_WITH_DATA_FLAG, FROM, METADATA_MESSAGE_FLAG,
REENTRANCY_ATTACKER_BINARY,
};

abigen!(
Expand All @@ -43,6 +44,10 @@ abigen!(
Contract(
name = "BridgeProxy",
abi = "packages/fungible-token/bridge-fungible-token/proxy/out/release/proxy-abi.json",
),
Contract(
name = "ReentrancyAttacker",
abi = "packages/fungible-token/bridge-fungible-token/reentrancy-attacker/out/release/reentrancy-attacker-abi.json",
)
);

Expand Down Expand Up @@ -344,6 +349,20 @@ pub(crate) async fn precalculate_deposit_id() -> ContractId {
compiled.contract_id()
}

pub(crate) async fn precalculate_reentrant_attacker_id(target: ContractId) -> ContractId {
let configurables = ReentrancyAttackerConfigurables::default()
.with_TARGET(target)
.unwrap();

let compiled = Contract::load_from(
REENTRANCY_ATTACKER_BINARY,
LoadConfiguration::default().with_configurables(configurables),
)
.unwrap();

compiled.contract_id()
}

/// Prefixes the given bytes with the test contract ID
pub(crate) fn prefix_contract_id(mut data: Vec<u8>, contract_id: ContractId) -> Vec<u8> {
// Turn contract id into array with the given data appended to it
Expand All @@ -368,6 +387,26 @@ pub(crate) async fn create_recipient_contract(
DepositRecipientContract::new(id, wallet)
}

pub(crate) async fn create_reentrancy_attacker_contract(
wallet: WalletUnlocked,
target: ContractId,
) -> ReentrancyAttacker<WalletUnlocked> {
let configurables = ReentrancyAttackerConfigurables::default()
.with_TARGET(target)
.unwrap();

let id = Contract::load_from(
REENTRANCY_ATTACKER_BINARY,
LoadConfiguration::default().with_configurables(configurables),
)
.unwrap()
.deploy(&wallet, TxPolicies::default())
.await
.unwrap();

ReentrancyAttacker::new(id, wallet)
}

/// Quickly converts the given hex string into a u8 vector
pub(crate) fn decode_hex(s: &str) -> Vec<u8> {
let data: StdResult<Vec<u8>, ParseIntError> = (2..s.len())
Expand Down
Loading