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
10 changes: 10 additions & 0 deletions context/progress-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ Update this file after every completed contract change, fix, or architectural de

## Completed

### Security: Close Unauthenticated Admin-Claim Branch in Reputation `set_admin()`
- **Problem:** `set_admin()` in `contracts/reputation-contract/src/lib.rs` contained an unauthenticated fallback branch when no admin was stored, allowing anyone to claim admin of the reputation contract without authorization.
- **Fix (`reputation-contract`):**
- Added explicit, one-time `initialize(env, admin) -> Result<(), ReputationError>` function that requires `admin.require_auth()` and checks `storage::has_admin(&env)` (rejects re-initialization with `AlreadyInitialized = 11`).
- Updated `set_admin(env, new_admin) -> Result<(), ReputationError>` to fetch `old_admin = storage::get_admin(&env)?` (panics/returns `NotInitialized` when uninitialized) and enforce `old_admin.require_auth()` + `access::require_admin(&env, &old_admin)`.
- Added `has_admin(env)` in `storage.rs`.
- Added `AlreadyInitialized` error variant in `errors.rs`.
- Added new unit tests covering: unauthenticated first-time `set_admin` rejection (`NotInitialized`), single authorized `initialize`, double initialization rejection (`AlreadyInitialized`), unauthenticated `initialize` rejection, and end-to-end updater flow preservation.
- Updated test setup in `reputation-contract/src/tests.rs` and `creditline-contract/src/tests.rs` (`RealIntegrationCtx`), deployment script (`scripts/deploy-testnet.sh`), deployment metadata (`contracts/deployed-testnet.json`), and contract documentation (`contracts/reputation-contract/README.md`).

### `approve_loan` Pending Loan Funding & Re-Validation Fix
- **Problem:** `approve_loan()` previously activated pending loans without validating vendor status, checking reputation score, or checking available pool liquidity, and without calling `fund_loan()` on the liquidity pool to lock contribution funds or transferring funds to the vendor.
- **Fix (`creditline-contract`):**
Expand Down
2 changes: 1 addition & 1 deletion contracts/creditline-contract/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2557,7 +2557,7 @@ impl RealIntegrationCtx {
let creditline: CreditLineContractClient<'static> =
unsafe { core::mem::transmute(creditline) };

reputation.set_admin(&admin);
reputation.initialize(&admin);
reputation.set_updater(&admin, &admin, &true);
reputation.set_updater(&admin, &creditline_id, &true);

Expand Down
2 changes: 1 addition & 1 deletion contracts/deployed-testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"id": "CC3BO57ZRJGA63QJBIBSOMI25Z3X2I5CYTARYRAUXUAILX6L3OWBL5SB",
"initialized": true,
"initializedAt": "2026-05-11",
"initMethod": "set_admin(new_admin)",
"initMethod": "initialize(admin)",
"wasmHash": "548ad3c1e0bca85a7adccb883879ed02e6bf93970d8af06ac8506d487a115da4"
},
"vendorRegistry": {
Expand Down
3 changes: 2 additions & 1 deletion contracts/reputation-contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ The Reputation Contract provides a decentralized way to track and update user re
- `set_score(updater: Address, user: Address, new_score: u32)` - Set a user's score to a specific value (requires updater auth)

### Admin Operations
- `set_admin(new_admin: Address)` - Set the admin address (requires current admin auth or initialization)
- `initialize(admin: Address)` - Initialize the contract with an admin (requires admin auth, can only be called once)
- `set_admin(new_admin: Address)` - Transfer admin to a new address (requires current admin auth)
- `get_admin() -> Address` - Get the current admin address

### Updater Operations
Expand Down
1 change: 1 addition & 0 deletions contracts/reputation-contract/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ pub enum ReputationError {
UpgradeNotProposed = 8,
UpgradeTimelockNotMet = 9,
UpgradeHashMismatch = 10,
AlreadyInitialized = 11,
}
53 changes: 32 additions & 21 deletions contracts/reputation-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,28 +184,39 @@ impl ReputationContract {
.unwrap_or_else(|err| soroban_sdk::panic_with_error!(&env, err))
}

/// Set the admin address for this contract
/// Requires authorization from current admin (or allows initial setup)
pub fn set_admin(env: Env, new_admin: Address) {
let old_admin_opt: Option<Address> = env.storage().instance().get(&storage::ADMIN_KEY);

if let Some(old_admin) = old_admin_opt {
// Admin exists, require current admin authorization
old_admin.require_auth();
access::require_admin(&env, &old_admin);

Self::enter_non_reentrant(&env);

storage::set_admin(&env, &new_admin);
events::emit_admin_changed(&env, &old_admin, &new_admin);

Self::exit_non_reentrant(&env);
} else {
// No admin exists, allow setting (initialization)
storage::set_admin(&env, &new_admin);
let dummy = new_admin.clone();
events::emit_admin_changed(&env, &dummy, &new_admin);
/// Initialize the contract with an admin. Can only be called once.
/// Requires authorization from admin.
pub fn initialize(env: Env, admin: Address) -> Result<(), ReputationError> {
admin.require_auth();

if storage::has_admin(&env) {
return Err(ReputationError::AlreadyInitialized);
}

Self::enter_non_reentrant(&env);

storage::set_admin(&env, &admin);
events::emit_admin_changed(&env, &admin, &admin);

Self::exit_non_reentrant(&env);
Ok(())
}

/// Set the admin address for this contract
/// Requires authorization from current admin
pub fn set_admin(env: Env, new_admin: Address) -> Result<(), ReputationError> {
let old_admin = storage::get_admin(&env)?;

old_admin.require_auth();
access::require_admin(&env, &old_admin);

Self::enter_non_reentrant(&env);

storage::set_admin(&env, &new_admin);
events::emit_admin_changed(&env, &old_admin, &new_admin);

Self::exit_non_reentrant(&env);
Ok(())
}

pub fn set_parameters_contract(env: Env, address: Address) {
Expand Down
5 changes: 5 additions & 0 deletions contracts/reputation-contract/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ pub fn get_admin(env: &Env) -> Result<Address, ReputationError> {
.ok_or(ReputationError::NotInitialized)
}

/// Check if an admin is set in storage
pub fn has_admin(env: &Env) -> bool {
env.storage().instance().has(&ADMIN_KEY)
}

/// Set the admin address in storage
pub fn set_admin(env: &Env, admin: &Address) {
env.storage().instance().set(&ADMIN_KEY, admin);
Expand Down
Loading
Loading