Skip to content

Conversation

@thcsomebody
Copy link
Contributor

Add a new DAORegistry contract (Phase-0 SOV-P0-2.3).

Changes:

  • Add module with and types
  • Implement with , , , (owner-only)
  • Add and variants and serialization tests
  • Add unit tests for registry and event behavior and wire up re-exports

Notes:

  • returns all DAOs sorted by ; pagination is a TODO.
  • The contract returns values for runtime emission; runtime wiring can publish events.

Please review and merge to .

Add dao_registry module with DAOEntry/DAOMetadata and DAORegistry implementation. Implement register_dao, get_dao, list_daos, update_metadata with owner-only access control and clear errors. Add DaoRegistered and DaoUpdated ContractEvent variants and serialization tests. Add unit tests for registry behavior and modify contracts re-exports.
@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 6, 2026

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

🔴 CRITICAL CODE REVIEW - DO NOT MERGE

I've completed a comprehensive code review. This PR has 4 CRITICAL issues blocking merge plus 6 important issues. Detailed inline comments follow on each file.

Merge Blocker: Please fix all CRITICAL issues before re-requesting review.

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

CRITICAL-1: Type Name Collision 🔴

File: lib-blockchain/src/contracts/dao_registry/types.rs (lines 17-26)

pub struct DAOMetadata {  // ❌ COLLISION with existing DAOMetadata in types/dao.rs
    pub dao_id: [u8; 32],
    pub token_addr: [u8; 32],
    pub class: String,
    pub metadata_hash: Option<[u8; 32]>,
    pub treasury: PublicKey,
    pub owner: PublicKey,
    pub created_at: u64,
}

Problem: This conflicts with the existing DAOMetadata struct in lib-blockchain/src/types/dao.rs which has completely different fields (dao_type, token_class, treasury_allocation).

Impact: Namespace confusion when both are re-exported. Callers won't know which DAOMetadata they're getting.

Fix: Rename to DAORegistryEntry or DAORegistryMetadata:

pub struct DAORegistryEntry {
    pub dao_id: [u8; 32],
    // ... rest of fields
}

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

CRITICAL-2: Orphaned Test File 🔴

File: lib-blockchain/src/contracts/dao_registry/mod.rs

pub mod core;
pub mod types;
// ❌ MISSING: #[cfg(test)] mod tests;

pub use core::DAORegistry;
pub use types::{DAOEntry, DAOMetadata};

Problem: The file lib-blockchain/src/contracts/dao_registry/tests.rs exists with 65 lines of tests, but it's NOT declared as a module. Rust will not compile these tests.

Impact: All tests in tests.rs are DEAD CODE and will never run.

Fix: Add the test module declaration:

pub mod core;
pub mod types;

#[cfg(test)]
mod tests;

pub use core::DAORegistry;
pub use types::{DAOEntry, DAOMetadata};

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

CRITICAL-3: Silent Failure on Contract Deserialization 🔴

File: lib-blockchain/src/contracts/integration/mod.rs (lines 74-85)

if transaction.memo.len() > 4 && &transaction.memo[0..4] == b"ZHTP" {
    let contract_data = &transaction.memo[4..];
    let (call, signature): (ContractCall, Signature) = 
        bincode::deserialize(contract_data)?;
    calls.push((call, signature));
}
// ❌ If memo format is invalid, we silently return Ok(Vec::new()) with NO ERROR!
Ok(calls)  // Returns empty Vec, caller has no idea what happened

Problem: When a contract execution transaction arrives with a malformed memo (missing "ZHTP" marker or corrupted data), the function returns an empty Vec instead of an error.

Impact:

  • Attacker could send malformed contract transactions and they'd silently disappear
  • Legitimate transactions could be corrupted in transit and would disappear silently
  • User sees no error, thinks contract executed but got zero results
  • Impossible to debug

Fix: Explicitly validate and return errors:

