ERC-223 수신 알림 콜백 토큰 표준 (Token with transaction handling model)

Final · 토큰 · 2017-05-03 작성

토큰을 컨트랙트로 보낼 때 받는 쪽의 tokenReceived 함수를 호출해 입금을 알리는 대체 가능 토큰 표준입니다. 토큰이 이더(ether)처럼 동작하게 만듭니다.

왜 필요한가

ERC-20 토큰은 받는 쪽에 아무 알림도 주지 않습니다. 사용자가 transfer 로 토큰을 컨트랙트에 보내면 토큰 컨트랙트의 잔액은 줄지만 받는 컨트랙트는 입금이 있었다는 사실 자체를 모릅니다. 그 토큰은 받는 컨트랙트 주소에 남은 채 꺼낼 방법이 없어집니다. 원문은 2023년 5월 9일 기준으로 조사한 ERC-20 토큰 50종에서 이렇게 잃어버린 금액이 2억 100만 달러라고 적습니다. 게다가 ERC-20 은 transfer 와 approve+transferFrom 두 가지 전송 방법을 두고 어느 쪽을 쓸지 사용자에게 맡깁니다. 잘못 고르면 토큰을 잃습니다. ERC-223 은 받는 쪽이 컨트랙트면 tokenReceived 를 호출해 입금을 알리고 이 함수가 없는 컨트랙트로 가는 전송은 되돌립니다. 전송 방법을 토큰이 알아서 정하므로 사용자가 고를 일이 없습니다.

구현해야 하는 것

심층 설명

받는 쪽이 컨트랙트면 tokenReceived 호출이 필수입니다

이 토큰을 받으려는 컨트랙트는 반드시 tokenReceived 를 구현해야 합니다. transfer 는 _to 가 컨트랙트이면 tokenReceived(address, uint256, bytes calldata) 를 호출하고 받는 컨트랙트에 이 함수가 없으면 트랜잭션이 실패하며 토큰 이동이 되돌려집니다. _to 가 외부 소유 계정(EOA)이면 tokenReceived 를 호출하지 않고 전송만 합니다. 컨트랙트 여부는 _to 의 코드 크기가 0인지로 판단합니다. 원문은 이 강제 호출을 핵심 보안 장치로 설명합니다. 받는 쪽이 토큰 처리 함수를 명시적으로 구현해야만 토큰을 받을 수 있으므로, 토큰을 처리할 준비가 안 된 컨트랙트에 돈이 묶이는 일을 막습니다.

transfer 는 두 가지 형태이며 _data 는 선택입니다

transfer(address _to, uint _value) 와 transfer(address _to, uint _value, bytes calldata _data) 두 형태가 있습니다. 둘 다 토큰을 옮기고 받는 쪽이 컨트랙트면 tokenReceived 를 호출하는 동작은 같습니다. _data 는 비워도 되지만 붙이면 가스가 더 듭니다. 참조 구현에서 _data 없는 transfer 는 hex"00000000" 을 빈 데이터로 넘깁니다. _data 는 이더 트랜잭션의 msg.data 처럼 받는 컨트랙트에서 두 번째 함수 호출을 인코딩하거나, 금융 거래를 체인에 공개 기록하는 용도로 쓸 수 있습니다. 이 가운데 bytes 를 받는 transfer(address, uint256, bytes calldata) 는 ERC-20 인터페이스와 하위 호환되지 않습니다.

tokenReceived 는 0x8943ec02 를 돌려줘야 합니다

tokenReceived(address _from, uint _value, bytes calldata _data) 는 토큰 컨트랙트가 호출합니다. 이 함수 안에서 msg.sender 는 토큰 컨트랙트 주소이고 _from 은 토큰을 보낸 사람입니다. 그래서 msg.sender 를 검사해 어떤 토큰이 들어왔는지 걸러낼 수 있습니다. 처리를 마치면 0x8943ec02 를 반환해야 합니다. 함수 이름은 반드시 tokenReceived 이고 매개변수는 address, uint256, bytes 순이어야 이 함수 시그니처 0x8943ec02 가 나옵니다. 받는 컨트랙트의 fallback 함수가 이 호출을 처리하는 경우도 있는데, 그때는 0x8943ec02 를 돌려주지 않을 수 있습니다. 이 함수는 EOA 가 직접 호출할 수도 있습니다.

