ERC-6120 범용 토큰 라우터 (Universal Token Router)

Review · 기타 · 2022-12-12 작성

모든 토큰을 ETH처럼 한 번의 호출로 쓸 수 있게 하는 공용 단일 라우터 컨트랙트입니다. 앱마다 승인(approve)하는 대신 이 라우터 하나에만 승인하면 됩니다.

왜 필요한가

ETH는 보내면서 동시에 컨트랙트를 실행할 수 있지만 널리 쓰이는 ERC-20 토큰은 이 방식이 안 됩니다. 그래서 앱을 쓰려면 먼저 토큰 사용을 허락하는 승인 거래를 보내고 그다음 실제 실행 거래를 또 보내는 두 단계를 거쳐야 합니다. 이 방식은 비용이 들고 사용 경험이 나쁠 뿐 아니라, 감사받지 않았거나 코드가 바뀔 수 있는(업그레이드 가능한) 컨트랙트에 토큰 사용 권한을 맡겨야 해서 허용량(allowance) 관련 버그와 탈취 사고가 많이 일어났습니다. 또 사용자는 앱 화면이 거래를 정직하게 만들었는지 확인할 방법이 없어 피싱에 쉽게 노출됐습니다. 이 표준은 토큰 승인과 앱 로직이 묶여 있는 구조가 문제라고 보고 둘을 분리하는 공용 라우터를 제안합니다.

구현해야 하는 것

심층 설명

exec 한 번으로 입력·실행·검증을 끝냅니다

exec(outputs, actions)가 유일한 진입점입니다. Output 구조체는 recipient, eip(0은 ETH, 그 외는 EIP 번호), token, id, amountOutMin으로 기대 잔액 변화를 정의합니다. UTR은 exec 시작과 끝에 recipient의 잔액을 기록하고 변화가 amountOutMin보다 작으면 INSUFFICIENT_OUTPUT_AMOUNT로 되돌립니다. ERC-721에는 특수 id인 ERC_721_BALANCE = keccak256('UniversalTokenRouter.ERC_721_BALANCE')가 예약되어 있어 특정 id 대신 recipient가 보유한 전체 수량을 검증할 수 있습니다. 과정이 아니라 결과를 검증하므로(security-by-result), 중간에 결함 있는 컨트랙트를 거쳐도 기대한 토큰을 받지 못하면 거래가 무효가 됩니다.

Action 호출과 토큰 오호출 방지 장치

Action은 inputs(입력 토큰), code(호출할 컨트랙트 주소), data(호출 데이터)로 구성됩니다. action 코드 컨트랙트는 NotToken 컨트랙트를 구현하거나 ERC-165 인터페이스 ID 0x61206120을 구현해야(MUST) UTR이 호출합니다. 이 검사는 UTR이 transferFrom 같은 허용량 지출 함수를 직접 호출하는 것을 막기 위한 장치입니다. NotToken은 allowance와 isApprovedForAll을 문자열을 반환하도록 덮어써서 ERC-20, ERC-721, ERC-1155 표준과 일부러 충돌시킵니다. 따라서 새 토큰 컨트랙트는 이 인터페이스 ID를 구현하면 안 됩니다(MUST NOT).

세 가지 입력 모드

Input 구조체는 mode, recipient, eip, token, id, amountIn으로 구성되고 mode는 세 가지입니다. PAYMENT(0)는 같은 거래 안에서 누구든 UTR.pay를 호출해 msg.sender에서 recipient로 최대 amountIn까지 가져갈 수 있게 지급을 예약합니다. TRANSFER(1)는 msg.sender에서 recipient로 토큰을 즉시 보냅니다. CALL_VALUE(2)는 액션 호출에 넘길 ETH 금액(value)을 기록합니다. 입력은 순서대로 처리되고 PAYMENT와 CALL_VALUE가 중복되면 마지막 amountIn만 유효합니다. 허용량과 PAYMENT는 다릅니다. 허용량은 특정 지출자가 언제든 누구에게나 보낼 수 있게 하지만 PAYMENT는 그 거래 안에서 정해진 recipient로만 보낼 수 있습니다.

pay와 discard, 지급의 수명

pay(payment, amount)의 payment는 abi.encode(payer, recipient, eip, token, id)로 인코딩합니다. PAYMENT 모드는 플래시론이나 Uniswap v3처럼 콜백 안에서 토큰을 당겨 가는(transfer-in-callback) 패턴에 권장됩니다. discard(payment, amount)는 토큰을 옮기지 않고 예약을 소멸시키는 함수로, 앱이 payer의 자기 토큰을 소각할 때나, 토큰이 아닌 가짜 지급 1단위를 소멸시켜 호출자가 지정한 주소가 실제 서명자인지 확인하는 발신자 인증에도 쓸 수 있습니다. 모든 지급 기록은 UTR 저장소에 올라갔다가 exec가 끝나기 전에 전부 지워지므로 거래 밖에서는 쓸 수 없습니다.

싱글톤 배포와 승인 없는 새 토큰

UTR은 EIP-1014 SingletonFactory로 모든 EVM 호환 네트워크의 같은 주소 0x69c4620b62D99f524c5B4dE45442FE2D7dD59576에 배포됩니다. 기존 토큰(ERC-20, ERC-721, ERC-1155)은 계정당 한 번만 UTR에 승인하면 되고 새 토큰은 ERC20WithUTR 예시처럼 allowance에서 지출자가 UTR이면 type(uint256).max를 반환하고 _spendAllowance를 건너뛰게 만들어 승인 거래 자체를 없앨 수 있습니다. Permit 형식의 서명은 지원하지 않는데, 이 표준의 목적이 새 토큰의 모든 승인 서명과 기존 토큰의 승인 대부분을 없애는 것이기 때문입니다.

기존 앱과의 호환성

수혜자 주소를 recipient(또는 to) 인자로 받는 앱 컨트랙트는 수정 없이 그대로 호환됩니다. 비호환인 것은 msg.sender를 수혜자로 내부 저장소에 기록하면서 소유권 이전 수단이 없는 컨트랙트뿐입니다. 토큰을 msg.sender에게 보내는 컨트랙트는 WethAdapter 예시처럼 recipient를 추가하는 어댑터가 필요하고 직접 허용량을 쓰는 기존 라우터는 AllowanceAdapter의 approveAndCall로 연결할 수 있습니다. 이런 헬퍼와 어댑터는 토큰이나 허용량을 보관하지 않으므로 자주 갱신해도 핵심 앱 컨트랙트의 보안에 거의 영향이 없습니다.

아직 검토 단계인 제안입니다

원문 상태는 Review로, 내용이 앞으로 바뀔 수 있는 제안 단계입니다. 원문에는 Derion Labs가 작성하고 Hacken의 감사를 받은 참조 구현이 실려 있습니다.

쓸 때 주의할 점

ERC 원문 · ← ERCs