ERC-2612 서명으로 토큰 사용 승인하기(Permit) (Permit Extension for EIP-20 Signed Approvals)
Final · 토큰 · 2020-04-13 작성
EIP-20 토큰의 사용 승인(approve)을 트랜잭션 대신 서명 한 장으로 처리합니다. 토큰 소유자가 ETH 없이도 승인할 수 있고, 승인과 실제 작업을 한 번에 묶을 수 있습니다.
왜 필요한가
EIP-20 토큰이 널리 쓰이는 이유 가운데 하나는 approve와 transferFrom의 조합입니다. 덕분에 토큰을 다른 사람에게 보내는 데 그치지 않고 앱 컨트랙트가 정해진 조건에 따라 내 토큰을 가져다 쓰게 할 수 있습니다. 그런데 approve 자체는 트랜잭션을 보낸 주소(msg.sender)를 기준으로 동작합니다. 그래서 토큰을 쓰는 첫 행동은 반드시 소유자가 직접 트랜잭션으로 해야 합니다. 컨트랙트와 상호작용하려면 approve 한 번, 실제 호출 한 번으로 트랜잭션이 두 번 필요합니다. 단순히 다른 사람에게 토큰을 보내는 경우에도 가스비를 낼 ETH를 따로 들고 있어야 합니다. 이 표준은 allowance를 서명된 메시지로 바꿀 수 있는 permit 함수 하나를 더해 이 한 가지 문제만 가장 작은 방식으로 풉니다.
구현해야 하는 것
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external: owner의 서명(v, r, s)을 검증한 뒤 allowance[owner][spender]를 value로 설정하고 nonce를 1 올립니다. 호출자는 누구여도 됩니다.function nonces(address owner) external view returns (uint): owner가 다음 Permit 서명에 써야 할 일련번호를 돌려줍니다. permit이 성공할 때마다 1씩 오릅니다.function DOMAIN_SEPARATOR() external view returns (bytes32): EIP-712 도메인 구분값을 돌려줍니다. 컨트랙트와 체인마다 고유해야 다른 곳에서 서명이 재사용되는 것을 막습니다.
심층 설명
추가되는 세 함수
호환 컨트랙트는 EIP-20 함수에 더해 세 함수를 구현해야 합니다. permit(owner, spender, value, deadline, v, r, s)는 서명을 받아 allowance[owner][spender]를 value로 설정합니다. nonces(owner)는 owner가 다음에 서명해야 할 번호를 돌려주며 permit이 성공할 때마다 1씩 오릅니다. DOMAIN_SEPARATOR()는 EIP-712 도메인 구분값으로, 이 컨트랙트와 체인에 고유해야 합니다. 시그니처에서 value와 deadline은 uint, 서명은 uint8 v와 bytes32 r, s로 받습니다.
permit이 성공하는 네 가지 조건
permit(owner, spender, value, deadline, v, r, s) 호출은 다음 네 조건을 모두 만족할 때만 allowance[owner][spender]를 value로 설정하고 nonces[owner]를 1 올리며 Approval 이벤트를 냅니다. 첫째, 현재 블록 시각이 deadline 이하여야 합니다. 둘째, owner가 영 주소가 아니어야 합니다. 셋째, 상태를 바꾸기 전의 nonces[owner]가 메시지의 nonce와 같아야 합니다. 넷째, r, s, v가 정해진 메시지에 대한 owner의 유효한 secp256k1 서명이어야 합니다. 하나라도 어긋나면 permit은 반드시 되돌려야(revert) 합니다. 이 되돌림 요구는 표준이 이미 널리 배포된 뒤에 추가됐지만 그 시점에 확인된 모든 구현과 일치했습니다.
서명하는 메시지의 구조
서명 대상은 EIP-712 형식으로, keccak256(abi.encodePacked(hex"1901", DOMAIN_SEPARATOR, 구조체 해시)) 형태입니다. 구조체 해시는 keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") 타입 해시와 owner, spender, value, nonce, deadline을 abi.encode로 묶어 keccak256한 값입니다. 타입 데이터로 보면 primaryType은 Permit이고 domain에는 토큰 이름(name), version, chainId, 토큰 주소(verifyingContract)가, message에는 owner, spender, value, nonce, deadline이 들어갑니다. 이 정의 어디에도 msg.sender가 없으므로 permit 호출자는 어떤 주소여도 됩니다.
DOMAIN_SEPARATOR 계산
DOMAIN_SEPARATOR는 EIP-712 요구사항을 따르되 컨트랙트와 체인마다 고유해야 다른 도메인에서 일어나는 재전송(replay) 공격을 막을 수 있습니다. 그 밖의 제약은 없습니다. 흔히 쓰는 계산은 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') 타입 해시와 keccak256(bytes(name)), keccak256(bytes(version)), chainid, address(this)를 abi.encode로 묶어 keccak256하는 방식입니다.
nonce와 deadline의 역할
nonces 매핑은 같은 서명을 두 번 쓰는 재전송을 막기 위한 장치입니다. 흔한 사용 방식은 릴레이어가 owner 대신 Permit을 제출하는 것인데, 이때 릴레이어는 제출할지 말지를 고를 수 있는 무료 옵션을 갖게 됩니다. 이것이 걱정된다면 owner는 deadline을 가까운 미래로 잡아 Permit의 유효 시간을 제한할 수 있습니다. 반대로 deadline을 uint(-1)로 두면 사실상 만료되지 않는 Permit이 됩니다. permit 함수 하나만으로도 EIP-20 토큰을 다루는 어떤 작업이든 ETH 대신 토큰 자체로 비용을 치르게 만들 수 있습니다.
범위를 permit 하나로 제한한 이유
ETH 없이 이더리움을 쓰게 하려는 제안은 오래전부터 여럿 있었지만 대부분 채택이 거의 없었습니다. 채택된 EIP-777 같은 경우는 기능이 많아 기존 컨트랙트에서 예상 밖 동작을 일으켰습니다. 이 표준은 EIP-20 approve의 추상화 부족이라는 한 가지 문제만 가장 작게 다룹니다. 모든 EIP-20 함수에 서명 버전(*_by_signature)을 붙이지 않은 이유는 두 가지입니다. transfer_by_signature의 수수료 정책이나 묶음 처리 방식 같은 세부가 용도마다 다르고 그런 기능은 permit과 보조 컨트랙트를 조합해 일반성을 잃지 않고 만들 수 있기 때문입니다. 컨트랙트 지갑도 비슷한 문제를 풀지만 아직 채택이 적고 소유자 EOA와 지갑 컨트랙트가 분리되어 UI를 따로 설계해야 하는 반면, permit은 UI를 거의 바꾸지 않고 같은 이점을 얻습니다.
기존 구현과의 차이
이 표준 이전에도 permit 함수는 여러 토큰에 있었고 가장 잘 알려진 것은 dai.sol의 구현입니다. dai는 value 대신 bool allowed를 받아 승인을 0 또는 uint(-1)로만 설정하고 deadline을 expiry라고 부릅니다. 이름 차이는 서명 메시지 내용까지 바꾸므로 단순한 표기 차이가 아닙니다. Stake 토큰(0x0Ae055097C6d159879521C384F1D2123D1f195e6)은 dai와 ABI가 같지만 의미가 달라 expiry >= block.timestamp인 동안만 transferFrom이 되는 만료형 승인을 발행합니다. 이 문서의 명세는 Uniswap V2 구현과 일치합니다.
쓸 때 주의할 점
- 앞지르기(front running): 서명자가 특정 상대가 제출하길 기대하더라도, 다른 누군가가 먼저 permit을 호출할 수 있습니다. 다만 서명자 입장에서 결과는 같습니다.
- owner가 영 주소인지 확인: ecrecover 프리컴파일은 잘못된 메시지를 받으면 조용히 실패하며 영 주소를 signer로 돌려줍니다. owner != address(0)을 반드시 확인해야 영 주소가 가진 좀비 자금에 대한 승인이 만들어지는 일을 막을 수 있습니다.
- 검열 가능성: 릴레이어는 Permit을 받고도 제출하지 않을 수 있습니다. deadline이 한 가지 완화책입니다. 서명자에게 ETH가 있다면 직접 제출할 수 있으며 그러면 앞서 서명한 Permit들이 무효가 될 수 있습니다.
- 승인 경쟁 조건(SWC-114): EIP-20 approve에 있는 표준적인 경쟁 조건(SWC-114)은 permit에도 그대로 적용됩니다.
- 체인 분기 시 재전송: DOMAIN_SEPARATOR에 chainId가 들어가는데 서명 때마다 다시 계산하지 않고 배포 시점에 고정해 두면, 나중에 체인이 갈라졌을 때 체인 간 재전송 공격이 일어날 수 있습니다.