This ERC defines an append-only interface for subject-linked compliance event records. Each record includes a subject, event type, outcome, technical actor, claimed authority, involved parties, evidence commitment, optional evidence location, versioned payload profile, operation reference, occurrence time, and recording time.
Events are indexed per subject and by event type. Corrections are recorded as new events linked to earlier records. A forward pointer on the corrected record prevents correction forks and allows consumers to resolve the terminal event in a correction chain.
This ERC is a reporting interface. It does not define compliance policy, identity verification, transfer restrictions, legal authority, or regulatory compliance. Stored records are attributable assertions, not proof that the reported action occurred or was lawful.
Compliance-relevant lifecycle actions are currently represented through application-specific events, token-local state changes, generic attestations, and off-chain databases. This fragmentation makes it difficult for contracts, indexers, auditors, and reporting systems to query comparable records across implementations.
Existing token and compliance standards primarily define token behavior, holder eligibility, transfer validation, or entity classification. Those capabilities do not provide a common stored record for subject-level lifecycle actions such as issuance, redemption, freezing, know-your-customer (KYC) status changes, regulatory holds, policy changes, and forced transfers.
A shared event-log interface provides:
The log can be called by a token, compliance module, governance executor, multisig, or other authorized recorder. It does not require any particular token standard or enforcement architecture.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
A subject is an application-defined entity identified by subjectId and,
optionally, contextualized by subjectType.
A compliance event is a stored assertion about a compliance-relevant action or state transition concerning a subject.
An actor is msg.sender at the time recordEvent is called. It identifies
the technical recorder, not necessarily the human decision-maker or legal
authority.
An authority is the recorder's claimed legal, regulatory, contractual, or governance basis for the event. The claim is not verified by the log.
A party is an EVM address associated with an explicit role in the event.
A payload profile is a versioned schema declaring how payload is encoded.
A correction chain is a linear sequence of events linked by
correctsIndex and correctedByIndex.
A terminal event is an event whose correctedByIndex equals
NO_CORRECTED_BY.
Implementations MUST use:
uint256 constant NO_CORRECTION = type(uint256).max;
uint256 constant NO_CORRECTED_BY = 0;
NO_CORRECTION indicates that an event does not correct an earlier event.
NO_CORRECTED_BY indicates that an event has no successor correction. Event
index zero is safe for this sentinel because a correction's index is always
greater than the index it corrects and therefore can never be zero.
A compliant log MUST implement:
interface IComplianceEventLog {
struct Party {
address addr;
bytes32 role;
}
struct ComplianceEvent {
bytes32 subjectId;
bytes32 subjectType;
bytes32 eventType;
bytes32 outcome;
address actor;
bytes32 authority;
Party[] parties;
bytes32 evidenceHash;
string evidenceURI;
bytes32 payloadProfileId;
bytes payload;
bytes32 operationRef;
uint64 occurredAt;
uint64 recordedAt;
uint256 correctsIndex;
uint256 correctedByIndex;
}
event ComplianceEventRecorded(
bytes32 indexed subjectId,
bytes32 indexed eventType,
address indexed actor,
uint256 eventIndex,
bytes32 outcome,
bytes32 authority,
uint64 occurredAt,
uint256 correctsIndex
);
function recordEvent(
bytes32 subjectId,
bytes32 subjectType,
bytes32 eventType,
bytes32 outcome,
bytes32 authority,
Party[] calldata parties,
bytes32 evidenceHash,
string calldata evidenceURI,
bytes32 payloadProfileId,
bytes calldata payload,
bytes32 operationRef,
uint64 occurredAt,
uint256 correctsIndex
) external returns (uint256 eventIndex);
function getEvent(
bytes32 subjectId,
uint256 eventIndex
) external view returns (ComplianceEvent memory);
function currentEventIndex(
bytes32 subjectId,
uint256 eventIndex
) external view returns (uint256);
function isEventCurrent(
bytes32 subjectId,
uint256 eventIndex
) external view returns (bool);
function eventCount(
bytes32 subjectId
) external view returns (uint256);
function eventCountByType(
bytes32 subjectId,
bytes32 eventType
) external view returns (uint256);
function eventByTypeAt(
bytes32 subjectId,
bytes32 eventType,
uint256 ordinal
) external view returns (uint256 eventIndex);
function lastRecordedEventByType(
bytes32 subjectId,
bytes32 eventType
) external view returns (uint256 eventIndex);
}
Event indices MUST be zero-based and scoped per subjectId. The returned
eventIndex MUST equal the subject's event count immediately before the new
event is appended.
recordEvent MUST be restricted to authorized recorders. The authorization
mechanism is implementation defined and MUST be documented.
For every accepted event, the implementation MUST:
actor to msg.sender;recordedAt to uint64(block.timestamp);correctedByIndex to NO_CORRECTED_BY;ComplianceEventRecorded; andThe implementation MUST reject the call if block.timestamp cannot be
represented as uint64.
evidenceHash MUST NOT be bytes32(0). evidenceURI MAY be empty when the
evidence location is private, unavailable on-chain, or exchanged out of band.
An empty URI does not weaken the requirement for a nonzero commitment.
The parties array MUST contain no more than 10 entries. payload MUST contain
no more than 2048 bytes. Empty party arrays and empty payloads are allowed.
This ERC does not require nonzero values for subjectId, subjectType,
eventType, outcome, authority, operationRef, party addresses, party
roles, or payloadProfileId. Applications requiring stricter semantics MUST
enforce and document them before calling recordEvent.
occurredAt represents when the reported action occurred. recordedAt
represents when the record was appended on-chain.
recordEvent MUST revert when occurredAt > block.timestamp.
Implementations SHOULD impose and document a maximum backdating interval. Different deployments can require different intervals, so the duration is not standardized by this ERC.
The log does not independently verify occurredAt. It is an assertion by the
recorder.
Once recorded, every event field MUST remain immutable except
correctedByIndex. Events MUST NOT be deleted.
Updating correctedByIndex is permitted only when accepting a valid correction
under the correction rules below.
An original or non-correction event MUST use NO_CORRECTION and MUST NOT use
EVT_CORRECTION, which is defined in the Event Types section below:
correctsIndex = NO_CORRECTION
eventType != EVT_CORRECTION
A correction event MUST use:
correctsIndex = index of the corrected event
eventType = EVT_CORRECTION
For a correction, recordEvent MUST:
correctsIndex to identify an earlier event under the same
subjectId;correctedByIndex to equal
NO_CORRECTED_BY;correctedByIndex to the new correction event index; andThe correction policy MUST NOT permit an ordinary recorder to correct another actor's event merely because both addresses can record events. It MAY authorize the original actor, a designated corrector, or an administrator. The policy MUST be documented.
Each event can be corrected at most once, preventing forks. A correction event can itself be corrected later, producing a linear chain.
The correcting record does not rewrite the original event's event type or payload. Recorders MUST place the corrected assertion in the new event's fields or an application-defined correction payload. Consumers MUST interpret the terminal event under the applicable profile and application policy.
currentEventIndex MUST revert when eventIndex >= eventCount(subjectId).
Otherwise, it MUST follow correctedByIndex until reaching
NO_CORRECTED_BY and return the terminal event index. Calling it on a terminal
event MUST return the supplied index.
isEventCurrent MUST revert when eventIndex >= eventCount(subjectId) and
otherwise return whether correctedByIndex == NO_CORRECTED_BY.
Because corrections always point to earlier events and each event has at most one successor, conforming correction chains cannot contain cycles or branches.
getEvent MUST return the complete stored event and MUST revert when
eventIndex >= eventCount(subjectId).
eventCount MUST return the number of events stored under a subject.
eventCountByType MUST return the number of events recorded with the exact
eventType under a subject.
eventByTypeAt MUST return the event index at the specified zero-based ordinal
and MUST revert when the ordinal is outside the type-specific index.
lastRecordedEventByType MUST return the greatest event index recorded with
the exact event type and MUST revert when no matching event exists.
lastRecordedEventByType describes recording order. It does not select the
event with the greatest occurredAt and does not resolve correction chains.
Correction events are indexed under EVT_CORRECTION, not under the event type
they correct. Consumers resolving an earlier event MUST use
currentEventIndex rather than assuming the last event of the original type is
its current state.
subjectId and subjectType are opaque to the log. Applications SHOULD use a
documented, domain-separated derivation and MUST NOT assume that equal subject
identifiers from independent logs have equal meaning without an explicit
coordination agreement.
The following subject-type identifiers are defined:
bytes32 constant SUBJECT_TOKEN =
keccak256("ERC-8328:SUBJECT_TYPE:TOKEN");
bytes32 constant SUBJECT_ADDRESS =
keccak256("ERC-8328:SUBJECT_TYPE:ADDRESS");
bytes32 constant SUBJECT_ASSET =
keccak256("ERC-8328:SUBJECT_TYPE:ASSET");
bytes32 constant SUBJECT_CASE =
keccak256("ERC-8328:SUBJECT_TYPE:CASE");
Applications MAY define custom subject types using a documented namespace and version.
The following event-type identifiers are defined:
bytes32 constant EVT_ISSUANCE =
keccak256("ERC-8328:EVENT_TYPE:ISSUANCE:V1");
bytes32 constant EVT_TRANSFER =
keccak256("ERC-8328:EVENT_TYPE:TRANSFER:V1");
bytes32 constant EVT_REDEMPTION =
keccak256("ERC-8328:EVENT_TYPE:REDEMPTION:V1");
bytes32 constant EVT_FREEZE =
keccak256("ERC-8328:EVENT_TYPE:FREEZE:V1");
bytes32 constant EVT_UNFREEZE =
keccak256("ERC-8328:EVENT_TYPE:UNFREEZE:V1");
bytes32 constant EVT_FORCED_TRANSFER =
keccak256("ERC-8328:EVENT_TYPE:FORCED_TRANSFER:V1");
bytes32 constant EVT_KYC_APPROVED =
keccak256("ERC-8328:EVENT_TYPE:KYC_APPROVED:V1");
bytes32 constant EVT_KYC_REVOKED =
keccak256("ERC-8328:EVENT_TYPE:KYC_REVOKED:V1");
bytes32 constant EVT_KYC_UPDATED =
keccak256("ERC-8328:EVENT_TYPE:KYC_UPDATED:V1");
bytes32 constant EVT_REGULATORY_HOLD =
keccak256("ERC-8328:EVENT_TYPE:REGULATORY_HOLD:V1");
bytes32 constant EVT_HOLD_RELEASED =
keccak256("ERC-8328:EVENT_TYPE:HOLD_RELEASED:V1");
bytes32 constant EVT_ALLOWLIST_ADDED =
keccak256("ERC-8328:EVENT_TYPE:ALLOWLIST_ADDED:V1");
bytes32 constant EVT_ALLOWLIST_REMOVED =
keccak256("ERC-8328:EVENT_TYPE:ALLOWLIST_REMOVED:V1");
bytes32 constant EVT_POLICY_CHANGE =
keccak256("ERC-8328:EVENT_TYPE:POLICY_CHANGE:V1");
bytes32 constant EVT_CORRECTION =
keccak256("ERC-8328:EVENT_TYPE:CORRECTION:V1");
EVT_TRANSFER is intended for compliance-significant transfer records, not as
a replacement for a token's ordinary transfer event. Applications SHOULD avoid
duplicating every routine token transfer unless the additional compliance
record is required by their reporting policy.
Custom event types SHOULD use a domain-separated namespace and explicit version.
The following party-role identifiers are defined:
bytes32 constant ROLE_SENDER =
keccak256("ERC-8328:PARTY_ROLE:SENDER");
bytes32 constant ROLE_RECEIVER =
keccak256("ERC-8328:PARTY_ROLE:RECEIVER");
bytes32 constant ROLE_TARGET =
keccak256("ERC-8328:PARTY_ROLE:TARGET");
bytes32 constant ROLE_BENEFICIARY =
keccak256("ERC-8328:PARTY_ROLE:BENEFICIARY");
bytes32 constant ROLE_CONTROLLER =
keccak256("ERC-8328:PARTY_ROLE:CONTROLLER");
bytes32 constant ROLE_SUBJECT =
keccak256("ERC-8328:PARTY_ROLE:SUBJECT");
Custom party roles SHOULD use a documented namespace and version.
The base Party type contains an EVM address. It cannot carry an arbitrary
hashed identity without changing the interface. Applications needing private
or non-address party identifiers require a separate extension or SHOULD omit
those parties from the public record.
The following outcome identifiers are defined:
bytes32 constant OUTCOME_APPROVED =
keccak256("ERC-8328:OUTCOME:APPROVED");
bytes32 constant OUTCOME_DENIED =
keccak256("ERC-8328:OUTCOME:DENIED");
bytes32 constant OUTCOME_PENDING =
keccak256("ERC-8328:OUTCOME:PENDING");
bytes32 constant OUTCOME_EXECUTED =
keccak256("ERC-8328:OUTCOME:EXECUTED");
bytes32 constant OUTCOME_EXPIRED =
keccak256("ERC-8328:OUTCOME:EXPIRED");
bytes32 constant OUTCOME_REVOKED =
keccak256("ERC-8328:OUTCOME:REVOKED");
This ERC does not define or enforce an event-type and outcome compatibility matrix. Applications MAY constrain combinations before recording. Consumers MUST NOT infer that a combination was validated merely because the log accepted it.
The following common authority identifiers are defined:
bytes32 constant AUTHORITY_INTERNAL_POLICY =
keccak256("ERC-8328:AUTHORITY:INTERNAL_POLICY:V1");
bytes32 constant AUTHORITY_COURT_ORDER =
keccak256("ERC-8328:AUTHORITY:COURT_ORDER:V1");
bytes32 constant AUTHORITY_REGULATOR =
keccak256("ERC-8328:AUTHORITY:REGULATOR:V1");
Custom authority identifiers SHOULD use a documented namespace and version. The field records a claim and does not authenticate the named authority.
The following payload-profile identifiers and ABI encodings are defined:
bytes32 constant PAYLOAD_TRANSFER_V1 =
keccak256("ERC-8328:PAYLOAD:TRANSFER:V1");
// abi.encode(
// address from,
// address to,
// uint256 amount,
// bytes32 routeRef
// )
bytes32 constant PAYLOAD_FREEZE_V1 =
keccak256("ERC-8328:PAYLOAD:FREEZE:V1");
// abi.encode(
// address target,
// uint256 amount,
// uint64 expiresAt,
// bytes32 reason
// )
bytes32 constant PAYLOAD_KYC_V1 =
keccak256("ERC-8328:PAYLOAD:KYC:V1");
// abi.encode(
// address subject,
// bytes32 jurisdiction,
// bytes32 riskTier,
// uint64 expiresAt
// )
bytes32 constant PAYLOAD_FORCED_TRANSFER_V1 =
keccak256("ERC-8328:PAYLOAD:FORCED_TRANSFER:V1");
// abi.encode(
// address from,
// address to,
// uint256 amount,
// bytes32 legalBasis
// )
A recorder declaring one of these profiles MUST encode the payload exactly as
specified. Consumers MUST inspect payloadProfileId before decoding.
The log is not required to decode payloads or validate compatibility between a payload profile, event type, parties, and outcome. Consumers SHOULD reject a malformed known profile. Unknown profile identifiers MUST be treated as opaque bytes.
Custom payload profiles SHOULD use a documented namespace and version and MUST define an exact encoding.
operationRef is an application-defined correlation identifier linking the
record to an underlying action or workflow. It MAY be bytes32(0) when no such
reference is available.
A contract cannot access its transaction hash or final log index while
executing. Therefore, this ERC does not require a transaction-hash and log-index
derivation. Applications SHOULD compute a correlation identifier before the
underlying action and recordEvent calls when both occur in one transaction.
Recording an event in the same transaction as the underlying action provides stronger linkage than recording it later, but the log still does not prove that the record accurately describes that action.
Compliant logs MUST implement ERC-165 and return true for
type(IComplianceEventLog).interfaceId.
ERC-165 indicates interface support only. It does not establish recorder trustworthiness, evidence validity, authority, policy correctness, or legal compliance.
An address-only or token-only key would exclude projects, assets, cases, policies, and application-defined entities. An opaque subject identifier allows one query model while leaving identity and namespace semantics to the application.
The technical account recording an event and the claimed basis for the action are different facts. A module can execute several actions under different mandates, while multiple modules can act under the same mandate.
An untyped address list cannot distinguish a sender, receiver, target, beneficiary, or controller. Explicit roles improve machine interpretation while keeping the set extensible.
Every record should commit to the evidence representation used by the recorder, but public retrieval can be inappropriate for confidential or regulated data. A nonzero hash preserves the commitment while an optional URI permits private distribution.
Opaque bytes without a declared schema prevent interoperable decoding. Versioned profile identifiers let consumers recognize stable base encodings and safely preserve unknown custom payloads.
Replacing an event would erase the prior assertion. A correction chain retains the full history and identifies the current terminal record. The forward pointer and single-successor rule prevent competing corrections to the same event.
A correction is a distinct lifecycle action. Indexing it under
EVT_CORRECTION preserves the original event-type history and lets consumers
query correction activity directly. Chain-resolution helpers provide current
state when needed.
EVM contracts cannot read historical logs. Storing records allows on-chain consumers to retrieve event details, traverse correction chains, and iterate by event type. Emitted events remain useful for off-chain indexing.
The same event and outcome identifiers can be used under different legal and application policies. The log standardizes representation and provenance, not the rule engine deciding which combinations are valid.
ERC-8106 defines ERC-20 value-flow
observations, Compliance Entity and Decentralized Entity (CE/DE)
classification, compliance flags, and bizId correlation. This ERC instead
stores subject-linked lifecycle records with authority and evidence fields,
versioned payloads, type indexing, and correction chains. It does not define
CE/DE classification or require each record to correspond to an ERC-20 balance
change.
ERC-3643 defines regulated-token behavior, identity registries, compliance modules, and lifecycle operations. This ERC is a reporting layer that such a system may call; it does not replace enforcement or identity checks.
ERC-7943 defines real-world asset (RWA) token behavior including transfer eligibility, freezing, and forced transfers. This ERC records attributed lifecycle assertions independently of any token interface.
ERC-7512 defines an on-chain representation of audit reports. This ERC instead defines a subject-indexed compliance-event timeline and correction model.
ERC-5851 defines on-chain verifiable credentials. Credentials can authorize or identify a recorder, but they do not define this event-log schema.
Generic attestation systems can represent compliance assertions through custom schemas. This ERC defines a dedicated stored interface, base identifiers, payload profiles, indexing behavior, and correction provenance.
This ERC introduces a new interface and does not modify existing token, identity, attestation, or compliance standards.
An existing token or compliance module can call a companion
IComplianceEventLog without changing its base token interface. Systems that do
not integrate with the log are unaffected.
The subject identifier is application defined, so adoption does not require a particular asset registry or token standard.
Implementations should test at least:
msg.sender;currentEventIndex from original, intermediate, and terminal events;isEventCurrent for corrected and terminal events;lastRecordedEventByType;EVT_CORRECTION;A Solidity reference implementation, constants library, unit tests, Medusa property tests, and independent audit are linked from the official discussion thread.
The reference implementation:
Role design and the 30-day backdating window are reference deployment choices. The size limits and externally observable interface behavior are requirements of this ERC.
The log proves that an authorized address recorded particular bytes. It does not prove that the record is true. A compromised, malicious, or incorrectly authorized recorder can submit false or misleading events.
authority is self-asserted. A recorder can claim a court order, regulator, or
internal policy that does not exist or does not authorize the action. Consumers
must verify authority evidence independently.
A compliance event is not proof that the underlying issuance, transfer, freeze, KYC decision, or other action occurred. Consumers requiring that proof must verify the referenced operation and its relationship to the record.
Fork prevention does not determine who is entitled to correct a record. A weak correction policy can let one recorder supersede another recorder's assertions. Implementations must document and enforce correction authority.
currentEventIndex traverses on-chain correction pointers. Although chains are
linear and acyclic, a long chain can consume substantial gas or make an on-chain
call impractical. Applications should avoid unnecessary repeated corrections
and may resolve long histories off-chain.
occurredAt is supplied by the recorder. Rejecting future timestamps prevents
one class of invalid input but does not establish historical accuracy. A
documented backdating limit reduces, but does not eliminate, fabricated history.
All event fields, dynamic payloads, party addresses, and URIs stored on a public chain are permanently observable. Implementations must not place personal, confidential, investigative, or legally restricted information on-chain merely because the interface permits it.
Public-chain deployments should use opaque or salted subject identifiers, minimal party arrays, generalized outcomes, redacted payloads, and evidence commitments whose preimages are distributed through appropriate access controls. Hashing low-entropy personal data without a secret salt does not provide meaningful privacy.
A nonzero evidence hash does not make evidence available or identify the hashing and document scheme by itself. Implementations must document how evidence commitments are derived and how authorized consumers obtain the preimage.
The log can store malformed known payloads and arbitrary unknown profiles. A
consumer that decodes without first checking payloadProfileId can
misinterpret attacker-controlled bytes. Consumers must use profile-aware
decoding and reject malformed known profiles.
The base log does not validate event-type and outcome combinations. Consumers must not treat a stored combination as policy-approved unless the recorder's application enforces the applicable matrix.
The log is append-only and grows monotonically. Authorization, bounded dynamic fields, reporting-frequency policy, and operational monitoring are necessary to control storage costs and spam.
Off-chain indexers can reconstruct timelines from events, but on-chain contracts cannot read historical logs. On-chain consumers must use storage getters. Indexers should reconcile events with storage when resolving correction chains and current state.
Copyright and related rights waived via CC0.