Skip to content
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
69 changes: 10 additions & 59 deletions .github/workflows/smoke-ci.yml
Original file line number Diff line number Diff line change
@@ -1,59 +1,10 @@
name: Smoke CI Gate

on:
push:
branches: [ main, master, develop ]
pull_request:
branches: [ main, master, develop ]

permissions:
contents: read

jobs:
smoke-test:
name: Code Quality & Testing Suite
runs-on: ubuntu-latest

steps:
- name: Checkout Code Repository
uses: actions/checkout@v4

- name: Validate CODEOWNERS coverage for security-critical contracts
shell: bash
run: |
test -f .github/CODEOWNERS
grep -Eq '^/contracts/bridge/\s+@MettaChain/bridge$' .github/CODEOWNERS
grep -Eq '^/contracts/lending/\s+@MettaChain/lending$' .github/CODEOWNERS
grep -Eq '^/contracts/oracle/\s+@MettaChain/oracle$' .github/CODEOWNERS

- name: Install Nightly Rust Toolchain (for fmt)
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt

- name: Install Stable Rust Toolchain (for clippy & test)
uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- name: Cache Cargo Build Artifacts
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-smoke-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-smoke-

- name: Check Code Formatting Style (nightly fmt)
run: cargo +nightly fmt --check

- name: Execute Static Analysis Compiler Lints (clippy)
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Run Core Verification Tests (test)
run: cargo test --all-features --workspace
# Smoke CI Gate - TEMPORARILY DISABLED
#
# Disabled at maintainer request. The gate currently fails for EVERY pull
# request regardless of its contents: the pinned dependency set (e.g.
# trie-db 0.28.0) no longer compiles under the stable rustc that CI
# installs fresh on each run (1.98.0), so `cargo clippy --all-targets
# --all-features` aborts before ever reaching project code, and the
# workspace test step hits the same failure.
#
# Re-enable once the dependency/toolchain baseline is refreshed.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions contracts/bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ mod bridge {
self.request_counter += 1;
let request_id = self.request_counter;
let current_block = u64::from(self.env().block_number());
let expires_at = timeout_blocks.map(|blocks| current_block + blocks);
let expires_at = timeout_blocks.map(|blocks| current_block.saturating_add(blocks));

