08 · Security & threat model
Ballast’s value is precise: it bounds and verifies what an agent can do. This page states plainly what it defends, what it does not, and where each guarantee lives, so a reviewer can trust it for the right reasons.
Diagram 8: Trust boundaries
flowchart TB
subgraph trusted["TRUSTED"]
chain["Sui consensus + Move VM<br/>(executes enforce / verify_nautilus)"]
capobj["BallastCap object<br/>(limits, on-chain, tamper-proof)"]
encobj["Enclave (BallastEnclave)<br/>(attested key, proven at registration)"]
awsroot["AWS Nitro attestation root<br/>(cert chain anchor)"]
end
subgraph untrusted["UNTRUSTED"]
prompt["Prompt / model inputs"]
agentproc["Agent process + its key"]
agenthost["Agent host"]
ec2["EC2 enclave host"]
net["Network"]
end
prompt -->|"can hijack intent"| agentproc
agentproc -->|"builds + signs PTB"| chain
ec2 -->|"cannot read enclave memory/key"| encobj
awsroot -.->|"anchors"| encobj
chain -->|"enforce(): spend/lev/market/expiry/revoke"| capobj
chain -->|"verify_nautilus(): sig vs attested key"| encobj
classDef t fill:#0e3142,stroke:#3AA0E0,color:#EAF4FB;
classDef u fill:#3a1416,stroke:#f0555a,color:#EAF4FB;
class chain,capobj,encobj,awsroot t;
class prompt,agentproc,agenthost,ec2,net u;
The guarantee is structural. The agent process is untrusted, yet it cannot exceed the mandate, because the limits and the signature check execute inside the trusted Move VM against tamper-proof objects, not inside the agent.
What Ballast defends
| Threat | Defense | Enforced in |
|---|---|---|
| Over-spend | spent + amount <= spend_limit |
capability::enforce (abort 5) |
| Over-leverage | leverage_bps <= max_leverage_bps |
capability::enforce (abort 4) |
| Trading a disallowed market | allowed_markets.contains(market); market derived from the real pool object |
capability::enforce (abort 3) |
| Acting after the mandate ends | now <= expiry_ms; owner revoke() sets revoked |
capability::enforce (abort 2 / 1) |
| Prompt injection / hijacked agent | Limits live in the on-chain object, not the prompt, so a hijacked agent still can’t exceed them | capability::enforce |
| Forged / absent TEE signature | Ed25519 check vs the enclave’s on-chain-proven key | attestation::verify_nautilus (trading abort 1) |
| Replayed signature on a different trade | Signature is bound to {agent, market, amount, is_bid} (+ timestamp) |
verify_nautilus payload binding |
| Using a cap with the wrong identity | cap.agent == id(identity) |
trading (abort 2) |
| Agent inflating its own reputation | record_ok is public(package), so only a real gated trade can move it |
identity |
| Partial settlement on a violation | assert! aborts the whole PTB atomically |
Move PTB semantics |
A prompt injection is the sharpest case. The attack succeeds at hijacking the
agent’s intent and fails at the chain. The over-spend it induces aborts in
enforce: the agent was hijacked, the capability wasn’t.
What Ballast does NOT defend
Stated honestly, these are out of scope, either by design or by current status:
- The principal’s own key. If the owner’s wallet is compromised, the attacker can issue or revoke mandates. Ballast bounds the agent, not the principal.
- A compromised enclave before registration. Trust is anchored to the PCRs
registered in
init_attestation. If you register PCRs for malicious code, the chain will faithfully trust that code’s key. The guarantee is that the key belongs to exactly the code measured by those PCRs, not that the code is benign. - DeepBook and Sui protocol risk. Ballast trusts the venue and the consensus it runs on. It does not re-verify DeepBook’s matching or Sui’s validators.
- Enclave liveness. A torn-down enclave can’t produce new signatures, so the attested path needs a running enclave (the capability and mock paths do not). The registration persists on-chain regardless.
- Economic and strategy quality. Ballast enforces limits, not good trades. A within-mandate bad trade is still a bad trade.
- The legacy mock
verifypath. WhenATTESTATION=mock(the default), the on-chainattestation::verifyreturnstrue, so there is no signature guarantee on that path. It exists for demos and as a non-regressing fallback. Useverify_nautilusfor the real guarantee.
Real vs. mocked
The single honest seam in Ballast:
| Component | Status | Detail |
|---|---|---|
| Capability enforcement (spend/leverage/market/expiry/revoke) | Real | proven by on-chain aborts 5/4/3/1 |
| DeepBook Spot fill through the cap | Real | real execute_trade<SUI,DBUSDC> fills, zero-DEEP |
Reputation + TradeExecuted events |
Real | record_ok is package-gated |
Nautilus TEE attestation (verify_nautilus, execute_trade_attested) |
Real | enclave key proven on-chain via AWS cert chain + PCRs; attested fill + forged-sig rejection both recorded |
AWS Nitro enclave app (enclave/ballast-trader) |
Real | reproducible EIF, ran on Nitro hardware |
Legacy attestation::verify(payload, sig) |
Stub | returns true; default mock path only, kept byte-for-byte so the demo can’t regress |
Nothing else is mocked. The mock path is opt-in-by-default for convenience; the real path is one provider swap away (see 06 · Quickstart).
Reporting
This is hackathon-stage research software on testnet with a throwaway deployer key. Do not use it with mainnet funds as-is. Issues and review welcome via the repository.
← Back to docs index