Skip to content
354 changes: 354 additions & 0 deletions ERCS/erc-8353.md

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions assets/erc-8353/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ERC-8353 reference implementation

Non-normative. Foundry project, solc 0.8.20.

| Path | What |
|---|---|
| `src/IVerificationGate.sol` | The interface: four transitions, two views, four events |
| `src/VerificationGate.sol` | Minimal abstract base implementing every normative requirement; policy is exposed as virtual hooks |
| `examples/MarketGate.sol` | Adapter: staked-marketplace shape — mandatory stake, arbiter revocation burns it, verifier depth recursive over the gate's own settled claims |
| `examples/IdentityGate.sol` | Adapter: credential-registry shape — zero stake, depth read from an external source, issuer-revocable |
| `test/VerificationGate.t.sol` | 15 tests covering both shapes and every normative requirement |

The two adapters differ only in the policy hooks (`weightOf`,
`_requiredStake`, `_promotionThreshold`, `_canSettle`, `_canRevoke`,
`_onSettled`) — the same interface serves a staked marketplace and a
zero-stake credential registry.

Run:

```
forge install foundry-rs/forge-std
forge test -vv
```

Not audited. This is a reference for implementers, not production code.
40 changes: 40 additions & 0 deletions assets/erc-8353/examples/IdentityGate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

import "../src/VerificationGate.sol";

/// @notice External verified-depth source (e.g. an identity registry's
/// verification tier). Per the draft, the source must itself satisfy
/// no-self-assignment or the gate's guarantee is void.
interface IDepthOracle {
function depthOf(address account) external view returns (uint256);
}

/// @title IdentityGate — identity-side adapter sketch
/// @notice Models the credential-registry shape: an issuer offers an
/// attestation about a subject; no stake anywhere (zero-stake gates
/// conform); endorsement weight is the endorser's identity-
/// verification depth read from an external oracle; claims typically
/// remain in Verified — revocable by the issuer indefinitely — and
/// `settle` is the deliberate, terminal point of no return.
contract IdentityGate is VerificationGate {
IDepthOracle public immutable depthOracle;
uint256 public immutable threshold;

constructor(IDepthOracle oracle, uint256 threshold_) {
depthOracle = oracle;
threshold = threshold_;
}

/// Anonymous endorsers (depth 0) weigh nothing, however many there are.
function weightOf(address verifier) public view override returns (uint256) {
return depthOracle.depthOf(verifier);
}

function _promotionThreshold(bytes32) internal view override returns (uint256) {
return threshold;
}

// _requiredStake stays 0; _canRevoke/_canSettle stay offerer-only —
// the issuer controls both revocation and finalization.
}
55 changes: 55 additions & 0 deletions assets/erc-8353/examples/MarketGate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

import "../src/VerificationGate.sol";

/// @title MarketGate — market-side adapter sketch
/// @notice Models the staked-marketplace shape: every claim (a capability bid)
/// MUST bond a fixed stake; wrong claims are revoked by a designated
/// arbiter and the stake burns; settlement is terminal and feeds the
/// subject's own verified depth. Verifier depth is recursive over this
/// gate's own settled claims: `weightOf` = settled claims as subject
/// (+ a one-time genesis seed so the system can bootstrap).
contract MarketGate is VerificationGate {
uint256 public immutable stakePerClaim;
uint256 public immutable threshold;
address public immutable arbiter;

mapping(address => uint256) public settledAsSubject;
mapping(address => uint256) public genesisWeight;

constructor(uint256 stakePerClaim_, uint256 threshold_, address arbiter_,
address[] memory genesisVerifiers, uint256 genesisWeight_)
{
stakePerClaim = stakePerClaim_;
threshold = threshold_;
arbiter = arbiter_;
for (uint256 i = 0; i < genesisVerifiers.length; i++)
genesisWeight[genesisVerifiers[i]] = genesisWeight_;
}

/// Depth = measured verified history in this gate, never a raw count of
/// endorsements received.
function weightOf(address verifier) public view override returns (uint256) {
return genesisWeight[verifier] + settledAsSubject[verifier];
}

function _requiredStake(address, bytes32, bytes calldata)
internal view override returns (uint256) { return stakePerClaim; }

function _promotionThreshold(bytes32) internal view override returns (uint256) {
return threshold;
}

/// Only the arbiter revokes (the marketplace's dispute outcome); the
/// claimant's stake burns via the base contract.
function _canRevoke(bytes32, address caller) internal view override returns (bool) {
return caller == arbiter;
}

/// A settled claim deepens its subject's own verified depth — the
/// recursion that makes weight expensive to farm.
function _onSettled(bytes32 claimId) internal override {
settledAsSubject[_claims[claimId].subject] += 1;
}
}
16 changes: 16 additions & 0 deletions assets/erc-8353/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[profile.default]
src = "src"
test = "test"
out = "out"
libs = ["lib"]
solc = "0.8.20"
optimizer = true
optimizer_runs = 200
verbosity = 2
fuzz = { runs = 256 }

