This document describes the contract upgrade mechanism with a 48-hour timelock protection. It provides a safe path for upgrading smart contracts on Soroban, protecting against rushed or malicious upgrades.
- Admin-Only Scheduling: Only administrators can schedule contract upgrades
- 48-Hour Timelock: Mandatory waiting period before upgrades can be executed
- Configurable Timelock: Owner can adjust timelock duration for different security profiles
- Event Emission: Emits events for scheduling, execution, and cancellation
- Execution Validation: Prevents premature execution and double execution
- State Inspection: Read-only methods to check upgrade status and remaining timelock
#[contracttype]
#[derive(Clone, Debug)]
pub struct ScheduledUpgrade {
pub new_wasm_hash: BytesN<32>, // Hash of new WASM bytecode
pub scheduled_at: u64, // Timestamp when upgrade was scheduled
pub executed: bool, // Whether upgrade has been executed
}Uses Soroban's instance storage for timelock-related data:
ScheduledUpgrade: The current scheduled upgrade (if any)UpgradeTimelock: Custom timelock duration (default: 172800 seconds = 48 hours)
Schedules a new contract upgrade to be executed after the timelock period.
Requirements:
- Caller must have Admin role
- No pending/unexecuted upgrade can be scheduled
- WASM hash must be provided
Events:
UpgradeScheduled- Contains: new_wasm_hash, scheduled_at, scheduled_by
Example:
let new_hash = BytesN::from_array(&env, &[/*32 bytes*/]);
UpgradeContract::schedule_upgrade(env, admin, new_hash);Executes a scheduled upgrade after the timelock has elapsed.
Requirements:
- Caller must have Admin role
- An upgrade must be scheduled
- 48 hours (or custom timelock) must have passed since scheduling
- Upgrade must not have been previously executed
Events:
UpgradeExecuted- Contains: new_wasm_hash, executed_at, executed_by
Reverts if:
"No upgrade scheduled"- No upgrade is currently scheduled"Timelock period not yet elapsed"- Not enough time has passed"Upgrade already executed"- Upgrade was already executed
Example:
UpgradeContract::execute_upgrade(env, admin);Cancels a pending upgrade before it's executed.
Requirements:
- Caller must be the Owner
Events:
UpgradeCancelled- Contains: new_wasm_hash, cancelled_by
Reverts if:
"Cannot cancel an executed upgrade"- Upgrade is already executed
Example:
UpgradeContract::cancel_upgrade(env, owner);Sets a custom timelock duration in seconds.
Requirements:
- Caller must be the Owner
- Duration must be > 0
Events:
TimelockUpdated- Contains: new_duration, updated_by
Default: 172800 seconds (48 hours)
Example:
// Set 24-hour timelock
UpgradeContract::set_timelock_duration(env, owner, 86400);Returns the currently scheduled upgrade, if any.
Returns: Option with ScheduledUpgrade data or None
Example:
let upgrade = UpgradeContract::get_scheduled_upgrade(env);
match upgrade {
Some(u) => println!("Scheduled: {:?}", u.new_wasm_hash),
None => println!("No upgrade scheduled"),
}Returns the current timelock duration in seconds.
Example:
let duration = UpgradeContract::get_timelock_duration(env);
println!("Timelock: {} seconds", duration);Returns seconds until scheduled upgrade can be executed.
Returns:
- 0 if no upgrade is scheduled
- 0 if timelock has already elapsed
- Remaining seconds otherwise
Example:
let remaining = UpgradeContract::get_upgrade_timelock_remaining(env);
if remaining > 0 {
println!("Wait {} more seconds", remaining);
} else {
println!("Ready to execute!");
}- Import the upgrade module in your contract:
use traqora_contracts::upgrade::{
UpgradeContract,
UpgradeStorage,
ScheduledUpgrade
};- Add upgrade functions to your contract implementation:
#[contractimpl]
pub struct MyContract;
impl MyContract {
pub fn schedule_upgrade(env: Env, admin: Address, new_wasm_hash: BytesN<32>) {
UpgradeContract::schedule_upgrade(env, admin, new_wasm_hash)
}
pub fn execute_upgrade(env: Env, admin: Address) {
UpgradeContract::execute_upgrade(env, admin)
}
pub fn get_scheduled_upgrade(env: Env) -> Option<ScheduledUpgrade> {
UpgradeContract::get_scheduled_upgrade(env)
}
pub fn get_upgrade_timelock_remaining(env: Env) -> u64 {
UpgradeContract::get_upgrade_timelock_remaining(env)
}
// ... other contract methods
}- The upgrade mechanism is now available:
- Admins can schedule upgrades
- Upgrades are protected by 48-hour timelock by default
- Owners can adjust timelock or cancel pending upgrades
- Minimum Wait: 48 hours (172800 seconds) by default
- Customizable: Owners can adjust for different threat models
- Prevents: Flash upgrades, malicious code injection without notice
- Schedule: Admin role required
- Execute: Admin role required
- Configure: Owner only (highest privilege)
- Cancel: Owner only
- Once executed, an upgrade cannot be re-executed
- A new upgrade must be scheduled for subsequent changes
The test suite includes comprehensive tests for rejecting premature execution:
#[test]
#[should_panic(expected = "Timelock period not yet elapsed")]
fn test_execute_upgrade_rejects_premature_execution() {
// Schedule at T0
// Try execute at T0 + 1 second
// Should panic with "Timelock period not yet elapsed"
}cargo test --manifest-path contracts/Cargo.toml upgrade_mechanismThe test suite covers:
- ✅ Basic scheduling and retrieval
- ✅ Authorization checks (admin/owner only)
- ✅ Timelock enforcement (premature execution prevention)
- ✅ Boundary conditions (exactly 48 hours)
- ✅ Multiple upgrades (sequential)
- ✅ Cancellation scenarios
- ✅ Custom timelock durations
- ✅ Remaining time calculations
- ✅ Double-execution prevention
- ✅ State transitions
Emitted when a new upgrade is scheduled.
Topic 0: ("upgrade", "scheduled")
Data: (new_wasm_hash, scheduled_at, scheduled_by)
Emitted when a scheduled upgrade is executed.
Topic 0: ("upgrade", "executed")
Data: (new_wasm_hash, executed_at, executed_by)
Emitted when a pending upgrade is cancelled.
Topic 0: ("upgrade", "cancelled")
Data: (new_wasm_hash, cancelled_by)
Emitted when timelock duration is changed.
Topic 0: ("upgrade", "timelock_updated")
Data: (new_duration, updated_by)
Off-chain monitoring should track upgrade events:
- Listen for
UpgradeScheduledto alert stakeholders - Verify
UpgradeExecutedafter execution - Alert on suspicious
UpgradeCancelledevents
Combine with governance system for voting:
- Governance votes on upgrade WASM hashes
- Governance execution triggers schedule_upgrade
- Governance committee executes after timelock
For large upgrades:
- Announce scheduled upgrade (community discussion)
- Wait 48+ hours for community review
- Execute only if no critical issues found
- Have rollback plan ready
Always test upgrades in staging:
- Test new WASM code in testnet
- Verify state migration logic
- Document any breaking changes
// Day 0: 14:00 UTC
// Admin schedules new upgrade
let new_wasm = get_new_wasm_hash();
UpgradeContract::schedule_upgrade(env, admin, new_wasm);
// UpgradeScheduled event emitted
// Day 0: 14:00 UTC -> Day 2: 14:00 UTC
// 48-hour wait period
// Community reviews code
// Monitoring systems alert stakeholders
// Day 2: 14:00 UTC (exactly 48 hours later)
// Admin executes upgrade
UpgradeContract::execute_upgrade(env, admin);
// UpgradeExecuted event emitted
// Contract upgraded with new WASM code
// Day 2: After execution
// Admin can schedule next upgrade if needed
UpgradeContract::schedule_upgrade(env, admin, next_wasm);Cause: Trying to execute before 48 hours have passed
Solution:
- Wait for timelock to expire
- Or reduce timelock:
set_timelock_duration(env, owner, shorter_time) - Or cancel and reschedule with adjusted time
Cause: Trying to schedule while another upgrade is pending
Solution:
- Wait for existing upgrade to execute, or
- Cancel existing upgrade:
cancel_upgrade(env, owner) - Then schedule new upgrade
Cause: Caller doesn't have Admin role
Solution:
- Use proper admin account
- Or grant Admin role via access control:
AccessControl::set_role(..., Role::Admin, true)
- v1.0.0: Initial release with 48-hour timelock
- schedule_upgrade function
- execute_upgrade function
- Configurable timelock
- Full test coverage