Skip to content

Commit 510434e

Browse files
Merge pull request #196 from Dataguru-tech/feat/Implement-Emergency-Shutdown-verification-rule
feat/Implement 'Emergency Shutdown' verification rule
2 parents bc363b1 + 0b6d034 commit 510434e

File tree

12 files changed

+406
-0
lines changed

12 files changed

+406
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Fuzzing
2+
3+
on:
4+
push:
5+
branches: [main]
6+
schedule:
7+
- cron: "0 2 * * *"
8+
9+
jobs:
10+
bolero-fuzz:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: dtolnay/rust-toolchain@nightly
15+
- run: cargo install cargo-bolero
16+
- run: cargo bolero test fuzz_raw_bytes_no_panic --time 60s
17+
- run: cargo bolero test fuzz_structured_message_no_panic --time 60s
18+
19+
cargo-fuzz:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- uses: dtolnay/rust-toolchain@nightly
24+
- run: cargo install cargo-fuzz
25+
- run: cargo fuzz run fuzz_cross_contract -- -max_total_time=120

contracts/my-contract/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "my-contract"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
borsh = "1.3"
8+
9+
[dev-dependencies]
10+
bolero = "0.10"
11+
bolero-generator = "0.10"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "my-contract-fuzz"
3+
version = "0.0.1"
4+
edition = "2021"
5+
publish = false
6+
7+
[package.metadata]
8+
cargo-fuzz = true
9+
10+
[dependencies]
11+
libfuzzer-sys = "0.4"
12+
my-contract = { path = ".." }
13+
borsh = "1.3"
14+
arbitrary = { version = "1", features = ["derive"] }
15+
16+
[[bin]]
17+
name = "fuzz_cross_contract"
18+
path = "fuzz_targets/fuzz_cross_contract.rs"
19+
test = false
20+
doc = false
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#![no_main]
2+
3+
use libfuzzer_sys::fuzz_target;
4+
use my_contract::handle_cross_contract_message;
5+
6+
fuzz_target!(|data: &[u8]| {
7+
let _ = handle_cross_contract_message(data);
8+
});

