Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
targets: wasm32-unknown-unknown
targets: wasm32v1-none

- name: Cache cargo
uses: Swatinem/rust-cache@v2
Expand All @@ -38,13 +38,13 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
targets: wasm32-unknown-unknown
targets: wasm32v1-none

- name: Cache cargo
uses: Swatinem/rust-cache@v2

- name: Build WASM (release)
run: cargo build --target wasm32-unknown-unknown --release
run: cargo build --target wasm32v1-none --release

- name: Run unit tests
run: cargo test --workspace --lib
Expand Down
19 changes: 8 additions & 11 deletions contracts/teachlink/src/access_logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Provides comprehensive, tamper-evident access logging for security auditing.
//! Every significant contract invocation is recorded with caller identity,
//! operation tag, outcome (success or failure with error code), and ledger
//! operation tag, outcome (success or failure), and ledger
//! timestamp. Log entries are stored in persistent storage and per-address
//! hourly call counts are maintained for temporal pattern analysis.

Expand Down Expand Up @@ -71,7 +71,7 @@ impl AccessLogger {
// --- Emit event ---
let (success, error_code) = match &outcome {
AccessOutcome::Success => (true, 0u32),
AccessOutcome::Failure { error_code } => (false, *error_code),
AccessOutcome::Failure => (false, 1u32),
};

AccessAttemptEvent {
Expand Down Expand Up @@ -180,15 +180,12 @@ impl AccessLogger {
}

// Outcome filter
if let Some(ref outcome_filter) = query.outcome_filter {
let matches = match (outcome_filter, &entry.outcome) {
(AccessOutcome::Success, AccessOutcome::Success) => true,
(
AccessOutcome::Failure { error_code: a },
AccessOutcome::Failure { error_code: b },
) => a == b,
_ => false,
};
if let Some(outcome_filter) = query.outcome_filter {
let want_success = outcome_filter == 0;
let matches = matches!(
(want_success, &entry.outcome),
(true, AccessOutcome::Success) | (false, AccessOutcome::Failure)
);
if !matches {
return false;
}
Expand Down
14 changes: 14 additions & 0 deletions contracts/teachlink/src/arbitration.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::bulk_limits;
use crate::errors::EscrowError;
use crate::storage::{ARBITRATORS, ESCROWS};
use crate::types::{ArbitratorProfile, Escrow, EscrowStatus};
Expand All @@ -10,6 +11,12 @@ impl ArbitrationManager {
pub fn register_arbitrator(env: &Env, profile: ArbitratorProfile) -> Result<(), EscrowError> {
profile.address.require_auth();

// Batch size check for DoS protection
bulk_limits::check_batch_size(profile.specialization.len())
.expect("Too many specializations");
bulk_limits::check_batch_size(profile.dispute_types_handled.len())
.expect("Too many dispute types");

let mut arbitrators: Map<Address, ArbitratorProfile> = env
.storage()
.instance()
Expand All @@ -29,6 +36,13 @@ impl ArbitrationManager {
profile: ArbitratorProfile,
) -> Result<(), EscrowError> {
address.require_auth();

// Batch size check for DoS protection
bulk_limits::check_batch_size(profile.specialization.len())
.expect("Too many specializations");
bulk_limits::check_batch_size(profile.dispute_types_handled.len())
.expect("Too many dispute types");

if address != profile.address {
return Err(EscrowError::SignerNotAuthorized);
}
Expand Down
14 changes: 12 additions & 2 deletions contracts/teachlink/src/bft_consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@
//! See `contracts/documentation/COLLABORATION.md` §Consensus for the
//! governance rationale behind the rotation policy.

use crate::bulk_limits;
use crate::errors::BridgeError;
use crate::events::{
ProposalCreatedEvent, ProposalExecutedEvent, ProposalVotedEvent, ValidatorRegisteredEvent,
ValidatorUnregisteredEvent,
};
use crate::storage::{
StorageKey, BRIDGE_PROPOSALS, CONSENSUS_STATE, PROPOSAL_COUNTER, PROPOSAL_EXPIRES_SEQ,
VALIDATORS, VALIDATOR_ACTIVITY_SEQ, VALIDATOR_INFO, VALIDATOR_STAKES,
StorageKey, BRIDGE_PROPOSALS, CONSENSUS_STATE, NETWORK_STATE, PROPOSAL_COUNTER,
PROPOSAL_EXPIRES_SEQ, VALIDATORS, VALIDATOR_ACTIVITY_SEQ, VALIDATOR_INFO, VALIDATOR_STAKES,
};
use crate::types::{
BridgeProposal, ConsensusState, CrossChainMessage, NetworkCondition, NetworkHealth,
Expand All @@ -76,6 +77,11 @@ pub const ROTATION_EPOCH_ROUNDS: u64 = 100;
/// Validators below this threshold are rotated out during epoch transitions.
pub const MIN_ACTIVE_REPUTATION: u32 = 40;

const MISS_THRESHOLD_DEGRADED: u32 = 3;
const MISS_THRESHOLD_CRITICAL: u32 = 5;
const TIMEOUT_MULTIPLIER_DEGRADED: u64 = 2;
const TIMEOUT_MULTIPLIER_CRITICAL: u64 = 3;

/// BFT Consensus Manager
pub struct BFTConsensus;

Expand Down Expand Up @@ -574,6 +580,8 @@ impl BFTConsensus {
let mut active_validators: u32 = 0;

for (validator, is_active) in validators.iter() {
// Gas budget check to prevent DoS from large validator sets
bulk_limits::check_gas_budget(env).expect("Budget exceeded");
if is_active {
active_validators += 1;
if let Some(stake) = stakes.get(validator.clone()) {
Expand Down Expand Up @@ -686,6 +694,8 @@ impl BFTConsensus {
.unwrap_or_else(|| Map::new(env));
let mut active = Vec::new(env);
for (validator, is_active) in validators.iter() {
// Gas budget check to prevent DoS from large validator sets
bulk_limits::check_gas_budget(env).expect("Budget exceeded");
if is_active {
active.push_back(validator.clone());
}
Expand Down
50 changes: 19 additions & 31 deletions contracts/teachlink/src/bridge.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::access_control::AccessControlManager;
use crate::bulk_limits;
use crate::errors::BridgeError;
use crate::events::{
BridgeCancelledEvent, BridgeCompletedEvent, BridgeFailedEvent, BridgeFeeUpdatedEvent,
Expand Down Expand Up @@ -111,6 +112,9 @@ impl Bridge {
&destination_address,
)?;

// Rate limiting for DoS protection
bulk_limits::check_rate_limit(env, &from)?;

let repo = BridgeRepository::new(env);

// Check if destination chain is supported
Expand Down Expand Up @@ -228,6 +232,12 @@ impl Bridge {
min_validators,
)?;

// Batch size check for validator signatures to prevent DoS
bulk_limits::check_batch_size_limit(
validator_signatures.len(),
bulk_limits::MAX_VALIDATOR_BATCH,
)?;

// Verify all signatures are from valid validators
for validator in validator_signatures.iter() {
if !repo.validators.is_validator(&validator) {
Expand Down Expand Up @@ -330,15 +340,6 @@ impl Bridge {
}
.publish(env);

// Audit log: record validator addition
let _ = crate::audit::AuditManager::log_validator_operation(
env,
true,
validator.clone(),
admin.clone(),
Bytes::new(env),
);

Ok(())
}

Expand Down Expand Up @@ -507,10 +508,10 @@ impl Bridge {
}
.publish(env);

// Audit log: record validator removal
// Audit log: record validator addition
let _ = crate::audit::AuditManager::log_validator_operation(
env,
false,
true,
validator.clone(),
admin.clone(),
Bytes::new(env),
Expand Down Expand Up @@ -554,15 +555,6 @@ impl Bridge {
}
.publish(env);

// Audit: configuration change - supported chain added
let _ = crate::audit::AuditManager::create_audit_record(
env,
crate::types::OperationType::ConfigUpdate,
admin.clone(),
Bytes::from_slice(env, &chain_id.to_be_bytes()),
Bytes::new(env),
);

Ok(())
}

Expand Down Expand Up @@ -636,12 +628,12 @@ impl Bridge {
}
.publish(env);

// Audit: fee update
// Audit: configuration change - supported chain removed
let _ = crate::audit::AuditManager::create_audit_record(
env,
crate::types::OperationType::FeeUpdate,
crate::types::OperationType::ConfigUpdate,
admin.clone(),
Bytes::from_slice(env, &fee.to_be_bytes()),
Bytes::from_slice(env, &chain_id.to_be_bytes()),
Bytes::new(env),
);

Expand Down Expand Up @@ -729,12 +721,12 @@ impl Bridge {
}
.publish(env);

// Audit: min validators updated
// Audit: fee recipient change
let _ = crate::audit::AuditManager::create_audit_record(
env,
crate::types::OperationType::ConfigUpdate,
admin.clone(),
Bytes::from_slice(env, &min_validators.to_be_bytes()),
Bytes::new(env),
Bytes::new(env),
);

Expand Down Expand Up @@ -821,19 +813,15 @@ impl Bridge {
/// Assumption: contract has already been initialized. This call panics otherwise.
pub fn get_token(env: &Env) -> Address {
let repo = BridgeRepository::new(env);
repo.config
.get_token()
.map_err(|_| BridgeError::StorageError)
repo.config.get_token().unwrap()
}

/// Get the admin address
///
/// Assumption: contract has already been initialized. This call panics otherwise.
pub fn get_admin(env: &Env) -> Address {
let repo = BridgeRepository::new(env);
repo.config
.get_admin()
.map_err(|_| BridgeError::StorageError)
repo.config.get_admin().unwrap()
}
}

Expand Down
Loading
Loading