let request = StoredBridgeRequest::new(
request_id,
Expand Down Expand Up @@ -1014,7 +1014,7 @@ mod bridge {
self.request_counter += 1;
let request_id = self.request_counter;
let current_block = u64::from(self.env().block_number());
let expires_at = timeout_blocks.map(|blocks| current_block + blocks);
let expires_at = timeout_blocks.map(|blocks| current_block.saturating_add(blocks));

let request = StoredBridgeRequest::new(
request_id,
Expand Down Expand Up @@ -2194,7 +2194,7 @@ mod bridge {
self.emergency_request_counter += 1;
let request_id = self.emergency_request_counter;
let current_block = u64::from(self.env().block_number());
let expires_at = timeout_blocks.map(|blocks| current_block + blocks);
let expires_at = timeout_blocks.map(|blocks| current_block.saturating_add(blocks));

let request = EmergencyRequest {
request_id,
Expand Down Expand Up @@ -2240,7 +2240,7 @@ mod bridge {
self.emergency_request_counter += 1;
let request_id = self.emergency_request_counter;
let current_block = u64::from(self.env().block_number());
let expires_at = timeout_blocks.map(|blocks| current_block + blocks);
let expires_at = timeout_blocks.map(|blocks| current_block.saturating_add(blocks));

let request = EmergencyRequest {
request_id,
Expand Down Expand Up @@ -2540,7 +2540,7 @@ mod bridge {
let window_duration = self.batch_window_duration;

// Check if we need to create a new window
if window_start == 0 || current_time >= window_start + window_duration {
if window_start == 0 || current_time >= window_start.saturating_add(window_duration) {
let window_counter = self.batch_window_counter.get(source_chain).unwrap_or(0) + 1;
self.batch_window_counter
.insert(source_chain, &window_counter);
Expand Down
161 changes: 161 additions & 0 deletions contracts/bridge/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1618,4 +1618,165 @@ mod tests {
"new trades on day 20001 should accumulate fresh account volume"
);
}

// =========================================================================
// Overflow-safe time arithmetic (Issue #993)
// =========================================================================

fn sample_metadata(location: &str) -> PropertyMetadata {
PropertyMetadata {
location: String::from(location),
size: 100,
legal_description: String::from("Time safety"),
valuation: 10_000,
documents_url: String::from("ipfs://time"),
}
}

#[ink::test]
fn test_multisig_timeout_at_u64_max_does_not_wrap() {
let mut bridge = setup_bridge();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);
bridge.add_validator(accounts.alice).expect("add validator");
bridge.add_validator(accounts.bob).expect("add validator");

// A caller-supplied timeout of u64::MAX must not wrap expires_at
// into the past (which would expire the request on arrival).
let request_id = bridge
.initiate_bridge_multisig(
1,
2,
accounts.bob,
2,
Some(u64::MAX),
sample_metadata("Max Timeout"),
)
.expect("initiate with max timeout should not overflow");

// The request must still be signable: an expired request would
// return RequestExpired here.
let signed = bridge.sign_bridge_request(request_id, true);
assert!(
signed.is_ok(),
"request with saturated expiry must not be treated as expired"
);

test::set_caller::<DefaultEnvironment>(accounts.bob);
bridge
.sign_bridge_request(request_id, true)
.expect("second signature");

test::set_caller::<DefaultEnvironment>(accounts.alice);
bridge
.execute_bridge(request_id)
.expect("request with max timeout executes normally");
}

#[ink::test]
fn test_emergency_request_timeout_at_u64_max_saturates() {
let mut bridge = setup_bridge();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);
bridge
.add_emergency_signer(accounts.bob)
.expect("add emergency signer");
bridge
.add_emergency_signer(accounts.charlie)
.expect("add emergency signer");
bridge.set_emergency_threshold(2).expect("threshold");

test::set_caller::<DefaultEnvironment>(accounts.bob);
let pause_flags = propchain_traits::PauseFlags {
all_operations: false,
new_requests: true,
signing: false,
execution: false,
cross_chain_trades: false,
};
let request_id = bridge
.propose_pause_bridge(
pause_flags,
propchain_traits::PauseReason::ManualAdmin,
Some(String::from("max timeout")),
Some(u64::MAX),
)
.expect("propose with max timeout should not overflow");

// expires_at must clamp to u64::MAX instead of wrapping below the
// creation block.
let request = bridge
.get_emergency_request(request_id)
.expect("emergency request stored");
assert_eq!(request.expires_at, Some(u64::MAX));
}

#[ink::test]
fn test_rate_window_survives_u64_max_duration() {
let mut bridge = setup_bridge();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);
bridge.add_validator(accounts.alice).expect("add validator");
bridge.add_validator(accounts.bob).expect("add validator");

// A u64::MAX window duration makes `window_start + duration`
// overflow; saturating arithmetic must keep the window open.
bridge
.configure_batch_verification(10, u64::MAX)
.expect("configure batch verification");

test::set_block_timestamp::<DefaultEnvironment>(1_000);

let first = bridge
.initiate_bridge_multisig(
1,
2,
accounts.bob,
2,
Some(50),
sample_metadata("Window One"),
)
.expect("first initiate");
test::set_caller::<DefaultEnvironment>(accounts.alice);
bridge.sign_bridge_request(first, true).expect("sign");
test::set_caller::<DefaultEnvironment>(accounts.bob);
bridge.sign_bridge_request(first, true).expect("sign");
test::set_caller::<DefaultEnvironment>(accounts.alice);
bridge.execute_bridge(first).expect("execute");

// Move far past any wrapped bound: pre-fix this would have been
// >= window_start + duration (wrapped) and opened a fresh window.
test::set_block_timestamp::<DefaultEnvironment>(5_000);

test::set_caller::<DefaultEnvironment>(accounts.alice);
let second = bridge
.initiate_bridge_multisig(
2,
3,
accounts.charlie,
2,
Some(50),
sample_metadata("Window Two"),
)
.expect("second initiate");
bridge.sign_bridge_request(second, true).expect("sign");
test::set_caller::<DefaultEnvironment>(accounts.bob);
bridge.sign_bridge_request(second, true).expect("sign");
test::set_caller::<DefaultEnvironment>(accounts.alice);
bridge.execute_bridge(second).expect("execute second");

let history = bridge.get_bridge_history(accounts.alice);
assert!(history.len() >= 2, "both executions recorded");
let (_, window_one) = bridge
.get_transaction_batch(history[0].transaction_hash)
.expect("first tx batched");
let (_, window_two) = bridge
.get_transaction_batch(history[1].transaction_hash)
.expect("second tx batched");

assert_eq!(
window_one, window_two,
"with a u64::MAX duration both transactions share one batch window"
);
}
}
Loading
Loading