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, so a queued call can execute at most once. The governance contract itself is the invoker of the queued call, so target contracts whose admin functions require owner auth must have the governance contract set as their owner for the call to succeed. 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 an emergency status change without waiting, for example freezing a trading market (Frozen) or starting its wind-down (Delisted). The governance contract is flow-agnostic: it forwards whatever u32 status value the owner passes and does not interpret it, so the meaning of each value is defined by the target contract (see the trading status lifecycle).

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.

Ownership functions

The contract implements OpenZeppelin Ownable, which adds four public entry points:

get_owner() -> Option<Address>
transfer_ownership(new_owner: Address, live_until_ledger: u32)
accept_ownership()
renounce_ownership()

Ownership transfer is two-step. The owner calls transfer_ownership with the new owner and a ledger deadline for acceptance, and the pending owner completes the transfer with accept_ownership before that ledger passes. renounce_ownership permanently gives up ownership. This is the admin-key rotation surface for the whole timelock. The exact behavior and error codes of these functions live in the OpenZeppelin stellar-access library.

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)
get_ownerPermissionless (read-only)
transfer_ownershipOwner only (starts two-step transfer)
accept_ownershipPending owner only
renounce_ownershipOwner only (irreversible)

The contract implements OZ Ownable only, and is permanently pinned to its deployed WASM: an upgradeable governance contract could swap out its own bytecode (and the timelock with it) under the owner key, which would defeat the point of the delay.

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 one pending delay change exists at a time: a subsequent set_delay overwrites it with a fresh unlock time. 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 instantly shortening the delay 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
1UnauthorizedDeclared in the error enum but never raised by code (see below)
770NotQueuedQueue entry not found or expired
771NotUnlockedTimelock delay not yet passed
772InvalidDelayDelay value is zero or exceeds the 60-day cap

Owner-only functions are gated by require_auth on the owner address, so an unauthorized call fails with a Soroban host auth error, not with contract error code 1. Integrators should not match on Unauthorized for auth failures.

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.