if transaction.transaction_type == TransactionType::ContractExecution {
    if transaction.memo.len() <= 4 {
        return Err(anyhow!("Contract execution transaction has insufficient memo data: expected >4 bytes, got {}", transaction.memo.len()));
    }
    if &transaction.memo[0..4] != b"ZHTP" {
        return Err(anyhow!("Invalid contract transaction marker: expected b\"ZHTP\", got {:?}", &transaction.memo[0..4]));
    }
    
    let contract_data = &transaction.memo[4..];
    match bincode::deserialize::<(ContractCall, Signature)>(contract_data) {
        Ok((call, signature)) => calls.push((call, signature)),
        Err(e) => return Err(anyhow!("Failed to deserialize contract call: {} (memo_len={})", e, contract_data.len())),
    }
}
Ok(calls)

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

CRITICAL-4: Catch-All Error Swallowing in Contract Execution 🔴

File: lib-blockchain/src/contracts/integration/mod.rs (lines 53-59)

match self.executor.execute_call(call, &mut context) {
    Ok(result) => results.push(result),
    Err(e) => {
        error!("Contract execution failed: {}", e);
        // ❌ PROBLEM: Error is logged but then...
        results.push(ContractResult::failure(context.gas_used));
        // ❌ We push a failure result (not propagate error)
    }
    // ❌ Function returns Ok(results) - CALLER CAN'T TELL IF EXECUTION SUCCEEDED OR FAILED!
}

Problem: Contract execution failures are caught, logged, but then returned as successful results. The caller gets Ok(Vec) whether contracts succeeded, failed, or crashed.

Impact:

  • Caller cannot distinguish success from failure - both return Ok()
  • Error message is too generic: "Contract execution failed" doesn't say WHICH contract or METHOD
  • Silent failures: transaction gets marked as successful even if all contracts failed
  • Potential state corruption: failed state mutations might leave blockchain inconsistent

Fix: Add detailed context to error logs:

match self.executor.execute_call(call, &mut context) {
    Ok(result) => results.push(result),
    Err(e) => {
        error!(
            "Contract execution failed: method={}, caller={:?}, contract_type={:?}, error: {}",
            call.method, 
            call.get_caller(), 
            call.contract_type,
            e
        );
        results.push(ContractResult::failure(context.gas_used));
    }
}

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

IMPORTANT-1: Missing Default Trait Implementation 🟠

File: lib-blockchain/src/contracts/dao_registry/core.rs

Problem: The DAORegistry struct has a new() constructor but does NOT implement the Default trait. Other contract structs in the codebase (MessageContract, GroupChat, etc.) all implement Default.

Impact: Inconsistent API. Users expecting Default::default() will get compile error.

Fix: Add Default implementation:

impl Default for DAORegistry {
    fn default() -> Self {
        Self::new()
    }
}

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

IMPORTANT-2: Duplicate Struct Types - DAOEntry and DAOMetadata are Identical 🟠

File: lib-blockchain/src/contracts/dao_registry/types.rs

pub struct DAOEntry {
    pub dao_id: [u8; 32],
    pub token_addr: [u8; 32],
    pub class: String,
    pub metadata_hash: Option<[u8; 32]>,
    pub treasury: PublicKey,
    pub owner: PublicKey,
    pub created_at: u64,
}

pub struct DAOMetadata {
    // ❌ IDENTICAL 7 fields!
    pub dao_id: [u8; 32],
    pub token_addr: [u8; 32],
    pub class: String,
    pub metadata_hash: Option<[u8; 32]>,
    pub treasury: PublicKey,
    pub owner: PublicKey,
    pub created_at: u64,
}

In core.rs get_dao():

Ok(DAOMetadata {
    dao_id: entry.dao_id,        // ❌ Manually copy all 7 fields!
    token_addr: entry.token_addr,
    class: entry.class.clone(),
    metadata_hash: entry.metadata_hash,
    treasury: entry.treasury.clone(),
    owner: entry.owner.clone(),
    created_at: entry.created_at,
})

