Cancel a pending operation. canceller must hold CANCELLER or GUARDIAN.
Takes the operation itself rather than its id, as [Self::execute] does, and rebuilds the
id from it. The id is a hash, so a canceller holding only that cannot be told apart from one
cancelling the owner's recovery path — and the alternative, a stored per-id marker, buys
that distinction with persistent state, a TTL to size, and a cleanup path to get right.
Rebuilding is free and makes the caller name what it is killing.
fn cancel(
env: soroban_sdk::Env,
canceller: soroban_sdk::Address,
target: soroban_sdk::Address,
function: soroban_sdk::Symbol,
args: soroban_sdk::Vec,
salt: soroban_sdk::BytesN<32>,
)
Execute a Ready operation, invoking target.function(args) authorized as this contract.
executor must authorize the call and hold the EXECUTOR role — the role is enforced on
every execution, there is no anonymous path. A deployment that wants permissionless
execution grants EXECUTOR to a designated relayer address explicitly. Returns the
target's result.
fn execute(
env: soroban_sdk::Env,
executor: soroban_sdk::Address,
target: soroban_sdk::Address,
function: soroban_sdk::Symbol,
args: soroban_sdk::Vec,
salt: soroban_sdk::BytesN<32>,
) -> soroban_sdk::Val
Execute a Ready upgrade(new_wasm_hash) self-operation scheduled under salt. Address and
storage are preserved. executor must hold EXECUTOR.
The salt matters here too: rolling back to a wasm hash this contract has run before is a normal recovery step, and a fixed salt made it permanently unschedulable.
fn upgrade(
env: soroban_sdk::Env,
executor: soroban_sdk::Address,
new_wasm_hash: soroban_sdk::BytesN<32>,
salt: soroban_sdk::BytesN<32>,
)
Whether account holds role.
fn has_role(
env: soroban_sdk::Env,
account: soroban_sdk::Address,
role: soroban_sdk::Symbol,
) -> bool
Schedule an admin (target, function, args) call on the timelock; returns its operation id.
The delay is derived from the method's [Tier] via [classify] — the caller does not choose
it, so a Sensitive change (upgrade, admin/ownership, live-pool risk) always carries the
elevated floor.
proposer must hold the role that owns the derived tier: ADL for apply_adl,
FEE_DISTRIBUTOR for set_fee_merkle_root, and PROPOSER for the governance tiers (see
[ensure_can_schedule]). The scoped keys reach their own tier and nothing else.
fn schedule(
env: soroban_sdk::Env,
proposer: soroban_sdk::Address,
target: soroban_sdk::Address,
function: soroban_sdk::Symbol,
args: soroban_sdk::Vec,
salt: soroban_sdk::BytesN<32>,
) -> soroban_sdk::BytesN<32>
The ADL-tier delay in ledgers. Zero selects [Self::execute_adl]; a non-zero value selects
the ordinary schedule/execute/cancel lifecycle.
fn adl_delay(env: soroban_sdk::Env) -> u32
Execute a Ready grant_role(account, role) self-operation scheduled under salt.
executor must hold EXECUTOR, exactly as on [Self::execute_self].
The salt is a parameter rather than a fixed zero because an executed id is Done forever —
the timelock never returns one to Unset, and cancel refuses a Done entry — so a
hardcoded salt made this entry point single-use per (function, account, role). Granting a
role, revoking it, and later restoring it is ordinary operations work, and the third step
reverted permanently with an error that read as a timelock bug.
fn grant_role(
env: soroban_sdk::Env,
executor: soroban_sdk::Address,
account: soroban_sdk::Address,
role: soroban_sdk::Symbol,
salt: soroban_sdk::BytesN<32>,
)
Immediately execute target.apply_adl(args) through governance.
ADL names a live candidate and defaults to a zero-ledger delay. A zero-delay operational
action is not a timelocked governance proposal, so this path deliberately creates no
[Operation], salt or pending state: the scoped backend is authorized here and the target
call either succeeds in this invocation or rolls back with it.
operator must hold the dedicated ADL role. The function is hard-coded rather than
supplied by the caller, so this capability reaches only apply_adl. A non-zero ADL delay
disables this gateway; delayed ADL operations use the ordinary schedule/execute/cancel
lifecycle.
fn execute_adl(
env: soroban_sdk::Env,
operator: soroban_sdk::Address,
target: soroban_sdk::Address,
args: soroban_sdk::Vec,
) -> soroban_sdk::Val
Execute a Ready revoke_role(account, role) self-operation scheduled under salt.
executor must hold EXECUTOR. See [Self::grant_role] for why the salt is a parameter.
fn revoke_role(
env: soroban_sdk::Env,
executor: soroban_sdk::Address,
account: soroban_sdk::Address,
role: soroban_sdk::Symbol,
salt: soroban_sdk::BytesN<32>,
)
Execute a Ready Sensitive operation whose target is this governance contract. Self-calls
cannot use [Self::execute] because Soroban rejects governance -> governance re-entry, so
this path consumes the operation with OZ's set_execute_operation and dispatches the
allowlisted action internally: upgrade, grant_role, revoke_role, or one of the five
per-tier set_*_delay retunes. Each retune takes one u32 of ledgers, bounded by
[MAX_DELAY_LEDGERS] as at construction, and is the only way to move that delay after
deploy — the values live in storage, so a later upgrade alone cannot reach them.
Like [Self::execute], executor must authorize the call and hold the EXECUTOR role:
scheduling and the Sensitive delay decide what can run and when, the executor decides
the moment it lands, and a guardian can cancel while it is pending.
fn execute_self(
env: soroban_sdk::Env,
executor: soroban_sdk::Address,
function: soroban_sdk::Symbol,
args: soroban_sdk::Vec,
salt: soroban_sdk::BytesN<32>,
)
Initialize governance: admin becomes owner + access-control admin and holds all seven
operational roles. One delay per [Tier], all in ledgers, each independent of the others and
each retunable afterwards by its own self-operation. DEFAULT_SENSITIVE_DELAY_LEDGERS,
[DEFAULT_ADL_DELAY_LEDGERS] and [DEFAULT_FEE_DISTRIBUTION_DELAY_LEDGERS] are the
recommended mainnet values; a dev/testnet deploy passes 0 for all five, which is the only
way to turn the timelock off — no tier's value silently zeroes another.
Handing ADL / FEE_DISTRIBUTOR to the off-chain services is a grant_role
self-operation, after which the admin should drop its own copies.
OZ's own min_delay is pinned to 0 and left there. It exists to stop a caller passing
schedule_operation a delay under the configured floor, and here no caller ever passes one:
[Self::schedule] is the only path in and derives the delay from the method's tier. Leaving
the library floor armed would only mean it silently overrode four of our five tier
fn __constructor(
env: soroban_sdk::Env,
admin: soroban_sdk::Address,
deployer_delay: u32,
standard_delay: u32,
sensitive_delay: u32,
adl_delay: u32,
fee_distribution_delay: u32,
)
Immediately invoke a creation-only admin call — create_pool, initialize_pool,
approve_pool, configure — authorized as this contract, bypassing the timelock. deployer
must hold DEPLOYER, and the method must classify as [Tier::Deployer] (else
NotADeployerOperation). These methods reject an already-configured target; LockYield also
tombstones every revoke_pool, so approve_pool cannot resurrect it. Restoring that pool
requires the separate, Sensitive reapprove_pool operation. Thus this fast path can bring
up a new pool but can never retune or restore a live one — that (and registering a margin
asset) stays on the Sensitive setters, reachable only through schedule + the delay.
Returns the target's result.
fn deployer_call(
env: soroban_sdk::Env,
deployer: soroban_sdk::Address,
target: soroban_sdk::Address,
function: soroban_sdk::Symbol,
args: soroban_sdk::Vec,
) -> soroban_sdk::Val
The Deployer tier's delay, in ledgers — for a creation-only op that is scheduled rather
than driven through the immediate [Self::deployer_call].
fn deployer_delay(env: soroban_sdk::Env) -> u32
Emergency brake: immediately (bypassing the timelock) pause target by invoking
target.set_paused(true) authorized as this contract. guardian must hold GUARDIAN. The
target must be a contract whose admin is this governance and that exposes set_paused(bool).
Pause only. paused == false is rejected (GuardianCanOnlyPause); unpausing is a
Tier::Standard set_paused(false) through schedule. The brake is deliberately
one-directional: halting during an incident cannot wait for a delay, but restarting is a
judgement call that should be visible as a pending operation and cancellable by anyone
holding CANCELLER or GUARDIAN. Forwarding the flag let a single compromised guardian key
clear a pause the instant it was set — reopening the exploit window with nothing scheduled
to cancel, while honest governance had to wait out the full delay to re-pause.
The paused argument survives the restriction rather than being dropped so the call shape
(and every runbook and script built on it) stays put,
fn guardian_pause(
env: soroban_sdk::Env,
guardian: soroban_sdk::Address,
target: soroban_sdk::Address,
paused: bool,
)
The [Tier] a method schedules at — lets ops tell ahead of time whether a call is an instant
deployer_call, a min-delay schedule, or a Sensitive-floor schedule.
fn operation_tier(env: soroban_sdk::Env, function: soroban_sdk::Symbol) -> Tier
The Standard tier's delay, in ledgers.
fn standard_delay(env: soroban_sdk::Env) -> u32
The elevated delay a Sensitive-tier op schedules at (ledgers), set at construction.
fn sensitive_delay(env: soroban_sdk::Env) -> u32
The delay a fee-distribution-tier op (set_fee_merkle_root) schedules at (ledgers).
fn fee_distribution_delay(env: soroban_sdk::Env) -> u32
Schedule a grant or revoke of role on account that no canceller or guardian can veto.
Owner-gated; returns the operation id. Execute it with execute_self once the Sensitive
delay has run, passing the same arguments and this path's reserved salt (readable from the
emitted operation, or rebuilt as 32 0xFF bytes).
The recovery lever, and the only one: role administration is otherwise a Sensitive
self-operation, and cancel accepts any pending operation from any canceller or guardian —
so a compromised holder vetoes its own removal indefinitely, and a compromised guardian
keeps the engine paused on top of that. Forbidding cancellation of every revoke_role
would swap that deadlock for a worse one: a compromised proposer could then strip every
other holder unopposed, and since granting a role is itself a proposer-scheduled operation,
nothing could put them back.
Timelocked like any other Sensitive change — the owner gets notice, not immediacy — and scoped to role changes alone.
salt is the o
fn owner_schedule_role_change(
env: soroban_sdk::Env,
account: soroban_sdk::Address,
role: soroban_sdk::Symbol,
grant: bool,
salt: soroban_sdk::BytesN<32>,
) -> soroban_sdk::BytesN<32>