diff --git a/TODO.md b/TODO.md
index a4c71af9..e826cccc 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,46 +1,19 @@
-Property-Based Invariant Tests - feature/blackboxai/property-tests
-
-Status: 0/6 Complete
-
-## Breakdown from Approved Plan
-
-### 1. Enhance proptest_helpers.rs [x]
-- [x] Complete TestOperation enum/strategies (register/report/deposit/claim/blacklist/multisig/pause)
-- [x] arb_valid_sequence generator (invariant-preserving sequences)
-- [x] Validate: cargo test proptest_helpers
-
-### 2. Hardened src/test.rs Properties [x]
-- [x] check_invariants_enhanced oracle + 7 properties (period/pagination/blacklist/concentration/multisig/pause/random)
-
-
-- [ ] check_invariants oracle: payout conservation/blacklist/concentration/pause/multisig/pagination
-- [ ] prop_period_ordering (strictly increasing)
-- [ ] prop_blacklist_enforcement (claims=0)
-- [x] prop_concentration_limits (enforce blocks)
-- [ ] prop_pagination_stability (deterministic register→paginate)
-- [ ] prop_multisig_threshold (below threshold fails)
-- [x] prop_pause_safety (mutations panic post-pause)
-- [ ] prop_random_operations (full sequences, seeds/shrinking)
-- [ ] Validate: cargo test prop_
-
-### 3. src/lib.rs Helpers (views only) [ ]
-- [ ] total_claimed_for_holder(issuer, holder) → oracle
-- [ ] NO mutations
-
-### 4. docs/property-based-invariant-tests.md [ ]
-- [ ] Update pass rates + minimal seeds
-
-### 5. Validation [ ]
-- [ ] cargo test --lib (100%)
-- [ ] cargo clippy --fix
-- [ ] cargo test prop_ -- --cases 1000 (stress)
-- [ ] Repro: RUST_LOG=proptest=trace cargo test prop_period_ordering --exact 1
-
-### 6. Git + PR [ ]
-- [ ] git checkout -b blackboxai/property-tests
-- [ ] Commit changes
-- [ ] gh pr create
-
-## Next Step
-**Step 1: Enhance proptest_helpers.rs → cargo test proptest_helpers**
+# TODO
+
+## feat/multisig-administered-offerings (#453)
+
+1. Inspect `src/lib.rs` around existing offering registration (`register_offering`) and multisig-related storage/API.
+2. Implement new entrypoint `register_offering_multisig` in `src/lib.rs`.
+ - Inputs: same as `register_offering` plus `multisig: Address`.
+ - Auth: require_auth resolution expectation documented against multisig.
+ - Fail-fast: cross-contract call `multisig.get_threshold()`; fail cleanly if call fails or method missing.
+ - Persist: store canonical issuer principal as `issuer` after validation.
+ - Events/indexer mapping: ensure `off_reg`/offering register event includes `is_multisig_admin=true` (or equivalent documented mapping).
+3. Add any required storage keys/flags in `src/lib.rs` to mark multisig-administered offerings.
+4. Add tests to `src/test_multisig_gas.rs` (or new test file if needed):
+ - success: multisig implements `get_threshold()`; registration succeeds.
+ - failure: multisig does not implement `get_threshold()`; registration fails cleanly.
+5. Validate edge cases: duplicate offering idempotency; ensure no partial writes on failed multisig validation.
+6. Run `cargo test --all` and `cargo clippy --all-targets --all-features -- -D warnings`.
+7. Update documentation/comments where needed for indexers and security notes.
diff --git a/TODO_blacklist_batch_prop.md b/TODO_blacklist_batch_prop.md
new file mode 100644
index 00000000..e376eff2
--- /dev/null
+++ b/TODO_blacklist_batch_prop.md
@@ -0,0 +1,13 @@
+TODO: #477 proptest harness for blacklist_add_many/remove_many order-independence
+
+1. Update src/test_blacklist_batch_prop.rs:
+ - Scope event collection to only events emitted by blacklist_add_many/remove_many (use before/after event indices).
+ - Enforce empty-input no-op semantics: if addrs empty => no bl_add events; if rem_addrs empty => no bl_rem events.
+ - Strengthen duplicate/overlap coverage in vectors.
+ - Assert final blacklist state equality as deduped set (sorted unique addresses).
+ - Assert emitted blacklist events equality as order-independent multiset.
+2. Run cargo test --all.
+3. Ensure all lint/format is clean.
+4. Commit as: test: add proptest for blacklist batch order-independence.
+5. Push branch and open PR.
+
diff --git a/src/test_blacklist_batch_prop.rs b/src/test_blacklist_batch_prop.rs
new file mode 100644
index 00000000..0d64289a
--- /dev/null
+++ b/src/test_blacklist_batch_prop.rs
@@ -0,0 +1,193 @@
+#![cfg(test)]
+
+use crate::{RevoraRevenueShare, RevoraRevenueShareClient};
+use proptest::prelude::*;
+use soroban_sdk::{
+ symbol_short,
+ Address, Env, Vec,
+};
+
+
+fn make_client(env: &Env) -> RevoraRevenueShareClient<'_> {
+ let id = env.register_contract(None, RevoraRevenueShare);
+ RevoraRevenueShareClient::new(env, &id)
+}
+
+fn setup_offering(env: &Env, issuer: &Address, token: &Address, ns: &soroban_sdk::Symbol) {
+ let client = make_client(env);
+ client.initialize(issuer, &None::
, &None::);
+ client.register_offering(issuer, ns, token, &1000u32, token, &0_i128);
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+enum BlEventKind {
+ Add,
+ Rem,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+struct NormalizedBlEvent {
+ kind: BlEventKind,
+ caller: Address,
+ investor: Address,
+}
+
+fn collect_blacklist_events(env: &Env) -> Vec {
+ // Event filtering: only consider bl_add/bl_rem.
+ // We normalize away ordering and compare as a sorted multiset.
+ let mut out: Vec = Vec::new(&env);
+
+ // Soroban SDK testutils: env.events().all() gives all published events
+ // as typed tuples we can match on.
+ for i in 0..env.events().all().len() {
+ // Each event is internally a tuple like: (topics..., data...)
+ // We rely on the topic symbol being present in the first position.
+ // Since this is a test harness and we only need deterministic comparisons,
+ // we use the Events decoding to get (symbol, data) shape.
+ let ev = env.events().all().get(i).unwrap();
+
+ // ev is (topic tuple, data tuple) under testutils.
+ // We only need the event symbol and (caller, investor).
+ // The event topic published by the contract is:
+ // (EVENT_BL_ADD, issuer, namespace, token)
+ // (EVENT_BL_REM, issuer, namespace, token)
+ // with data (caller, investor)
+ let topics = ev.topics();
+ if topics.is_empty() {
+ continue;
+ }
+ // topics[0] is the event symbol.
+ let sym: soroban_sdk::Symbol = topics.get(0).unwrap();
+
+ if sym == symbol_short!("bl_add") {
+ let data = ev.data();
+ let caller: Address = data.get(0).unwrap();
+ let investor: Address = data.get(1).unwrap();
+ out.push_back(NormalizedBlEvent { kind: BlEventKind::Add, caller, investor });
+ } else if sym == symbol_short!("bl_rem") {
+ let data = ev.data();
+ let caller: Address = data.get(0).unwrap();
+ let investor: Address = data.get(1).unwrap();
+ out.push_back(NormalizedBlEvent { kind: BlEventKind::Rem, caller, investor });
+ }
+ }
+
+ out
+}
+
+fn sort_std_normalized(mut events: std::vec::Vec) -> std::vec::Vec {
+ events.sort();
+ events
+}
+
+
+proptest! {
+ #![proptest_config(ProptestConfig {
+ cases: 128,
+ ..ProptestConfig::default()
+ })]
+
+ #[test]
+ fn blacklist_batch_add_remove_order_independent(
+ addrs in proptest::collection::vec(any::(), 0..50),
+ rem_addrs in proptest::collection::vec(any::(), 0..50),
+ shuffle_seed in any::(),
+ ) {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let issuer = Address::generate(&env);
+ let token = Address::generate(&env);
+ let ns = symbol_short!("def");
+
+ setup_offering(&env, &issuer, &token, &ns);
+ let client = make_client(&env);
+
+ // Prebuild a stable pool of 256 addresses.
+ // Duplicates/overlaps are intentionally produced because u8 values are mapped by index.
+ let mut pool: std::vec::Vec = std::vec::Vec::with_capacity(256);
+
+ for i in 0u16..256u16 {
+ pool.push(Address::generate(&env));
+ }
+ let mut to_addr_vec = |bytes: &std::vec::Vec| -> Vec {
+ let mut out: Vec = Vec::new(&env);
+ for &b in bytes.iter() {
+ out.push_back(pool[b as usize].clone());
+ }
+ out
+ };
+
+ let add_vec_a: Vec = to_addr_vec(&addrs);
+ let rem_vec_a: Vec = to_addr_vec(&rem_addrs);
+
+ // Execution A: original order.
+ let mut env_a = env.clone();
+ env_a.mock_all_auths();
+ setup_offering(&env_a, &issuer, &token, &ns);
+ let client_a = make_client(&env_a);
+ client_a.blacklist_add_many(&issuer, &issuer, &ns, &token, &add_vec_a);
+ client_a.blacklist_remove_many(&issuer, &issuer, &ns, &token, &rem_vec_a);
+ let final_state_a = client_a.get_blacklist(&issuer, &ns, &token);
+ let events_a = collect_blacklist_events(&env_a);
+
+
+ // Execution B: shuffled order, but identical multiset of input addresses.
+ let mut rng = proptest::test_runner::TestRng::deterministic(shuffle_seed);
+ let mut add_vec_b: Vec = add_vec_a.clone();
+ let mut rem_vec_b: Vec = rem_vec_a.clone();
+
+ // Convert Soroban Vec to std for shuffling, then back.
+ let mut add_std: std::vec::Vec = Vec::into_iter(add_vec_b).collect();
+ let mut rem_std: std::vec::Vec = Vec::into_iter(rem_vec_b).collect();
+
+ add_std.shuffle(&mut rng);
+ rem_std.shuffle(&mut rng);
+
+ let add_vec_b2: Vec = {
+ let mut out: Vec = Vec::new(&env);
+ for a in add_std { out.push_back(a); }
+ out
+ };
+ let rem_vec_b2: Vec = {
+ let mut out: Vec = Vec::new(&env);
+ for a in rem_std { out.push_back(a); }
+ out
+ };
+
+ let mut env_b = env.clone();
+ env_b.mock_all_auths();
+ setup_offering(&env_b, &issuer, &token, &ns);
+ let client_b = make_client(&env_b);
+ client_b.blacklist_add_many(&issuer, &issuer, &ns, &token, &add_vec_b2);
+ client_b.blacklist_remove_many(&issuer, &issuer, &ns, &token, &rem_vec_b2);
+ let final_state_b = client_b.get_blacklist(&issuer, &ns, &token);
+ let events_b = collect_blacklist_events(&env_b);
+
+ // Assert final blacklist state identical.
+ // Contract uses deterministic insertion-order, but because batch dedups based on first
+ // occurrence, shuffling should still converge to same *set*; however order may differ.
+ // The requirement says identical final state, so compare as set by sorting.
+ let mut a_state_std: std::vec::Vec = Vec::into_iter(final_state_a).collect();
+ let mut b_state_std: std::vec::Vec = Vec::into_iter(final_state_b).collect();
+ a_state_std.sort();
+ b_state_std.sort();
+ prop_assert_eq!(a_state_std, b_state_std);
+
+ // Assert events form an order-equivalent multiset.
+ let mut a_events_std: std::vec::Vec = Vec::into_iter(events_a).collect();
+ let mut b_events_std: std::vec::Vec = Vec::into_iter(events_b).collect();
+ a_events_std.sort();
+ b_events_std.sort();
+ prop_assert_eq!(a_events_std, b_events_std);
+
+ // Empty input no-op with no events.
+ if addrs.is_empty() && rem_addrs.is_empty() {
+ // After setup only, no blacklist ops were called with empty vectors (contract returns Ok()).
+ // However other setup events exist; we only care bl_* events.
+ prop_assert!(a_events_std.is_empty());
+ prop_assert!(b_events_std.is_empty());
+ }
+ }
+}
+