# Disable file-system access in tests (security default)
fs_permissions = []

# Strict re-entrancy + invariant settings
remappings = []
52 changes: 52 additions & 0 deletions assets/erc-8353/src/IVerificationGate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

/// @title Staked Weighted Verification Gate (interface per draft/erc-draft.md)
/// @notice A claim carries no trusted status until verified by third parties;
/// verification is weighted by the verifier's own verified depth.
interface IVerificationGate {
enum Status { None, Offered, Verified, Settled, Revoked }

/// @notice A claim was registered and entered the Offered state.
event ClaimOffered(bytes32 indexed claimId, address indexed subject,
address indexed offerer, bytes32 claimHash, uint256 stake);

/// @notice A third party verified the claim, contributing `weight`.
event ClaimVerified(bytes32 indexed claimId, address indexed verifier,
uint256 weight, bytes32 evidenceHash);

/// @notice A Verified claim was finalized. Settled is terminal.
event ClaimSettled(bytes32 indexed claimId);

/// @notice The claim was revoked. The record persists.
event ClaimRevoked(bytes32 indexed claimId, address indexed revoker,
bytes32 reasonHash);

/// @notice Register a claim about `subject`. Binds msg.value as stake, if any.
/// @dev MUST emit ClaimOffered. MUST revert if a required stake is missing.
function offer(address subject, bytes32 claimHash, bytes calldata data)
external payable returns (bytes32 claimId);

/// @notice Endorse a claim as a third party.
/// @dev MUST emit ClaimVerified with the weight actually credited.
/// Calls from the claim's subject or offerer MUST NOT contribute
/// weight (this implementation reverts). A verifier MUST NOT
/// contribute weight to the same claim more than once (this
/// implementation reverts on repeat calls). MUST revert unless the
/// claim is Offered or Verified.
function verify(bytes32 claimId, bytes32 evidenceHash) external;

/// @notice Finalize a Verified claim. MUST revert unless Verified.
function settle(bytes32 claimId) external;

/// @notice Revoke a claim. MUST revert unless Offered or Verified —
/// Settled is terminal and cannot be revoked.
function revoke(bytes32 claimId, bytes32 reasonHash) external;

/// @notice The gate: consumers MUST check this before relying on a claim.
function statusOf(bytes32 claimId)
external view returns (Status status, uint256 weight);

/// @notice The verifier's own verified depth, as computed by this implementation.
function weightOf(address verifier) external view returns (uint256);
}
148 changes: 148 additions & 0 deletions assets/erc-8353/src/VerificationGate.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

import "./IVerificationGate.sol";

