ERC-6357 단일 컨트랙트 멀티콜 (Single-contract Multi-delegatecall)
Last Call · 기타 · 2023-01-18 작성
스마트 컨트랙트에 multicall 함수를 표준으로 정해, 일반 계정(EOA)이 트랜잭션 한 번으로 같은 컨트랙트의 여러 함수를 호출하고, 하나라도 실패하면 전부 되돌릴 수 있게 합니다.
왜 필요한가
이더리움에서 외부 소유 계정(EOA)이 보내는 트랜잭션 하나는 기본적으로 함수 호출 하나만 담습니다. 예를 들어 ERC-721 대체 불가능 토큰(NFT)을 여러 개 옮기려면 옮길 개수만큼 트랜잭션을 따로 보내야 합니다. 트랜잭션에는 건마다 기본 수수료 21000 가스가 붙기 때문에, NFT 하나를 옮길 때마다 이 비용을 반복해서 내게 되어 사용자의 돈이 낭비됩니다. 또 호출이 여러 트랜잭션으로 쪼개지면 일부만 성공하고 일부는 실패하는 어중간한 상태가 생길 수 있습니다. 이 표준은 abi로 인코딩한 호출 데이터 여러 건을 배열로 받아 컨트랙트가 자기 자신을 차례로 호출하게 하는 multicall 함수 하나를 공통 인터페이스로 정합니다. 호출 중 하나라도 실패하면 전체가 되돌려지므로, 여러 작업을 한 트랜잭션에 묶으면서도 전부 성공하거나 전부 취소되는 것이 보장됩니다.
구현해야 하는 것
function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results);: abi 인코딩된 호출 데이터 배열을 받아 각 항목으로 자기 자신을 delegatecall하고 반환값들을 배열로 돌려줍니다. 어느 호출이라도 실패하면 전체가 되돌려집니다.function multicallPayable(bytes[] calldata data, uint256[] values) external payable virtual returns (bytes[] memory results);: 선택 함수입니다. multicall과 같지만 이더를 함께 받을 수 있고 values 배열로 각 호출에 적용할 유효 msg.value를 지정합니다. values의 합은 실제 보낸 msg.value를 넘을 수 없습니다.
심층 설명
필수 함수 multicall
이 표준을 구현하는 컨트랙트는 IMulticall 인터페이스를 반드시 구현해야 합니다. 핵심인 multicall은 abi 인코딩된 호출 데이터 배열(bytes[] calldata data)을 받아 각 항목을 가지고 자기 자신을 delegatecall합니다. 모든 호출이 성공하면 각 호출의 반환값을 abi 인코딩한 배열(bytes[] memory results)을 돌려줍니다. 어느 한 delegatecall이라도 실패하면 함수 전체가 되돌려지므로, 한 트랜잭션에 묶인 호출들은 전부 실행되거나 전부 취소됩니다. 자기 자신에 대한 delegatecall이므로 실행 대상은 같은 컨트랙트의 함수들입니다.
선택 함수 multicallPayable과 그 이유
multicallPayable은 선택(OPTIONAL) 함수입니다. multicall과 같은 방식으로 동작하되 payable로 선언되어 이더를 함께 받을 수 있고 추가 인자 values(uint256[])로 각 호출에 적용할 유효 msg.value를 지정합니다. values의 합은 트랜잭션에 실제로 보낸 msg.value를 넘지 않아야 합니다. 원문 Rationale은 이 함수를 필수로 두지 않은 이유로 msg.value를 호출별로 나누는 일이 항상 구현 가능하지는 않다는 점을 듭니다. 그래서 필수 요건은 multicall 하나로 최소화되어 있습니다.
기존 멀티콜과의 호환성과 사용 예
원문은 이 표준이 대부분의 기존 multicall 함수와 호환된다고 밝힙니다. 사용 예로는 Ethers 라이브러리를 쓰는 테스트 사례가 실려 있습니다. token.interface.encodeFunctionData('transfer', [addressA, amt])와 같은 방식으로 transfer 호출 두 건을 인코딩한 뒤 token.multicall에 배열로 넘기면, ERC-20 토큰 amt 단위를 addressA와 addressB 두 주소에 원자적으로, 즉 한 트랜잭션 안에서 함께 전송할 수 있습니다.
참조 구현
원문에는 OpenZeppelin 구현에서 파생된 추상 컨트랙트 Multicall이 참조 구현으로 실려 있습니다. multicall은 data 배열 길이만큼 results 배열을 만든 뒤, 반복문으로 각 항목에 대해 address(this).delegatecall(data[i])를 실행합니다. 호출 결과의 성공 여부를 require(success)로 검사해 하나라도 실패하면 전체를 되돌립니다. 성공한 호출의 반환 데이터(returndata)는 results[i]에 담아 마지막에 배열로 반환합니다. 선택 함수인 multicallPayable은 참조 구현에 포함되어 있지 않습니다.
제안 상태와 메타데이터
이 제안은 Gavin John(@Pandapip1)이 작성해 2023-01-18에 만들어졌고 Standards Track의 ERC 범주에 속합니다. 논의는 Ethereum Magicians 포럼에서 진행되었습니다. 원문 기준 상태는 Last Call이며 마감일(last-call-deadline)은 2023-11-10입니다. Last Call은 최종 확정(Final) 전의 마지막 의견 수렴 단계이므로, 구현 전에 제안의 최신 상태와 내용 변경 여부를 확인하는 것이 안전합니다.
쓸 때 주의할 점
- multicallPayable은 지원 가능한 경우에만 구현: multicallPayable은 컨트랙트가 이를 제대로 지원할 수 있을 때만 사용해야 합니다. 순진하게 구현하면 공격자가 같은 이더를 가지고 payable 함수를 여러 번 호출할 수 있습니다.