Smart Contract: AgentFirewall

The AgentFirewall contract is the on-chain core of ENShell. Deployed on Sepolia at 0x410f4D119EF857879E42625381DB131457db78A7, it handles agent registration, action queuing, CRE report processing, threat scoring, and trust mesh operations.

Solidity 0.8.28 | Inherits: OpenZeppelin Ownable, IERC1155Receiver | 63 tests passing

Data Structures

Agent

struct Agent {
    bytes32 ensNode;          // ENS namehash of agent.enshell.eth
    address agentAddress;     // Agent's wallet address
    address owner;            // Who registered the agent (msg.sender)
    uint256 spendLimit;       // Maximum spend limit in wei
    uint256 threatScore;      // EMA-based cumulative threat score
    uint256 strikes;          // Strike count (incremented on high-risk actions)
    bool active;              // Whether the agent is operational
    bool worldIdVerified;     // Reserved for World ID integration
    uint256 registeredAt;     // Block timestamp of registration
}

QueuedAction

struct QueuedAction {
    string agentId;           // Agent identifier string
    address target;           // Target contract/EOA
    uint256 value;            // ETH value in wei
    bytes data;               // Transaction calldata
    bytes32 instructionHash;  // keccak256 of the encrypted instruction
    uint256 queuedAt;        // Block timestamp when queued
    bool resolved;            // Whether the CRE has processed this
    uint8 decision;           // 0=PENDING, 1=APPROVED, 2=ESCALATED, 3=BLOCKED
}

Access Control Model

This is a shared ecosystem contract -- there is no global admin gate on registration. Access control is per-agent:

Function Who Can Call
registerAgentSimple Anyone (permissionless)
submitAction Agent owner only
approveAction / rejectAction Agent owner only
deactivateAgent / reactivateAgent Agent owner or contract owner
setAllowedTarget / setAllowedTargets Agent owner or contract owner
onReport Chainlink KeystoneForwarder only
Admin setters (thresholds, forwarder, etc.) Contract owner only

The owner field in the Agent struct is set to msg.sender during registerAgentSimple(). This means whoever registers the agent controls it. The contract owner can also deactivate/reactivate any agent as a safety measure.

EMA Threat Scoring

When the CRE submits a report via onReport(), the contract updates the agent's threat score using an Exponential Moving Average:

newScore = (EMA_ALPHA * rawScore + (EMA_SCALE - EMA_ALPHA) * previousScore) / EMA_SCALE

With EMA_ALPHA = 300 and EMA_SCALE = 1000, this gives 30% weight to the new score and 70% to history:

newScore = (300 * rawScore + 700 * previousScore) / 1000

Example progression:

Action Raw Score Previous EMA New EMA Strikes
First action (safe) 5,000 0 1,500 0
Second action (suspicious) 50,000 1,500 16,050 1
Third action (safe) 2,000 16,050 11,835 1

The EMA smooths out noise -- a single suspicious action doesn't immediately ruin an agent's reputation, but persistent bad behavior accumulates.

Strike System

  • If rawScore >= escalateThreshold (40,000), the agent's strike count increments
  • At maxStrikes (5), the agent is automatically deactivated (frozen)
  • Strikes persist even after reactivation -- they represent historical bad behavior
  • The threat score and strike count are written to ENS text records after every update

Policy Thresholds

Threshold Default Purpose
escalateThreshold 40,000 Raw score above this increments strikes
blockThreshold 70,000 Not used in EMA; CRE maps scores to decisions
maxStrikes 5 Auto-deactivation threshold

ENS Integration

Agent registration creates an ENS subdomain via the NameWrapper:

  1. registerAgentSimple() calls nameWrapper.setSubnodeRecord() to create {agentId}.enshell.eth
  2. Sets initial ENS text records via the Public Resolver:
    • avatar -- default ENShell avatar URL
    • description -- "ENShell AI Agent"
    • threat-score -- "0"
    • threat-strikes -- "0"