/// @title VerificationGate — minimal reference implementation
/// @notice Implements the four-state lifecycle and every MUST of the draft.
/// Policy (weight function, stake sizing, promotion threshold,
/// revocation authorization) is exposed as virtual hooks — see
/// examples/ for the market-side and identity-side adapters.
abstract contract VerificationGate is IVerificationGate {
struct Claim {
address subject;
address offerer;
bytes32 claimHash;
uint256 stake;
uint256 weight;
Status status;
}

/// Slashed stake is burned here — never redistributed, and in particular
/// never to the party that triggered the revocation.
address public constant BURN = address(0xdEaD);

mapping(bytes32 => Claim) internal _claims;
mapping(bytes32 => mapping(address => bool)) internal _verified;
uint256 private _nonce;

error BadStatus(bytes32 claimId, Status actual);
error SelfVerification(bytes32 claimId, address caller);
error AlreadyVerified(bytes32 claimId, address caller);
error InsufficientStake(uint256 required, uint256 provided);
error NotAuthorized(bytes32 claimId, address caller);
error TransferFailed();

// ── transitions ──────────────────────────────────────────────────────

function offer(address subject, bytes32 claimHash, bytes calldata data)
external payable returns (bytes32 claimId)
{
uint256 required = _requiredStake(subject, claimHash, data);
if (msg.value < required) revert InsufficientStake(required, msg.value);

claimId = keccak256(abi.encode(subject, msg.sender, claimHash, _nonce++));
_claims[claimId] = Claim({
subject: subject,
offerer: msg.sender,
claimHash: claimHash,
stake: msg.value,
weight: 0,
status: Status.Offered
});
emit ClaimOffered(claimId, subject, msg.sender, claimHash, msg.value);
}

function verify(bytes32 claimId, bytes32 evidenceHash) external {
Claim storage c = _claims[claimId];
if (c.status != Status.Offered && c.status != Status.Verified)
revert BadStatus(claimId, c.status);
if (msg.sender == c.subject || msg.sender == c.offerer)
revert SelfVerification(claimId, msg.sender);
if (_verified[claimId][msg.sender])
revert AlreadyVerified(claimId, msg.sender);

_verified[claimId][msg.sender] = true;
uint256 w = weightOf(msg.sender);
c.weight += w;
emit ClaimVerified(claimId, msg.sender, w, evidenceHash);

if (c.status == Status.Offered && c.weight >= _promotionThreshold(claimId))
c.status = Status.Verified;
}

function settle(bytes32 claimId) external {
Claim storage c = _claims[claimId];
if (c.status != Status.Verified) revert BadStatus(claimId, c.status);
if (!_canSettle(claimId, msg.sender)) revert NotAuthorized(claimId, msg.sender);

c.status = Status.Settled; // effects before interaction
emit ClaimSettled(claimId);
_onSettled(claimId);
_payout(c.offerer, _releaseStake(claimId));
}

function revoke(bytes32 claimId, bytes32 reasonHash) external {
Claim storage c = _claims[claimId];
if (c.status != Status.Offered && c.status != Status.Verified)
revert BadStatus(claimId, c.status); // Settled is terminal
if (!_canRevoke(claimId, msg.sender)) revert NotAuthorized(claimId, msg.sender);

c.status = Status.Revoked; // record persists forever
emit ClaimRevoked(claimId, msg.sender, reasonHash);
_payout(BURN, _releaseStake(claimId)); // slash: burn, never redistribute
}

// ── views ────────────────────────────────────────────────────────────

function statusOf(bytes32 claimId)
external view returns (Status status, uint256 weight)
{
Claim storage c = _claims[claimId];
return (c.status, c.weight);
}

/// @inheritdoc IVerificationGate
function weightOf(address verifier) public view virtual returns (uint256);

// ── policy hooks (implementation freedom per the draft) ──────────────

/// Stake required at offer. Default: none (zero-stake gates conform).
function _requiredStake(address, bytes32, bytes calldata)
internal view virtual returns (uint256) { return 0; }

/// Accumulated verifier weight needed for Offered → Verified.
function _promotionThreshold(bytes32) internal view virtual returns (uint256) {
return 1;
}

/// Who may finalize. Default: the offerer.
function _canSettle(bytes32 claimId, address caller)
internal view virtual returns (bool)
{
return caller == _claims[claimId].offerer;
}

/// Who may revoke. Default: the offerer.
function _canRevoke(bytes32 claimId, address caller)
internal view virtual returns (bool)
{
return caller == _claims[claimId].offerer;
}

/// Post-settlement hook (e.g. update the settled party's depth).
function _onSettled(bytes32 claimId) internal virtual {}

// ── internals ────────────────────────────────────────────────────────

function _releaseStake(bytes32 claimId) private returns (uint256 amount) {
amount = _claims[claimId].stake;
_claims[claimId].stake = 0;
}

function _payout(address to, uint256 amount) private {
if (amount == 0) return;
(bool ok, ) = payable(to).call{value: amount}("");
if (!ok) revert TransferFailed();
}
}
Loading
Loading