Stamp

Contract reference

AgentLifecycleRegistry, Solidity 0.8.26. One contract, about 530 lines with comments, no dependencies beyond OpenZeppelin's EIP-712 and signature helpers.

Addresses

Lifecycle registry0x0ab009ab9120842e740b40a7e93ac3bdb46099cc · block 128955708
ERC-8004 identity registry0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 (proxy; implementation 0x7274e874ca62410a93bd8bf61c69d8045e399c02, version 2.0.0)
EIP-712 domainname Agent Lifecycle Protocol, version 0.4, chainId 56, verifying contract = the lifecycle registry

Functions

FunctionCallerRules
register(agentId, controller, policyDigest, evidence, operationId)Current ERC-8004 ownerOpens the record at REGISTERED, sequence 1, epoch 0. Requires a nonzero agent wallet that differs from the owner, a controller that is not the agent wallet, and nonzero policy and evidence hashes. Once per agent, ever.
transition(agentId, expectedSequence, next, authorityEpoch, policyDigest, evidence, operationId)Owner or controllerMoves along a legal edge. expectedSequence must equal the current sequence. policyDigest must equal the stored one. To ACTIVE: controller only, owner and wallet unchanged, epoch strictly higher and at most 1,000,000 above. Other targets: epoch equal to the stored one.
rebind(agentId, expectedSequence, newController, policyDigest, evidence, operationId)Current ownerRefreshes the owner and wallet snapshots, sets controller and policy, raises the epoch by one. Not allowed while ACTIVE or after RETIRED.
flagMismatch(agentId)AnyoneSuspends a REGISTERED or ACTIVE record whose live owner or wallet differs from its snapshot. The evidence hash is computed on chain from the mismatch.
getRecord(agentId)ViewThe full record.
liveStatus(agentId)ViewState plus ownerMatches, walletMatches and operational (ACTIVE and both matching).
agentCount(), agentIds(start, count)ViewEvery recorded agent in registration order. No indexer needed.
getOperation(caller, operationId)ViewThe request hash and resulting sequence of an operation, for reconciliation.
handshakeDigest(...), verifyHandshake(..., signature)ViewEIP-712 digest and full on-chain check of an ALP-DISC handshake. Returns (ok, reason).
agentRegistry()View"eip155:56:0x8004a169…", the identity string used in every record.

State machine

States are numbered as in ALP v0.4: 0 none, 1 draft, 2 provisioned, 3 registered, 4 active, 5 suspended, 6 retired. Legal edges: registered to active, suspended or retired; active to suspended or retired; suspended to active or retired. Nothing leaves retired.

Idempotency

Every write takes a bytes32 operationId, namespaced by the caller. Re-sending the exact same request with the same id returns the original sequence without a second effect, even if the record moved on since. Re-using the id with any change reverts with IdempotencyConflict. In this binding the on-chain id is sha256(utf8(operation_id)) of the human-readable id used in ALP records.

Events

event Transition(
  uint256 indexed agentId, State from, State to, uint64 indexed sequence,
  bytes32 evidence, address indexed actor, address controller,
  uint64 authorityEpoch, bytes32 policyDigest, bytes32 operationId,
  uint64 previousBlock, uint256 blockNumber, uint256 timestamp
);
event Rebound(
  uint256 indexed agentId, uint64 indexed sequence, address owner, address wallet,
  address controller, uint64 authorityEpoch, bytes32 policyDigest, bytes32 evidence
);

previousBlock links each stamp to the one before it, so a client walks the full history with one single-block eth_getLogs per stamp. A re-bind emits Transition with from == to; treat it as an authority change, not a state change.

Errors

AlreadyRecordedThe agent already has a record (including a retired one).
NotRecordedNo record yet.
RetiredIsTerminalThe record is retired; nothing can change it.
NotOwner / UnauthorizedCaller lacks the role for this change.
ControllerFrozenThe owner changed since the snapshot; the controller may only suspend.
InvalidEdge(from, to)Not an arrow of the state machine. Re-binding while ACTIVE also returns this.
StaleSequence(expected, current)Someone else changed the record first. Re-read and decide again.
StaleEpoch / EpochJumpEpoch did not move as required, or jumped too far.
PolicyMismatchThe policy hash differs from the stored one. Only the owner changes policy, by re-binding.
WalletUnset / DedicatedWalletRequired / BindingChangedThe ERC-8004 wallet is missing, equals the owner, or changed.
InvalidControllerZero address or the agent wallet.
IdempotencyConflict(operationId)Same operation id was used before with a different request.
NothingToFlagOwner and wallet still match, or the state cannot be flagged.

Handshake reasons

verifyHandshake returns (false, reason) instead of reverting: 1 no record, 2 retired, 3 owner changed, 4 wallet unset or changed, 5 stale sequence, 6 stale epoch, 7 time window (issued more than 30 s after the latest block, expired, or longer than 300 s), 8 bad signature. Signatures are tried as plain ECDSA first, then ERC-1271, so EIP-7702 delegated accounts work. Nonce single use and audience binding are the caller's job.

Tests

60 tests with Foundry: 50 unit and fuzz tests (including a 512-run random walk asserting that sequence and epoch never decrease and retired never changes), 4 regression tests from the internal review, and 6 tests against the live ERC-8004 registry on a BSC mainnet fork, including a real EIP-712 wallet binding, an ERC-1271 smart wallet and a transfer that clears the wallet.