연결 중…
연결 중…
전체 보기
개발자 조엘과 삽질
Telegram · @joelmundev

Issue. 12: 컨트랙트 개발자가 알려주는 approve & allowance, ERC20Permit, Permit2 기본 개념 익히기

게시
2025.12.02 10:46
조회
335
포워드
11

DApp을 써봤다면 예치하기 전에 먼저 얼마를 'approve' 하라는 팝업 창을 한번쯤은 봤을 것임. 그리고 approve 트잭을 보내면 귀찮게 예치 팝업이 그 후에 뜨고 그제서야 예치를 할 수 있음. 오늘은 그것과 관련된 얘기를 좀 해보려고 함. 왜? 의외로 approve, ERC20Permit, Permit2 등을 모르는 사람들이 많아서.

개발자가 아닌 사람도 최대한 이해할 수 있도록 적어보려고 하긴 했는데 모르겠으면 그냥 키워드 이 글에서 얻어서 더 찾아보는걸로.

1. approve & allowance

이게 왜 필요한지부터 설명해보자. 만약 DApp에서 유저가 소유하고 있는 ERC20 토큰을 x만큼 가져오고 싶다면, 가장 간단한 방법은 사실
그냥 유저가 .transfer 함수를 직접 호출해서 DApp 컨트랙트로 보내버리는 것이다.

근데 문제는, 이러면 토큰이 전송될때 컨트랙트가 custom logic을 실행할 수 없다는 점. 예를 들면 ERC20 토큰이 x만큼 들어왔을 때, msg.sender (트잭 쏜사람)의 포인트를 x만큼 온체인에 기록한다든지.

그래서 사람들이 생각해낸게 바로 .transferFrom 란 함수임. 이건 뭐 어떻게 작동하냐면, 유저가 직접 호출하는게 아니고, 유저가 상호작용하고자 하는 컨트랙트의 함수가 .transferFrom을 대신 호출해줌.

예를 들자면 이런 식으로.


function pullTokenFromUser() external {
IERC20(TOKEN).transferFrom(msg.sender, address(this), 10); // 10 만큼 유저한테로부터 컨트랙트에 ERC20 송금
points[msg.sender] += 10; // 10 포인트 주기
}

이렇게 하면 유저가 ERC20의 transfer를 직접 호출하지 않고, pullTokenFromUser를 호출하게 됨. 이점은 커스텀 로직이 실행 가능함. 대부분의 DApp들이 이렇게 돌아감.

근데 여기서 중대한 문제는 보다시피, 유저의 ERC20 토큰을 전송할 권한을 유저 본인이 아닌 스마트 컨트랙트가 가지게 된다는 것. 그러면 스마트 컨트랙트가 혹여나 악성 코드가 있거나 취약점이 있다면 누군가가 당신의 토큰을 가져가 버릴 수도 있다. 그래서 가끔씩 텔레그램 채널 보면 어디 해킹 사건 터니면 approve 해놓은거 revoke 하라는 말이 돌아다니는 거다 (https://revoke.cash 참고).

ERC20::approve 가 실행되면 내부적으로는 allowance라는 매핑 안에 유저가 해당 주소에게 전송 권한이 위임된 양을 증가시킨다. 그리고 transferFrom이 한번 호출될 때마다 allowance의 값이 감소하고, 0에 도달하면 에러가 나며 더 이상 전송할 수 없게 된다. 더 위임하고 싶으면 또 approve를 호출하면 됨.

만약 자꾸 쓰다보면 allowance가 계속 낮아져서 계속 approve해야 하는게 귀찮다 싶으면 보통 uint256의 가장 큰 숫자인 type(uint256).max 로 승인을 때려버리기도 한다. 기본적으로 이렇게 승인을 요구하는 DApp들이 많은데, 트잭 보내기 전에 월렛에서 이 값을 내가 직접 수정할 수 있으니 좀 안전하지 않을 것 같은 앱이다 싶으면 꼼꼼하게 체크해서 값을 낮추는게 좋은 방법.

그래서 approve를 꼭 사용해야 할 때 가장 좋은 security practice는 전송되어야 하는만큼만 승인하고, 다음에 또 전송해야 할때 또 approve 하는것임.

2. ERC20Permit

여기까지 읽었다면 approve의 문제가 무엇인지 당신은 이미 알고 있다. 안전하고 싶으면 컨트랙트와 상호작용 할 때마다 a. approve 트잭, b. 실제 컨트랙트 함수 호출 트잭으로 두 번 트잭을 보내줘야 한다. 귀찮고, 가스비도 더 나가고, 시간도 더 걸리고. 아무도 이걸 원하지 않는다.

그래서 사람들이 생각해낸게 ERC20Permit임.

사실 진짜 별거 없고, ERC20를 상속하는 컨트랙트인데 가장 중요하게는 permit이란 함수가 있음. 얘를 사용하면 ERC20::approve랑 컨트랙트의 custom logic을 한꺼번에 실행이 가능하다.

예를 들면 이렇게.


function pullTokenFromUserWithPermit(
address token,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
// 여기서 approve 해버림
IERC20Permit(token).permit(
msg.sender,
address(this),
amount,
deadline,
v,
r,
s
);
// 유저한테서 토큰 가져오기
IERC20(token).transferFrom(
msg.sender,
address(this),
amount
);
points[msg.sender] += amount; // amount만큼 포인트 주기
}

근데 뭐가 달라져서 가능하냐면, 유저가 오프체인에서 누가 누구의 돈을 언제까지 얼마를 전송할 수 있는지에 대한 데이터에 서명을 하기 때문임. 서명은 100% 오프체인이기 때문에 가스비가 절대 들지 않음 (월렛에서 팝업 뜨고 버튼은 눌러주긴 해야함).

그러면 그 서명을 컨트랙트 함수에 갖다주면 서명이 올바른지 확인하고, 내부적으로 approve를 바로 실행해주고, 그 후에 바로 transferFrom을 호출할 수 있게 됨.

가장 중요 포인트는 두 번 보내야 하는 트잭이 하나가 된다는 점. permit이 approve를 내부적으로 실행시켜주기 때문에 그 후에 컨트랙트는 동일 트잭 내에서 실행시키고 싶은 로직을 실행시키면 됨.

(여기까지 잘 읽은 분들 중 질문이 생겼을 수도 있다. 그러면 pullTokenFromUserWithPermit 안에서 굳이 permit을 호출하지 않고 approve를 호출하면 되는 거 아닌가? 라는 질문이 들 수 있는데, 사실 이건 단순한 이유로 되지 않는다. 답은 본인이 직접 approve 함수 구현체를 찾아보시길)

3. Permit2

여기까지 읽었으면 수고했음. 이게 마지막임. ERC20Permit를 읽다가 문제를 발견했다면 그게 Permit2의 존재 이유일 가능성이 높다.

ERC20Permit은 가장 고통스러운 게, 이미 배포된 ERC20 토큰인데 ERC20Permit을 사용하지 않는 토큰이면 토큰 컨트랙트를 다시 배포/업글하지 않는 이상 ERC20Permit을 사용할 수가 없음 → 결국 approve & allowance 사용해야 하는 문제로 돌아감. 이거 말고 여러 문제가 있지만 일단 이것만 설명.

조회335포워드11텔레그램에서 보기