Problem: Code duplication and maintenance burden. If a field is added to one, the other is forgotten.

Fix: Remove duplication:

// Option 1: Type alias
pub type DAOMetadata = DAOEntry;

// Option 2: Return reference from get_dao()
pub fn get_dao(&self, token_addr: [u8; 32]) -> Result<&DAOEntry, String>

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

IMPORTANT-3: Untyped class Field - Uses String Instead of DAOType Enum 🟠

File: lib-blockchain/src/contracts/dao_registry/types.rs (line 9)

pub struct DAOEntry {
    pub dao_id: [u8; 32],
    pub token_addr: [u8; 32],
    pub class: String,  // ❌ UNTYPED: Accepts any string
    pub metadata_hash: Option<[u8; 32]>,
    // ...
}

In tests:

.register_dao(token, "NP".to_string(), None, treasury, owner)  // ❌ String, not type-safe

Problem: Using String allows any value: "NP", "np", "NonProfit", "INVALID", etc. No type safety.

Solution: The codebase already has DAOType enum in lib-blockchain/src/types/dao.rs with NP and FP variants.

Fix: Use the existing DAOType enum:

use crate::types::dao::DAOType;

pub struct DAOEntry {
    pub dao_id: [u8; 32],
    pub token_addr: [u8; 32],
    pub class: DAOType,  // ✅ Type-safe, only NP or FP allowed
    pub metadata_hash: Option<[u8; 32]>,
    // ...
}

// In tests:
.register_dao(token, DAOType::NP, None, treasury, owner)  // ✅ Type-safe

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

IMPORTANT-4: Weak Error Messages in DAORegistry 🟠

File: lib-blockchain/src/contracts/dao_registry/core.rs (lines 74-90)

pub fn get_dao(&self, token_addr: [u8; 32]) -> Result<DAOMetadata, String> {
    match self.token_index.get(&token_addr) {
        Some(dao_id) => match self.registry.get(dao_id) {
            Some(entry) => Ok(...),
            None => Err("DAO entry not found for id".to_string()),  // ❌ Which id? No context!
        },
        None => Err("DAO not found for token address".to_string()),  // ❌ Which address? No help!
    }
}

Problem: Error messages don't include actual values being looked up. Developers cannot debug or understand what tokens are registered.

Fix: Include actual values and registry state in error messages:

pub fn get_dao(&self, token_addr: [u8; 32]) -> Result<DAOMetadata, String> {
    match self.token_index.get(&token_addr) {
        Some(dao_id) => match self.registry.get(dao_id) {
            Some(entry) => Ok(...),
            None => {
                // Data corruption: index points to missing DAO
                Err(format!(
                    "Data corruption: token index points to non-existent DAO (token_addr={:?}, dao_id={:?}, registry_size={})",
                    hex::encode(&token_addr),
                    hex::encode(dao_id),
                    self.registry.len()
                ))
            }
        },
        None => Err(format!(
            "DAO not found for token address: {} (registry_size={}, token_indices={})",
            hex::encode(&token_addr),
            self.registry.len(),
            self.token_index.len()
        )),
    }
}

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

IMPORTANT-5: Incomplete Error Propagation Chain 🟠

File: lib-blockchain/src/contracts/integration/mod.rs (lines 38-50)

let mut context = ExecutionContext::new(
    call.get_caller().map_err(|e| anyhow\!(e))?,  // ❌ Error wrapped but no context
    height,
    timestamp,
    self.calculate_gas_limit(transaction)?,  // ❌ If fails, no txn context
    transaction.id().into(),
);

let caller_key = self.extract_public_key(transaction)?;  // ❌ No txn height/context
if \!self.executor.validate_signature(&call, &signature, &caller_key)? {  // ❌ Which call?
    return Err(anyhow\!("Invalid signature"));  // ❌ Which signature failed? Which call?
}

