Skip to content
Draft
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
19 changes: 4 additions & 15 deletions contracts/multisig/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

mod storage;

use soroban_sdk::{
contract, contracterror, contractimpl, Address, Bytes, Env, Symbol, Vec,
};
use soroban_sdk::{contract, contracterror, contractimpl, Address, Bytes, Env, Symbol, Vec};
use storage::{
bump_instance, increment_id, load_config, load_proposal, save_config, save_proposal, Config,
PolicySnapshot, Proposal,
Expand Down Expand Up @@ -49,11 +47,7 @@ impl MultisigContract {
owners: Vec<Address>,
threshold: u32,
) -> Result<(), Error> {
if env
.storage()
.instance()
.has(&storage::DataKey::Config)
{
if env.storage().instance().has(&storage::DataKey::Config) {
return Err(Error::AlreadyInitialized);
}
if threshold == 0 || threshold as usize > owners.len() as usize {
Expand Down Expand Up @@ -186,13 +180,8 @@ impl MultisigContract {

// Invoke the target contract. The args are already encoded – pass as
// a raw Vec<Val> by deserialising from Bytes via the environment.
let args_vec: soroban_sdk::Vec<soroban_sdk::Val> =
soroban_sdk::Vec::from_array(&env, []);
env.invoke_contract::<soroban_sdk::Val>(
&proposal.target,
&proposal.function,
args_vec,
);
let args_vec: soroban_sdk::Vec<soroban_sdk::Val> = soroban_sdk::Vec::from_array(&env, []);
env.invoke_contract::<soroban_sdk::Val>(&proposal.target, &proposal.function, args_vec);

Ok(())
}
Expand Down
49 changes: 40 additions & 9 deletions contracts/multisig/tests/multisig_test.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
#![cfg(test)]

use soroban_sdk::{
contract, contractimpl,
testutils::Address as _,
vec, Address, Bytes, Env, Symbol,
contract, contractimpl, testutils::Address as _, vec, Address, Bytes, Env, Symbol,
};

use multisig::{MultisigContract, MultisigContractClient};
Expand All @@ -24,7 +22,15 @@ impl NoopContract {
// Helpers
// ---------------------------------------------------------------------------

fn setup(env: &Env) -> (MultisigContractClient<'_>, Address, Address, Address, Address) {
fn setup(
env: &Env,
) -> (
MultisigContractClient<'_>,
Address,
Address,
Address,
Address,
) {
let contract_id = env.register(MultisigContract, ());
let client = MultisigContractClient::new(env, &contract_id);

Expand Down Expand Up @@ -60,7 +66,12 @@ fn basic_proposal_reaches_threshold() {
let (client, _admin, owner_a, owner_b, _owner_c) = setup(&env);

let target = register_noop(&env);
let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env));
let id = client.propose(
&owner_a,
&target,
&Symbol::new(&env, "noop"),
&empty_args(&env),
);

assert_eq!(client.approve(&owner_b, &id), 2);
client.execute(&id);
Expand All @@ -76,7 +87,12 @@ fn reconfigure_does_not_affect_open_proposal() {
let (client, admin, owner_a, owner_b, _owner_c) = setup(&env);

let target = register_noop(&env);
let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env));
let id = client.propose(
&owner_a,
&target,
&Symbol::new(&env, "noop"),
&empty_args(&env),
);

// Reconfigure: remove owner_b, add a brand-new owner, keep threshold 2.
let new_owner = Address::generate(&env);
Expand Down Expand Up @@ -125,7 +141,12 @@ fn execute_before_threshold_fails() {
env.mock_all_auths();
let (client, _admin, owner_a, _owner_b, _owner_c) = setup(&env);
let target = register_noop(&env);
let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env));
let id = client.propose(
&owner_a,
&target,
&Symbol::new(&env, "noop"),
&empty_args(&env),
);
// Only 1 approval (threshold = 2).
let result = client.try_execute(&id);
assert!(result.is_err());
Expand All @@ -138,7 +159,12 @@ fn duplicate_approval_rejected() {
env.mock_all_auths();
let (client, _admin, owner_a, _owner_b, _owner_c) = setup(&env);
let target = register_noop(&env);
let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env));
let id = client.propose(
&owner_a,
&target,
&Symbol::new(&env, "noop"),
&empty_args(&env),
);
let result = client.try_approve(&owner_a, &id);
assert!(result.is_err());
}
Expand All @@ -152,7 +178,12 @@ fn threshold_snapshot_isolation() {

let target = register_noop(&env);
// Propose with threshold=2 snapshotted.
let id = client.propose(&owner_a, &target, &Symbol::new(&env, "noop"), &empty_args(&env));
let id = client.propose(
&owner_a,
&target,
&Symbol::new(&env, "noop"),
&empty_args(&env),
);

// Raise threshold to 3 via reconfigure.
let owners = vec![&env, owner_a.clone(), owner_b.clone(), owner_c.clone()];
Expand Down
2 changes: 1 addition & 1 deletion contracts/stellar_insights/src/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ pub fn signed_payload(
payload.append(&Bytes::from_array(env, &snapshot_hash.to_array()));
payload.append(&Bytes::from_array(env, &source_data_hash.to_array()));
payload
}
}
51 changes: 47 additions & 4 deletions contracts/tests/privilege_escalation_test.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,52 @@
//! Privilege Escalation Verification Suite
//! Refer to `contracts/upgrade/tests/privilege_escalation_test.rs` for full Soroban host test execution.

#[cfg(test)]
mod tests {
/// Verify that contract privilege graph documentation specifies all required formal invariants
/// across the contract inventory.
#[test]
fn test_privilege_escalation_docs_and_artifacts() {
assert!(std::path::Path::new("../docs/contract-privilege-graph.md").exists() || std::path::Path::new("docs/contract-privilege-graph.md").exists());
assert!(std::path::Path::new("upgrade/src/scope.rs").exists() || std::path::Path::new("contracts/upgrade/src/scope.rs").exists());
assert!(std::path::Path::new("upgrade/tests/privilege_escalation_test.rs").exists() || std::path::Path::new("contracts/upgrade/tests/privilege_escalation_test.rs").exists());
fn test_privilege_graph_formal_invariants_specification() {
let doc_path = if std::path::Path::new("docs/contract-privilege-graph.md").exists() {
"docs/contract-privilege-graph.md"
} else {
"../docs/contract-privilege-graph.md"
};
let content = std::fs::read_to_string(doc_path).expect("failed to read privilege graph doc");

// Verify all contract systems are audited in the privilege graph
assert!(content.contains("UpgradeManager"));
assert!(content.contains("MultisigContract"));
assert!(content.contains("StellarInsights"));
assert!(content.contains("TimeLockedTransactions"));
assert!(content.contains("EscrowContract"));
assert!(content.contains("TokenSwap"));
assert!(content.contains("Analytics"));

// Verify formal invariant rules are documented
assert!(content.contains("Governance Root Isolation Invariant"));
assert!(content.contains("Upgrade Manager Non-Self-Modification Invariant"));
assert!(content.contains("Transitive Authority Non-Redirection Invariant"));
assert!(content.contains("Non-Delegation of Governance Authority Invariant"));
assert!(content.contains("UpgradeManagerAlreadySet"));
}

/// Verify that scope.rs defines formal invariants and restrictions.
#[test]
fn test_scope_formal_specification() {
let scope_path = if std::path::Path::new("contracts/upgrade/src/scope.rs").exists() {
"contracts/upgrade/src/scope.rs"
} else {
"upgrade/src/scope.rs"
};
let content = std::fs::read_to_string(scope_path).expect("failed to read scope.rs");

assert!(content.contains("TargetScope"));
assert!(content.contains("validate_target_scope"));
assert!(content.contains("is_restricted_target"));
assert!(content.contains("TargetOutOfScope"));
assert!(content.contains("Governance Root Isolation Invariant"));
assert!(content.contains("Upgrade Manager Non-Self-Modification Invariant"));
assert!(content.contains("Transitive Authority Non-Redirection Invariant"));
}
}
30 changes: 26 additions & 4 deletions contracts/upgrade/src/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,36 @@ use soroban_sdk::{Address, Env};

use crate::{Error, GovernanceConfig};

/// Formal invariants governing target scope validation and transitive authorization boundaries:
///
/// 1. **Governance Root Isolation Invariant**:
/// $$\forall t \in \text{Address}, t = \text{config.governance} \implies \text{validate\_target\_scope}(t) = \text{Err}(\text{TargetOutOfScope})$$
/// Prevents upgrade proposals from targeting the Governance multi-signature contract. This guarantees that
/// governance rules, threshold configurations, and owner sets cannot be overridden via upgrade proposals.
///
/// 2. **Upgrade Manager Non-Self-Modification Invariant**:
/// $$\forall t \in \text{Address}, t = \text{env.current\_contract\_address}() \implies \text{validate\_target\_scope}(t) = \text{Err}(\text{TargetOutOfScope})$$
/// Prevents `UpgradeManager` from modifying its own executable WASM. This guarantees that proposal lifecycles,
/// approver thresholds, storage test evidence validation, and target scope checks cannot be bypassed or dismantled.
///
/// 3. **Transitive Authority Non-Redirection Invariant**:
/// Governed target contracts (such as `StellarInsights`) bind their `UpgradeManager` reference during initialization
/// as a write-once value (`UpgradeManagerAlreadySet`). Code installed at a governed target cannot reassign or
/// redirect upgrade authority to rogue managers, nor can it bypass the UpgradeManager authentication required for
/// subsequent `governance_upgrade` or `migrate_schema` calls.
///
/// 4. **Non-Delegation of Governance Authority Invariant**:
/// Governed target contracts hold domain-specific capabilities (e.g. snapshot storage) and cannot acquire
/// or delegate governance administrative powers over other contracts in the privilege graph.
pub struct TargetScope;

impl TargetScope {
/// Validates whether a target contract address is eligible for upgrade proposals.
///
/// # Restrictions
/// - An upgrade proposal MUST NOT target the Governance contract (`config.governance`).
/// - An upgrade proposal MUST NOT target the UpgradeManager contract itself (`env.current_contract_address()`).
/// # Enforced Invariants
/// - **Governance Isolation**: Proposal MUST NOT target the Governance contract (`config.governance`).
/// - **Manager Self-Upgrade Defense**: Proposal MUST NOT target the UpgradeManager contract itself (`env.current_contract_address()`).
/// - **Transitive Scope Boundary**: Prevents capture or replacement of core authorization infrastructure.
pub fn validate_target_scope(
env: &Env,
target: &Address,
Expand All @@ -21,7 +43,7 @@ impl TargetScope {
Ok(())
}

/// Returns `true` if the target address matches any restricted contract address.
/// Returns `true` if the target address matches any restricted contract address in the privilege graph.
pub fn is_restricted_target(env: &Env, target: &Address, governance: &Address) -> bool {
if target == governance {
return true;
Expand Down
Loading