Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
49fdf49
Add ERC: Agent Pre-Sign Risk Report
soli0x Oct 31, 2026
cc6b93f
Agent Pre-Sign Risk Report: discussions-to
soli0x Oct 31, 2026
e21fc46
presign report: signers in the registration file, issuer-listing none…
soli0x Oct 31, 2026
9466a24
presign report: what a 7702 delegate lets whom do (delegate-single-ca…
soli0x Nov 1, 2026
a1c7826
presign report: delegate kinds count outside callers only (the accoun…
soli0x Nov 1, 2026
d29e607
presign report: test vectors for the delegate kinds (V1-V7, the 29 Oc…
soli0x Nov 1, 2026
e15eaf1
presign report: the subject carries the EIP-7702 authorizationList (i…
soli0x Nov 1, 2026
4ac4561
ERC-8481: assign number (eip header, file name, discussions-to); fix …
soli0x Nov 2, 2026
384e1fa
ERC-8481: walidator: link first ERC references, name unmerged proposa…
soli0x Nov 2, 2026
96b4efc
ERC-8481: auditing anchored reports evaluates the signer binding as o…
soli0x Nov 2, 2026
f5c77ef
Add ERC-8481 anchor and historical signer vectors
haot3x Nov 3, 2026
090eed4
ERC-8481: historical audits run step 2's ERC-1271 isValidSignature at…
soli0x Nov 3, 2026
36bc246
ERC-8481: historical step 2 also reads whether the signer has code at…
soli0x Nov 3, 2026
5280ee0
Merge pull request #1 from haot3x/test/8481-historical-signers
soli0x Nov 3, 2026
08a266f
ERC-8481 assets: README states the historical step 2 rule (now normat…
soli0x Nov 3, 2026
1810662
ERC-8481: on-list list document (pinned, normalized, attestation, CAI…
soli0x Nov 3, 2026
3ae0617
ERC-8481 assets: on-list list document vectors (normalization, list h…
metafrogmeme-droid Nov 3, 2026
9abaacf
ERC-8481: on-list hash over the normalized form (publish normalized S…
soli0x Nov 3, 2026
aa3aa86
Merge pull request #2 from metafrogmeme-droid/erc-8481-on-list-vectors
soli0x Nov 3, 2026
0149e6c
ERC-8481: ERC-55 prefix (eipw markdown-refs)
soli0x Nov 3, 2026
0f45f10
ERC-8481 assets: on-list vectors follow 9abaacf
metafrogmeme-droid Nov 3, 2026
d21f46e
ERC-8481: empty lists conform; bech32 vs base58 inside bip122; other …
soli0x Nov 3, 2026
ad9172d
Merge pull request #3 from metafrogmeme-droid/erc-8481-on-list-9abaacf
soli0x Nov 3, 2026
f347df9
ERC-8481 assets: on-list vectors follow d21f46e (empty list, bip122 e…
metafrogmeme-droid Nov 4, 2026
dc2672f
ERC-8481 assets: on-list README for d21f46e (rulings table, counts, r…
metafrogmeme-droid Nov 4, 2026
e1446b2
ERC-8481: a bip122 string that is neither valid bech32 nor valid Base…
soli0x Nov 5, 2026
f7287cf
Merge pull request #4 from metafrogmeme-droid/erc8481-vectors-d21f46e
soli0x Nov 5, 2026
4fee54a
ERC-8481 assets: upstream page fixtures as .html.txt (same bytes, sam…
soli0x Nov 5, 2026
4dad3bf
ERC-8481: request-level coverage status for incomplete decoding (revi…
soli0x Nov 5, 2026
eb21c0e
ERC-8481: report coverage vs envelope coverage, optional path on appr…
soli0x Nov 5, 2026
83b4c5a
ERC-8481: simulation kinds (sim.outcome, sim.balance-change, sim.appr…
soli0x Nov 8, 2026
98133f3
ERC-8481: sentence case and articles where proposal names replaced ER…
soli0x Nov 8, 2026
d622089
ERC-8481: text.injection kind (instructions addressed to an agent, si…
soli0x Nov 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,008 changes: 1,008 additions & 0 deletions ERCS/erc-8481.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions assets/erc-8481/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/out/
/cache/
/lib/
181 changes: 181 additions & 0 deletions assets/erc-8481/HistoricalSignerVectors.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {IPresignReportAnchor} from "./IPresignReportAnchor.sol";
import {PresignReportAnchor} from "./PresignReportAnchor.sol";

/// Test-only identity source. It models just the owner/wallet binding for one agent.
contract ReportSourceFixture {
address public owner;
address public wallet;
address private immutable controller = msg.sender;

function setOwner(address who) external {
require(msg.sender == controller);
owner = who;
}

function setWallet(address who) external {
require(msg.sender == controller);
wallet = who;
}

function bound(address who) external view returns (bool) {
return who != address(0) && (who == owner || who == wallet);
}
}

contract Rotating1271Fixture {
address public key;

constructor(address initialKey) {
key = initialKey;
}

function rotate(address next) external {
require(msg.sender == key && next != address(0));
key = next;
}

function publish(IPresignReportAnchor anchor, bytes32 subject, bytes32 report) external {
require(msg.sender == key);
anchor.anchor(subject, report, "ipfs://report");
}

function isValidSignature(bytes32 digest, bytes calldata signature) external view returns (bytes4) {
if (signature.length != 65) return bytes4(0xffffffff);
bytes32 r = bytes32(signature[0:32]);
bytes32 s = bytes32(signature[32:64]);
uint8 v = uint8(signature[64]);
return ecrecover(digest, v, r, s) == key ? bytes4(0x1626ba7e) : bytes4(0xffffffff);
}
}

/// Snapshot replay models an archive read at a known registry block. This is not
/// an archive RPC client or a full report verifier: hashes are fixed test inputs.
contract HistoricalSignerVectorsTest is Test {
PresignReportAnchor anchor;
ReportSourceFixture source;
uint256 constant KEY = 0x82598481;
bytes32 constant SUBJECT = keccak256("subject fixture");
bytes32 constant CONTEXT = keccak256("context fixture");
bytes32 constant REPORT = keccak256("report fixture");

function setUp() public {
vm.warp(1000);
vm.roll(100);
anchor = new PresignReportAnchor();
source = new ReportSourceFixture();
}

function digest(uint256 registryChain) internal pure returns (bytes32) {
bytes32 domain = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId)"),
keccak256("Agent Pre-Sign Risk Report"),
keccak256("1"),
registryChain
)
);
bytes32 report = keccak256(
abi.encode(
keccak256("PresignReport(bytes32 subjectDigest,bytes32 contextDigest,bytes32 reportHash)"),
SUBJECT,
CONTEXT,
REPORT
)
);
return keccak256(abi.encodePacked(hex"1901", domain, report));
}

function signature(bytes32 d) internal pure returns (bytes memory) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(KEY, d);
return abi.encodePacked(r, s, v);
}

function publishEOA() internal {
vm.prank(vm.addr(KEY));
anchor.anchor(SUBJECT, REPORT, "ipfs://report");
}

function assertOriginalAnchor(address signer) internal view {
(uint64 timestamp, uint64 number) = anchor.anchoredAt(signer, REPORT);
assertEq(timestamp, 1000);
assertEq(number, 100);
}

function test_walletClearingRejectsCurrentBindingButPreservesHistoricalBinding() public {
address signer = vm.addr(KEY);
source.setWallet(signer);
publishEOA();
uint256 historical = vm.snapshotState();
vm.warp(1100);
vm.roll(110);
source.setWallet(address(0));
assertFalse(source.bound(signer), "current pre-sign binding is revoked");
assertOriginalAnchor(signer);
assertTrue(vm.revertToState(historical));
assertTrue(source.bound(signer), "binding at the anchoring still exists");
}

function test_ownerTransferDoesNotMoveEarlierAnchor() public {
address signer = vm.addr(KEY);
source.setOwner(signer);
publishEOA();
uint256 historical = vm.snapshotState();
vm.warp(1100);
vm.roll(110);
source.setOwner(address(0xBEEF));
publishEOA(); // permissionless publication cannot restore identity authority
assertFalse(source.bound(signer));
assertOriginalAnchor(signer);
assertTrue(vm.revertToState(historical));
assertTrue(source.bound(signer));
}

function test_bindingAddedLaterCannotAuthorizeAnEarlierAnchor() public {
address signer = vm.addr(KEY);
publishEOA();
uint256 historical = vm.snapshotState();
vm.warp(1100);
source.setWallet(signer);
assertTrue(source.bound(signer), "current binding alone would misattribute the report");
assertTrue(vm.revertToState(historical));
assertFalse(source.bound(signer), "not bound when the report was anchored");
}

function test_1271RotationNeedsHistoricalSignatureStateEvenIfBindingUnchanged() public {
address key = vm.addr(KEY);
Rotating1271Fixture signer = new Rotating1271Fixture(key);
source.setWallet(address(signer));
bytes32 d = digest(block.chainid);
bytes memory sig = signature(d);
vm.prank(key);
signer.publish(anchor, SUBJECT, REPORT);
assertEq(signer.isValidSignature(d, sig), bytes4(0x1626ba7e));
uint256 historical = vm.snapshotState();

vm.warp(1100);
vm.roll(110);
vm.prank(key);
signer.rotate(address(0xBEEF));
assertTrue(source.bound(address(signer)), "registry binding did not change");
assertEq(signer.isValidSignature(d, sig), bytes4(0xffffffff), "current signature is revoked");
assertOriginalAnchor(address(signer));

// Reading only the historical binding while calling today's ERC-1271 state
// still loses this report. Replay both reads in the earlier state to show why.
assertTrue(vm.revertToState(historical));
assertTrue(source.bound(address(signer)));
assertEq(signer.isValidSignature(d, sig), bytes4(0x1626ba7e));
}

function test_signatureDomainUsesRegistryChain() public pure {
bytes32 correctDigest = digest(1);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(KEY, correctDigest);
assertEq(ecrecover(correctDigest, v, r, s), vm.addr(KEY));
// An anchor's different chain ID must not replace the registry's signing domain.
assertTrue(ecrecover(digest(42161), v, r, s) != vm.addr(KEY));
}
}
11 changes: 11 additions & 0 deletions assets/erc-8481/IPresignReportAnchor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.24;

interface IPresignReportAnchor {
event ReportAnchored(
address indexed source, bytes32 indexed subjectDigest, bytes32 indexed reportHash, string reportURI
);

function anchor(bytes32 subjectDigest, bytes32 reportHash, string calldata reportURI) external;
function anchoredAt(address source, bytes32 reportHash) external view returns (uint64 timestamp, uint64 blockNumber);
}
Loading
Loading