Problem: Multiple fallible operations chain with ? but lose context on error:

  1. call.get_caller() error gets double-wrapped with anyhow\!(e)
  2. calculate_gas_limit() failure has no transaction context
  3. Signature validation error doesn't say WHICH contract call or WHICH signature failed

Fix: Add contextual error messages:

let caller_result = call.get_caller()
    .map_err(|e| anyhow\!("Failed to extract caller from contract call: {}", e))?;

let gas_limit = self.calculate_gas_limit(transaction)
    .map_err(|e| anyhow\!("Failed to calculate gas limit for transaction (txid={:?}): {}", transaction.id(), e))?;

let mut context = ExecutionContext::new(
    caller_result,
    height,
    timestamp,
    gas_limit,
    transaction.id().into(),
);

let caller_key = self.extract_public_key(transaction)
    .map_err(|e| anyhow\!("Failed to extract public key for signature validation: {}", e))?;

if \!self.executor.validate_signature(&call, &signature, &caller_key)? {
    return Err(anyhow\!(
        "Signature validation failed for contract call (method={}, caller={:?}, txid={:?})",
        call.method,
        caller_result,
        transaction.id()
    ));
}

@umwelt
Copy link
Contributor

umwelt commented Jan 6, 2026

IMPORTANT-6: Insufficient Test Coverage 🟠

File: lib-blockchain/src/contracts/dao_registry/tests.rs

Current Coverage: Only 3 tests covering ~40% of functionality

Critical Test Gaps:

  1. No Duplicate Token Registration Test

    • What happens if you register the same token twice?
    • Creates orphaned DAOs or overwrites existing ones?
  2. No Registry Desynchronization Test

    • No invariant test that token_indexregistry stay in sync
    • If one is updated, is the other?
  3. No Update Non-Existent DAO Test

    • What happens if update_metadata called on non-existent DAO?
    • Does error get propagated correctly?
  4. No Event Data Integrity Test

    • Current test checks event.event_type() == "DaoRegistered" only
    • Doesn't verify event contents match registered DAO
  5. No Field Immutability Test

    • What happens if you try to change token_addr after registration?
    • Are all immutable fields actually immutable?
  6. No Boundary Condition Tests

    • Empty registry behavior
    • Large registries (1000+ DAOs)
    • Zero/invalid addresses
  7. No Error Path Tests

    • Only "happy path" tested
    • Missing: duplicate tokens, invalid owners, invalid treasury, etc.

Fix: Add comprehensive tests before merge. Minimum 5 critical tests needed:

#[test]
fn test_duplicate_token_registration_fails() { ... }

#[test]
fn test_registry_token_index_sync_invariant() { ... }

#[test]
fn test_update_metadata_nonexistent_dao_fails() { ... }

#[test]
fn test_event_data_matches_registered_dao() { ... }

#[test]
fn test_field_immutability_after_creation() { ... }

Copy link
Contributor

@umwelt umwelt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛔ REVIEW SUMMARY - DO NOT MERGE

Comprehensive code review completed. This PR has critical issues blocking merge.

Issues Found:

  • 4 CRITICAL issues (lines 17-26, lines 74-90, lines 53-59, lines 74-85 across files)
  • 6 IMPORTANT issues (code quality, type safety, error handling, test coverage)

Merge Blocker:

ALL 4 critical issues MUST be fixed before re-requesting review:

  1. ✋ Type name collision: DAOMetadata conflicts with existing type in types/dao.rs
  2. ✋ Orphaned test file: tests.rs not declared in mod.rs (dead code)
  3. ✋ Silent failures: Contract deserialization failures ignored instead of errored
  4. ✋ Error swallowing: Contract execution failures masked as success

See detailed inline comments above for:

  • Exact code locations
  • Why each issue matters
  • Specific code fixes needed

@umwelt umwelt added the CHANGES REQUESTED PR that is blocked for CR label Jan 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CHANGES REQUESTED PR that is blocked for CR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants