Skip to content
Merged
Changes from 1 commit
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
141 changes: 139 additions & 2 deletions contracts/collateral-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#![no_std]

use soroban_sdk::{
contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol,
contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol, String,
};

/// Contract errors
Expand Down Expand Up @@ -44,6 +44,8 @@ pub struct Collateral {
pub realized_value: i128,
pub expiry_ts: u64,
pub metadata_hash: BytesN<32>,
pub metadata_uri: String,
pub is_verified: bool,
Comment on lines +48 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Cross-contract Collateral schema is now inconsistent and can break integration.

After adding metadata_uri and is_verified here, contracts/risk-assessment/src/lib.rs (Lines 274-286 in provided context) still uses the old Collateral shape. This can break cross-contract decoding at runtime.

Suggested alignment patch (risk-assessment side)
 // contracts/risk-assessment/src/lib.rs
 #[contracttype]
 #[derive(Clone, Debug)]
 pub struct Collateral {
     pub id: u64,
     pub owner: Address,
     pub face_value: i128,
     pub realized_value: i128,
     pub expiry_ts: u64,
+    pub metadata_uri: String,
+    pub is_verified: bool,
     pub registered_at: u64,
     pub last_valuation_ts: u64,
     pub locked: bool,
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/collateral-registry/src/lib.rs` around lines 47 - 48, The
Collateral struct used for cross-contract calls was changed to add metadata_uri:
String and is_verified: bool in the collateral-registry crate, but the
Collateral definition in the risk-assessment crate still uses the old shape;
update the Collateral struct in the risk-assessment code to include the new
fields (metadata_uri and is_verified) with the same types and ensure its
(de)serialization derives match the registry (e.g., Borsh/Serde derives) and
update any places that construct, decode, or pattern-match on Collateral
(references: the Collateral struct, metadata_uri, is_verified and any
cross-contract decode/serialize helpers) so cross-contract decoding succeeds.

pub registered_at: u64,
pub last_valuation_ts: u64,
pub locked: bool,
Expand Down Expand Up @@ -87,6 +89,7 @@ impl CollateralRegistry {
/// * `face_value` - Face value of the collateral (must be > 0)
/// * `expiry_ts` - Expiry timestamp (must be in future)
/// * `metadata_hash` - SHA-256 hash of off-chain metadata
/// * `metadata_uri` - URI pointing to off-chain metadata (IPFS/S3)
///
/// # Returns
/// The sequential collateral ID
Expand All @@ -99,6 +102,7 @@ impl CollateralRegistry {
face_value: i128,
expiry_ts: u64,
metadata_hash: BytesN<32>,
metadata_uri: String,
) -> Result<u64, ContractError> {
owner.require_auth();

Expand Down Expand Up @@ -137,6 +141,8 @@ impl CollateralRegistry {
realized_value: face_value,
expiry_ts,
metadata_hash: metadata_hash.clone(),
metadata_uri: metadata_uri.clone(),
is_verified: false,
registered_at: current_ts,
last_valuation_ts: current_ts,
locked: false,
Expand Down Expand Up @@ -355,6 +361,36 @@ impl CollateralRegistry {

Ok(())
}

/// Verify collateral (admin only)
///
/// # Arguments
/// * `id` - Collateral ID to verify
///
/// # Events
/// Emits `CollateralVerified` event
pub fn verify_collateral(env: Env, id: u64) -> Result<(), ContractError> {
let admin: Address = env
.storage()
.instance()
.get(&symbol_short!("admin"))
.unwrap();

admin.require_auth();

let mut collateral: Collateral = env
.storage()
.persistent()
.get(&id)
.ok_or(ContractError::CollateralNotFound)?;

collateral.is_verified = true;
env.storage().persistent().set(&id, &collateral);

env.events().publish((symbol_short!("coll_verified"),), (id,));
Comment on lines +440 to +443
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

verify_collateral does not implement toggle/set behavior from the objective.

Line 387 always forces is_verified = true. The linked objective calls for verification status to be toggle/settable; currently there is no way to unverify.

Possible fix (toggle behavior)
-        collateral.is_verified = true;
+        collateral.is_verified = !collateral.is_verified;
         env.storage().persistent().set(&id, &collateral);

-        env.events().publish((symbol_short!("coll_verified"),), (id,));
+        env.events()
+            .publish((symbol_short!("coll_verified"),), (id, collateral.is_verified));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/collateral-registry/src/lib.rs` around lines 387 - 390, The
verify_collateral logic currently always sets collateral.is_verified = true;
change it to implement toggle/settable behavior in the verify_collateral
function: read the incoming intent (either a boolean parameter like
desired_verified or a toggle flag) and update collateral.is_verified accordingly
(either set to the provided value or flip its current value), then persist with
env.storage().persistent().set(&id, &collateral) and publish a clear event
reflecting the new state (use the existing (symbol_short!("coll_verified"),) for
verified and emit a corresponding event or include the new boolean in the
payload so callers can distinguish verify vs unverify); ensure you update any
function signature or event payload used by verify_collateral to
accept/propagate the desired state.


Ok(())
}
}

#[cfg(test)]
Expand Down Expand Up @@ -393,13 +429,15 @@ mod test {
// Register collateral
let future_ts = env.ledger().timestamp() + 86400; // 1 day from now
let metadata_hash = BytesN::from_array(&env, &[1; 32]);
let metadata_uri = String::from_slice(&env, "ipfs://QmTest123");

let result = CollateralRegistry::register_collateral(
env.clone(),
owner.clone(),
1000,
future_ts,
metadata_hash,
metadata_uri,
);

assert!(result.is_ok());
Expand All @@ -413,6 +451,7 @@ mod test {
assert_eq!(collateral.face_value, 1000);
assert_eq!(collateral.realized_value, 1000);
assert_eq!(collateral.locked, false);
assert_eq!(collateral.is_verified, false);
});
}

Expand All @@ -433,12 +472,14 @@ mod test {
// Register collateral
let future_ts = env.ledger().timestamp() + 86400;
let metadata_hash = BytesN::from_array(&env, &[1; 32]);
let metadata_uri = String::from_slice(&env, "ipfs://QmTest456");
let collateral_id = CollateralRegistry::register_collateral(
env.clone(),
owner,
1000,
future_ts,
metadata_hash,
metadata_uri,
)
.unwrap();

Expand Down Expand Up @@ -468,13 +509,15 @@ mod test {

let future_ts = env.ledger().timestamp() + 86400;
let metadata_hash = BytesN::from_array(&env, &[1; 32]);
let metadata_uri = String::from_slice(&env, "ipfs://QmTest789");

let result = CollateralRegistry::register_collateral(
env.clone(),
owner,
0, // Invalid amount
future_ts,
metadata_hash,
metadata_uri,
);

assert_eq!(result, Err(ContractError::InvalidAmount));
Expand All @@ -497,13 +540,15 @@ mod test {

let past_ts = env.ledger().timestamp() - 1; // Already expired
let metadata_hash = BytesN::from_array(&env, &[1; 32]);
let metadata_uri = String::from_slice(&env, "ipfs://QmTestExp");

let result = CollateralRegistry::register_collateral(
env.clone(),
owner,
1000,
past_ts,
metadata_hash,
metadata_uri,
);

assert_eq!(result, Err(ContractError::CollateralExpired));
Expand All @@ -524,6 +569,8 @@ mod test {

let future_ts = env.ledger().timestamp() + 86400;
let metadata_hash = BytesN::from_array(&env, &[1; 32]);
let metadata_uri1 = String::from_slice(&env, "ipfs://QmTestDup1");
let metadata_uri2 = String::from_slice(&env, "ipfs://QmTestDup2");

// Register first collateral
CollateralRegistry::register_collateral(
Expand All @@ -532,6 +579,7 @@ mod test {
1000,
future_ts,
metadata_hash.clone(),
metadata_uri1,
)
.unwrap();

Expand All @@ -542,6 +590,7 @@ mod test {
2000,
future_ts,
metadata_hash, // Same hash
metadata_uri2,
);

assert_eq!(result, Err(ContractError::DuplicateMetadata));
Expand All @@ -563,7 +612,8 @@ mod test {

let future_ts = env.ledger().timestamp() + 86400;
let metadata_hash = BytesN::from_array(&env, &[1; 32]);
let collateral_id = client.register_collateral(&owner, &1000, &future_ts, &metadata_hash);
let metadata_uri = String::from_slice(&env, "ipfs://QmLockTest");
let collateral_id = client.register_collateral(&owner, &1000, &future_ts, &metadata_hash, &metadata_uri);

client.lock_collateral(&collateral_id);
assert!(client.is_locked(&collateral_id));
Expand Down Expand Up @@ -604,12 +654,14 @@ mod test {
// Register collateral
let future_ts = env.ledger().timestamp() + 86400;
let metadata_hash = BytesN::from_array(&env, &[1; 32]);
let metadata_uri = String::from_slice(&env, "ipfs://QmUnauthorized");
let collateral_id = CollateralRegistry::register_collateral(
env.clone(),
owner,
1000,
future_ts,
metadata_hash,
metadata_uri,
)
.unwrap();

Expand All @@ -618,4 +670,89 @@ mod test {
assert_eq!(result, Err(ContractError::Unauthorized));
});
}

#[test]
fn test_verify_collateral_success() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let owner = Address::generate(&env);
let contract_id = env.register_contract(None, CollateralRegistry);

env.as_contract(&contract_id, || {
// Initialize
CollateralRegistry::initialize(env.clone(), admin.clone()).unwrap();

// Register collateral
let future_ts = env.ledger().timestamp() + 86400;
let metadata_hash = BytesN::from_array(&env, &[1; 32]);
let metadata_uri = String::from_slice(&env, "ipfs://QmVerifyTest");
let collateral_id = CollateralRegistry::register_collateral(
env.clone(),
owner,
1000,
future_ts,
metadata_hash,
metadata_uri,
)
.unwrap();

// Verify collateral as admin
let verify_result = CollateralRegistry::verify_collateral(env.clone(), collateral_id);
assert!(verify_result.is_ok());

// Verify is_verified is true
let collateral =
CollateralRegistry::get_collateral(env.clone(), collateral_id).unwrap();
assert_eq!(collateral.is_verified, true);
});
}

#[test]
fn test_verify_collateral_unauthorized() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let unauthorized = Address::generate(&env);
let owner = Address::generate(&env);
let contract_id = env.register_contract(None, CollateralRegistry);

env.as_contract(&contract_id, || {
CollateralRegistry::initialize(env.clone(), admin).unwrap();

// Register collateral
let future_ts = env.ledger().timestamp() + 86400;
let metadata_hash = BytesN::from_array(&env, &[1; 32]);
let metadata_uri = String::from_slice(&env, "ipfs://QmUnauthorizedVerify");
let collateral_id = CollateralRegistry::register_collateral(
env.clone(),
owner,
1000,
future_ts,
metadata_hash,
metadata_uri,
)
.unwrap();

// Try to verify with non-admin (should fail due to auth)
let result = CollateralRegistry::verify_collateral(env.clone(), collateral_id);
assert_eq!(result, Err(ContractError::Unauthorized));
});
}

#[test]
fn test_verify_collateral_not_found() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let contract_id = env.register_contract(None, CollateralRegistry);

env.as_contract(&contract_id, || {
CollateralRegistry::initialize(env.clone(), admin).unwrap();

// Try to verify non-existent collateral
let result = CollateralRegistry::verify_collateral(env.clone(), 999);
assert_eq!(result, Err(ContractError::CollateralNotFound));
});
}
}