1. 개요1-1. 기존의 한계1.2. 해결책: Token Bound Account, TBA)2. 아키텍처 및 레지스트리 (Architecture & Registry)2.1. 싱글톤 레지스트리 (Singleton Registry)2.2. 프록시 구조 (ERC-1167 Minimal Proxy)2.3. 레지스트리 인터페이스3. 계정 인터페이스 (Account Interface)3.1. 필수 기능3.2. 서명 검증 (ERC-1271)4. 실행 인터페이스 (Execution Interface)4.1. execute 함수4.2. 파라미터 및 operation 상세5. 설계 근거 (Rationale)6. 보안 고려사항 (Security Considerations)6.1. 사기 방지 (Fraud Prevention)6.2. 소유권 순환 (Ownership Cycles)7. 구조
1. 개요
1-1. 기존의 한계
현재 ERC-721 NFT는 소유권 증명에 머무르는 경우가 대부분이라, 스스로 에이전트처럼 행동하거나 다른 온체인 자산을 직접 소유하지는 않는다.
- 예: RPG 게임 캐릭터를 NFT로 발행하더라도, 캐릭터가 획득한 아이템은 캐릭터 NFT 안에 들어가는 것이 아니라 여전히 유저의 지갑에 별도로 존재한다.
- 이 제안의 목표는 온체인 상에서 "캐릭터가 칼을 쥐고 있는 상태", "자동차가 바퀴와 엔진을 가지고 있는 상태" 같은 구성을 자연스럽게 표현할 수 있게 만드는 데 있다.
1.2. 해결책: Token Bound Account, TBA)
이 표준은 ERC-721 토큰마다 고유하고 결정론적인(Deterministic) 스마트 컨트랙트 계정 주소를 하나씩 붙인다.
- 이 계정은 해당 NFT에 영구적으로 묶여 있다.
- 계정의 제어권(Control) 은 항상 NFT의 현재 소유자(Holder)가 가진다.
- 기존 ERC-721 컨트랙트 코드를 따로 수정할 필요는 없다.
2. 아키텍처 및 레지스트리 (Architecture & Registry)
시스템은 크게 두 부분으로 나뉜다.
- 싱글톤 레지스트리(Registry)
- 계정 구현체(Account Implementation)
2.1. 싱글톤 레지스트리 (Singleton Registry)
- 역할: 모든 TBA 주소를 계산하고 생성하는 단일 진입점이다.
- 특징:
- 무허가형(Permissionless): 소유자가 없고, 누구나 사용할 수 있다.
- 불변성(Immutable): 한 번 배포된 뒤에는 코드가 바뀌지 않는다.
- 고정 주소: 모든 EVM 호환 체인에서 아래 주소를 사용하는 것을 목표로 한다.
0x000000006551c19487814612e58FE06813775758
- 주소 생성 원리 (
CREATE2): implementation(계정 구현 컨트랙트 주소)chainIdtokenContract(NFT 컨트랙트 주소)tokenIdsalt(구분자)
TBA 주소는 아래 값들을 조합해서 만들어진다.
이 방식을 쓰면 실제로 계정을 체인에 배포하기 전에도 미리 주소를 알 수 있는 카운터팩추얼(Counterfactual) 계정을 만들 수 있다.
2.2. 프록시 구조 (ERC-1167 Minimal Proxy)
- 레지스트리는 TBA를 ERC-1167 Minimal Proxy 형태로 배포한다.
- 이때 프록시 바이트코드 뒤에 다음과 같은 불변 데이터를 붙인다.
salt,chainId,tokenContract,tokenId
- 이렇게 구성해 두면 TBA 컨트랙트는 자신의 코드 끝부분을 읽어 "어떤 NFT에 묶여 있는 계정인지" 스스로 알아낼 수 있다.
2.3. 레지스트리 인터페이스
createAccount- TBA를 실제로 생성(배포)하는 함수다.
- 이미 같은 조합으로 만들어진 계정이 있다면 새로 배포하지 않고 그 주소만 돌려준다.
account- 계정을 생성하지 않고, 주어진 파라미터에 해당하는 계산된 주소만 돌려주는 view 함수다.
3. 계정 인터페이스 (Account Interface)
생성된 TBA는 반드시
IERC6551Account 인터페이스를 구현해야 한다.3.1. 필수 기능
receive()- 계정이 ETH를 직접 받을 수 있어야 한다.
token()- 이 계정이 어떤 NFT에 묶여 있는지 나타내는 정보를 반환한다.
chainId,tokenContract,tokenId- 이 값은 계정이 만들어진 이후에는 변하지 않는다.
state()- 계정의 상태 값(예: 트랜잭션 nonce 등)을 반환한다.
- 계정 상태가 바뀔 때마다 이 값도 함께 달라져야 한다.
isValidSigner(signer, context)- 특정 주소(
signer)가 이 계정을 제어할 권한이 있는지 확인하는 함수다. - 기본적으로 NFT의 현재 소유자를 유효한 서명자로 본다.
3.2. 서명 검증 (ERC-1271)
- TBA는 스마트 컨트랙트 계정이라 자체적인 개인키를 가지지 않는다.
- 대신
isValidSignature를 구현해서 NFT 소유자가 서명한 메시지를 유효하다고 인정하는 방식을 쓴다.
- 이렇게 하면 TBA를 DApp 로그인이나 메시지 서명의 주체로 활용할 수 있다.
4. 실행 인터페이스 (Execution Interface)
TBA가 외부로 트랜잭션을 날릴 때 사용하는 핵심 인터페이스가
IERC6551Executable이다.4.1. execute 함수
function execute( address to, uint256 value, bytes calldata data, uint8 operation ) external payable returns (bytes memory);
4.2. 파라미터 및 operation 상세
to: 호출 대상 주소
value: 전송할 ETH 양
data: 실행할 데이터(함수 호출 인코딩 등)
operation: 실행 방식(opcode)0(CALL): 일반적인 컨트랙트 호출 및 ETH 전송에 쓰는 가장 기본 모드다.1(DELEGATECALL): 외부 코드를 TBA의 스토리지 컨텍스트에서 실행한다. 계정 기능을 확장할 때 활용할 수 있다.2(CREATE): 새로운 컨트랙트를 배포한다.3(CREATE2): 결정론적인 주소로 컨트랙트를 배포한다.
5. 설계 근거 (Rationale)
이 표준이 이런 구조를 택한 이유는 크게 다음과 같다.
- 싱글톤 레지스트리
- 여러 체인에서 동일한 주소 체계를 유지하기 쉽다.
- 인덱싱이나 탐색 로직을 단순하게 가져갈 수 있다.
- 카운터팩추얼(Counterfactual) 계정
- 계정을 실제로 배포하기 전에 자산을 먼저 입금받는 패턴을 지원한다.
- 초기에 불필요하게 드는 가스비를 줄일 수 있다.
- Factory 대신 Registry라는 이름
- 한 번 계정을 만드는 행위보다, 그 이후에 반복적으로 주소를 조회하는 행위가 더 핵심이라는 점을 드러내기 위한 네이밍이다.
- 다중 계정 허용 (Account Ambiguity)
- 하나의 NFT가 여러 개의 TBA를 가질 수 있다.
salt값을 다르게 두어 "저축용", "지출용"처럼 계정을 용도별로 분리하는 패턴을 지원한다.
6. 보안 고려사항 (Security Considerations)
6.1. 사기 방지 (Fraud Prevention)
악의적인 판매자가 NFT에 묶인 자산을 빼돌릴 수 있는 전형적인 시나리오는 다음과 같다.
- 판매자 Alice가 TBA에 10 ETH가 들어 있는 NFT를 마켓플레이스에 올린다.
- 구매자 Bob이 이를 보고 구매 트랜잭션을 전송한다.
- 트랜잭션이 처리되기 직전, Alice가 TBA에서 10 ETH를 먼저 인출한다(Front-running).
- 결국 Bob은 속이 빈 계정이 묶인 NFT를 받게 된다.
- 대응:
- 마켓플레이스 측에서 거래가 체결되는 시점에 TBA의 잔고나 상태(
state) 를 함께 검증하는 절차를 두는 식으로 막을 수 있다.
6.2. 소유권 순환 (Ownership Cycles)
- NFT A의 TBA 안으로 다시 NFT A를 전송하면, 말 그대로 자기 자신을 소유하는 루프가 생긴다.
- 이 경우 TBA를 제어할 수 있는 외부 주체가 사라지기 때문에, NFT와 그 안에 들어 있는 모든 자산이 영구적으로 잠길 수 있다.
- 애플리케이션 레벨에서는 이런 전송을 애초에 불가능하게 막는 로직을 두는 편이 안전하다.
7. 구조
- Registry 구현체
CREATE2를 사용해서 프록시를 배포한다.- 어셈블리(Assembly) 코드를 이용해 프록시 바이트코드 뒤에 불변 데이터를 붙이는 로직을 포함한다.
- Account 구현체
execute함수 내부에서_isValidSigner(msg.sender)를 호출해 권한을 먼저 확인한다.- 이후
operation값에 따라call,delegatecall등을 실행한다.
6551에 토큰 담아주기
담긴 토큰을 옮기기