After every CRE analysis (via onReport()), the contract updates:

  • threat-score -- the new EMA score as a string
  • threat-strikes -- the current strike count

These text records make the agent's reputation portable -- any ENS-aware application can read them.

NameWrapper and ERC1155

The ENS NameWrapper manages subdomains as ERC1155 tokens. The AgentFirewall contract implements IERC1155Receiver so it can receive NameWrapper tokens during subdomain creation. It also advertises ERC165 support for:

  • IERC1155Receiver (0x4e2312e0)
  • Chainlink IReceiver (0x805f2132)
  • ERC165 (0x01ffc9a7)

Required Permissions

Before the contract can create subdomains, it must be approved as an operator on:

  • ENS Registry -- setApprovalForAll(firewall, true)
  • NameWrapper -- setApprovalForAll(firewall, true)
  • Public Resolver -- setApprovalForAll(firewall, true)

The scripts/approve-ens.ts script handles this setup. The parent domain enshell.eth must be wrapped in the NameWrapper first (via scripts/wrap-ens.ts).

CRE Report Processing

The onReport(bytes metadata, bytes report) function is the callback from Chainlink's KeystoneForwarder. It:

  1. Decodes the report: (agentId, actionId, decision, rawThreatScore)
  2. Verifies the action exists and is not already resolved
  3. Sets the action's decision and marks it resolved
  4. Updates the agent's threat score via EMA
  5. Increments strikes if rawThreatScore >= escalateThreshold
  6. Auto-deactivates the agent if strikes reach maxStrikes
  7. Writes updated scores to ENS text records
  8. Emits appropriate events (ActionApproved, ActionEscalated, ActionBlocked, ThreatScoreUpdated)

Only the address stored in forwarder (the KeystoneForwarder) can call this function. This ensures only DON-attested reports can resolve actions.

Human Approval (Ledger Flow)

For escalated actions, the agent owner can call:

  • approveAction(uint256 actionId) -- resolves the action as APPROVED
  • rejectAction(uint256 actionId) -- resolves the action as BLOCKED

Both functions require:

  • The caller is the agent's owner
  • The action's current decision is ESCALATED (2)
  • The action has not been previously resolved by another path

Target Allowlisting

Agents can have an allowlist of target addresses:

setAllowedTarget(string agentId, address target, bool allowed)
setAllowedTargets(string agentId, address[] targets, bool allowed)
isTargetAllowed(string agentId, address target) → bool

The allowlist is informational and used by the CRE during analysis. It does not enforce execution at the contract level (the contract queues actions regardless).

Events

Event Parameters When Emitted
AgentRegistered agentId, ensNode, owner, agentAddress, spendLimit, worldIdVerified Agent registration
AgentDeactivated agentId, reason Freeze / auto-freeze
AllowedTargetUpdated agentId, target, allowed Target allowlist change
ActionSubmitted actionId, agentId, target, value, instructionHash Action queued
ActionApproved actionId, agentId CRE approves or owner approves
ActionEscalated actionId, agentId, threatScore CRE escalates
ActionBlocked actionId, agentId, reason CRE blocks or owner rejects
ThreatScoreUpdated agentId, previousScore, newScore, rawDetectionScore, strikes After CRE report
TrustChecked checkerAgentId, targetAgentId, threatScore, strikes, trusted Trust mesh check

Deployment

The contract is deployed via Hardhat Ignition with four constructor parameters:

{
  "ensResolver": "0xE99638b40E4Fff0129D56f03b55b6bbC4BBE49b5",
  "nameWrapper": "0x0635513f179D50A207757E05759CbD106d7dFcE8",
  "ensParentNode": "0x98e097...",
  "forwarder": "0x15fC6ae953E024d975e77382eEeC56A9101f9F88"
}

The deploy pipeline (scripts/deploy.sh) handles deployment + ENS operator approval in sequence.