Skip to main content

Governance Contract

The GovernanceContract is a general-purpose timelock proxy for deferred admin operations. It queues arbitrary contract calls with a mandatory delay, giving users time to react to parameter changes before they take effect. Emergency status changes bypass the delay for immediate halts.

Constructor

__constructor(owner: Address, delay: u64)
  • owner -- Admin address authorized to queue/cancel calls and set status.
  • delay -- Mandatory waiting period in seconds before queued calls can execute. Must be in the range [1, 5_184_000] (1 second to 60 days). Panics with InvalidDelay (772) if zero or above the 60-day cap.

Entry Points

queue

queue(target: Address, fn_name: Symbol, args: Vec<Val>) -> u32

Owner only. Queues an arbitrary contract call to execute after the delay. Returns a monotonically increasing nonce identifying the queued call. The call is stored with unlock_time = now + delay.

cancel

cancel(nonce: u32)

Owner only. Cancels a queued call before it is executed. Panics with NotQueued (770) if the nonce is not found or has expired.

execute

execute(nonce: u32)

Permissionless. Executes a queued call after the delay has passed. The queued entry is removed from storage before the external call (CEI pattern). Panics with NotQueued (770) if not found, or NotUnlocked (771) if the delay has not yet passed.

set_status

set_status(target: Address, status: u32)

Owner only. Immediately calls set_status(status) on the target contract, bypassing the timelock delay. This allows emergency pause (Frozen, AdminOnIce) without waiting.

set_delay

set_delay(new_delay: u64)

Owner only. Queues a delay change. The new delay must be in [1, 5_184_000] seconds (60-day cap). The change is subject to the current delay before it can take effect, preventing instant delay reduction attacks. Panics with InvalidDelay (772) if the value is out of range.

apply_delay

apply_delay()

Permissionless. Applies a pending delay change after the current delay has passed. Panics with NotQueued (770) if no pending delay change exists, or NotUnlocked (771) if the current delay has not yet passed.

get_delay

get_delay() -> u64

Permissionless. Returns the current delay in seconds.

get_queued

get_queued(nonce: u32) -> QueuedCall

Permissionless. Returns the queued call for a given nonce. Panics with NotQueued (770) if not found or expired.

Access Control

FunctionAuth
queueOwner only
cancelOwner only
executePermissionless (after delay)
set_statusOwner only (immediate, no delay)
set_delayOwner only (queues change subject to current delay)
apply_delayPermissionless (after current delay)
get_delayPermissionless (read-only)
get_queuedPermissionless (read-only)
upgradeOwner only

The contract implements OZ Ownable and Upgradeable (owner-only upgrade).

Two-Step Delay Change

The delay itself cannot be changed instantly. set_delay stores a PendingDelay { new_delay, unlock_time } in temporary storage, where unlock_time = now + current_delay. Only after the current delay elapses can anyone call apply_delay to activate the new value. This prevents an attacker who gains owner access from immediately reducing the delay to zero and bypassing the timelock.

Storage

Storage TypeKeyValue
InstanceDelayCurrent delay in seconds (u64)
InstanceNonceMonotonically incrementing counter (u32)
TemporaryQueued(nonce)QueuedCall { target, fn_name, args, unlock_time }
TemporaryPendingDelayPendingDelay { new_delay, unlock_time }

TTL Calculation

Queued entries and pending delay changes use Soroban temporary storage. The TTL is derived from the delay:

delay_ledgers = delay_seconds / 5
threshold = max(2 * delay_ledgers, 1_day_ledgers)
bump = threshold + 1_day_ledgers

Where 1_day_ledgers = 17280 (assuming 5-second ledger close time). The 2x multiplier ensures the entry survives long enough to be executed after the delay, with a minimum floor of 1 day. If the TTL expires before execution, the entry is silently pruned with no event or notification.

Error Codes

CodeNameDescription
1UnauthorizedCaller is not the contract owner
770NotQueuedQueue entry not found or expired
771NotUnlockedTimelock delay not yet passed
772InvalidDelayDelay value is zero or exceeds the 60-day cap

Events

EventTopicsData
Queuednoncetarget, fn_name, unlock_time
Executednoncetarget, fn_name
Cancellednonce(no data)
StatusSettargetstatus
DelaySetNoneold_delay, new_delay

The set_delay function emits a Queued event with nonce = u32::MAX and target = self to signal the pending delay change through the same event stream as regular queued calls.