재진입을 막기 위해 tokenReceived 는 마지막에 호출합니다

토큰 컨트랙트는 다른 모든 작업을 끝낸 뒤에 _to 의 tokenReceived 를 호출해야 합니다. 받는 컨트랙트가 콜백 안에서 토큰 컨트랙트를 다시 호출하는 재진입 공격을 막기 위한 순서입니다. transfer 가 payable 이고 이더가 함께 들어왔다면 그 이더를 _to 에 토큰과 같이 전달해야 하며 순서는 이더 전달, 토큰 잔액 갱신, tokenReceived 호출 순입니다. 참조 구현도 보내는 사람과 받는 사람의 balances 를 먼저 바꾼 뒤 isContract 검사를 거쳐 tokenReceived 를 호출하고 Transfer 이벤트를 남깁니다.

name, symbol, decimals 는 선택 항목입니다

totalSupply, balanceOf, name, symbol, decimals 는 ERC-20 과 같은 기능을 합니다. 이 가운데 name, symbol, decimals 는 OPTIONAL 이어서 사용성을 위해 제공할 수 있지만 인터페이스나 다른 컨트랙트는 이 값이 있다고 기대해서는 안 됩니다. Transfer 이벤트는 ERC-20 의 Transfer 와 비슷하고 호환되며 여기에 bytes _data 가 더 붙습니다. 참조 구현은 "223" 을 돌려주는 standard() 함수도 두고 있습니다.

컨트랙트 입금이 한 트랜잭션으로 끝납니다

ERC-223 은 보내는 쪽이 전송을 시작하고 받는 쪽이 처리하는 푸시 방식입니다. 그래서 컨트랙트에 입금할 때 transfer 한 번이면 됩니다. ERC-20 은 approve 와 transferFrom 두 번을 호출해야 합니다. 원문이 적은 가스 비교는 ERC-20 입금이 approve 약 46 gas 와 transferFrom 약 75K gas, ERC-223 입금이 transfer 와 받는 쪽 처리를 합쳐 약 54K gas 입니다. 또 받는 쪽이 모든 전송을 처리하므로 잘못된 전송을 거부해 사용자 실수를 바로잡을 수 있습니다. 컨트랙트와 EOA 모두 하나의 transfer 로 보내므로 사용자가 전송 방법을 고를 필요가 없습니다.

ERC-20 과의 차이와 호환성

ERC-20 은 transfer 와 approve+transferFrom 두 가지 전송 방법을 두고 어느 쪽을 쓸지 사용자에게 맡깁니다. ERC-20 의 transfer 로 컨트랙트에 보내면 받는 컨트랙트는 입금을 전혀 인식하지 못하고 토큰은 복구할 방법 없이 묶입니다. 원문 예시의 ERC20Receiver 는 deposit 함수로 transferFrom 을 받는데, tokenA 가 아닌 토큰의 입금을 막을 수 없고 transfer 로 들어온 tokenA 는 deposits 에 기록되지 않습니다. 반면 ERC223Receiver 는 tokenReceived 에서 msg.sender == tokenA 를 require 로 확인해 다른 토큰을 거부하고 deposits[_from] 을 늘립니다. ERC-223 토큰은 EOA 든 컨트랙트든 같은 transfer 로 보내야 합니다.

참조 구현의 컨트랙트 판별은 완전하지 않습니다

참조 구현의 Address.isContract 는 extcodesize 로 코드 크기를 확인합니다. 컨트랙트의 생성자(constructor)가 실행되는 동안에는 코드가 아직 저장되지 않아 0 을 돌려주므로, 이 함수가 false 를 돌려줘도 EOA 라고 단정하면 안 된다고 주석에 적혀 있습니다. 참조 구현의 토큰은 생성자에서 name, symbol, decimals 를 한 번만 정하고 balances 매핑으로 잔액을 관리하며 IERC223Recipient 추상 컨트랙트로 tokenReceived 시그니처를 정의합니다.

쓸 때 주의할 점

ERC 원문 · ← ERCs