contracts/my-contract/src/lib.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use borsh::{BorshDeserialize, BorshSerialize};
2+
3+
#[derive(Debug, BorshSerialize, BorshDeserialize, Clone)]
4+
pub struct CrossContractMessage {
5+
pub sender: [u8; 32],
6+
pub method_id: u8,
7+
pub payload: Vec<u8>,
8+
pub nonce: u64,
9+
}
10+
11+
#[derive(Debug, PartialEq)]
12+
pub enum ContractError {
13+
DeserializationFailed,
14+
UnknownMethod,
15+
InvalidPayload(String),
16+
OverflowDetected,
17+
}
18+
19+
pub fn handle_cross_contract_message(raw: &[u8]) -> Result<String, ContractError> {
20+
let msg = CrossContractMessage::try_from_slice(raw)
21+
.map_err(|_| ContractError::DeserializationFailed)?;
22+
match msg.method_id {
23+
0 => handle_transfer(&msg.payload),
24+
1 => handle_query(&msg.payload),
25+
2 => handle_callback(&msg.payload),
26+
_ => Err(ContractError::UnknownMethod),
27+
}
28+
}
29+
30+
fn handle_transfer(payload: &[u8]) -> Result<String, ContractError> {
31+
if payload.len() < 8 {
32+
return Err(ContractError::InvalidPayload("too short".into()));
33+
}
34+
let amount = u64::from_le_bytes(payload[0..8].try_into().unwrap());
35+
let fee = amount.checked_mul(3).ok_or(ContractError::OverflowDetected)?;
36+
Ok(format!("transfer: amount={amount}, fee={fee}"))
37+
}
38+
39+
fn handle_query(payload: &[u8]) -> Result<String, ContractError> {
40+
let key = std::str::from_utf8(payload)
41+
.map_err(|_| ContractError::InvalidPayload("invalid utf8".into()))?;
42+
Ok(format!("query: key={key}"))
43+
}
44+
45+
fn handle_callback(payload: &[u8]) -> Result<String, ContractError> {
46+
if payload.is_empty() {
47+
return Err(ContractError::InvalidPayload("empty callback".into()));
48+
}
49+
Ok(format!("callback: {} bytes", payload.len()))
50+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use bolero::{check, generator::*};
2+
use borsh::BorshSerialize;
3+
use my_contract::{handle_cross_contract_message, CrossContractMessage};
4+
5+
#[test]
6+
fn fuzz_raw_bytes_no_panic() {
7+
check!()
8+
.with_type::<Vec<u8>>()
9+
.for_each(|data| {
10+
let _ = handle_cross_contract_message(data);
11+
});
12+
}
13+
14+
#[test]
15+
fn fuzz_structured_message_no_panic() {
16+
check!()
17+
.with_generator(gen::<(u8, Vec<u8>, u64)>())
18+
.for_each(|(method_id, payload, nonce)| {
19+
let msg = CrossContractMessage {
20+
sender: [0u8; 32],
21+
method_id: *method_id,
22+
payload: payload.clone(),
23+
nonce: *nonce,
24+
};
25+
if let Ok(encoded) = borsh::to_vec(&msg) {
26+
let _ = handle_cross_contract_message(&encoded);
27+
}
28+
});
29+
}
30+
31+
#[test]
32+
fn fuzz_truncated_messages() {
33+
check!()
34+
.with_type::<Vec<u8>>()
35+
.with_max_len(16)
36+
.for_each(|data| {
37+
let _ = handle_cross_contract_message(data);
38+
});
39+
}
40+
41+
#[test]
42+
fn fuzz_oversized_payload_no_oom() {
43+
check!()
44+
.with_generator(gen::<Vec<u8>>().with().len(0usize..=1_000_000))
45+
.for_each(|data| {
46+
let _ = handle_cross_contract_message(data);
47+
});
48+
}

my-contract/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "my-contract"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
borsh = "1.3"
8+
9+
[dev-dependencies]
10+
bolero = "0.10"
11+
bolero-generator = "0.10"

my-contract/src/circuit_breaker.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use crate::{
2+
guards::{require_admin, require_active, GuardError},
3+
state::{ContractState, ShutdownState},
4+
};
5+
6+
pub fn pause(state: &mut ContractState, caller: &[u8; 32]) -> Result<(), GuardError> {
7+
require_admin(state, caller)?;
8+
require_active(state)?;
9+
state.shutdown = ShutdownState::Paused;
10+
Ok(())
11+
}
12+
13+
pub fn emergency_shutdown(
14+
state: &mut ContractState,
15+
caller: &[u8; 32],
16+
) -> Result<(), GuardError> {
17+
require_admin(state, caller)?;
18+
state.shutdown = ShutdownState::Emergency;
19+
Ok(())
20+
}
21+
22+
pub fn unpause(state: &mut ContractState, caller: &[u8; 32]) -> Result<(), GuardError> {
23+
require_admin(state, caller)?;
24+
match state.shutdown {
25+
ShutdownState::Paused => { state.shutdown = ShutdownState::Active; Ok(()) }
26+
ShutdownState::Emergency => Err(GuardError::EmergencyShutdown),
27+
ShutdownState::Active => Ok(()),
28+
}
29+
}

my-contract/src/guards.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use crate::state::{ContractState, ShutdownState};
2+
3+
#[derive(Debug, PartialEq)]
4+
pub enum GuardError {
5+
ContractPaused,
6+
EmergencyShutdown,
7+
Unauthorized,
8+
}
9+
10+
impl std::fmt::Display for GuardError {
11+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12+
match self {
13+
GuardError::ContractPaused => write!(f, "Contract is paused"),
14+
GuardError::EmergencyShutdown => write!(f, "Emergency shutdown is active"),
15+
GuardError::Unauthorized => write!(f, "Caller is not authorized"),
16+
}
17+
}
18+
}
19+
20+
pub fn require_active(state: &ContractState) -> Result<(), GuardError> {
21+
match state.shutdown {
22+
ShutdownState::Active => Ok(()),
23+
ShutdownState::Paused => Err(GuardError::ContractPaused),
24+
ShutdownState::Emergency => Err(GuardError::EmergencyShutdown),
25+
}
26+
}
27+
28+
pub fn require_admin(state: &ContractState, caller: &[u8; 32]) -> Result<(), GuardError> {
29+
if &state.admin == caller {
30+
Ok(())
31+
} else {
32+
Err(GuardError::Unauthorized)
33+
}
34+
}
35+
36+
pub fn require_admin_and_active(
37+
state: &ContractState,
38+
caller: &[u8; 32],
39+
) -> Result<(), GuardError> {
40+
require_admin(state, caller)?;
41+
require_active(state)
42+
}

my-contract/src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
pub mod circuit_breaker;
2+
pub mod guards;
3+
pub mod state;
4+
5+
use guards::{require_active, GuardError};
6+
use state::ContractState;
7+
8+
pub fn deposit(
9+
state: &mut ContractState,
10+
_caller: &[u8; 32],
11+
amount: u64,
12+
) -> Result<(), GuardError> {
13+
require_active(state)?;
14+
state.balance = state.balance.checked_add(amount)
15+
.ok_or(GuardError::Unauthorized)?;
16+
Ok(())
17+
}
18+
19+
pub fn withdraw(
20+
state: &mut ContractState,
21+
_caller: &[u8; 32],
22+
amount: u64,
23+
) -> Result<(), GuardError> {
24+
require_active(state)?;
25+
if state.balance < amount {
26+
return Err(GuardError::Unauthorized);
27+
}
28+
state.balance -= amount;
29+
Ok(())
30+
}
31+
32+
pub fn update_data(
33+
state: &mut ContractState,
34+
_caller: &[u8; 32],
35+
new_data: Vec<u8>,
36+
) -> Result<(), GuardError> {
37+
require_active(state)?;
38+
state.data = new_data;
39+
Ok(())
40+
}
41+
42+
pub fn get_balance(state: &ContractState) -> u64 {
43+
state.balance
44+
}
45+
46+
pub fn get_shutdown_status(state: &ContractState) -> &state::ShutdownState {
47+
&state.shutdown
48+
}

0 commit comments

Comments
 (0)