ERC-8325 자산 앵커 레지스트리 (Asset Anchor Registry)
Final · 토큰 · 2026-07-04 작성
토큰 컨트랙트나 토큰 ID를 오프체인 자산 주장 기록(앵커)에 묶어 두는 레지스트리 인터페이스입니다. 어떤 토큰이 어떤 기록에 묶였고 지금도 유효한지를 공통 방식으로 조회할 수 있게 합니다.
왜 필요한가
토큰은 메타데이터에 블록체인 밖(오프체인, off-chain) 자산을 나타낸다고 적을 수 있지만 다른 컨트랙트가 그 주장을 확인할 공통 인터페이스가 없습니다. 토큰이 어떤 기록에 묶였다고 주장하는지, 레지스트리도 같은 관계를 기록하고 있는지, 그 결속이 해당 레지스트리 안에서 배타적인지, 컨트랙트 전체에 적용되는지 특정 토큰 ID에만 적용되는지, 기록이 활성인지 만료·중단·무효화되었는지를 조회할 방법이 없습니다. 그래서 같은 자산 주장도 발행자마다 다르게 표현되고 소비자는 구현마다 다른 메타데이터와 레지스트리에 의존해야 합니다. 기관이나 규제 감독을 받는 배포는 어떤 토큰이 언제 어떤 자산 주장에 기록되었고 지금도 유효한지에 대한 지속적인 답이 필요하지만 주장만 있는 토큰화(assertion-only tokenization)로는 독립적인 소비자가 공통 인터페이스로 그 관계를 검증할 수 없습니다.
구현해야 하는 것
event AnchorDeactivated(bytes32 indexed anchorId, string reason);: 앵커가 영구 중단될 때 사유와 함께 발생합니다.function getAnchor(bytes32 anchorId) external view returns (AnchorRecord memory);: 저장된 앵커 기록 전체를 돌려주고 모르는 앵커 ID면 되돌립니다(revert).function isBound(bytes32 anchorId) external view returns (bool);: 앵커에 토큰이 결속되어 있는지를 돌려줍니다. 수명주기나 무효화와 무관한 결속 여부만 봅니다.function getMetadata(bytes32 anchorId) external view returns (AnchorMetadata memory);: 앵커의 메타데이터(자산 분류, 관할, 증명 일자, 만료 시점, URI 등)를 돌려줍니다.function registeredBy(bytes32 anchorId) external view returns (address);: 앵커를 등록한 주소를 돌려주며 등록 후 바뀌지 않습니다.function isActive(bytes32 anchorId) external view returns (bool);: 앵커가 중단되지 않았고 만료 전인지를 돌려줍니다.function isBindingValid(bytes32 anchorId) external view returns (bool);: 결속되어 있고 무효화되지 않은 경우에만 true를 돌려줍니다.function anchorId() external view returns (bytes32);: 컨트랙트 전체 결속 토큰이 자신이 묶인 앵커 ID를 선언합니다. 배포 후 바뀌지 않습니다.function anchorRegistry() external view returns (address);: 토큰이 자신이 결속을 선언한 레지스트리 주소를 돌려줍니다. 배포 후 바뀌지 않습니다.function isAnchorActive() external view returns (bool);: 선언한 레지스트리 기준으로 앵커가 활성인지를 반영합니다.function anchorIdOf(uint256 tokenId) external view returns (bytes32);: 특정 토큰 ID가 묶인 앵커 ID를 돌려주고 결속되지 않은 토큰 ID면 되돌립니다.function isAnchorActiveFor(uint256 tokenId) external view returns (bool);: 특정 토큰 ID의 앵커가 활성인지를 선언한 레지스트리 기준으로 반영합니다.
심층 설명
앵커와 두 개의 해시 커밋
앵커는 오프체인 자산 주장에 대한 커밋과 메타데이터를 담는 레지스트리 기록입니다. 등록 시 legalHash(법적 근거)와 evidenceHash(증빙)를 분리해 받으며 둘 다 bytes32(0)이면 거부해야 합니다. 앵커 ID는 anchorId = keccak256(abi.encode(legalHash, evidenceHash))로 파생되므로 같은 해시 쌍은 구현이 달라도 같은 ID를 만들고 이미 등록된 anchorId의 재등록은 거부해야 합니다. 등록 성공 시 registeredAt을 uint64(block.timestamp)로 저장하고 boundToken은 address(0), bindingScope는 bytes32(0), boundTokenId는 0, active는 true로 초기화한 뒤 AnchorRegistered를 발생시킵니다. 등록 권한 부여 방식은 구현이 정하되 그 정책을 문서화해야 합니다.
결속 범위와 토큰 ID 0 문제
결속 범위는 두 가지 상수로 구분합니다. BINDING_SCOPE_CONTRACT는 keccak256("ERC-8325:BINDING_SCOPE:CONTRACT"), BINDING_SCOPE_TOKEN_ID는 keccak256("ERC-8325:BINDING_SCOPE:TOKEN_ID")입니다. 컨트랙트 범위에서는 tokenId가 사용하지 않는 정식 값으로서 반드시 0이어야 하고 토큰 ID 범위에서는 0을 포함한 모든 uint256 값이 유효합니다. 토큰 ID 0을 컨트랙트 결속의 신호값으로 해석해서는 안 됩니다. 범위를 명시적으로 두는 이유는 토큰 ID 0을 실제 ERC-721이나 ERC-1155 토큰으로 결속하는 경우와 컨트랙트 전체 결속을 구분하기 위해서입니다.
메타데이터 인코딩과 검증 규칙
등록 메타데이터는 AnchorMetadata 구조체(assetClass, jurisdiction, attestationDate, expiresAt, uri, extensions)를 순서대로 abi.encode한 것이어야 합니다. assetClass와 jurisdiction은 bytes32(0)이 아니어야 하고 attestationDate는 0이 아니며 block.timestamp보다 늦을 수 없습니다. expiresAt은 attestationDate보다 늦어야 하고 등록 시점의 block.timestamp보다 이르면 안 됩니다. uri는 비어 있으면 안 되고 extensions는 비어 있어도 됩니다. assetClass는 문서화된 분류 체계의 도메인 구분 식별자를, 주 관할이 한 국가인 경우 jurisdiction은 대문자 ISO 3166-1 alpha-2 코드에서 파생한 식별자를 쓰는 것이 권장됩니다. URI는 커밋에 대응하는 자료의 위치를 가리킬 뿐, 특정 스킴이나 가용성을 보장하지 않습니다.
결속의 배타성과 이력 불변성
bindToken은 모르는 앵커, 비활성이거나 만료된 앵커, 0 토큰 주소, 지원하지 않는 범위, 이미 결속 필드가 설정된 앵커를 거부해야 합니다. 결속 전에 같은 결속 튜플 (token, bindingScope, tokenId)에 유효한 앵커가 이미 연결되어 있지 않은지 확인해야 하며 고유성 키는 keccak256(abi.encode(token, bindingScope, tokenId))로 파생하는 것이 권장됩니다. 결속 성공 시 boundToken, bindingScope, boundTokenId를 설정하고 TokenBound를 발생시키며 이 세 이력 필드는 중단이나 무효화 이후를 포함해 다시는 수정될 수 없습니다. registerAndBind는 같은 요건을 원자적으로 적용합니다. 토큰이 anchorRegistry()를 노출하면 그 반환 주소는 결속을 수행하는 레지스트리와 같아야 하고 토큰 쪽 인터페이스가 없는 토큰도 결속할 수 있지만 그 기록은 레지스트리 단독 결속일 뿐 상호 선언 결속이 아닙니다.
수명주기: 활성, 만료, 재증명
모든 준수 레지스트리는 수명주기 인터페이스를 구현해야 합니다. isActive는 AnchorRecord.active가 false이거나 block.timestamp > expiresAt이면 false를 돌려주며 정확히 expiresAt 시점까지는 활성입니다. deactivateAnchor는 권한 있는 호출자만 쓸 수 있고 active를 false로 만들며 수동 중단은 영구적이어서 reattest로 되돌릴 수 없습니다. reattest는 수동 중단된 앵커를 거부해야 하고 newAttestationDate는 0이 아니고 block.timestamp보다 늦지 않으며 기존 attestationDate보다 이르면 안 됩니다. newExpiresAt은 block.timestamp와 newAttestationDate보다 늦어야 하고 기존 expiresAt보다 이르면 안 됩니다. 즉 재증명으로 만료를 앞당길 수는 없습니다.
선택적 복구 인터페이스
분쟁이 생긴 결속을 무효화할 수 있는 레지스트리는 IAssetAnchorRegistryRecovery를 구현해야 하며 복구 자체는 선택 사항입니다. invalidateTokenBinding은 권한 있는 복구 역할로 제한되고 모르거나 결속되지 않았거나 이미 무효화된 앵커와 0인 reasonHash를 거부해야 합니다. 무효화에 성공하면 boundToken, bindingScope, boundTokenId를 보존한 채 isBindingValid가 false가 되고 앵커는 영구 중단되며 활성이었다면 AnchorDeactivated를, 그리고 TokenBindingInvalidated를 발생시킵니다. 구현은 해당 결속 튜플을 해제해 다른 앵커가 묶이게 할 수 있지만 무효화된 기록은 계속 조회 가능해야 하고 그 기록 자체를 다시 결속해서는 안 됩니다. 복구는 상당한 관리 신뢰를 요구하므로 최소 레지스트리 인터페이스에 포함되지 않았습니다.
상호 선언 결속의 6가지 검증
소비자가 결속을 상호 선언된 것으로 취급하려면 여섯 가지를 모두 확인해야 합니다. getAnchor가 기대한 토큰·범위·토큰 ID를 돌려주는지, isBound가 true인지, isActive가 true인지, 복구가 구현된 경우 isBindingValid가 true인지, 토큰이 해당 토큰 쪽 인터페이스를 지원하는지, 토큰이 같은 레지스트리와 앵커를 보고하는지입니다. 하나라도 실패하면 현재의 상호 선언 결속으로 취급해서는 안 됩니다. 준수 레지스트리는 ERC-165를 구현해 레지스트리와 수명주기 인터페이스 ID에 true를 돌려줘야 하고 복구를 구현하면 그 ID에도 true를 돌려줘야 합니다. ERC-165는 인터페이스 지원만 알려줄 뿐 올바른 동작이나 오프체인 주장의 유효성을 증명하지 않습니다.
이 표준이 보증하지 않는 것
보증 범위는 의도적으로 레지스트리 한 인스턴스로 제한됩니다. 독립된 레지스트리나 다른 체인의 배포가 같은 오프체인 자산에 대한 주장을 각각 등록할 수 있고 전역 고유성이나 레지스트리 간 충돌 해결은 제공되지 않습니다. anchorId에 체인 식별자나 레지스트리 주소가 포함되지 않으므로 전역 식별이 필요한 소비자는 (chainId, registry, anchorId) 조합을 써야 합니다. 거버넌스도 열어 두어 단일 발행자, 규제 등록기관, 다중서명, DAO, 무허가 프로토콜 모두 운영할 수 있으며 어떤 레지스트리를 신뢰할지는 소비자가 선택합니다. 발행량이나 지분 비율 같은 배분 제약도 표준화하지 않고 토큰과 응용 컨트랙트의 책임으로 남깁니다. 이 ERC는 오프체인 자산의 실존, 소유권, 법적 유효성, 가치를 성립시키지 않습니다.
쓸 때 주의할 점
- 레지스트리 신뢰: 레지스트리는 자기 상태가 구조 규칙을 만족한다는 것만 증명합니다. 악의적이거나 침해된 등록자는 형식상 유효한 해시와 메타데이터로 거짓 주장을 등록할 수 있으므로, 소비자는 운영자, 권한 정책, 업그레이드 권한, 법적 맥락을 평가해야 합니다.
- 법적·물리적 사실 보증 없음: 해시나 상호 선언 결속은 오프체인 자산이 존재한다거나 문서가 진본이라거나 토큰이 법적 권리를 부여한다는 것을 증명하지 않습니다. 그런 판단은 외부 검증이 필요합니다.
- 등록과 결속 사이의 경쟁: 등록과 결속이 분리되면 그 사이에 무권한 호출자가 결속을 시도할 틈이 생깁니다. 구현은 결속을 권한 있는 당사자로 제한해야 하고 원자성이 필요한 등록자는 registerAndBind를 써야 합니다.
- 악성 토큰과 인터페이스 사칭: 토큰은 토큰 쪽 인터페이스에서 임의 값을 돌려주거나 ERC-165 지원을 거짓으로 주장할 수 있습니다. 소비자는 레지스트리 기록을 독립적으로 확인해야 하고 결속 중 토큰을 호출하는 레지스트리는 정적 호출을 쓰고 비정상 반환과 되돌림을 처리해야 합니다.
- 복구 권한: 복구 관리자는 정당한 결속도 무효화할 수 있고 튜플을 해제하는 구현에서는 대체 앵커 결속이 가능해집니다. 운영 배포는 복구 권한을 다중서명, 거버넌스, 타임락 등으로 보호하고 소비자는 TokenBindingInvalidated와 AnchorDeactivated 이벤트를 감시해야 합니다.