ERC-7518 동적 컴플라이언스 증권형 토큰 (Dynamic Compliant Interop Security Token)
Review · 토큰 · 2023-09-14 작성
ERC-1155를 확장해 한 컨트랙트 안에서 파티션별로 권리와 규제 규칙이 다른 증권형 토큰을 관리하는 표준입니다. 토큰 잠금, 주소 동결, 강제 이전, 배당 지급, 서명 바우처 기반 규제 준수 검사를 제공합니다.
왜 필요한가
부동산이나 펀드 같은 실물 자산을 토큰으로 만들려면 증권 규제를 지켜야 합니다. 투자자 자격에 따라 전송을 막거나, 보유 기간 동안 토큰을 잠그거나, 키를 잃어버린 투자자의 토큰을 회수하거나, 규제 위반 계정을 동결하는 기능이 필요합니다. 그런데 기존 토큰 표준에는 이런 기능이 표준화되어 있지 않아 발행사마다 제각각 구현해야 하고 지갑이나 거래 서비스가 공통 방식으로 연동하기 어렵습니다. 또 같은 자산이라도 미국 공인 투자자용과 해외 투자자용처럼 규제가 다른 투자자 집단별로 토큰을 나눠 관리해야 하는데, 집단마다 별도 컨트랙트를 배포하면 비용이 늘고 관리가 복잡해집니다. 이 표준은 하나의 컨트랙트 안에서 tokenId별로 구획(파티션)을 나누고 구획마다 다른 규제 규칙을 적용하며 규제 준수 기능을 공통 인터페이스로 제공해 이 문제를 풉니다.
구현해야 하는 것
event TokensLocked(address indexed account, uint indexed id, uint256 amount, uint256 releaseTime);: 계정의 토큰이 특정 해제 시각까지 잠겼을 때 발생하는 이벤트입니다.event TokensForceTransferred(address indexed from, address indexed to, uint indexed id, uint256 amount);: 관리자가 토큰을 강제로 이전했을 때 발생하는 이벤트입니다.event AddressFrozen(address indexed account, bytes data);: 계정이 동결되었을 때 발생하는 이벤트입니다.event PayoutDelivered(address indexed from, address indexed to, uint256 amount);: 배당 등 지급이 전달되었을 때 발생하는 이벤트입니다.function transferableBalance(address account, uint id) external view returns (uint);: 계정의 해당 토큰 ID 잔액 중 지금 전송할 수 있는 양을 돌려줍니다. 전체 잔액에서 잠긴 잔액을 뺀 값입니다.function lockedBalanceOf(address account, uint256 id) external view returns (uint256);: 계정의 해당 토큰 ID에서 잠겨 있는 잔액을 돌려줍니다.function restrictTransfer(uint id) external returns (bool);: 특정 토큰 ID 전체의 전송을 제한합니다.function removeRestriction(uint id) external returns (bool);: 토큰 ID에 걸린 전송 제한을 해제합니다. 제한된 적이 없는 ID면 되돌립니다.function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) override external;: 토큰을 전송합니다. 전송 전에 반드시 canTransfer를 호출해 전송 가능 여부를 확인해야 합니다.function canTransfer(address from, address to, uint id, uint amount, bytes calldata data) external view returns (bool status);: 전송이 허용되는지 검사합니다. 양쪽 주소의 동결 여부, 토큰 ID의 제한 여부, 전송 가능 잔액을 확인합니다.function lockTokens(address account, uint id, uint256 amount, uint256 releaseTime) external returns (bool);: 계정의 토큰 일정량을 지정한 해제 시각까지 잠급니다.function unlockToken(address account, uint256 id) external;: 해제 시각이 지난 잠긴 토큰을 모두 풉니다.function forceTransfer(address from, address to, uint256 id, uint256 amount, bytes memory data) external returns (bool);: 일반 전송 제한을 우회해 토큰을 강제로 이전합니다. 키 분실 복구나 규제 조치에 씁니다.function freezeAddress(address account, bytes calldata data) external returns (bool);: 계정을 동결해 전송과 지급 수령을 막습니다.function unFreeze(address account, bytes memory data) external returns (bool);: 동결된 계정을 해제합니다.function payout(address calldata to, uint256 calldata amount) public returns (bool);: 한 주소에 배당이나 이자 같은 지급을 보냅니다. 받는 주소가 동결 상태면 되돌립니다.function batchPayout(address[] calldata to, uint256[] calldata amount) public returns (bool);: 여러 주소에 각각의 금액을 한 번의 트랜잭션으로 지급합니다.
심층 설명
tokenId를 파티션으로 쓰는 구조
ERC-1155의 tokenId 하나하나를 독자적인 권리와 규칙을 가진 파티션으로 씁니다. 같은 tokenId의 토큰끼리는 서로 대체 가능하고 다른 tokenId와는 대체 불가능합니다. 발행사는 주식 종류, 관할 규제, 발행 회차 등을 서로 다른 tokenId로 발행해 구분합니다. 원문이 드는 예로, 미국 공인 투자자용(Reg D)과 비미국 투자자용(Reg S) 파티션을 나눠 투자자 자격에 따라 해당 파티션에 토큰을 발행하고, 보유 기간이 끝나면 새 통합 파티션을 만들어 기존 파티션 토큰을 예치하고 통합 토큰을 받는 방식으로 합칠 수 있습니다. 상업용 부동산이라면 층마다 파티션을 두고 각 파티션을 여러 토큰으로 쪼개 지분 소유를 표현합니다.
잠금과 전송 가능 잔액
lockTokens는 계정의 토큰 일정량을 releaseTime까지 잠급니다. 보유 잔액보다 많은 양을 잠그려 하면 되돌려야 하고 권한 있는 주체만 호출할 수 있어야 합니다. transferableBalance는 balanceOf(account, id) - lockedBalanceOf(account, id)로 계산한 전송 가능 잔액을 돌려줘야 합니다. unlockToken은 해제 시각이 block.timestamp 이하가 된 토큰을 모두 풀어야 하며 풀 토큰이 없으면 가스를 아끼기 위해 되돌리는 것이 권장됩니다. 베스팅이나 의무 보유 기간 구현에 쓰입니다.
canTransfer 검사 규칙
canTransfer는 전송 허용 여부를 판단하는 view 함수입니다. from과 to가 동결 주소가 아닌지, 해당 id가 전송 제한 상태가 아닌지, amount가 전송 가능 잔액 안인지 반드시 확인해야 하고 필요하면 외부 컨트랙트를 호출해 검증할 수 있습니다. 상태를 바꾸거나 부수 효과를 내서는 안 됩니다. safeTransferFrom은 전송 진행 전에 반드시 canTransfer를 호출해야 하며 받는 주소가 영 주소이거나 잔액이 부족하면 되돌려야 합니다. 바우처를 쓰는 구현은 data 인자로 받은 서명 바우처의 서명, 만료, from·to·id·amount 결속을 함께 검증합니다.
강제 이전과 주소 동결
forceTransfer는 일반 전송 제한과 승인 검사를 우회해 토큰을 옮깁니다. from 주소가 동결 상태가 아니면 되돌려야 하고 to 주소가 동결 상태여도 되돌려야 합니다. 즉 먼저 freezeAddress로 문제 계정을 동결한 뒤에 강제 이전하는 순서입니다. 원문은 자금세탁 등 혐의가 확인된 투자자의 계정을 동결하고 발행사나 회수 대리인 주소로 토큰을 옮기는 시나리오와, 키 분실 시 복구 용도를 듭니다. freezeAddress는 해당 계정의 전송과 지급 수령을 막아야 하고 unFreeze는 동결을 풉니다. 두 기능 모두 권한 있는 주체만 쓸 수 있도록 접근 통제가 필요합니다.
컴플라이언스 승인 확장(IERC7518Authorization)
선택 확장으로, 전송 1건을 승인하는 EIP-712 형식의 바우처를 정의합니다. ComplianceAuthorization 구조체는 token, authority, operationHash, policyId, contextHash, nonce, validAfter, validBefore 필드로 구성되며 token은 address(this)와 같아야 하고 유효 기간은 validAfter 이상 validBefore 미만입니다. operationHash는 호출자, 당사자, id, 수량, 응용 데이터를 묶어 계산하므로 다른 전송에 재사용할 수 없습니다. 바우처는 ERC-1155 data 인자에 매직 값 bytes4(0x7518a001)로 시작하는 표준 봉투로 담습니다. 전송 시 바우처 검증 후 같은 트랜잭션 안에서, 그리고 외부 수신자 훅 호출 전에 Consumed로 표시해 재사용을 막아야 합니다. revokeAuthorization은 미사용 바우처를 Revoked로 바꾸며 Consumed와 Revoked는 최종 상태로 유지됩니다. authority가 컨트랙트면 ERC-1271로 서명을 검증하고, EIP-712 도메인은 ERC-5267로 공개해야 합니다.
자산 프로필·역할·액션 기록 확장
세 가지 선택 확장이 더 있습니다. IERC7518AssetProfile은 자산 조회용으로, assetKey는 chainid, 토큰 컨트랙트, tokenId, partitionId를 abi.encode해 keccak256한 값이고, assetProfile은 컨트랙트 업그레이드를 거쳐도 같은 경제적 자산을 식별하는 assetId와 현재 정책 policyId, profileURI 문서의 keccak256 해시인 profileHash를 돌려줍니다. identifier는 ISIN, DTI, CUSIP 같은 식별 체계의 값을 조회합니다. IERC7518Roles는 REGISTRAR, TRANSFER_AGENT, PAYING_AGENT, COMPLIANCE_AUTHORITY 네 가지 역할 키를 정의하고 역할 변경마다 RoleUpdated를 발생시켜야 합니다. IERC7518ActionRecord는 분할, 공개매수, 투표 같은 자산 액션을 기록하며 상태는 0 Unknown부터 4 Cancelled까지이고 완료나 취소된 액션은 이전 상태로 되돌릴 수 없습니다. 확장을 함께 구현하면 결합 규칙이 적용됩니다. 예를 들어 승인 확장과 역할 확장을 같이 쓰면 바우처 소모 시점에 authority가 COMPLIANCE_AUTHORITY 역할을 보유해야 합니다.
다른 표준과의 상호운용(래핑)
다른 표준의 토큰을 이 표준으로 감싸는 래핑 인터페이스 IERC1155Wrapper를 정의합니다. wrapToken은 원본 토큰을 온체인 금고형 컨트랙트에 잠그고 같은 양의 이 표준 토큰을 발행하며 unwrapToken은 반대로 이 표준 토큰을 소각하고 원본을 돌려줍니다. 언래핑 시 해당 토큰이 이 표준의 잠금 기능에 걸려 있지 않은지 확인해야 합니다. ERC-20 발행분은 전체 공급량을 하나의 파티션에 대응시킬 수 있고 기본값으로 tokenId = 0을 쓰는 것이 권장됩니다. ERC-721 어댑터는 원본 tokenId마다 파티션 하나를, ERC-1155 어댑터는 공급과 권리가 같은 경우 동일 tokenId 파티션을 대응시킵니다.
인터페이스 식별자와 제안 상태
구현은 ERC-1155와 ERC-165를 구현해야 하고 supportsInterface에서 ERC-1155 식별자 0xd9b67a26과 IERC7518 식별자 0x5a264a5d에 true를 돌려줘야 합니다. 선택 확장은 각자 별도 식별자를 갖습니다. IERC7518AssetProfile은 0xe8aa3d31, IERC7518Roles는 0x779da628, IERC7518Authorization은 0x5b880cc8, IERC7518ActionRecord는 0x9118aff3입니다. 한 확장의 지원이 다른 확장의 지원을 의미하지 않습니다. 이 제안은 현재 Review 상태라서 내용이 아직 바뀔 수 있다는 점에 유의해야 합니다.
쓸 때 주의할 점
- 핵심 기능 접근 통제: forceTransfer, freezeAddress, lockTokens처럼 잔액과 전송에 큰 영향을 주는 함수는 역할 기반 권한 같은 적절한 접근 통제로 권한 있는 주체만 실행할 수 있게 해야 합니다.
- 오프체인 바우처 보안: 바우처를 만드는 컴플라이언스 서비스는 무단 발급이나 변조를 막는 보안 체계를 갖춰야 하고 컨트랙트는 전송을 허용하기 전에 바우처의 진위와 유효성을 철저히 검증해야 합니다.
- 리플레이 방지: EIP-712 도메인, 유효 기간, 작업 해시, 승인 상태를 검증해야 합니다. 승인은 ERC-1155 수신자 훅 호출 전에 사용 완료로 표시해야 하고 전송이 되돌려지면 사용 표시도 함께 되돌아갑니다.
- 컨트랙트 서명자 위험: ERC-1271 서명 검증은 신뢰할 수 없는 코드를 호출할 수 있습니다. 가스를 제한하고 잘못된 반환값을 처리하며 재진입으로 승인이나 전송 상태가 바뀌지 않게 막아야 합니다.
- 지급 보안: payout과 batchPayout은 권한 있는 주체만 시작할 수 있어야 하고 총 지급액이 가용 잔액을 넘지 않는지 확인해야 합니다. 접근 통제와 입력 검증이 부정 지급을 막는 핵심입니다.