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:
registerAgentSimple()callsnameWrapper.setSubnodeRecord()to create{agentId}.enshell.eth- Sets initial ENS text records via the Public Resolver:
avatar-- default ENShell avatar URLdescription-- "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 stringthreat-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:
- Decodes the report:
(agentId, actionId, decision, rawThreatScore) - Verifies the action exists and is not already resolved
- Sets the action's
decisionand marks itresolved - Updates the agent's threat score via EMA
- Increments strikes if
rawThreatScore >= escalateThreshold - Auto-deactivates the agent if strikes reach
maxStrikes - Writes updated scores to ENS text records
- 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 APPROVEDrejectAction(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.