# Grantline Documentation (https://grantline-zeta.vercel.app/docs)
Learn how Grantline gives AI agents bounded authority over capital through Mandates, Preflight, delegation, controller approval, and traceable execution.
Grantline is the financial authority layer between an AI agent's intent and execution. An agent proposes an action, Grantline checks the active [Mandate](/docs/concepts/mandates), and the authority layer returns `ALLOW`, `ESCALATE`, or `DENY` before the [Vault](/docs/concepts/vaults) moves capital.
The concepts explain the model. Enforcement and execution show how the current contracts apply it. Guides and reference pages then give integrators the exact fields, calls, and evidence.
**Current MVP**
The current contracts are deployed on X Layer testnet. They support Vault
custody, typed `TRANSFER` and `SWAP` Action Plans, Mandates with validity
windows, delegation, Preflight, owner-approved escalation, revocation, pausing,
nonce cancellation, and native-asset USD valuation through Chainlink. The live
deployment exercises most features; native-USD and SWAP require a
manifest-configured feed and adapter to be active.
## Start with the model [#start-with-the-model]
* [Strategy vs authority](/docs/concepts/strategy-vs-authority) explains the distinction between an agent's intent and Grantline's authorisation decision.
* [Mandates](/docs/concepts/mandates) explains the rules that bind an agent to a Vault, including lifecycle, validity windows, and native-USD limits.
* [Vaults](/docs/concepts/vaults) explains the custody boundary around controlled capital, including UUPS upgrades and pausing.
* [Action Plans](/docs/concepts/action-plans) explains the structured proposal format with `TRANSFER` and `SWAP` actions.
* [Decisions](/docs/concepts/decisions) explains the `ALLOW`, `ESCALATE`, and `DENY` outcomes.
* [Delegation](/docs/concepts/delegation) explains how authority moves to a sub-agent without becoming broader.
* [Preflight](/docs/concepts/preflight) explains the native-balance and native-USD balance checks.
* [Guardians](/docs/concepts/guardians) explains the planned model for adding external conditions.
* [Records](/docs/concepts/records) explains how authority, decisions, and execution outcomes stay traceable.
## Follow an action through the system [#follow-an-action-through-the-system]
* [Security model](/docs/enforcement/security-model) explains the current boundaries between agent intent, authority, execution, and custody, including the GrantlineAdmin coordinator.
* [Enforcement boundary](/docs/enforcement/enforcement-boundary) follows the contract path from a signed plan to the Vault.
* [Action commitments, signatures and nonces](/docs/enforcement/action-commitments-signatures-and-nonces) explains the canonical plan digest, domain binding, replay protection, escalation reservations, and nonce cancellation.
* [Escalation](/docs/enforcement/escalation) explains controller approval and current-state re-evaluation.
* [Revocation](/docs/enforcement/revocation) explains how authority stops without erasing its history, including pausing as a lighter alternative.
* [Transaction lifecycle](/docs/execution/transaction-lifecycle) separates authorisation, execution, and receipt status.
* [X Layer testnet](/docs/execution/networks/x-layer-testnet) records the current network and environment-specific integration boundaries.
* [Contracts and deployments](/docs/execution/contracts-and-deployments) explains the manifest and contract wiring.
## Guides [#guides]
* [Vault and Mandate setup](/docs/guides/vault-and-mandate-setup) walks through creating a Vault, funding it, and creating a Mandate.
* [Normal execution](/docs/guides/normal-execution) covers signing, evaluating, and executing `TRANSFER` and `SWAP` plans.
* [Escalated execution](/docs/guides/escalated-execution) covers the full escalation lifecycle from submission to execution.
* [Delegation](/docs/guides/delegation) covers creating child Mandates and managing inherited authority.
* [Lifecycle management](/docs/guides/lifecycle-management) covers pausing, revoking, cancelling nonces, updating rules, and validity windows.
* [Inspecting evidence](/docs/guides/inspecting-evidence) shows how to read receipts, events, and explorer data.
## Reference [#reference]
* [Mandate rules](/docs/reference/mandate-rules) lists the exact rule fields, units, zero-disabled values, and inheritance behaviour.
* [Action Plan reference](/docs/reference/action-plan) lists the current action types, parameter encoding, and signature fields.
* [Decisions and failures](/docs/reference/decisions-and-failures) lists evaluator outputs and failure codes without treating them as execution records.
* [Events](/docs/reference/events) explains the current registry, escalation, Vault, executor, and facade event surface.
## Current implementation [#current-implementation]
The current contracts run on X Layer testnet and include:
* Vault custody, UUPS upgrades, and onchain execution events
* typed `TRANSFER` and `SWAP` Action Plans with EIP-712 signatures
* Mandates with validity windows, native-amount limits, and native-USD limits
* inherited Mandate rules, native-balance Preflight, and native-USD Preflight
* controller-approved escalation and revocation
* Vault and Mandate pausing for temporary suspension
* targeted nonce cancellation for recovery
* delegation to sub-agents with bounded authority and depth cap of 2
* GrantlineAdmin as the non-upgradeable protocol coordinator
* native-asset USD valuation through Chainlink price feeds
## For AI agents and tools [#for-ai-agents-and-tools]
*
/llms.txt
provides a concise index of the documentation.
*
/llms-full.txt
provides all documentation as one plain-text resource.
* Add `.mdx` to any docs URL to fetch that page as Markdown, such as /docs/concepts/mandates.mdx.
*
/sitemap.xml
provides the canonical URL map for crawlers and tools.
# Roadmap (https://grantline-zeta.vercel.app/docs/roadmap)
Planned direction for external conditions, integration tools, and indexed decision evidence.
**Planned direction**
The current contracts include Vault custody, `TRANSFER` and `SWAP` actions,
Mandates with validity windows, delegation, Preflight, owner-approved
escalation, revocation, pausing, nonce cancellation, and native-asset USD
valuation through Chainlink. The live deployment has native-USD and `SWAP`
available when configured through the manifest. The capabilities below
describe where Grantline can go next.
## Destination and capability policies [#destination-and-capability-policies]
A future destination policy would let an owner restrict a Mandate to approved targets. That would give a Treasury or Payments sub-agent a clear destination boundary in addition to amount limits. A delegated Mandate should be able to narrow an inherited policy, while the policy must remain clear for approvals that are already pending when the list changes. The current `TRANSFER` action has no such allowlist and accepts any non-zero recipient.
The same policy needs to cover actions that create reusable downstream authority, such as token allowances or operator approvals. An authorised action should not create a capability that bypasses later Grantline evaluation unless the Mandate explicitly permits that capability. The action-specific rules and enforcement path for this are not defined yet.
## Shared cumulative ancestor budgets [#shared-cumulative-ancestor-budgets]
A shared cumulative budget would limit total usage across an authority chain. It would prevent a parent and its sub-agents from each spending up to a separate allowance and exceeding the amount the owner intended for the whole group. The budget could become a shared boundary alongside the current per-plan amount limits, with usage and pending approval capacity accounted for across the lineage.
## Guardians [#guardians]
[Preflight](/docs/concepts/preflight) currently checks the projected native balance and native-USD balance left in a [Vault](/docs/concepts/vaults). [Guardians](/docs/concepts/guardians) are a planned way to bring relevant context from outside the current contract state into the same authorisation decision.
A Guardian could answer a narrow question chosen by the owner, such as whether an asset is eligible, whether a market condition is within policy, whether a counterparty is acceptable, or whether an organisation's policy permits the action. It would add context to a decision; it would not choose an agent's strategy or replace the Vault's custody boundary.
The first proposed direction is an RWA Guardian for tokenised real-world assets. The product direction may later include other specialised condition modules, but no Guardian check or external condition provider is part of the current contracts.
## SDK and API [#sdk-and-api]
The current integration surface uses the contract interfaces, external wallet tooling, and the [cast guides](/docs/guides/vault-and-mandate-setup). A future SDK could make the same flow easier to use by helping integrators construct Action Plans, sign them, submit them, read decisions, manage Escalations, and inspect execution evidence.
A future API could provide server-side coordination and presentation around those contract calls. The authority decision would remain in the deployed contracts, and signing authority would remain with the configured agent rather than with a hosted service.
When those tools are introduced, their package, authentication, and response design will be documented alongside the working service. This roadmap describes the jobs they should make easier, not an interface that exists today.
## Indexing and Decision Receipts [#indexing-and-decision-receipts]
The current contracts emit committed events from the registry, escalation manager, Vault, and executor, and expose scoped onchain indexes for Vaults, Mandates, and Escalations. Integrators can inspect those records and transaction receipts directly using [Events](/docs/reference/events) and the [inspecting evidence guide](/docs/guides/inspecting-evidence).
An offchain indexer could organise that evidence across deployments by Mandate, authority lineage, nonce, and action digest. A Decision Receipt could then present the proposal, authority, decision path, controller approval, transaction, and execution outcome in one human-readable view.
The future receipt is better understood as an evidence bundle than as a renamed activity log. It may connect the Action Plan and `actionDigest` with Mandate lineage, the effective rules used, Preflight results, Guardian evidence, controller approval, transaction status, and downstream failure details. A Mandate revision or effective-policy commitment may help make that explanation reproducible, but neither has a defined canonical construction today.
The distinction between an authorisation decision and an execution outcome must remain visible. An `ALLOW` result can lead to a reverted transaction, an approved Escalation can remain unexecuted, and a read-only `DENY` does not create a committed event. Contract-side indexes are current; cross-record offchain indexing and Decision Receipts remain deferred until the supporting service and data model exist.
# Action Plans (https://grantline-zeta.vercel.app/docs/concepts/action-plans)
Action Plans are structured proposals that describe what an agent wants to do under its authority.
An Action Plan is the agent-facing proposal that Grantline evaluates. It describes one or more intended actions in a form that can be checked against an active [Mandate](/docs/concepts/mandates) before execution.
## A structured proposal [#a-structured-proposal]
An Action Plan gives Grantline a complete object to evaluate rather than a request to execute arbitrary Vault calls. It carries the identity of the authority being used, the proposing agent, replay and timing controls, and an ordered set of typed actions.
The proposal format can expand as Grantline supports more capabilities, while the principle stays the same: the agent submits a structured request that Grantline can evaluate before execution.
## Action types [#action-types]
The current contracts support two action types:
* **TRANSFER** (version 1): moves native asset or ERC-20 tokens from the Vault to a recipient.
* **SWAP** (version 1): swaps tokens through a configured adapter (currently Uniswap V3). The adapter validates the route before execution.
A plan can contain multiple actions of either type. The executor runs them in array order.
## Ordered and atomic [#ordered-and-atomic]
An Action Plan can contain multiple actions. Grantline evaluates the plan as a whole, which prevents separate actions from being used to bypass a boundary that applies to their combined effect.
The current executor runs actions in order and treats the plan as one operation. If a later action fails, the transaction reverts, so earlier capital movement is not left behind as a successful completion.
## Signed intent [#signed-intent]
The agent signs the proposal it wants to submit. The signature binds the agent to the exact Action Plan and the Mandate ID it uses, so changing the plan's signed values produces a different commitment that must be checked again. The evaluator reads current Mandate rules and conditions when it checks the plan, so the signature does not freeze historical permissions. See [Action commitments, signatures and nonces](/docs/enforcement/action-commitments-signatures-and-nonces) for the current EIP-712 rules and digest usage.
Action Plans are proposals, not permission by themselves. The active Mandate, current conditions, and any required approval still determine whether the plan may proceed.
See [Decisions](/docs/concepts/decisions) for the result of evaluation.
# Decisions (https://grantline-zeta.vercel.app/docs/concepts/decisions)
Grantline resolves a proposal to ALLOW, ESCALATE, or DENY before controlled capital can move.
Grantline returns one of three authorisation decisions before the executor can move capital.
## `ALLOW` [#allow]
The proposal passed the checks that apply to its effective authority, actions, and current conditions. It may enter the authorised execution path.
`ALLOW` authorises the path. It does not guarantee that a downstream system, recipient, or asset integration will accept the action.
## `ESCALATE` [#escalate]
The proposal crossed a configured boundary that permits additional approval. It must be reviewed by the Vault controller before it can execute.
Approval applies to the proposal that was reviewed. It does not permanently bypass Grantline, because the relevant authority and conditions still need to be valid when execution occurs.
## `DENY` [#deny]
The proposal failed a required authority or validation check, or it crossed a boundary that cannot be approved through [Escalation](/docs/enforcement/escalation). It cannot proceed through the authorised execution path.
## Decision versus execution [#decision-versus-execution]
The decision answers whether the proposal is authorised. The execution result answers whether the authorised action completed.
For example:
```text
Decision: ALLOW
Execution: FAILED
```
That is different from:
```text
Decision: DENY
Execution: NO EXECUTION SUBMITTED
```
The technical response includes a failure code and aggregate values that help an integrator explain the result. See [Decisions and failures](/docs/reference/decisions-and-failures) for the current fields; the three decision meanings remain stable as the evaluation surface grows.
# Delegation (https://grantline-zeta.vercel.app/docs/concepts/delegation)
Delegation gives a sub-agent a narrower authority grant while preserving the lineage back to its source.
An authorised agent can delegate a narrower authority grant to a sub-agent when its own [Mandate](/docs/concepts/mandates) allows it. The sub-agent has its own identity, but it remains connected to the parent grant and the [Vault](/docs/concepts/vaults) above it.
```text
Capital controller
↓
Treasury agent
↓
Execution sub-agent
```
The governing rule is simple:
> Delegated authority may become narrower, but never broader.
## Inherited boundaries [#inherited-boundaries]
The effective authority of a sub-agent is constrained by the grant that created it and by the active lineage above it. A sub-agent may receive fewer actions, tighter limits, stricter conditions, or less discretion, but it cannot use delegation to create authority its parent did not have. The current implementation applies this inheritance to:
* native amount limits;
* native-USD amount limits;
* escalation permissions;
* delegation permission;
* Preflight floors (native and native-USD); and
* the validity window.
In the current model:
```text
effective authority
= current child Mandate
∩ active parent boundaries
∩ active ancestor boundaries
```
The [Preflight](/docs/concepts/preflight) check then applies its own state boundary to the proposed action. It does not give the child authority that is absent from its active lineage.
## Delegation depth [#delegation-depth]
The current contracts support a root Mandate, a child Mandate, and one further sub-agent level. Maximum delegation depth is 2.
At depth 2, `canDelegate` is forced off. A grandchild cannot create further descendants regardless of what its rules specify. This prevents unbounded delegation chains.
## Pausing and delegation [#pausing-and-delegation]
Pausing an ancestor blocks execution for all descendants. A paused parent means the child's lineage is not active for evaluation, even if the child's own status is `ACTIVE`. Unpausing the ancestor restores execution for the entire lineage.
## Administration and revocation [#administration-and-revocation]
The Vault controller and authorised parent agents can administer the grants within their respective boundaries. [Revocation](/docs/enforcement/revocation) of an ancestor prevents descendant authority from being used, while preserving the records needed to understand the lineage and its history.
See [Mandates](/docs/concepts/mandates) for the authority grant itself and [Decisions](/docs/concepts/decisions) for how an inactive or insufficient lineage affects a proposal.
# Guardians (https://grantline-zeta.vercel.app/docs/concepts/guardians)
Planned external conditions that can add verified context to a Grantline decision.
**Planned direction**
Guardians are not part of the current contracts. This page describes the
direction Grantline may take as it adds external conditions to the current
authority model.
A Guardian is intended to evaluate a narrow external condition around an action. It can bring context such as asset eligibility, market conditions, counterparty status, or an organisation's policy into a Grantline decision, while the agent remains responsible for proposing the action and the Mandate remains the source of authority.
The first proposed direction is an RWA Guardian for tokenised real-world assets. A Guardian would answer a configured condition; it would not choose the agent's strategy, replace the Mandate, or take custody of the Vault.
## The planned model [#the-planned-model]
```text
External source
↓
Evidence
↓
Guardian condition
↓
PASS / FAIL / UNKNOWN
↓
Grantline decision
```
The result would be an input to Grantline's decision path, alongside the authority and state checks already performed by the current system. An external condition would not create authority that the Mandate and active lineage do not already provide.
## Evidence should be explainable [#evidence-should-be-explainable]
The future Guardian model needs to make the evidence behind a condition visible enough to evaluate and investigate. The intended direction is that evidence should:
* identify or be attributable to its issuer;
* apply to the action being checked, rather than to an unrelated request;
* expose when it was observed and when it should no longer be trusted; and
* make an unavailable, stale, invalid, or unknown result explicit.
Required external conditions should not be certified solely by the acting agent. Otherwise the condition would add another name for the same authority rather than an independent check.
Unknown or unusable evidence should never silently become authorisation. The Mandate or future Guardian policy will need to determine whether that situation produces `DENY` or `ESCALATE`.
## Guardians and current checks [#guardians-and-current-checks]
The current [Preflight](/docs/concepts/preflight) implementation checks the projected native balance remaining in a Vault. It does not evaluate market data, asset eligibility, counterparties, or organisational policy. Guardians are a planned external-condition layer, not a replacement for Preflight.
Grantline also needs to keep external conditions separate from downstream execution. A future Guardian and the current Mandate may both pass while a token or protocol still rejects the transaction. In that case Grantline's authorisation result and the downstream execution failure remain separate facts. No final evidence structure, result enum, issuer registry, quorum rule, or external provider has been chosen.
See [Preflight](/docs/concepts/preflight) for the current condition check, [Decisions](/docs/concepts/decisions) for authorisation outcomes, and [Roadmap](/docs/roadmap) for the wider future direction.
# Mandates (https://grantline-zeta.vercel.app/docs/concepts/mandates)
Mandates define the authority an agent may use over controlled capital and the boundaries around it.
A Mandate is an authority grant connecting an agent to controlled capital. It defines what the agent may propose, the boundaries around those actions, and the conditions under which more approval is required.
## What a Mandate establishes [#what-a-mandate-establishes]
A Mandate can describe:
* the agent allowed to act;
* the [Vault](/docs/concepts/vaults) covered by the authority;
* permitted actions and their limits (native amounts, native-USD amounts, `SWAP` routes);
* conditions that must hold before an action proceeds (Preflight floors);
* when the authority is valid (`validAfter` / `validUntil`);
* when additional approval is required;
* whether narrower authority may be delegated; and
* how the authority begins, changes, and ends.
The model can expand with new actions, assets, conditions, and approval paths while keeping the same basic authority boundary.
## Authority is bounded [#authority-is-bounded]
A Mandate does not give an agent unrestricted control. It grants a defined area of authority, and every [Action Plan](/docs/concepts/action-plans) is checked against the boundaries that apply when it is evaluated.
Updates can change what an agent is allowed to do. [Revocation](/docs/enforcement/revocation) permanently stops future use of the authority. [Pausing](/docs/enforcement/revocation) temporarily suspends it without destroying the record.
## Lifecycle [#lifecycle]
A Mandate has three possible states:
* **ACTIVE**: the authority can be used for evaluation and execution.
* **PAUSED**: temporarily suspended. Blocks execution, escalation submission and approval, and new child Mandate creation. Deposits, withdrawals, and Mandate administration still work. Resuming restores the same authority.
* **REVOKED**: permanently stopped. Cannot be resumed. The original records and history are preserved.
```text
ACTIVE → PAUSED → ACTIVE (resumable)
ACTIVE → REVOKED (permanent)
```
The Vault controller and authorised parent agents can pause and revoke within their respective boundaries.
## Validity windows [#validity-windows]
Each Mandate carries `validAfter` and `validUntil` timestamps. The authority cannot be used outside this window. Before `validAfter`, the Mandate is not active. After `validUntil`, it stops being active. A value of `0` for either field means no constraint on that side.
Validity windows intersect across lineage. A child's effective window is the tightest combination of every active ancestor:
```text
effective validAfter = latest ancestor validAfter
effective validUntil = earliest ancestor validUntil
```
This is different from the Action Plan `deadline`, which limits one signed proposal. A Mandate window applies to the authority itself.
## Effective authority [#effective-authority]
The authority an agent can use is the effective authority that remains after the current Mandate is combined with every active ancestor above it:
```text
effective authority
= current Mandate
∩ active ancestor boundaries
```
This is why a child Mandate cannot broaden the authority that created it, and why revoking an ancestor affects the descendants below it. The current contracts apply this model to the implemented Mandate limits, escalation permissions, delegation permission, the [Preflight](/docs/concepts/preflight) rules, and the validity window. Pausing an ancestor also blocks descendant execution.
## Authority lineage [#authority-lineage]
Mandates form a lineage when an authorised agent delegates a narrower grant to a sub-agent. The current contracts support a root Mandate, a child Mandate, and one further sub-agent level (maximum depth of 2). At depth 2, `canDelegate` is forced off so no further delegation is possible.
A proposal is valid only when the authority behind it, including any relevant ancestors, remains active and unpaused.
See [Delegation](/docs/concepts/delegation) for the general inheritance model and [Decisions](/docs/concepts/decisions) for how a proposal's result is expressed.
# Preflight (https://grantline-zeta.vercel.app/docs/concepts/preflight)
Preflight checks whether the conditions around an action are safe before execution.
Preflight is the set of checks Grantline performs against the state surrounding a proposal before controlled capital can move. It answers whether the action would leave the relevant [Vault](/docs/concepts/vaults) inside the boundaries attached to its [Mandate](/docs/concepts/mandates).
## Before execution [#before-execution]
The current Preflight check considers the proposal as a whole and evaluates two dimensions:
* **Native balance**: compares the projected Vault native balance after the plan's aggregate native outflow against the inherited minimum reserve.
* **Native-USD balance**: compares the USD value of the projected remaining native balance against the inherited USD floor, using the configured Chainlink feed.
Each dimension can independently produce `DENY` or `ESCALATE` depending on how the Mandate configured that condition. When both dimensions hold, the proposal can continue to the other checks.
## What Preflight does not decide [#what-preflight-does-not-decide]
Preflight does not decide whether a strategy is profitable or whether an external asset is eligible. It is an additional execution gate, not a source of authority. [Guardians](/docs/concepts/guardians) are the planned way to bring external context into an authorisation decision; the current contracts only have the native-balance and native-USD balance checks.
See [Decisions](/docs/concepts/decisions) for how a failed Preflight check affects authorisation and [Mandates](/docs/concepts/mandates) for how conditions attach to authority.
# Records (https://grantline-zeta.vercel.app/docs/concepts/records)
Records connect authority, proposals, decisions, approvals, and execution outcomes over time.
Grantline needs a record of how authority was used, not only a decision made in isolation. Records connect the proposal, the authority behind it, the conditions considered, any approval, and the eventual execution outcome.
## Traceability [#traceability]
The current contracts emit durable onchain events for authority changes, approvals, custody changes, and successful execution. These records make it possible to follow the path from a granted boundary to the action that used it.
The event stream is evidence, not a complete human-readable receipt. [Events](/docs/reference/events) show what committed, while the transaction receipt shows whether the enclosing transaction succeeded.
## State transitions [#state-transitions]
Every authority lifecycle change produces a committed event:
* `MandateCreated` records the initial rules, Preflight rules, and validity window.
* `MandateUpdated` records replacement rules and window.
* `MandatePaused` / `MandateUnpaused` record temporary suspension and resumption.
* `MandateRevoked` records permanent termination.
* `NonceCancelled` records permanent invalidation of an unused nonce.
These events accumulate over time. An observer can reconstruct what authority existed, when it changed, and when it stopped being usable.
The current contracts also expose scoped onchain indexes for Vaults, Mandates, and Escalations. A controller can enumerate its current Vaults, then read the Mandates attached to those Vaults; agents and creators can use their corresponding Mandate indexes, while Escalation indexes retain records across pending, approved, denied, and executed states. These views make direct contract inspection possible without scanning every record or relying on an offchain service.
## Decisions and failed paths [#decisions-and-failed-paths]
A read-only evaluation can return `DENY` without creating a state change. A submitted execution can also fail before completion, so the absence of a successful execution record does not mean no proposal was evaluated.
Records distinguish an authorisation result from an execution outcome. A failed transaction may leave a receipt with status `0` but no committed events from that transaction.
## Decision Receipts [#decision-receipts]
Future offchain indexing could assemble the proposal, authority lineage, checks, approval, transaction, and outcome into a human-readable Decision Receipt. That receipt would be an evidence bundle built from committed contract data, contract-side indexes, and indexed transaction context, not an event emitted by the current contracts. Grantline does not currently run that offchain indexer or publish those receipts.
See [Decisions](/docs/concepts/decisions) for the distinction between authorisation and execution.
# Strategy vs authority (https://grantline-zeta.vercel.app/docs/concepts/strategy-vs-authority)
Grantline decides whether an agent is authorised to take an action; it does not choose the agent's strategy.
An AI agent can decide what it wants to do. Grantline decides whether the active authority permits it.
That distinction separates three stages:
1. The agent forms an intention.
2. The agent turns that intention into an [Action Plan](/docs/concepts/action-plans) under a [Mandate](/docs/concepts/mandates).
3. Grantline evaluates the plan before the controlled execution path can act.
Grantline does not decide whether the agent's strategy is profitable or sensible. It checks whether the proposal fits the authority that an owner or another authorised agent granted, together with the conditions attached to it and the current state relevant to the action.
## Authorisation is not execution [#authorisation-is-not-execution]
`ALLOW`, `ESCALATE`, and `DENY` are [authorisation decisions](/docs/concepts/decisions), not execution results.
* `ALLOW` means the proposal passed the current checks and may enter the authorised execution path.
* `ESCALATE` means the proposal requires additional approval before execution.
* `DENY` means the proposal cannot proceed through the authorised path.
An allowed action can still fail when the downstream execution system rejects it. That failure is an execution result, not a change to the earlier authorisation decision.
## The authority boundary [#the-authority-boundary]
The agent proposes an action, but it does not own the controlled capital. Grantline checks the proposal against the authority granted to that agent, and only a proposal that satisfies those boundaries can reach the execution path. The [Vault](/docs/concepts/vaults) remains the custody boundary, so a signing key does not become an unrestricted capital-movement key.
See [Decisions](/docs/concepts/decisions) for how Grantline communicates the result of that check.
# Vaults (https://grantline-zeta.vercel.app/docs/concepts/vaults)
Vaults hold controlled capital while the controller retains custody and control.
A Vault holds the capital that a Mandate governs. An agent receives permission to propose actions against that capital; it does not receive ownership of the Vault or a private-key path around its controls.
## Controller and authority [#controller-and-authority]
The Vault controller is responsible for custody, deposits, withdrawals, and administration. Grantline's execution authority acts for the Vault only through the controls placed around the [Mandate](/docs/concepts/mandates) and [Action Plan](/docs/concepts/action-plans).
This separates custody from operation. An allowed action can proceed without the controller signing every routine transaction, while an action that crosses a configured boundary can require the controller's approval through [Escalation](/docs/enforcement/escalation).
## A boundary around capital [#a-boundary-around-capital]
The Vault is the final controlled boundary between an authorised proposal and capital movement. Its generic execution surface accepts calls only from its configured authority, so an agent cannot turn a valid signing identity into unrestricted access to the Vault.
Vaults are UUPS upgradeable proxies. The protocol admin controls upgrades through `GrantlineAdmin`, which derives authority from `Grantline.owner()`. Once the Vault interface and enforcement stack stabilise, the intended direction is a non-upgradeable Vault boundary.
## Pausing [#pausing]
The Vault controller can pause and unpause the Vault through Grantline. A paused Vault blocks:
* autonomous agent execution (normal and escalated)
* escalation submission and approval
* new Mandate creation against the Vault
A paused Vault still allows controller deposits, withdrawals, and Mandate administration (updates, revocation, pausing, nonce cancellation). This lets the controller recover or restructure authority without losing custody.
## Deposits and withdrawals [#deposits-and-withdrawals]
The controller deposits native asset or ERC-20 tokens into the Vault through Grantline. Withdrawals are owner-controlled and go directly from the Vault to the specified recipient.
Grantline's guarantee is limited to capital held behind this execution boundary. An agent may still obtain another wallet, acquire assets outside the Vault, or use an off-platform resource that Grantline does not control.
See [Mandates](/docs/concepts/mandates) for the authority applied to a Vault and [Decisions](/docs/concepts/decisions) for the outcomes that precede execution.
# Action commitments, signatures and nonces (https://grantline-zeta.vercel.app/docs/enforcement/action-commitments-signatures-and-nonces)
How Grantline commits an exact Action Plan to its authority domain and prevents it from being executed twice.
The Action Plan digest is Grantline's canonical commitment to the proposed action. It is derived from the complete ordered [Action Plan](/docs/concepts/action-plans) and its EIP-712 domain, then used wherever the current contracts need to identify that exact plan.
Grantline uses EIP-712 to sign the complete Action Plan rather than an unstructured message. The digest is computed by `ActionSignature` and verified by `MandateEvaluator` before the executor can consume a nonce or call the [Vault](/docs/concepts/vaults).
## The canonical action commitment [#the-canonical-action-commitment]
The current digest includes:
* the `mandateId` and `agent` in the plan;
* the proposal `nonce` and `deadline`; and
* the complete ordered action array, including each action's type, version, and exact parameter bytes.
The EIP-712 domain adds `Grantline`, version `1`, the active chain ID, and the deployed `MandateEvaluator` address. Changing any signed plan value, the action order, the evaluator, or the chain changes the digest.
Mandate rules are not copied into the signed bytes. The signature binds the agent to the exact Action Plan and Mandate ID it uses; the evaluator reads the current Mandate rules, active lineage, validity window, and Preflight state when it checks the plan. A signature therefore does not freeze earlier permissions.
`actionDigest` is derived at the points that need it. It is not a field on `ActionPlan`, and the current `EvaluationResult` does not return it directly.
## What the signature covers [#what-the-signature-covers]
The signed plan includes the Mandate ID, agent address, proposal nonce, deadline, and complete ordered action array. Each action contributes its type, version, and exact parameter bytes to the digest. Changing an action, its order, its parameters, its deadline, or its Mandate ID produces a different commitment.
The evaluator accepts a signature only when it recovers to the agent recorded in the Mandate. Recovery requires a 65-byte signature with `v` equal to `27` or `28`, and rejects an `s` value above the secp256k1 half-order. The low-`s` check prevents signature malleability from creating alternate valid encodings of the same proposal.
## Where the digest is used [#where-the-digest-is-used]
The current contract paths use the derived commitment consistently when they need to identify a complete plan:
* `EscalationManager.submit` computes the digest, stores the complete plan and signature behind it, and reserves the plan's nonce.
* `EscalationManager.approve` and `deny` address the stored escalation by its digest.
* `VaultExecutor.execute` computes the digest for a successful normal execution and emits it in `ActionPlanExecuted`.
* `VaultExecutor.executeEscalated` reloads the stored plan, evaluates current state again, recomputes the digest, and rejects the execution if it differs from the reserved value.
The escalation and execution events therefore identify an exact signed plan, while the registry and evaluator still provide the current authority state that was checked around it.
## Proposal nonces [#proposal-nonces]
A nonce identifies an execution slot for a Mandate and agent. It does not require nonces to execute in numerical order, so a later valid proposal can proceed while an earlier proposal is unused.
For a normal `ALLOW` path, `VaultExecutor` asks `MandateRegistry` to consume the `(Mandate, agent, nonce)` slot after evaluation and before external Vault calls. The registry checks the active lineage, the agent binding, the current Vault authority, and whether the slot was already used or reserved.
The executor rejects a normal plan when that slot is reserved for an escalation. A successful normal execution marks the slot used, and a later proposal using it fails. If any later call reverts, the nonce update reverts with the transaction, so a failed downstream execution does not permanently consume the slot.
## Escalation reservations [#escalation-reservations]
When `EscalationManager.submit` accepts an `ESCALATE` result, it computes the exact Action Plan digest and reserves the plan's nonce against that digest. The reservation prevents an ordinary execution from using the same slot and prevents another escalation from replacing the stored plan.
The reservation is shared through `MandateRegistry`, not kept only in the manager. The current executor's configured escalation manager is the only caller allowed to create a reservation, and an escalated execution must consume the same digest that was reserved. Pending or denied reservations remain binding, while successful approved execution deletes the reservation and marks the nonce used.
## Nonce cancellation [#nonce-cancellation]
The Mandate agent or the Vault controller can permanently invalidate an unused, unreserved nonce through `Grantline.cancelNonce()`. This is a targeted recovery mechanism: it does not pause or revoke the broader authority.
Cancellation marks the nonce as used. A cancelled nonce cannot be consumed by a normal execution or reserved for an escalation. The Registry repeats the authority check before cancelling, so a parent agent cannot cancel a child's nonces.
Cancellation remains available during pause, outside validity windows, and after revocation. It is a recovery-only restriction on a specific execution slot.
## Nonce-aware evaluation [#nonce-aware-evaluation]
The evaluator returns `DENY` with distinct failure codes for nonce problems:
* `NONCE_USED`: the nonce was already consumed or cancelled.
* `NONCE_RESERVED`: the nonce is reserved for an escalation digest.
Public evaluation reports both as `DENY`. The internal evaluator mode (used during approved escalation execution) accepts only the exact reservation digest that matches the reserved nonce.
## Deadlines and rollback [#deadlines-and-rollback]
A deadline of `0` means no expiry. Any other deadline is an absolute Unix timestamp checked against the chain's `block.timestamp`; the plan is accepted while the current timestamp is at or before the deadline. Callers should leave a safety margin for inclusion delay rather than depend on an exact second.
Nonce consumption, Vault calls, event emission, and escalation status changes happen in one EVM transaction. If a downstream call or a later validation step reverts, those state changes and events do not persist. A receipt with status `0` proves that the transaction failed, not that a nonce was permanently consumed.
See [Escalation](/docs/enforcement/escalation) for the approval lifecycle and [Transaction lifecycle](/docs/execution/transaction-lifecycle) for how these checks appear in normal and escalated execution.
# Enforcement boundary (https://grantline-zeta.vercel.app/docs/enforcement/enforcement-boundary)
Grantline keeps proposal evaluation, approval, and capital movement on an authorised contract path.
Grantline separates the agent's signed proposal from the Vault that holds capital. The `Grantline` facade routes all operations to internal modules. The evaluator decides whether the proposal is authorised, the executor translates an authorised plan into Vault calls, and the Vault accepts calls only from its configured authority. This is the contract boundary that prevents an agent's signing key from becoming direct Vault authority.
```text
Agent signs an Action Plan
↓
Grantline routes to MandateEvaluator
↓
VaultExecutor accepts an allowed or approved plan
↓
Vault performs the transfer or swap
```
## Contract path [#contract-path]
`Grantline` is the only external entry point. It validates that the caller is configured, computes the action digest, and routes to the correct module. Direct calls to internal modules fail.
`MandateEvaluator` is the decision boundary. It checks the [Mandate](/docs/concepts/mandates) and active lineage, the validity window, the agent binding, the signature, the deadline, the complete [Action Plan](/docs/concepts/action-plans), amount rules, `SWAP` route validation, and [Preflight](/docs/concepts/preflight) state. It returns `ALLOW`, `ESCALATE`, or `DENY` with the first relevant failure code; it never moves capital itself.
`VaultExecutor` is the execution boundary above the Vault. Its normal entrypoint accepts only an `ALLOW` result. Its escalated entrypoint loads the complete plan stored by `EscalationManager`, confirms that the escalation is approved, recomputes the evaluation against current state, and rejects the plan only when the current result is `DENY`. An approved plan can therefore remain executable when current rules still return `ALLOW` or `ESCALATE`, but approval never skips re-evaluation.
The executor checks that the supplied Vault is the Vault named by the Mandate, consumes the appropriate nonce through `MandateRegistry`, executes every action in order, and emits the successful execution record. A downstream failure reverts the complete transaction, so the executor cannot report a partially completed plan as successful.
## Who can call what? [#who-can-call-what]
The agent's authority comes from the signature recovered from the Action Plan. The public executor entrypoint does not trust the transaction sender as the agent; it trusts the plan's signed `agent` field after the evaluator confirms that it matches the Mandate. In the current integration, the agent or operator submits the transaction and pays gas directly. Sponsored submission is future work and would not change this contract boundary.
Escalation submission is permissionless after the evaluator returns `ESCALATE`, but the manager can reserve a nonce only when it is exposed by the current Vault authority. Approval and denial belong to the current Vault controller. The current Vault authority is the `VaultExecutor`, which is also the only component allowed to consume a normal or reserved nonce for execution.
## Why direct Vault calls fail [#why-direct-vault-calls-fail]
The Vault's generic `execute` function is restricted to its `authority` address. The controller controls deposits, withdrawals, ownership, and authority assignment, while the agent address in a Mandate is not given direct Vault authority. An agent that bypasses the evaluator and calls the Vault directly therefore fails the Vault's authority check.
The controller must explicitly configure the executor as the Vault authority. This keeps custody in the Vault, keeps action translation in the executor, and prevents a signing key from becoming an unrestricted capital-movement key.
## Re-evaluation and re-entrancy [#re-evaluation-and-re-entrancy]
Approved escalations are evaluated again immediately before execution. Current Mandate rules, active lineage, validity window, and the projected Vault balance still apply, so tightening or revoking authority can stop an approved plan before capital moves. Pausing the Vault or Mandate also blocks execution.
Both public executor entrypoints share one OpenZeppelin `ReentrancyGuard` lock. If a recipient makes an external callback during a native or token transfer, that callback cannot start a nested plan through the executor while the active plan is using its point-in-time Preflight balance.
See [Decisions](/docs/concepts/decisions) for the meaning of the authorisation result and [Transaction lifecycle](/docs/execution/transaction-lifecycle) for the committed and reverted paths.
# Escalation (https://grantline-zeta.vercel.app/docs/enforcement/escalation)
Escalation routes a boundary-crossing proposal to controller approval without bypassing current authority checks.
Escalation is an approval path for a proposal that crossed a configured amount or [Preflight](/docs/concepts/preflight) boundary. It does not turn a denied proposal into an executable one: the evaluator must return `ESCALATE`, the [Vault](/docs/concepts/vaults) controller must approve the stored proposal, and the executor must accept its current evaluation before capital moves.
## Lifecycle [#lifecycle]
```text
ESCALATE result
↓
PENDING
↙ ↘
DENIED APPROVED
↓
EXECUTED
```
The manager also exposes `NONE` for a digest that has no stored escalation. A stored escalation moves from `PENDING` to `APPROVED`, `DENIED`, or `EXECUTED`; it cannot be submitted twice or move backwards.
## Submission [#submission]
`EscalationManager.submit` calls the evaluator itself and accepts only an `ESCALATE` result. It computes the EIP-712 digest, reserves the plan's nonce through the registry, stores the complete Action Plan and signature, and marks the record `PENDING`.
The manager checks that the Mandate lineage is active, the Vault is not paused, and the Mandate is within its validity window before accepting the submission. Submission is permissionless after evaluation, so the transaction sender does not become the approving authority.
## Approval and denial [#approval-and-denial]
Only the current Vault controller can approve or deny a pending escalation. Approval records the digest and changes its status to `APPROVED`; denial changes it to `DENIED`. The controller is approving or denying the exact stored plan, not a later plan that happens to reuse its nonce.
Approval checks that the Mandate and its full lineage are active, unpaused, and within the validity window. It does not permanently bypass the evaluator: the executor evaluates the stored plan again at execution time, so a later rule update, balance change, expired deadline, revocation, or pause can still prevent execution.
Denial does not release the reserved nonce. The reservation remains part of the historical binding for that digest, and the slot cannot be reused through the ordinary path.
## Execution [#execution]
`VaultExecutor.executeEscalated` accepts only an `APPROVED` record. It loads the stored plan and signature, evaluates them against current state, recomputes the digest, consumes the matching reserved nonce, executes the complete plan, and asks the manager to mark the digest `EXECUTED`.
The current evaluation may return `ALLOW` or `ESCALATE` for an approved escalation because controller approval has already covered the configured boundary. A current `DENY` reverts before Vault calls. If a downstream action fails, the transaction rolls back the nonce consumption, execution event, and status change, leaving the escalation `APPROVED` for a later retry if the underlying condition changes.
## Revocation and pausing during the lifecycle [#revocation-and-pausing-during-the-lifecycle]
Revocation blocks new escalation reservations and approval through the active-lineage checks. The controller can still deny a pending escalation after revocation, which closes the operational record without authorising it. An already approved escalation remains `APPROVED` as history, but current evaluation returns `DENY` for an inactive lineage and prevents execution.
Pausing the Vault blocks escalation submission, approval, and execution. Pausing the Mandate blocks submission and approval. The stored escalation remains during the pause; approval or execution can proceed after unpause if the Mandate is still within its validity window and the lineage is otherwise active.
See [Action commitments, signatures and nonces](/docs/enforcement/action-commitments-signatures-and-nonces) for reservation behaviour and [Revocation](/docs/enforcement/revocation) for the authority lifecycle that affects pending and approved escalations.
# Revocation (https://grantline-zeta.vercel.app/docs/enforcement/revocation)
Revocation stops future use of a Mandate and its descendants while preserving authority history.
Revocation changes a Mandate from `ACTIVE` to `REVOKED` and records the revocation time. It does not delete the Mandate, its parent reference, its agent address, or the events that explain its history. It is the current contract's way to permanently stop a Mandate.
Pausing is a lighter alternative that temporarily suspends authority without destroying the record. See the pause section below for the distinction.
## Who can revoke [#who-can-revoke]
The Vault controller can revoke any Mandate for that Vault. The direct parent agent can revoke its child while the parent lineage is active. A parent agent cannot administer a child through an inactive parent, and an unrelated agent cannot revoke either record.
The same administrator boundary applies to Mandate updates and pauses. Updates are read by the evaluator at execution time, so a tightened rule can affect a plan that was signed before the update.
## Active lineage [#active-lineage]
The registry stores each Mandate's status, but it does not write a separate `REVOKED` status into every descendant. Instead, the evaluator walks the Mandate and every ancestor when it checks `isLineageActive`.
```text
Root Mandate: REVOKED
↓
Child Mandate: ACTIVE record, inactive lineage
↓
Sub-agent proposal: DENY / MANDATE_INACTIVE
```
This preserves the original records while making the ancestor's revocation effective across the entire delegated authority graph. A descendant cannot use its own `ACTIVE` record to bypass a revoked ancestor.
## Normal proposals [#normal-proposals]
A normal proposal from a revoked Mandate or descendant receives `DENY` with `MANDATE_INACTIVE`. `VaultExecutor` reverts before it consumes the nonce or calls the Vault, so revocation does not create a capital movement or a successful execution record.
The registry also checks the active lineage when consuming a nonce. This keeps replay protection aligned with authority state rather than allowing a stale signed plan to use a previously valid slot.
## Escalations [#escalations]
Revocation affects every stage of an escalation:
* a new escalation cannot reserve a nonce for an inactive lineage
* a pending escalation cannot be approved after its Mandate or ancestor is revoked
* a pending escalation can still be denied by the Vault controller to close it operationally
* an approved escalation remains stored as history, but current evaluation prevents execution
The reserved digest is not silently reassigned when authority changes. This keeps the old proposal's history intact and prevents a different plan from inheriting its execution slot.
## Pausing as an alternative [#pausing-as-an-alternative]
Pausing a Mandate temporarily suspends authority without permanently destroying it. The stored Mandate remains `PAUSED` rather than `REVOKED`, and resuming restores the same authority.
Pausing blocks:
* normal and escalated execution
* escalation submission and approval
* new child Mandate creation
Pausing does not block:
* controller deposits and withdrawals
* Mandate updates, revocation, and nonce cancellation
The effect on delegated sub-agents follows the same lineage rules as revocation: a paused ancestor blocks execution for descendants. The evaluator returns `DENY` with `MANDATE_PAUSED` instead of `MANDATE_INACTIVE`.
An administrator can pause and later unpause a Mandate. The pause and unpause transitions produce committed events (`MandatePaused`, `MandateUnpaused`) that record who paused and when.
## Historical record [#historical-record]
`MandateRevoked` records the Mandate, revoking administrator, and timestamp. The original `MandateCreated` and any `MandateUpdated` events remain available, so an observer can reconstruct what authority existed before revocation and when it stopped being usable.
See [Delegation](/docs/concepts/delegation) for the parent and sub-agent model and [Escalation](/docs/enforcement/escalation) for the lifecycle of a stored approval request.
# Security model (https://grantline-zeta.vercel.app/docs/enforcement/security-model)
How Grantline keeps agent authority bounded from a signed proposal to controlled capital.
Grantline enforces authority at the point where a proposal can move Vault capital. The current contracts separate agent intent, Mandate evaluation, controller approval, and Vault execution, so the agent's key is never the Vault authority.
## The current enforcement path [#the-current-enforcement-path]
```text
Agent signs a typed Action Plan
↓
Grantline routes to MandateEvaluator
↓
ALLOW / ESCALATE / DENY
↓
Grantline routes to VaultExecutor or EscalationManager
↓
Vault
```
`Grantline` is the stable public entrypoint for all controller and agent workflows. It routes operations to the correct internal module. The other components are internal modules that do not accept direct external calls.
For a normal execution, `VaultExecutor` evaluates the signed plan when the execution call is made. An `ALLOW` result lets it consume the shared nonce, call the Vault, and execute the ordered actions atomically. An `ESCALATE` result is submitted to `EscalationManager`, which stores the complete plan and signature for the current Vault controller to approve or deny. The executor evaluates an approved escalation again against current state before it moves capital.
## GrantlineAdmin [#grantlineadmin]
`GrantlineAdmin` is a separate non-upgradeable coordinator that manages protocol administration. It has no independent owner; its authority derives from `Grantline.owner()`.
Its responsibilities:
* **Module configuration**: validates and wires the registry, evaluator, escalation manager, executor, Vault factory, and swap adapters.
* **Module upgrades**: upgrades internal module proxies through UUPS.
* **Vault template changes**: sets the implementation used for future Vault proxies.
* **Existing Vault upgrades**: upgrades individual Vault proxies while preserving authority, owner, pause state, and recorded identity.
* **Vault controller reassignment**: changes the controller of an existing Vault.
* **Adapter validation**: checks that swap adapters expose the correct component type, grantline binding, and configuration.
Replacing the coordinator updates one Grantline pointer. The old coordinator loses module and Vault upgrade authority without a module-ownership migration.
## The Vault is the capital boundary [#the-vault-is-the-capital-boundary]
The agent signs an Action Plan, but the agent address is not the Vault's execution authority. The Vault accepts its generic execution surface only from the configured authority, which is the executor for the agent-led path. A direct call from the agent therefore cannot turn a valid signature into unrestricted Vault access.
The Vault controller remains responsible for custody and administration. That controller can deposit and withdraw capital, change the Vault authority, transfer ownership, and administer the Mandates that govern the Vault. Routine agent execution does not require the controller to sign each transaction, while configured boundary crossings can require controller approval through Escalation.
Grantline's guarantee is limited to capital held behind this execution boundary. An agent may still obtain another wallet, acquire assets outside the Vault, or use an off-platform resource that Grantline does not control.
## Effective authority [#effective-authority]
The evaluator determines a Mandate's effective authority from its current rules, active ancestor boundaries, and the effective validity window:
```text
effective authority
= current Mandate
∩ active ancestor boundaries
∩ validity window
```
Delegation can narrow that result but cannot broaden it. Revoking a Mandate or an ancestor makes the lineage inactive for future evaluation. Pausing an ancestor blocks execution for descendants. The current implementation applies this inheritance to the implemented Mandate limits, escalation permissions, delegation permission, Preflight rules, and validity window.
Effective authority is not the same thing as every check that can affect execution. Preflight is an additional state gate: it checks whether the projected Vault balance remains above the inherited reserve and can return `DENY` or `ESCALATE` according to the configured rule. Guardians are a planned way to add external conditions; no Guardian check runs in the current contracts.
Escalation is also separate from ordinary effective authority. It gives the controller a path to review a configured boundary crossing, but approval is tied to the exact stored plan and execution still checks current Mandate state, lineage, nonce state, and conditions.
## Authorisation is separate from execution [#authorisation-is-separate-from-execution]
`ALLOW`, `ESCALATE`, and `DENY` describe what Grantline permits next. They do not describe whether a transaction was mined successfully or whether a downstream asset call accepted the action.
```text
Authorisation: ALLOW
Execution: FAILED
```
An authorised transfer can still revert during the Vault call or in a downstream token integration. A read-only `DENY` returns a result without creating a state change, while a reverted transaction does not preserve events emitted inside that transaction.
## Metering is not enforcement [#metering-is-not-enforcement]
A counter can measure how much authority has been used, but it does not stop capital from moving by itself. A budget becomes an enforcement rule only when the execution path checks the same state before calling the Vault or another capital-holding system. Shared cumulative budgets remain future work, so the current contracts do not claim to enforce a total allowance across siblings.
## Current limitations [#current-limitations]
The current typed action surface supports `TRANSFER` version `1` and `SWAP` version `1`. `TRANSFER` does not yet enforce a destination allowlist; an authorised transfer can send capital to any non-zero address. `SWAP` validates routes through the configured adapter, but adapter support is immutable after one-time module configuration.
See [Vaults](/docs/concepts/vaults) for the custody model, [Action commitments, signatures and nonces](/docs/enforcement/action-commitments-signatures-and-nonces) for the canonical plan digest, and [Transaction lifecycle](/docs/execution/transaction-lifecycle) for the execution and receipt boundary.
# Contracts and deployments (https://grantline-zeta.vercel.app/docs/execution/contracts-and-deployments)
The tracked deployment manifest defines the Grantline deployment identity, wiring, authority, and runtime hashes.
The X Layer testnet manifest is `contracts/deployments/xlayer-testnet.json`. It contains the Grantline proxy, internal module proxies and implementations, Vault template, dependency wiring, authority, versions, and runtime code hashes.
## Current wiring [#current-wiring]
```text
Grantline (user-facing facade)
├── GrantlineAdmin (protocol coordinator)
├── MandateRegistry
├── MandateEvaluator
├── EscalationManager
├── VaultExecutor
├── VaultFactory
│ └── Vault proxies
└── Swap adapters (e.g. UniswapV3Adapter)
```
> Arrows show contract dependency or call direction.
The roles are separate so each part has one job:
* `Grantline` is the stable public entrypoint for controller and agent workflows. It routes all operations to internal modules.
* `GrantlineAdmin` is a non-upgradeable protocol coordinator. It manages module configuration, module upgrades, Vault template changes, existing Vault upgrades, Vault controller reassignment, and adapter validation. It has no independent owner; its authority derives from `Grantline.owner()`.
* `Vault` holds native and token capital, exposes controller custody through Grantline, and accepts agent execution only from its configured executor.
* `MandateRegistry` stores Mandates, lineage, effective rules, nonce usage, and escalation reservations.
* `MandateEvaluator` reads the registry and returns a structured authorisation result without moving capital.
* `EscalationManager` stores complete escalated plans, accepts permissionless submission through Grantline, and restricts approval or denial to the current controller.
* `VaultExecutor` binds the evaluator, escalation manager, registry, and Vault execution path, then translates supported actions (TRANSFER, SWAP) into atomic Vault calls.
* `VaultFactory` creates Vault proxies, validates implementations, and manages the Vault template.
* Swap adapters (e.g. `UniswapV3Adapter`) validate and execute token swaps. Adapter support is immutable after one-time module configuration.
## Deployment manifests [#deployment-manifests]
Each registered Grantline deployment will produce its own table below in the declared order:
### X Layer Testnet
| Component | Address |
| --- | --- |
| Grantline | `0x77324b24a9290da85217b0d22925c5d9034b2062` |
| GrantlineAdmin | `0x5e7465087a6590a0902fe6f2cb4f286d35f26627` |
| MandateRegistry | `0x5603cdaefb0a8554fb2362efc5883ce679e407f0` |
| MandateEvaluator | `0x4b89916f69909c67d60b7b8146edd501911024ce` |
| EscalationManager | `0xaaa3f7086e28e97adbca1c5bfc3261827cba3058` |
| VaultExecutor | `0x12ff6bab7696987048f91cf54f21ff81ff4c8310` |
| VaultFactory | `0xe4cd291d8ce89ebd6f3ee468b0ed15b01fd60495` |
| Vault Implementation | `0x7d2aaa9f61aec7c880b1aac7694fff651ab91121` |
After deployment, the verifier checks that the evaluator, escalation manager, executor, factory, Vault proxies, and swap adapters all match the manifest-defined wiring and versions.
## Verification before use [#verification-before-use]
Load the contracts environment and run the read-only verification script:
```sh
cd contracts
set -a
source .env
set +a
forge script script/VerifyGrantlineDeployment.s.sol:VerifyGrantlineDeployment --rpc-url xlayer_testnet
```
`VerifyGrantlineDeployment` checks the expected chain, proxy implementations, runtime hashes, initialiser state, UUPS identifiers, ownership, authority, controller assignments, module wiring, adapter configuration, factory state, and every recorded Vault. A mismatch stops verification instead of allowing an internally inconsistent manifest to look valid.
## Updating a deployment [#updating-a-deployment]
Run `DeployGrantline` for the fresh Grantline stack, then verify its runtime code and dependency wiring before treating it as current. Update the tracked manifest only after the broadcast succeeds and the resulting addresses, authority, versions, and code hashes have been checked. This ordering keeps a failed or misdirected deployment visible instead of hiding it behind an edited record.
Use the addresses from the current manifest rather than an old integration log. Deployment scripts verify those values, and the relevant network page should be updated whenever the deployment changes.
See [X Layer testnet](/docs/execution/networks/x-layer-testnet) for network-specific details and [Enforcement boundary](/docs/enforcement/enforcement-boundary) for how the deployed components constrain execution.
# Transaction lifecycle (https://grantline-zeta.vercel.app/docs/execution/transaction-lifecycle)
A Grantline transaction separates authorisation, nonce handling, Vault calls, and the final receipt.
An [Action Plan](/docs/concepts/action-plans) can be evaluated without moving capital, then submitted through the executor when the caller is ready to transact. The [authorisation result](/docs/concepts/decisions) and the transaction receipt answer different questions: the first says whether the plan is permitted, while the second says whether the submitted operation committed.
## Normal execution [#normal-execution]
```text
Signed Action Plan
↓
Grantline.evaluate (read-only)
↓
ALLOW
↓
Grantline.execute(plan, signature)
↓
VaultExecutor executes actions in order
↓
Vault calls and ActionPlanExecuted
```
`Grantline.execute` computes the digest and calls `VaultExecutor.execute`. The evaluator checks the complete signed plan. A result other than `ALLOW` becomes `EvaluationDenied`, so the executor does not call the Vault for a denied or escalatable normal plan. For an allowed plan, it confirms that the supplied Vault matches the Mandate, computes the digest, rejects a used or reserved nonce, and consumes the nonce through the registry before the first external Vault call.
The executor then translates each supported action into a Vault call. Native transfers call the Vault with value and an empty payload. Token transfers call the token's `transfer` function through the Vault and accept the standard empty or `true` return conventions. `SWAP` actions call `vault.executeSwap(adapter, params)` after adapter route validation. Once every action succeeds, the executor emits `ActionPlanExecuted`.
## Escalated execution [#escalated-execution]
```text
ESCALATE
↓
Grantline.submitEscalation(plan, signature)
↓
EscalationManager stores plan and reserves nonce
↓
Vault controller approves
↓
Grantline.executeEscalated(digest)
↓
Current evaluation and exact digest check
↓
Reserved nonce consumed, actions execute, status becomes EXECUTED
```
The escalated entrypoint loads the complete plan and signature from `EscalationManager`. It requires `APPROVED`, evaluates the stored proposal again, and rejects only a current `DENY`. It then recomputes the digest, consumes the matching reservation, executes the actions, and marks the manager record `EXECUTED` in the same transaction.
## Denied and failed paths [#denied-and-failed-paths]
A read-only evaluation can return `DENY` with a failure code and no state change. If a caller submits that plan to the normal executor, the transaction reverts with `EvaluationDenied` before the registry consumes a nonce or the Vault receives a call.
An `ALLOW` result does not guarantee downstream success. A recipient can reject a native call, a token can return false or malformed data, a token target can fail validation, or a `SWAP` output can fall below `minAmountOut`. The executor reverts the complete plan when any action fails, so earlier actions do not remain completed and `ActionPlanExecuted` does not persist.
The same atomicity applies to escalated execution. If the action loop or the final escalation status update fails, the nonce, Vault calls, execution event, and status change all roll back. An approved escalation can therefore remain `APPROVED` after a failed attempt.
## Reading the receipt [#reading-the-receipt]
Receipt status `1` means the submitted transaction committed. Receipt status `0` means it reverted. A successful `ActionPlanExecuted` event proves that the complete plan reached the executor's successful execution point; it does not prove that an earlier proposal was allowed, because a read-only evaluation leaves no event, and it does not replace the transaction receipt when diagnosing a reverted attempt.
The `ActionPlanExecuted` event records the digest, Mandate, agent, Vault, nonce, aggregate native amount, USD valuation of the outflow, action count, projected remaining balance, and USD valuation of the remaining balance.
See [Enforcement boundary](/docs/enforcement/enforcement-boundary) for the contract roles and [Records](/docs/concepts/records) for the traceability model.
# Delegation (https://grantline-zeta.vercel.app/docs/guides/delegation)
Create child Mandates, inspect inherited authority, and manage delegated agents.
This guide walks through creating child Mandates, inspecting inherited authority, and managing the delegation lifecycle. It assumes a root Mandate already exists (see [Vault and Mandate setup](/docs/guides/vault-and-mandate-setup)).
## Load the deployment values [#load-the-deployment-values]
```sh
cd contracts
set -a
source .env
set +a
MANIFEST="$DEPLOYMENT_MANIFEST_PATH"
GRANTLINE="$(jq -r '.grantline.proxy' "$MANIFEST")"
REGISTRY="$(jq -r '.modules.registry.proxy' "$MANIFEST")"
RPC="$XLAYER_TESTNET_RPC_URL"
CONTROLLER="$DEPLOYER_ADDRESS"
ROOT_MANDATE_ID=1
```
## Create a child Mandate [#create-a-child-mandate]
The root Mandate's `canDelegate` must be `true`. The child rules must be narrower than the parent's effective rules.
```sh
CHILD_AGENT=0xChildAgent
cast send "$GRANTLINE" \
"createChildMandate(uint256,address,(bool,uint256,uint256,bool,uint256,uint256,bool),(uint256,bool,uint256,bool),uint64,uint64)(uint256)" \
"$ROOT_MANDATE_ID" \
"$CHILD_AGENT" \
"(true, 0, 5000000000000000000, true, 0, 0, false)" \
"(0, false, 0, false)" \
0 \
0 \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
The child rules set a tighter maximum of 5 OKB (vs the parent's 10 OKB). `canDelegate` is `true` so this Mandate can create further children.
Read the `MandateCreated` event to get the child mandate ID.
## Inspect lineage [#inspect-lineage]
```sh
CHILD_MANDATE_ID=2
cast call "$GRANTLINE" \
"getLineage(uint256)(uint256[])" \
"$CHILD_MANDATE_ID" \
--rpc-url "$RPC"
```
The result is `[1, 2]` — the root Mandate followed by the child.
## Check effective rules on the child [#check-effective-rules-on-the-child]
```sh
cast call "$GRANTLINE" \
"getEffectiveRules(uint256)((bool,uint256,uint256,bool,uint256,uint256,bool))" \
"$CHILD_MANDATE_ID" \
--rpc-url "$RPC"
```
The effective `maxNativeAmount` is the tightest of the parent and child. If the parent has `maxNativeAmount = 10 OKB` and the child has `maxNativeAmount = 5 OKB`, the effective maximum is 5 OKB.
## Execute through the child [#execute-through-the-child]
The child agent signs a plan using the same EIP-712 flow. The evaluator checks the child's effective authority (the intersection with the parent).
If the child agent tries to transfer more than the effective maximum:
* with `escalateNativeAmount` enabled: returns `ESCALATE`
* without escalation: returns `DENY` with `NATIVE_AMOUNT_ABOVE_MAXIMUM`
## Create a grandchild (depth 2) [#create-a-grandchild-depth-2]
```sh
GRANDCHILD_AGENT=0xGrandchildAgent
cast send "$GRANTLINE" \
"createChildMandate(uint256,address,(bool,uint256,uint256,bool,uint256,uint256,bool),(uint256,bool,uint256,bool),uint64,uint64)(uint256)" \
"$CHILD_MANDATE_ID" \
"$GRANDCHILD_AGENT" \
"(false, 0, 2000000000000000000, false, 0, 0, false)" \
"(0, false, 0, false)" \
0 \
0 \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
At depth 2, `canDelegate` is forced off. Even if the rules specify `canDelegate: true`, the registry normalises it to `false`. The grandchild cannot create further descendants.
```sh
GRANDCHILD_MANDATE_ID=3
cast call "$GRANTLINE" \
"getLineage(uint256)(uint256[])" \
"$GRANDCHILD_MANDATE_ID" \
--rpc-url "$RPC"
```
The result is `[1, 2, 3]`.
## Revoking the root [#revoking-the-root]
When the root Mandate is revoked, both the child and grandchild become inactive:
```sh
cast send "$GRANTLINE" \
"revokeMandate(uint256)()" \
"$ROOT_MANDATE_ID" \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
Check that the child's lineage is no longer active:
```sh
cast call "$GRANTLINE" \
"isLineageActive(uint256)(bool)" \
"$CHILD_MANDATE_ID" \
--rpc-url "$RPC"
```
This returns `false`. A proposal from the child or grandchild now returns `DENY` with `MANDATE_INACTIVE`.
## Updating child rules [#updating-child-rules]
The controller can tighten a child's rules:
```sh
cast send "$GRANTLINE" \
"updateMandate(uint256,(bool,uint256,uint256,bool,uint256,uint256,bool),(uint256,bool,uint256,bool),uint64,uint64)()" \
"$CHILD_MANDATE_ID" \
"(false, 0, 3000000000000000000, false, 0, 0, false)" \
"(0, false, 0, false)" \
0 \
0 \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
The new maximum is 3 OKB. `canDelegate` is set to `false`, so the child can no longer create descendants (though its existing children remain as records).
Attempting to broaden the rules (e.g. setting a higher maximum than the parent) fails with `ChildRulesExceedParent`.
See [Delegation](/docs/concepts/delegation) for the product model and [Mandate rules](/docs/reference/mandate-rules) for the exact inheritance semantics.
# Escalated execution (https://grantline-zeta.vercel.app/docs/guides/escalated-execution)
Submit, approve, and execute a plan through the ESCALATE path.
This guide walks through the full escalation lifecycle: creating a plan that triggers `ESCALATE`, submitting it for controller review, approving or denying it, and executing the approved plan.
## Load the deployment values [#load-the-deployment-values]
```sh
cd contracts
set -a
source .env
set +a
MANIFEST="$DEPLOYMENT_MANIFEST_PATH"
GRANTLINE="$(jq -r '.grantline.proxy' "$MANIFEST")"
REGISTRY="$(jq -r '.modules.registry.proxy' "$MANIFEST")"
MANAGER="$(jq -r '.modules.escalationManager.proxy' "$MANIFEST")"
RPC="$XLAYER_TESTNET_RPC_URL"
AGENT="$AGENT_ADDRESS"
```
## Create an escalatable plan [#create-an-escalatable-plan]
A plan triggers `ESCALATE` when it crosses a boundary with escalation enabled. For example, a plan that exceeds `maxNativeAmount` with `escalateNativeAmount` set to `true`:
```json
{
"mandateId": "1",
"agent": "0xAgent",
"nonce": "3",
"deadline": "0",
"actions": [
{
"actionType": "TRANSFER",
"version": 1,
"asset": "native",
"to": "0xRecipient",
"amount": "15000000000000000000"
}
]
}
```
The amount is 15 OKB, which exceeds the 10 OKB maximum. Because `escalateNativeAmount` is `true`, the evaluator returns `ESCALATE` instead of `DENY`.
### Sign and evaluate [#sign-and-evaluate]
Sign the plan using the EIP-712 flow from [Normal execution](/docs/guides/normal-execution). Then evaluate:
```sh
cast call "$GRANTLINE" \
"evaluate((uint256,address,uint256,uint256,(uint8,uint8,bytes)[]),bytes)((uint8,uint8,uint256,uint256,uint256,uint256,uint256))" \
"($MANDATE_ID, $AGENT, 3, 0, ((0, 1, $TRANSFER_PARAMS)))" \
"$SIGNATURE" \
--rpc-url "$RPC"
```
The result should show `decision: 1` (`ESCALATE`) and `failureCode: 19` (`NATIVE_AMOUNT_ABOVE_MAXIMUM`).
## Submit the escalation [#submit-the-escalation]
Anyone can submit after the evaluator returns `ESCALATE`. The Grantline facade computes the digest and routes to the escalation manager:
```sh
cast send "$GRANTLINE" \
"submitEscalation((uint256,address,uint256,uint256,(uint8,uint8,bytes)[]),bytes)(bytes32)" \
"($MANDATE_ID, $AGENT, 3, 0, ((0, 1, $TRANSFER_PARAMS)))" \
"$SIGNATURE" \
--rpc-url "$RPC" \
--private-key "$AGENT_PRIVATE_KEY"
```
The return value is the action digest. Read the `EscalationSubmitted` event to confirm:
```sh
cast logs --rpc-url "$RPC" --address "$MANAGER" \
'EscalationSubmitted(bytes32,uint256,address,address,uint256,uint256,uint256,uint256,uint64)' \
--from-block 0
```
## Check escalation state [#check-escalation-state]
```sh
DIGEST=0x...
cast call "$GRANTLINE" \
"escalationStatus(bytes32)(uint8)" \
"$DIGEST" \
--rpc-url "$RPC"
cast call "$GRANTLINE" \
"getEscalation(bytes32)((uint256,address,uint256,uint256,(uint8,uint8,bytes)[]),bytes,address,uint8,uint64)" \
"$DIGEST" \
--rpc-url "$RPC"
```
Status `1` is `PENDING`. The stored plan and signature are the evidence for what the controller will review.
## Controller approves [#controller-approves]
Only the Vault controller can approve. The Grantline facade verifies the controller before routing to the manager:
```sh
cast send "$GRANTLINE" \
"approveEscalation(bytes32)()" \
"$DIGEST" \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
Read the `EscalationApproved` event:
```sh
cast logs --rpc-url "$RPC" --address "$MANAGER" \
'EscalationApproved(bytes32,uint256,address,uint64)' \
--from-block 0
```
The status is now `2` (`APPROVED`).
## Execute the approved escalation [#execute-the-approved-escalation]
The executor loads the stored plan, evaluates it again against current state, and executes if the current result is not `DENY`:
```sh
cast send "$GRANTLINE" \
"executeEscalated(bytes32)(bytes32)" \
"$DIGEST" \
--rpc-url "$RPC" \
--private-key "$AGENT_PRIVATE_KEY"
```
The executor recomputes the digest, confirms it matches the reserved value, consumes the reserved nonce, executes the actions, and marks the escalation `EXECUTED`.
## Controller denies [#controller-denies]
Denial closes the operational record without authorising execution:
```sh
cast send "$GRANTLINE" \
"denyEscalation(bytes32)()" \
"$DIGEST" \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
The status becomes `3` (`DENIED`). The reserved nonce is not released. The slot cannot be reused through the ordinary path.
## Key points [#key-points]
* Approval is tied to the exact stored plan. A later plan that reuses the nonce cannot inherit the approval.
* The executor re-evaluates at execution time. If the Mandate was revoked, paused, or its rules changed after approval, the current evaluation may return `DENY` and prevent execution.
* If the execution reverts (downstream failure), the escalation stays `APPROVED`. The controller can retry later.
* A denied escalation leaves the nonce reserved. The agent or controller can cancel it with `cancelNonce` to release the slot.
See [Escalation](/docs/enforcement/escalation) for the lifecycle mechanics and [Inspecting evidence](/docs/guides/inspecting-evidence) for reading escalation events.
# Inspecting evidence (https://grantline-zeta.vercel.app/docs/guides/inspecting-evidence)
Read receipts, events, and explorer data to understand what happened onchain.
Execution evidence has two parts: the transaction receipt tells you whether a submitted transaction committed, while events explain the state changes and successful execution that the transaction contained. A read-only `DENY` result has no transaction receipt unless a caller submits it and causes a revert.
## Load the current stack [#load-the-current-stack]
```sh
cd contracts
set -a
source .env
set +a
MANIFEST="$DEPLOYMENT_MANIFEST_PATH"
RPC="$XLAYER_TESTNET_RPC_URL"
EXPLORER="$XLAYER_TESTNET_EXPLORER_URL"
GRANTLINE="$(jq -r '.grantline.proxy' "$MANIFEST")"
VAULT="$VAULT_ADDRESS"
REGISTRY="$(jq -r '.modules.registry.proxy' "$MANIFEST")"
EXECUTOR="$(jq -r '.modules.executor.proxy' "$MANIFEST")"
MANAGER="$(jq -r '.modules.escalationManager.proxy' "$MANIFEST")"
```
Use the manifest values for every lookup. Set `VAULT` to the actual Vault address from a `VaultCreated` event or your own records; the manifest does not track individual Vault addresses.
## Scoped onchain reads [#scoped-onchain-reads]
The fresh contract architecture exposes direct indexes for the records a controller or agent needs to inspect. These views return addresses, Mandate IDs, or escalation digests; hydrate each result with `getVault`, `getMandate`, or `getEscalation` when the full record is needed.
Read the Vaults currently controlled by an address through Grantline:
```sh
CONTROLLER=0x...
cast call "$GRANTLINE" \
"controllerVaultCount(address)(uint256)" \
"$CONTROLLER" \
--rpc-url "$RPC"
cast call "$GRANTLINE" \
"controllerVaultAt(address,uint256)(address)" \
"$CONTROLLER" \
0 \
--rpc-url "$RPC"
```
Read Mandates through the registry by Vault, creator, or authorised agent:
```sh
cast call "$REGISTRY" "vaultMandateCount(address)(uint256)" "$VAULT" --rpc-url "$RPC"
cast call "$REGISTRY" "vaultMandateAt(address,uint256)(uint256)" "$VAULT" 0 --rpc-url "$RPC"
cast call "$REGISTRY" "creatorMandateCount(address)(uint256)" "$CONTROLLER" --rpc-url "$RPC"
cast call "$REGISTRY" "creatorMandateAt(address,uint256)(uint256)" "$CONTROLLER" 0 --rpc-url "$RPC"
cast call "$REGISTRY" "agentMandateCount(address)(uint256)" "$AGENT" --rpc-url "$RPC"
cast call "$REGISTRY" "agentMandateAt(address,uint256)(uint256)" "$AGENT" 0 --rpc-url "$RPC"
```
For Escalations, use the manager's global, Vault, or agent index and then load each digest:
```sh
cast call "$MANAGER" "escalationCount()(uint256)" --rpc-url "$RPC"
cast call "$MANAGER" "escalationAt(uint256)(bytes32)" 0 --rpc-url "$RPC"
cast call "$MANAGER" "vaultEscalationCount(address)(uint256)" "$VAULT" --rpc-url "$RPC"
cast call "$MANAGER" "vaultEscalationAt(address,uint256)(bytes32)" "$VAULT" 0 --rpc-url "$RPC"
cast call "$MANAGER" "agentEscalationCount(address)(uint256)" "$AGENT" --rpc-url "$RPC"
cast call "$MANAGER" "agentEscalationAt(address,uint256)(bytes32)" "$AGENT" 0 --rpc-url "$RPC"
DIGEST=0x...
cast call "$MANAGER" "getEscalation(bytes32)" "$DIGEST" --rpc-url "$RPC"
```
Escalation indexes retain records in their submitted history, including `PENDING`, `APPROVED`, `DENIED`, and `EXECUTED` statuses. The contract returns the stored order; an application can sort active records before finalized records for presentation. `submittedBy` remains part of each Escalation record, but it is not a separate index.
## Successful execution [#successful-execution]
Given a transaction hash from a successful normal execution:
```sh
TX_HASH=0x...
cast receipt "$TX_HASH" --rpc-url "$RPC"
cast tx "$TX_HASH" --rpc-url "$RPC"
```
Receipt status `1` means the complete transaction committed.
Read the `ActionPlanExecuted` event from the executor:
```sh
cast logs \
--rpc-url "$RPC" \
--address "$EXECUTOR" \
'ActionPlanExecuted(bytes32,uint256,address,address,uint256,uint256,uint256,uint256,uint256,uint256)' \
--from-block 0
```
Fields: `actionDigest`, `mandateId`, `agent`, `vault`, `nonce`, `nativeAmount`, `nativeUsdValue`, `actionCount`, `nativeBalanceAfter`, `nativeBalanceUsdValue`.
Inspect the Vault's low-level call evidence:
```sh
cast logs \
--rpc-url "$RPC" \
--address "$VAULT" \
'ExecutionAttempted(address,address,uint256,bytes32,bool,bytes32)' \
--from-block 0
```
`ExecutionAttempted` shows the authority, target, value, calldata hash, low-level success flag, and result hash for a Vault call. It does not by itself prove that the complete Action Plan succeeded; use the transaction status and executor event together.
## Escalation lifecycle [#escalation-lifecycle]
For an escalated digest, query the manager state and its events:
```sh
DIGEST=0x...
cast call "$MANAGER" \
"statusOf(bytes32)(uint8)" \
"$DIGEST" \
--rpc-url "$RPC"
cast call "$MANAGER" \
"getEscalation(bytes32)" \
"$DIGEST" \
--rpc-url "$RPC"
```
Status enum: `0` NONE, `1` PENDING, `2` APPROVED, `3` DENIED, `4` EXECUTED.
Read the escalation events:
```sh
cast logs \
--rpc-url "$RPC" \
--address "$MANAGER" \
'EscalationSubmitted(bytes32,uint256,address,address,uint256,uint256,uint256,uint256,uint64)' \
--from-block 0
cast logs --rpc-url "$RPC" --address "$MANAGER" \
'EscalationApproved(bytes32,uint256,address,uint64)' --from-block 0
cast logs --rpc-url "$RPC" --address "$MANAGER" \
'EscalationDenied(bytes32,uint256,address,uint64)' --from-block 0
cast logs --rpc-url "$RPC" --address "$MANAGER" \
'EscalationExecuted(bytes32,uint256,address,uint256,uint64)' --from-block 0
```
The stored plan and signature are the evidence for what the controller approved. Query the manager events for approval, denial, and execution timestamps, then inspect the executor receipt for the final capital movement.
## Revoked authority failure [#revoked-authority-failure]
A revoked Mandate or ancestor causes evaluation to return `DENY` with `MANDATE_INACTIVE`. If a caller submits that plan to the executor, the transaction reverts before the Vault call:
```sh
FAILED_TX=0x...
cast receipt "$FAILED_TX" --rpc-url "$RPC"
```
Receipt status `0` and revert data show that the submitted attempt failed. Events emitted by that reverted transaction do not persist, so the absence of an executor event is expected. The persistent evidence is the earlier `MandateRevoked` event and the stored Mandate status.
## Paused mandate [#paused-mandate]
A paused Mandate causes evaluation to return `DENY` with `MANDATE_PAUSED`. Read the `MandatePaused` event:
```sh
cast logs \
--rpc-url "$RPC" \
--address "$REGISTRY" \
'MandatePaused(uint256,address,uint64)' \
--from-block 0
```
## Cancelled nonce [#cancelled-nonce]
A cancelled nonce causes evaluation to return `DENY` with `NONCE_USED`. Read the `NonceCancelled` event:
```sh
cast logs \
--rpc-url "$RPC" \
--address "$GRANTLINE" \
'NonceCancelled(uint256,address,uint256,address,uint64)' \
--from-block 0
```
Verify the nonce state:
```sh
cast call "$GRANTLINE" \
"getNonceState(uint256,uint256)(bool,bytes32)" \
"$MANDATE_ID" "$NONCE" \
--rpc-url "$RPC"
```
The `used` field should be `true` for a cancelled nonce.
## Reverted transaction [#reverted-transaction]
A reverted receipt proves that a submitted transaction failed, but it does not create a persistent event stream for the failed path. Events emitted inside a reverted transaction are rolled back.
Use `cast tx` to inspect the revert data:
```sh
cast tx "$FAILED_TX" --rpc-url "$RPC"
```
Then check earlier committed events to understand the authority state that led to the failure.
## What the evidence proves [#what-the-evidence-proves]
* A successful `ActionPlanExecuted` event proves that the complete plan reached the executor's successful point in a committed transaction.
* An `EscalationApproved` event proves that the Vault controller approved the stored digest, not that it later executed successfully.
* A `MandateRevoked` event proves that an administrator revoked the Mandate at the recorded time; descendant lineage becomes inactive when evaluated.
* A `MandatePaused` event proves that an administrator paused the Mandate; execution is blocked until unpause.
* A `NonceCancelled` event proves that a specific nonce was permanently invalidated.
* A reverted receipt proves that a submitted transaction failed, but it does not create a persistent event stream for the failed path.
See [Events](/docs/reference/events) for event fields and [Transaction lifecycle](/docs/execution/transaction-lifecycle) for the relationship between decisions, calls, and receipts.
# Lifecycle management (https://grantline-zeta.vercel.app/docs/guides/lifecycle-management)
Pause, revoke, cancel nonces, update rules, and manage validity windows.
This guide covers the authority lifecycle tools: pausing Vaults and Mandates, revoking Mandates, cancelling nonces, updating rules, and managing validity windows.
## Load the deployment values [#load-the-deployment-values]
```sh
cd contracts
set -a
source .env
set +a
MANIFEST="$DEPLOYMENT_MANIFEST_PATH"
GRANTLINE="$(jq -r '.grantline.proxy' "$MANIFEST")"
REGISTRY="$(jq -r '.modules.registry.proxy' "$MANIFEST")"
RPC="$XLAYER_TESTNET_RPC_URL"
CONTROLLER="$DEPLOYER_ADDRESS"
VAULT=0x...
MANDATE_ID=1
```
## Pause a Vault [#pause-a-vault]
Pausing the Vault blocks autonomous agent execution, escalation submission, and new Mandate creation. Deposits, withdrawals, and Mandate administration still work.
```sh
cast send "$GRANTLINE" \
"pauseVault(address)()" \
"$VAULT" \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
Verify the pause state:
```sh
cast call "$GRANTLINE" \
"getVault(address)((address,address,address,address,address,uint64,uint256,bool))" \
"$VAULT" \
--rpc-url "$RPC"
```
The last field (`paused`) should be `true`.
An agent that tries to execute against a paused Vault receives `DENY` with `VAULT_PAUSED`.
### Unpause a Vault [#unpause-a-vault]
```sh
cast send "$GRANTLINE" \
"unpauseVault(address)()" \
"$VAULT" \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
## Pause a Mandate [#pause-a-mandate]
Pausing a Mandate blocks execution and escalation for that specific Mandate. Other Mandates on the same Vault are unaffected.
```sh
cast send "$GRANTLINE" \
"pauseMandate(uint256)()" \
"$MANDATE_ID" \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
Read the `MandatePaused` event:
```sh
cast logs --rpc-url "$RPC" --address "$GRANTLINE" \
'MandatePaused(uint256,address)' \
--from-block 0
```
An agent that tries to execute against a paused Mandate receives `DENY` with `MANDATE_PAUSED`.
### Unpause a Mandate [#unpause-a-mandate]
```sh
cast send "$GRANTLINE" \
"unpauseMandate(uint256)()" \
"$MANDATE_ID" \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
The Mandate returns to `ACTIVE` status. All previously valid authority is restored.
## Revoke a Mandate [#revoke-a-mandate]
Revocation is permanent. The Mandate cannot be resumed.
```sh
cast send "$GRANTLINE" \
"revokeMandate(uint256)()" \
"$MANDATE_ID" \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
Read the `MandateRevoked` event:
```sh
cast logs --rpc-url "$RPC" --address "$REGISTRY" \
'MandateRevoked(uint256,address,uint64)' \
--from-block 0
```
A proposal from a revoked Mandate or any descendant returns `DENY` with `MANDATE_INACTIVE`.
## Update Mandate rules [#update-mandate-rules]
The controller can change rules, Preflight rules, and the validity window at any time:
```sh
cast send "$GRANTLINE" \
"updateMandate(uint256,(bool,uint256,uint256,bool,uint256,uint256,bool),(uint256,bool,uint256,bool),uint64,uint64)()" \
"$MANDATE_ID" \
"(true, 0, 8000000000000000000, true, 0, 0, false)" \
"(1000000000000000000, true, 0, false)" \
0 \
0 \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
Updates take effect immediately. A plan signed before the update is evaluated against the current rules.
For a child Mandate, the new rules must be narrower than the parent's effective rules. Attempting to broaden fails with `ChildRulesExceedParent`.
## Cancel a nonce [#cancel-a-nonce]
The agent or Vault controller can permanently invalidate an unused, unreserved nonce:
```sh
cast send "$GRANTLINE" \
"cancelNonce(uint256,uint256)()" \
"$MANDATE_ID" \
1 \
--rpc-url "$RPC" \
--private-key "$AGENT_PRIVATE_KEY"
```
Read the `NonceCancelled` event:
```sh
cast logs --rpc-url "$RPC" --address "$GRANTLINE" \
'NonceCancelled(uint256,address,uint256,address,uint64)' \
--from-block 0
```
A cancelled nonce cannot be used for execution or reserved for an escalation. This is useful for invalidating a signed plan that was never submitted.
Cancellation works even during pause, outside validity windows, or after revocation. It is a recovery-only restriction on a specific execution slot.
## Validity windows [#validity-windows]
### Create a time-bounded Mandate [#create-a-time-bounded-mandate]
```sh
cast send "$GRANTLINE" \
"createMandate(address,address,(bool,uint256,uint256,bool,uint256,uint256,bool),(uint256,bool,uint256,bool),uint64,uint64)(uint256)" \
"$VAULT" \
"$AGENT" \
"(false, 0, 5000000000000000000, true, 0, 0, false)" \
"(0, false, 0, false)" \
1700000000 \
1700010000 \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
The Mandate is valid between timestamps `1700000000` and `1700010000`. Outside this window, the evaluator returns `DENY` with `MANDATE_NOT_YET_VALID` or `MANDATE_EXPIRED`.
### Check effective validity window [#check-effective-validity-window]
```sh
cast call "$GRANTLINE" \
"getEffectiveValidityWindow(uint256)(uint64,uint64)" \
"$MANDATE_ID" \
--rpc-url "$RPC"
```
For a child Mandate, the effective window is the intersection of the child and all ancestor windows.
### Update the window [#update-the-window]
```sh
cast send "$GRANTLINE" \
"updateMandate(uint256,(bool,uint256,uint256,bool,uint256,uint256,bool),(uint256,bool,uint256,bool),uint64,uint64)()" \
"$MANDATE_ID" \
"(false, 0, 5000000000000000000, true, 0, 0, false)" \
"(0, false, 0, false)" \
0 \
1700020000 \
--rpc-url "$RPC" \
--private-key "$CONTROLLER_PRIVATE_KEY"
```
The window is now open-ended on the start side and expires at `1700020000`.
See [Mandates](/docs/concepts/mandates) for the lifecycle model and [Mandate rules](/docs/reference/mandate-rules) for the exact field semantics.
# Normal execution (https://grantline-zeta.vercel.app/docs/guides/normal-execution)
Sign, evaluate, and execute a TRANSFER or SWAP Action Plan through the ALLOW path.
This guide covers signing an Action Plan, evaluating it read-only, and executing it through the normal `ALLOW` path. It shows both `TRANSFER` and `SWAP` examples.
## Load the deployment values [#load-the-deployment-values]
```sh
cd contracts
set -a
source .env
set +a
MANIFEST="$DEPLOYMENT_MANIFEST_PATH"
GRANTLINE="$(jq -r '.grantline.proxy' "$MANIFEST")"
EVALUATOR="$(jq -r '.modules.evaluator.proxy' "$MANIFEST")"
REGISTRY="$(jq -r '.modules.registry.proxy' "$MANIFEST")"
RPC="$XLAYER_TESTNET_RPC_URL"
AGENT="$AGENT_ADDRESS"
```
## TRANSFER example [#transfer-example]
### Human-readable plan [#human-readable-plan]
```json
{
"mandateId": "1",
"agent": "0xAgent",
"nonce": "1",
"deadline": "0",
"actions": [
{
"actionType": "TRANSFER",
"version": 1,
"asset": "native",
"to": "0xRecipient",
"amount": "1000000000000000000"
}
]
}
```
Native `asset` is encoded as the zero address. The amount is `1 OKB` in raw base units.
### Encode the action parameters [#encode-the-action-parameters]
```ts
import { encodeAbiParameters, parseEther, zeroAddress } from "viem";
const asset = zeroAddress;
const recipient = "0xRecipient" as `0x${string}`;
const parameters = encodeAbiParameters(
[{ type: "address" }, { type: "address" }, { type: "uint256" }],
[asset, recipient, parseEther("1")],
);
const actionPlan = {
mandateId: 1n,
agent: agentAddress,
nonce: 1n,
deadline: 0n,
actions: [
{
actionType: 0,
version: 1,
parameters,
},
],
} as const;
```
For a token transfer, set `asset` to the token contract address and use the token's raw base units for `amount`.
### Sign the EIP-712 message [#sign-the-eip-712-message]
```ts
const domain = {
name: "Grantline",
version: "1",
chainId: 1952,
verifyingContract: evaluatorAddress,
} as const;
const types = {
Action: [
{ name: "actionType", type: "uint8" },
{ name: "version", type: "uint8" },
{ name: "parameters", type: "bytes" },
],
ActionPlan: [
{ name: "mandateId", type: "uint256" },
{ name: "agent", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "actions", type: "Action[]" },
],
} as const;
const signature = await walletClient.signTypedData({
account: agentAddress,
domain,
types,
primaryType: "ActionPlan",
message: actionPlan,
});
```
### Evaluate before submitting [#evaluate-before-submitting]
Use `cast` to evaluate the plan read-only:
```sh
cast call "$GRANTLINE" \
"evaluate((uint256,address,uint256,uint256,(uint8,uint8,bytes)[]),bytes)((uint8,uint8,uint256,uint256,uint256,uint256,uint256))" \
"($MANDATE_ID, $AGENT, 1, 0, ((0, 1, $TRANSFER_PARAMS)))" \
"$SIGNATURE" \
--rpc-url "$RPC"
```
The result fields are: `decision`, `failureCode`, `failedActionIndex`, `nativeAmount`, `nativeUsdValue`, `nativeBalanceAfter`, `nativeBalanceUsdValue`.
An `ALLOW` result (decision `0`) means the plan can be executed.
### Execute [#execute]
Submit the transaction through the Grantline facade:
```sh
cast send "$GRANTLINE" \
"execute((uint256,address,uint256,uint256,(uint8,uint8,bytes)[]),bytes)(bytes32)" \
"($MANDATE_ID, $AGENT, 1, 0, ((0, 1, $TRANSFER_PARAMS)))" \
"$SIGNATURE" \
--rpc-url "$RPC" \
--private-key "$AGENT_PRIVATE_KEY"
```
### Verify [#verify]
Read the `ActionPlanExecuted` event:
```sh
cast logs --rpc-url "$RPC" --address "$GRANTLINE" \
'ActionPlanExecuted(bytes32,uint256,address,address,uint256)' \
--from-block 0
```
Check that the nonce is now consumed:
```sh
cast call "$GRANTLINE" \
"getNonceState(uint256,uint256)(bool,bytes32)" \
"$MANDATE_ID" 1 \
--rpc-url "$RPC"
```
The first field (`used`) should be `true`. The second field (`reservation`) should be the zero hash.
## SWAP example [#swap-example]
### Human-readable plan [#human-readable-plan-1]
```json
{
"mandateId": "1",
"agent": "0xAgent",
"nonce": "2",
"deadline": "1700000000",
"actions": [
{
"actionType": "SWAP",
"version": 1,
"swapAdapterId": "UNISWAP_V3",
"tokenIn": "0xTokenIn",
"amountIn": "1000000000000000000",
"tokenOut": "0xTokenOut",
"minAmountOut": "950000000000000000",
"hops": [
{
"pool": "0xPoolAddress",
"tokenIn": "0xTokenIn",
"tokenOut": "0xTokenOut"
}
]
}
]
}
```
### Encode SWAP parameters [#encode-swap-parameters]
```ts
const swapParameters = encodeAbiParameters(
[
{ type: "uint8" }, // swapAdapterId
{ type: "address" }, // tokenIn
{ type: "uint256" }, // amountIn
{ type: "address" }, // tokenOut
{ type: "uint256" }, // minAmountOut
{
type: "tuple[]",
components: [
{ name: "pool", type: "address" },
{ name: "tokenIn", type: "address" },
{ name: "tokenOut", type: "address" },
],
},
],
[
1, // SwapAdapterId.UNISWAP_V3
tokenInAddress,
parseEther("1"),
tokenOutAddress,
parseEther("0.95"),
[{ pool: poolAddress, tokenIn: tokenInAddress, tokenOut: tokenOutAddress }],
],
);
```
The plan uses `actionType: 1` (SWAP) and `version: 1`.
### Evaluate and execute [#evaluate-and-execute]
The evaluate and execute calls follow the same pattern as `TRANSFER`. The evaluator validates the `SWAP` route through the configured adapter before returning `ALLOW`. If the adapter is not configured or the route is invalid, the evaluation returns `DENY` with `SWAP_UNSUPPORTED` or `INVALID_SWAP_ROUTE`.
```sh
cast send "$GRANTLINE" \
"execute((uint256,address,uint256,uint256,(uint8,uint8,bytes)[]),bytes)(bytes32)" \
"($MANDATE_ID, $AGENT, 2, 0, ((1, 1, $SWAP_PARAMS)))" \
"$SIGNATURE" \
--rpc-url "$RPC" \
--private-key "$AGENT_PRIVATE_KEY"
```
## Failed execution [#failed-execution]
If evaluation returns `DENY`, the executor reverts before calling the Vault. A submitted transaction reverts with `EvaluationDenied` and leaves no committed events.
If evaluation returns `ALLOW` but a downstream action fails (recipient rejects, token returns false, `SWAP` output below minimum), the complete transaction reverts. The nonce is not consumed. The receipt status is `0` with revert data.
See [Decisions and failures](/docs/reference/decisions-and-failures) for the full list of failure codes.
# Vault and Mandate setup (https://grantline-zeta.vercel.app/docs/guides/vault-and-mandate-setup)
Create a Vault, fund it with native asset or tokens, and create a Mandate with configured authority.
This guide walks through the full setup flow: creating a Vault, funding it, and creating a Mandate that binds an agent to the Vault's capital. It uses the deployment manifest and Foundry's `cast` commands.
## Load the deployment values [#load-the-deployment-values]
Run this from `contracts/` after copying `.env.example` to `.env` and setting the public RPC and manifest values. The commands below read only public values; keep private keys out of this workflow.
```sh
cd contracts
set -a
source .env
set +a
MANIFEST="$DEPLOYMENT_MANIFEST_PATH"
GRANTLINE="$(jq -r '.grantline.proxy' "$MANIFEST")"
REGISTRY="$(jq -r '.modules.registry.proxy' "$MANIFEST")"
RPC="$XLAYER_TESTNET_RPC_URL"
CONTROLLER="$DEPLOYER_ADDRESS"
```
The controller is the address that will own the Vault and administer its Mandates.
## Create a Vault [#create-a-vault]
Call `createVault` through the Grantline facade. The caller becomes the Vault controller.
```sh
cast send "$GRANTLINE" \
"createVault()(address)" \
--rpc-url "$RPC" \
--private-key "$DEPLOYER_PRIVATE_KEY"
```
Read the `VaultCreated` event from the transaction receipt to get the Vault address:
```sh
TX_HASH=0x...
cast logs --rpc-url "$RPC" --address "$GRANTLINE" \
'VaultCreated(address,address,address,address,address,uint64)' \
--from-block 0 | grep "$TX_HASH"
```
The event fields are: `vault`, `controller`, `owner`, `authority`, `implementation`, `version`.
Verify the Vault with the `getVault` view:
```sh
cast call "$GRANTLINE" \
"getVault(address)((address,address,address,address,address,uint64,uint256,bool))" \
"$VAULT" \
--rpc-url "$RPC"
```
This returns the controller, owner (should be Grantline), authority (should be the executor), implementation, version, native balance, and pause state.
## Fund the Vault with native asset [#fund-the-vault-with-native-asset]
Deposit native OKB through the facade:
```sh
cast send "$GRANTLINE" \
"depositNative(address)()" \
"$VAULT" \
--value 1ether \
--rpc-url "$RPC" \
--private-key "$DEPLOYER_PRIVATE_KEY"
```
Verify the balance:
```sh
cast balance "$VAULT" --rpc-url "$RPC"
```
## Fund the Vault with ERC-20 tokens [#fund-the-vault-with-erc-20-tokens]
First approve the Vault to spend tokens from the controller's wallet, then deposit:
```sh
TOKEN=0x...
cast send "$TOKEN" \
"approve(address,uint256)(bool)" \
"$GRANTLINE" \
1000000000000000000 \
--rpc-url "$RPC" \
--private-key "$DEPLOYER_PRIVATE_KEY"
cast send "$GRANTLINE" \
"depositToken(address,address,uint256)()" \
"$VAULT" \
"$TOKEN" \
1000000000000000000 \
--rpc-url "$RPC" \
--private-key "$DEPLOYER_PRIVATE_KEY"
```
Check the token balance:
```sh
cast call "$VAULT" "tokenBalance(address)(uint256)" "$TOKEN" --rpc-url "$RPC"
```
## Create a Mandate [#create-a-mandate]
Define the rules for the agent. The example below sets a maximum native amount of 10 OKB, with escalation enabled, and no native-USD limits.
```sh
AGENT=0x...
cast send "$GRANTLINE" \
"createMandate(address,address,(bool,uint256,uint256,bool,uint256,uint256,bool),(uint256,bool,uint256,bool),uint64,uint64)(uint256)" \
"$VAULT" \
"$AGENT" \
"(false, 0, 10000000000000000000, true, 0, 0, false)" \
"(0, false, 0, false)" \
0 \
0 \
--rpc-url "$RPC" \
--private-key "$DEPLOYER_PRIVATE_KEY"
```
The arguments are:
1. `vault`: the Vault address
2. `agent`: the agent that will sign Action Plans
3. `rules`: `(canDelegate, minNativeAmount, maxNativeAmount, escalateNativeAmount, minNativeUsd, maxNativeUsd, escalateNativeUsd)`
4. `preflightRules`: `(minNativeBalance, escalateNativeBalance, minNativeUsdBalance, escalateNativeUsdBalance)`
5. `validAfter`: Unix timestamp (0 = no start constraint)
6. `validUntil`: Unix timestamp (0 = no end constraint)
Read the `MandateCreated` event to get the mandate ID:
```sh
cast logs --rpc-url "$RPC" --address "$REGISTRY" \
'MandateCreated(uint256,address,address,uint256,uint8,(bool,uint256,uint256,bool,uint256,uint256,bool),(uint256,bool,uint256,bool),uint64,uint64,address,uint64)' \
--from-block 0
```
## Read the Mandate [#read-the-mandate]
```sh
MANDATE_ID=1
cast call "$GRANTLINE" \
"getMandate(uint256)((uint256,address,address,address,address,uint256,uint8,uint8,(bool,uint256,uint256,bool,uint256,uint256,bool),(uint256,bool,uint256,bool),uint64,uint64,uint64,uint64))" \
"$MANDATE_ID" \
--rpc-url "$RPC"
```
The returned MandateView includes the controller, Vault, agent, creator, parent ID, delegation depth, status, rules, Preflight rules, validAfter, validUntil, createdAt, and revokedAt.
## Read records by scope [#read-records-by-scope]
The contracts expose direct onchain indexes for the records used by a controller or agent. These reads avoid scanning every Vault or Mandate and return addresses or IDs that can be hydrated with the full views above.
Read the Vaults currently controlled by an address through the Grantline facade:
```sh
CONTROLLER=0x...
cast call "$GRANTLINE" \
"controllerVaultCount(address)(uint256)" \
"$CONTROLLER" \
--rpc-url "$RPC"
cast call "$GRANTLINE" \
"controllerVaultAt(address,uint256)(address)" \
"$CONTROLLER" \
0 \
--rpc-url "$RPC"
```
Read Mandates attached to a Vault, created by an address, or assigned to an agent through the registry:
```sh
cast call "$REGISTRY" "vaultMandateCount(address)(uint256)" "$VAULT" --rpc-url "$RPC"
cast call "$REGISTRY" "vaultMandateAt(address,uint256)(uint256)" "$VAULT" 0 --rpc-url "$RPC"
cast call "$REGISTRY" "creatorMandateCount(address)(uint256)" "$CONTROLLER" --rpc-url "$RPC"
cast call "$REGISTRY" "creatorMandateAt(address,uint256)(uint256)" "$CONTROLLER" 0 --rpc-url "$RPC"
cast call "$REGISTRY" "agentMandateCount(address)(uint256)" "$AGENT" --rpc-url "$RPC"
cast call "$REGISTRY" "agentMandateAt(address,uint256)(uint256)" "$AGENT" 0 --rpc-url "$RPC"
```
For a root Mandate, `creator` is the controller that created it. For a delegated Mandate, `creator` is the parent agent that created the child, while the agent index identifies the account authorised to sign Action Plans.
## Check effective authority [#check-effective-authority]
Read the active lineage and effective rules:
```sh
cast call "$GRANTLINE" \
"getLineage(uint256)(uint256[])" \
"$MANDATE_ID" \
--rpc-url "$RPC"
cast call "$GRANTLINE" \
"getEffectiveRules(uint256)((bool,uint256,uint256,bool,uint256,uint256,bool))" \
"$MANDATE_ID" \
--rpc-url "$RPC"
cast call "$GRANTLINE" \
"getEffectivePreflightRules(uint256)((uint256,bool,uint256,bool))" \
"$MANDATE_ID" \
--rpc-url "$RPC"
cast call "$GRANTLINE" \
"getEffectiveValidityWindow(uint256)(uint64,uint64)" \
"$MANDATE_ID" \
--rpc-url "$RPC"
```
For a root Mandate, the effective rules are the same as the stored rules. For a child Mandate, the effective rules are the intersection with the parent's boundaries.
## Check Vault context [#check-vault-context]
Read the Vault owner, authority, and balance to confirm the setup:
```sh
cast call "$VAULT" "owner()(address)" --rpc-url "$RPC"
cast call "$VAULT" "authority()(address)" --rpc-url "$RPC"
cast balance "$VAULT" --rpc-url "$RPC"
```
The Vault is now ready for an agent to sign and execute Action Plans against it.
See [Normal execution](/docs/guides/normal-execution) for the next step.
# Action Plan (https://grantline-zeta.vercel.app/docs/reference/action-plan)
The current ActionPlan, TRANSFER, SWAP, parameter encoding, and EIP-712 signing shape.
The current contracts accept an ordered `ActionPlan` containing one or more typed actions. Two action types are supported: `TRANSFER` version `1` and `SWAP` version `1`. This page describes the contract interface; use [Normal execution](/docs/guides/normal-execution) for integration examples.
## Solidity types [#solidity-types]
The current definitions are:
```solidity
library ActionTypes {
enum ActionType {
TRANSFER,
SWAP
}
enum SwapAdapterId {
NONE,
UNISWAP_V3
}
struct ActionPlan {
uint256 mandateId;
address agent;
uint256 nonce;
uint256 deadline;
Action[] actions;
}
struct Action {
ActionType actionType;
uint8 version;
bytes parameters;
}
struct TransferParameters {
address asset;
address recipient;
uint256 amount;
}
struct SwapParameters {
SwapAdapterId swapAdapterId;
address tokenIn;
uint256 amountIn;
address tokenOut;
uint256 minAmountOut;
SwapHop[] hops;
}
struct SwapHop {
address pool;
address tokenIn;
address tokenOut;
}
}
```
`ActionType.TRANSFER` has enum value `0` and `TRANSFER_VERSION` is `1`. `ActionType.SWAP` has enum value `1` and `SWAP_VERSION` is `1`. The `actions` array must contain at least one action. The evaluator checks each action's type, version, and parameter bytes before it applies the Mandate rules.
## Transfer parameters [#transfer-parameters]
`TransferParameters` is ABI-encoded with `abi.encode`, in this order:
```text
(address asset, address recipient, uint256 amount)
```
The resulting `parameters` value is exactly 96 bytes for the current action. A zero `asset` means the Vault's native asset. A non-zero `asset` is treated as a token contract and is called through the Vault. `recipient` and `amount` must both be non-zero, and token amounts use the token's own raw base units.
A readable representation of a native transfer can look like this before ABI encoding:
```json
{
"mandateId": "1",
"agent": "0xAgent",
"nonce": "1",
"deadline": "0",
"actions": [
{
"actionType": "TRANSFER",
"version": 1,
"parameters": {
"asset": "0x0000000000000000000000000000000000000000",
"recipient": "0xRecipient",
"amount": "1000000000000000000"
}
}
]
}
```
The JSON is explanatory only. The signed Solidity value contains `actionType: 0` and the ABI-encoded bytes, not the readable nested object. `deadline: 0` disables expiry; otherwise the evaluator rejects the plan after the Unix timestamp in `deadline`.
## Swap parameters [#swap-parameters]
`SwapParameters` is ABI-encoded with `abi.encode`. The evaluator validates the route through the configured adapter before accepting the action.
A readable representation of a swap can look like this before ABI encoding:
```json
{
"mandateId": "1",
"agent": "0xAgent",
"nonce": "2",
"deadline": "1700000000",
"actions": [
{
"actionType": "SWAP",
"version": 1,
"parameters": {
"swapAdapterId": "UNISWAP_V3",
"tokenIn": "0xTokenIn",
"amountIn": "1000000000000000000",
"tokenOut": "0xTokenOut",
"minAmountOut": "950000000000000000",
"hops": [
{
"pool": "0xPoolAddress",
"tokenIn": "0xTokenIn",
"tokenOut": "0xTokenOut"
}
]
}
}
]
}
```
The JSON is explanatory only. `swapAdapterId` maps to the configured adapter through the Grantline facade. `deadline` on the `ActionPlan` must be non-zero for plans containing `SWAP` actions; the evaluator rejects a `SWAP` plan with `deadline: 0` as `INVALID_SWAP_PARAMETERS`. `minAmountOut` sets the output floor; the executor reverts if the actual output is below it. Each hop describes one pool in the route, and hops chain together through consecutive `tokenOut`/`tokenIn` addresses.
The evaluator checks that the adapter is configured, calls `validateSwap` on the adapter, and accumulates the input amount for native-USD valuation. Route validation is adapter-specific: the Uniswap V3 adapter checks factory ownership, pool fees, token continuity, and deadline.
## EIP-712 domain and types [#eip-712-domain-and-types]
The signature domain is:
```text
name: Grantline
version: 1
chainId:
verifyingContract: MandateEvaluator address from the deployment manifest
```
The domain type is:
```text
EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
```
The primary types are:
```text
Action(uint8 actionType,uint8 version,bytes parameters)
SwapHop(address pool,address tokenIn,address tokenOut)
ActionPlan(uint256 mandateId,address agent,uint256 nonce,uint256 deadline,Action[] actions)
```
The evaluator hashes each action's parameter bytes, hashes the ordered action hashes as the `actions` field, hashes the complete `ActionPlan`, and combines that struct hash with the domain separator using the EIP-712 `\x19\x01` prefix. Any change to the `mandateId`, agent, nonce, deadline, action order, version, or parameters changes the digest. Changes to the Mandate's current rules are read during evaluation and do not alter the signed digest.
The signature must be 65 bytes, use recovery identifier `27` or `28`, use a low-`s` value, and recover to the agent stored on the Mandate. The verifying contract is the deployed `MandateEvaluator`, not the `VaultExecutor` or `EscalationManager`.
## Current action surface [#current-action-surface]
`TRANSFER` version `1` and `SWAP` version `1` are the current action surface. The executor runs actions in array order: native transfers, token transfers, or swaps. A failed action reverts the complete transaction. Other action types are not part of this contract version. A public SDK and API will provide higher-level integration surfaces when they are available.
See [Normal execution](/docs/guides/normal-execution) for encoding and signing examples.
# Decisions and failures (https://grantline-zeta.vercel.app/docs/reference/decisions-and-failures)
The evaluator's current decision values, failure codes, result fields, and transaction boundary.
`MandateEvaluator.evaluate` returns a structured result. The decision describes what the authority layer permits next. It does not describe whether a transaction was mined or whether a downstream call succeeded; see [Transaction lifecycle](/docs/execution/transaction-lifecycle) for that distinction.
## Decision values [#decision-values]
| Value | Meaning |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ALLOW` | The plan satisfies the active Mandate, signature, action, amount, and Preflight checks, so `VaultExecutor.execute` may continue. |
| `ESCALATE` | A configured amount or Preflight boundary was crossed and the relevant escalation flag is enabled, so the plan may be submitted to `EscalationManager` for owner approval. |
| `DENY` | The plan is not authorised, or the relevant boundary was crossed without permission to escalate. The executor rejects it before moving capital. |
An `ESCALATE` result does not approve the plan. Submission stores the complete plan and signature, and the Vault owner must approve the stored digest before `executeEscalated` can proceed. The executor evaluates the stored plan again at execution time.
## Result fields [#result-fields]
```solidity
struct EvaluationResult {
uint8 decision;
uint8 failureCode;
uint256 failedActionIndex;
uint256 nativeAmount;
uint256 nativeUsdValue;
uint256 nativeBalanceAfter;
uint256 nativeBalanceUsdValue;
}
```
`failedActionIndex` is the zero-based action index for an action-specific validation failure. `type(uint256).max` means that no individual action is identified, which is the normal value for a successful result, rule-level failures, and plan-level failures. `nativeAmount` is the aggregate native outflow in raw base units. `nativeUsdValue` is the USD valuation of that outflow when a Chainlink feed is available. `nativeBalanceAfter` is the projected Vault native balance after execution. `nativeBalanceUsdValue` is the USD valuation of the projected remaining balance.
## Plan and authority failures [#plan-and-authority-failures]
These failures happen before amount rules are applied and always return `DENY`:
| Failure code | Meaning |
| ----------------------- | -------------------------------------------------------------------- |
| `MANDATE_NOT_FOUND` | `mandateId` is zero or outside the registry's current Mandate count. |
| `MANDATE_INACTIVE` | The Mandate or one of its ancestors is revoked. |
| `MANDATE_NOT_YET_VALID` | Current block timestamp is before the effective `validAfter`. |
| `MANDATE_EXPIRED` | Current block timestamp is after the effective `validUntil`. |
| `MANDATE_PAUSED` | The Mandate or one of its ancestors is paused. |
| `VAULT_PAUSED` | The Vault referenced by the Mandate is paused. |
| `AGENT_MISMATCH` | The plan agent differs from the agent stored on the Mandate. |
| `EXPIRED` | `deadline` is non-zero and earlier than the current block timestamp. |
| `EMPTY_PLAN` | The plan contains no actions. |
## Signature and nonce failures [#signature-and-nonce-failures]
| Failure code | Meaning |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `INVALID_SIGNATURE` | The EIP-712 signature is malformed, does not recover to the Mandate agent, uses an invalid recovery value, or uses a high-`s` value. |
| `NONCE_USED` | The nonce for this (Mandate, agent) pair was already consumed by a previous execution or cancelled. |
| `NONCE_RESERVED` | The nonce is reserved for an escalation digest. A normal execution cannot use it. |
The evaluator binds the digest to the current chain and evaluator address, then requires the recovered signer to match the Mandate agent. `NONE` means that no failure was found; the evaluator returns it with `ALLOW`.
## Action validity [#action-validity]
These failures identify the first invalid action where applicable:
| Failure code | Meaning |
| --------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `INVALID_ACTION` | The action version is not the current version for its type, or its parameter bytes are empty. |
| `INVALID_ACTION_PARAMETERS` | The current transfer parameter bytes are not exactly the 96-byte ABI encoding expected by `TransferParameters`. |
| `INVALID_RECIPIENT` | The decoded transfer recipient is the zero address. |
| `INVALID_AMOUNT` | The decoded transfer amount is zero. |
## Amount bounds [#amount-bounds]
These failures compare aggregate plan amounts with effective Mandate rules:
| Failure code | Meaning |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AMOUNT_OVERFLOW` | Aggregate native amount would overflow `uint256`. |
| `NATIVE_AMOUNT_BELOW_MINIMUM` | Aggregate native amount is below a configured non-zero minimum. The result becomes `ESCALATE` only when effective native escalation remains enabled. |
| `NATIVE_AMOUNT_ABOVE_MAXIMUM` | Aggregate native amount exceeds a configured non-zero maximum. The result becomes `ESCALATE` only when effective native escalation remains enabled. |
| `NATIVE_USD_VALUE_BELOW_MINIMUM` | Aggregate native-asset USD valuation is below a configured non-zero minimum. The result becomes `ESCALATE` only when effective native-USD escalation remains enabled. |
| `NATIVE_USD_VALUE_ABOVE_MAXIMUM` | Aggregate native-asset USD valuation exceeds a configured non-zero maximum. The result becomes `ESCALATE` only when effective native-USD escalation remains enabled. |
## Valuation [#valuation]
| Failure code | Meaning |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `NATIVE_USD_VALUATION_UNAVAILABLE` | A native-USD limit is enabled, a required Chainlink quote is unavailable, and the evaluator cannot complete valuation. |
## Preflight [#preflight]
| Failure code | Meaning |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PREFLIGHT_NATIVE_BALANCE_BELOW_MINIMUM` | The projected Vault native balance after aggregate native outflow is below the effective Preflight floor. The result becomes `ESCALATE` only when effective Preflight escalation remains enabled. |
| `PREFLIGHT_NATIVE_USD_BALANCE_BELOW_MINIMUM` | The USD value of projected Vault native balance after execution is below the effective native-USD Preflight floor. The result becomes `ESCALATE` only when effective escalation remains enabled. |
## SWAP failures [#swap-failures]
| Failure code | Meaning |
| ------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `SWAP_UNSUPPORTED` | No configured adapter exists for the requested `SwapAdapterId`. |
| `INVALID_SWAP_PARAMETERS` | Decoded swap parameters fail validation (zero amounts, empty hops). |
| `INVALID_SWAP_ROUTE` | The adapter's `validateSwap` call rejected the route (wrong factory, bad pool, token mismatch, fee issues). |
Native amount rules apply to the aggregate native outflow of both `TRANSFER` and `SWAP` actions. `SWAP` actions contribute their input amount to the native total when the input is native or wrapped native.
## Transaction boundary [#transaction-boundary]
Evaluation is a read-only call and does not create a durable event. `VaultExecutor.execute` accepts only `ALLOW`; `DENY` and `ESCALATE` cause the executor call to revert. `EscalationManager.submit` accepts only `ESCALATE`, while approval and execution are separate transactions.
If a submitted executor transaction reverts, events emitted during that transaction do not persist. A reverted receipt proves that a transaction attempt failed, but it is not a persisted authorisation record. Persistent records come from committed events such as `MandateRevoked`, `EscalationSubmitted`, or `ActionPlanExecuted`.
See [Transaction lifecycle](/docs/execution/transaction-lifecycle) for the contract path and [Events](/docs/reference/events) for the durable event surface.
# Events (https://grantline-zeta.vercel.app/docs/reference/events)
The current registry, escalation, Vault, executor, and facade events and what each one proves.
Committed events are the current onchain evidence surface. An event can show that a state transition or execution step committed, but it cannot replace the transaction receipt: a reverted transaction leaves no events behind. See [Records](/docs/concepts/records) for how events fit into the broader traceability model.
## MandateRegistry [#mandateregistry]
| Event | Fields | What it records |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MandateCreated` | `mandateId` indexed, `vault` indexed, `agent` indexed, `parentMandateId`, `delegationDepth`, `MandateRules rules`, `PreflightRules preflightRules`, `validAfter`, `validUntil`, `createdBy`, `createdAt` | Creation of a Mandate and the rules, Preflight rules, and validity window written at creation time. |
| `MandateUpdated` | `mandateId` indexed, `MandateRules rules`, `PreflightRules preflightRules`, `validAfter`, `validUntil`, `updatedBy` indexed, `updatedAt` | A committed replacement of the stored rules, Preflight rules, and validity window. Effective lineage rules still apply when the evaluator reads them. |
| `MandateRevoked` | `mandateId` indexed, `revokedBy` indexed, `revokedAt` | Revocation of a Mandate. A revoked ancestor also makes descendant lineage inactive during evaluation. |
| `MandatePaused` | `mandateId` indexed, `pausedBy` indexed, `pausedAt` | Pausing of a Mandate. A paused ancestor blocks execution, escalation submission, and new child Mandate creation. |
| `MandateUnpaused` | `mandateId` indexed, `unpausedBy` indexed, `unpausedAt` | Unpausing of a previously paused Mandate. |
| `NonceReservationCreated` | `mandateId` indexed, `agent` indexed, `nonce` indexed, `digest` | Reservation of a nonce for one escalation digest. |
| `NonceReservationConsumed` | `mandateId` indexed, `agent` indexed, `nonce` indexed, `digest` | Consumption of an escalation reservation when the approved plan executes. |
The registry events describe authority and nonce state. `MandateUpdated` carries the rules and validity window supplied to that update, while `getEffectiveRules`, `getEffectivePreflightRules`, and `getEffectiveValidityWindow` are still needed to see the inherited result used by the evaluator.
## Grantline (facade) [#grantline-facade]
| Event | Fields | What it records |
| --------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `VaultCreated` | `vault` indexed, `controller` indexed, `owner` indexed, `authority`, `implementation`, `version` | A new Vault proxy was created and registered. |
| `VaultPaused` | `vault` indexed, `pausedBy` indexed | A Vault controller paused the Vault. Execution is blocked until unpause. |
| `VaultUnpaused` | `vault` indexed, `unpausedBy` indexed | A Vault controller unpaused the Vault. |
| `MandatePaused` | `mandateId` indexed, `pausedBy` indexed | A Mandate was paused through the facade. |
| `MandateUnpaused` | `mandateId` indexed, `unpausedBy` indexed | A Mandate was unpaused through the facade. |
| `NonceCancelled` | `mandateId` indexed, `agent` indexed, `nonce` indexed, `cancelledBy`, `cancelledAt` | An unused, unreserved nonce was permanently invalidated by the agent or Vault controller. |
| `ActionPlanSubmitted` | `actionDigest` indexed, `mandateId` indexed, `agent` indexed, `submittedBy` | A signed plan was submitted to the escalation manager for owner review. |
| `ActionPlanExecuted` | `actionDigest` indexed, `mandateId` indexed, `agent` indexed, `vault`, `nonce` | A plan completed executor-side action processing. The executor's `ActionPlanExecuted` event carries the full detail. |
## EscalationManager [#escalationmanager]
| Event | Fields | What it records |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `EscalationSubmitted` | `actionDigest` indexed, `mandateId` indexed, `agent` indexed, `submittedBy`, `nonce`, `nativeAmount`, `nativeUsdValue`, `nativeBalanceUsdValue`, `submittedAt` | A complete plan and signature were stored after evaluation returned `ESCALATE`, and its nonce was reserved. |
| `EscalationApproved` | `actionDigest` indexed, `mandateId` indexed, `controller` indexed, `approvedAt` | The current Vault controller approved the stored escalation while its Mandate lineage was active. It does not prove later execution. |
| `EscalationDenied` | `actionDigest` indexed, `mandateId` indexed, `controller` indexed, `deniedAt` | The current Vault controller denied a pending escalation. |
| `EscalationExecuted` | `actionDigest` indexed, `mandateId` indexed, `agent` indexed, `nonce`, `executedAt` | An approved escalation completed executor-side action processing and was marked executed. |
The manager stores the full `ActionPlan` and signature behind the digest. Query `getEscalation` when an event identifies a digest; the event alone does not contain the original parameters or signature.
The manager also exposes global, Vault-scoped, and agent-scoped digest indexes for direct onchain lookup. These indexes retain the full escalation history, while the events remain the authoritative evidence for when submission, approval, denial, and execution transitions committed.
## Vault [#vault]
| Event | Fields | What it records |
| ---------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `AuthorityUpdated` | `previousAuthority` indexed, `newAuthority` indexed | A change to the address allowed to call the Vault's execution surface. |
| `ExecutionAttempted` | `authority` indexed, `target` indexed, `value`, `dataHash`, `success`, `resultHash` | A low-level call made through the Vault, including its target, native value, calldata hash, call result, and returned-data hash. |
| `NativeDeposited` | `from` indexed, `amount` | Native asset received by the Vault. |
| `NativeWithdrawn` | `to` indexed, `amount` | Owner-controlled native withdrawal from the Vault. |
| `OwnershipTransferred` | `previousOwner` indexed, `newOwner` indexed | A change to Vault ownership. The owner controls custody and escalation approval. |
| `TokenDeposited` | `token` indexed, `from` indexed, `amount` | Token custody received by the Vault. |
| `TokenWithdrawn` | `token` indexed, `to` indexed, `amount` | Owner-controlled token withdrawal from the Vault. |
`ExecutionAttempted` is low-level call evidence, not an independent authorisation result. The executor's committed `ActionPlanExecuted` event and the transaction receipt are needed to establish that the complete plan succeeded.
## VaultExecutor [#vaultexecutor]
`ActionPlanExecuted` has this exact shape:
```solidity
event ActionPlanExecuted(
bytes32 indexed actionDigest,
uint256 indexed mandateId,
address indexed agent,
address vault,
uint256 nonce,
uint256 nativeAmount,
uint256 nativeUsdValue,
uint256 actionCount,
uint256 nativeBalanceAfter,
uint256 nativeBalanceUsdValue
);
```
It records the digest that was signed, the Mandate and agent, the Vault used, the consumed nonce, aggregate native amount, USD valuation of the native outflow, the number of actions, projected native balance after evaluation, and USD valuation of the projected remaining balance. Because it is emitted after the action loop, its presence in a committed transaction is evidence that all actions reached the executor's successful completion point.
## What events do not prove [#what-events-do-not-prove]
Events do not prove that a read-only evaluation was performed at a particular time, that an unsubmitted `DENY` was attempted, or that an offchain indexer or assembled receipt exists. Contract-side indexes identify stored records, while an `EscalationApproved` event proves approval of the stored digest and `ActionPlanExecuted` proves committed execution of that digest; the two events answer different questions.
Events emitted by a transaction that reverts are rolled back. For a failed executor call, inspect the receipt status and revert data, then use earlier committed registry or manager events to understand the authority state that led to the failure.
See [Inspecting evidence](/docs/guides/inspecting-evidence) for `cast` and explorer workflows.
# Mandate rules (https://grantline-zeta.vercel.app/docs/reference/mandate-rules)
The current Mandate rules, Preflight rules, and validity window fields, units, inheritance, and escalation behaviour.
`MandateRegistry` stores the authority that binds an agent to a Vault. The stored rules are the rules written for that Mandate. The evaluator uses effective rules, which combine the active Mandate with every active ancestor in its lineage. These effective rules are the current implementation of the [effective authority](/docs/concepts/mandates) that a proposal can actually use.
## MandateRules [#mandaterules]
The current Solidity shape is:
```solidity
struct MandateRules {
bool canDelegate;
uint256 minNativeAmount;
uint256 maxNativeAmount;
bool escalateNativeAmount;
uint256 minNativeUsd;
uint256 maxNativeUsd;
bool escalateNativeUsd;
}
```
| Field | Meaning |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `canDelegate` | Whether this Mandate can create a narrower Mandate for a sub-agent. The effective value must remain enabled through the whole lineage. |
| `minNativeAmount` | Minimum aggregate native amount for a plan, in the native asset's raw base units. A zero value disables this minimum. |
| `maxNativeAmount` | Maximum aggregate native amount for a plan, in raw base units. A zero value disables this maximum. |
| `escalateNativeAmount` | Whether a native amount violation may produce `ESCALATE`; otherwise it produces `DENY`. This remains enabled only when every active ancestor enables it. |
| `minNativeUsd` | Minimum aggregate native-asset USD valuation, expressed as a whole-dollar threshold. A zero value disables this minimum. |
| `maxNativeUsd` | Maximum aggregate native-asset USD valuation, expressed as a whole-dollar threshold. A zero value disables this maximum. |
| `escalateNativeUsd` | Whether a native-USD amount violation may produce `ESCALATE`; otherwise it produces `DENY`. This remains enabled only when every active ancestor enables it. |
Native amounts are integer units of the native token. On the current X Layer testnet, OKB uses 18 decimals, so `1000000000000000000` represents `1 OKB`. Native-USD thresholds are whole dollars; the evaluator multiplies them by the Chainlink feed's decimal scale internally. Deployments without a configured Chainlink feed reject nonzero native-USD rules.
Both bounds in one denomination may be zero-disabled, but when both are non-zero the minimum cannot exceed the maximum. The registry rejects an invalid range when a Mandate is created or updated.
## PreflightRules [#preflightrules]
The current Solidity shape is:
```solidity
struct PreflightRules {
uint256 minNativeBalance;
bool escalateNativeBalance;
uint256 minNativeUsdBalance;
bool escalateNativeUsdBalance;
}
```
| Field | Meaning |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `minNativeBalance` | Minimum native balance that must remain in the Vault after the plan's aggregate native outflow, in the native asset's raw base units. A zero value disables the floor. |
| `escalateNativeBalance` | Whether a projected balance below the floor may produce `ESCALATE`; otherwise it produces `DENY`. This remains enabled only when every active ancestor enables it. |
| `minNativeUsdBalance` | Minimum USD value of Vault native balance after execution, expressed as a whole-dollar threshold. A zero value disables the floor. |
| `escalateNativeUsdBalance` | Whether a projected USD balance below the floor may produce `ESCALATE`; otherwise it produces `DENY`. This remains enabled only when every active ancestor enables it. |
The evaluator applies these fields to the projected Vault balance after the plan's aggregate native outflow. A Preflight breach is separate from the amount range: a plan can be below a native amount maximum and still fail because the Vault would retain too little native balance or its remaining balance would fall below a USD floor.
## Validity windows [#validity-windows]
Each Mandate carries `validAfter` and `validUntil` fields (both `uint64`). These define when the authority can be used:
* `validAfter`: the Mandate cannot authorise proposals before this Unix timestamp. A value of `0` means no start constraint.
* `validUntil`: the Mandate cannot authorise proposals after this Unix timestamp. A value of `0` means no end constraint.
When both are non-zero, `validUntil` must be at or after `validAfter`. The registry rejects an invalid window at creation and update time.
Validity windows intersect across lineage. The effective window is the tightest combination of every active ancestor:
```text
effective validAfter = latest ancestor validAfter
effective validUntil = earliest ancestor validUntil
```
Outside the effective window, the Mandate is not active for evaluation even if its stored status is `ACTIVE`.
## Inheritance [#inheritance]
The registry supports a maximum delegation depth of `2`. A child Mandate cannot broaden its parent's effective authority:
* the effective minimum is the greatest active minimum in the lineage;
* the effective non-zero maximum is the tightest active maximum in the lineage;
* `canDelegate` and every escalation flag remain enabled only when all active lineage entries enable them;
* the effective Preflight floor is the greatest active floor, and its escalation flag is also the conjunction of the lineage flags;
* the effective validity window is the intersection of all ancestor windows.
At the maximum delegation depth, `canDelegate` is forced off. `getEffectiveRules`, `getEffectivePreflightRules`, and `getEffectiveValidityWindow` require an active lineage, while `getLineage` can still be used to inspect a revoked lineage.
## Deployment-gated behaviour [#deployment-gated-behaviour]
Native-USD rules and Preflight floors require a Chainlink feed to be configured on the evaluator. Deployments with a feed support `minNativeUsd`, `maxNativeUsd`, `minNativeUsdBalance`, and `escalateNativeUsdBalance`; the evaluator reads the current price from the feed at evaluation time and fails closed when the answer is unavailable or invalid. Deployments without a feed reject nonzero native-USD thresholds at Mandate creation and update time.
`SWAP` actions require a swap adapter to be configured on the Vault's executor. Deployments with an adapter validate pool registration, route topology, output floors, and deadline before executing. Deployments without one return `SWAP_UNSUPPORTED`.
Nonce cancellation, per-Vault and per-Mandate pausing, and validity windows are fully enforced regardless of deployment configuration.
See [Inspect a Mandate](/docs/guides/vault-and-mandate-setup) for read-only calls and [Mandates](/docs/concepts/mandates) for the stable product model.
# X Layer testnet (https://grantline-zeta.vercel.app/docs/execution/networks/x-layer-testnet)
Current Grantline contract evidence on X Layer testnet, including network settings and deployment-specific limitations.
The current contracts MVP is deployed and exercised on X Layer testnet. This page records the network and deployment-specific evidence; shared authorisation and execution mechanics belong in the enforcement and execution pages.
## Network settings [#network-settings]
| Setting | Value |
| --------- | ----------------------------------------------------------------------------- |
| Network | X Layer testnet |
| Chain ID | `1952` |
| Gas token | `OKB` |
| RPC | `https://testrpc.xlayer.tech/terigon` |
| Explorer | [OKX X Layer testnet explorer](https://web3.okx.com/explorer/x-layer-testnet) |
## Deployment status [#deployment-status]
Its addresses, dependency wiring, runtime hashes, Vault authority, and evaluator settings are recorded in the deployment manifest and shown in [Contracts and deployments](/docs/execution/contracts-and-deployments#deployment-manifests).
The deployment is verified against the expected chain and runtime hashes before the integration flow runs. The manifest is authoritative for this environment, so the network page does not duplicate its addresses and a deployment update has one public reference point.
## Submission status [#submission-status]
The current testnet integration uses the root or delegated agent as the transaction submitter, and that account pays its own gas. This describes the integration that was exercised on this network, not a requirement of the authority model. The evaluator, nonce, escalation, executor, and Vault checks still apply regardless of who submits the transaction.
Sponsored submission remains deferred. A future relayer will be documented on the relevant integration page once it exists and has been tested.
## Evidence covered [#evidence-covered]
The X Layer testnet integration exercised:
* a successful root `ALLOW` transfer;
* a hard-denied over-limit plan and a consumed-nonce replay;
* escalation submission, controller approval, and execution;
* delegated authority through a child and a grandchild Mandate;
* inherited Preflight denial followed by controller-approved execution;
* ancestor tightening and lineage revocation;
* failed execution receipts that moved no funds;
* Vault and Mandate pausing and unpausing;
* validity window enforcement (`validAfter` and expired `validUntil`);
* controller nonce cancellation;
* minimum native amount enforcement;
* a hook recipient receiving native transfer; and
* Vault upgrade preserving pause state.
This is evidence of the current contract flow on this network. Use [Inspecting evidence](/docs/guides/inspecting-evidence) for the reusable receipt, event, and explorer workflow.
## Deployment-specific limitations [#deployment-specific-limitations]
The current deployment has no Chainlink USD feed or `SWAP` adapter configured. Native-USD rules, Preflight floors, and `SWAP` actions are not available on this deployment until a fresh deployment with a configured feed and adapter is performed.
Offchain indexing, sponsored submission, production USD resolution, and assembled Decision Receipts remain deferred. Those are product or integration capabilities, not properties of X Layer testnet itself.
See [Transaction lifecycle](/docs/execution/transaction-lifecycle) for shared transaction behaviour and [Enforcement boundary](/docs/enforcement/enforcement-boundary) for the contract path that applies on this network.