
출처 - https://www.adrianhetman.com/unboxing-erc20-approve-issues/
Openzeppelin의 SafeERC20 라이브러리를 읽어보다가 아래 코멘트를 읽고 해당 issue에 대해 조사해 보았다.
이 글에서는
safeIncreaseAllowance와safeDecreaseAllowance에 대해서는 다루지 않으며
increaseAllowance와decreaseAllowance에 대해서만 설명한다.하지만 대부분의 경우
safeIncreaseAllowance와safeDecreaseAllowance를 사용하도록 권장하고 있으며, 이에 대해서는 이후 글에서 다룰 예정이다.
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
ERC20 standard의 함수인데 IERC-approve 문제가 있다고 하여 서치해 보았다.
approve에 대한 front-running 문제 시나리오는 다음과 같다.
Alice가 Bob에게 30 만큼 approve하였다.
(token.approve(0xBob, 30))
Alice가 마음이 바뀌었다. approve를 0로 줄인다.
(token.approve(0xBob, 0))
Bob이 이 트랜잭션이 채굴되기 전, mempool에서 이걸 보았다.
채굴이 되기 전에 이걸 먼저 자기한테 옮기는 트랜잭션을 쏜다.
(token.transferFrom(0xAlice, 0xBob, 30))
Alice의 approve 트랜잭션은 그냥 allowance를 0 -> 0 로 바꾸게 된다.
위의 예시에서는 allowance를 0으로 바꾸었지만, 만일 20으로 바꾸게 된다면 어떨까?
allowance는 단순히 값을 overwrite하기 때문에 다음과 같은 시나리오가 발생할 수 있다.
Alice가 Bob에게 30 만큼 approve하였다.
(token.approve(0xBob, 30))
Alice가 마음이 바뀌었다. allowance를 20으로 줄인다.
(token.approve(0xBob, 20))
Bob이 이 트랜잭션이 채굴되기 전, mempool에서 이걸 보았다.
채굴이 되기 전에 이걸 먼저 자기한테 옮기는 트랜잭션을 쏜다.
(token.transferFrom(0xAlice, 0xBob, 30))
Alice의 approve 트랜잭션은 allowance를 0 -> 20 으로 바꾸게 된다.
Bob은 또 20만큼을 옮긴다.
Bob은 Alice의 의도와 달리, 총
50만큼을 사용하였다.
만일,10만큼 늘려주고자 하는 의도로40으로 호출하였다면 Bob은 총70만큼을 사용하였을 것이다.
Non-standard인 SafeERC20의 increaseAllowance, decreaseAllowance를 사용하도록 권장하고 있다.
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}
동일하게 _approve()함수를 사용하지만, 기존 값에다 덮어쓰는 것이 아니라,
기존 값에다가 더해주는 방식이다.
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}
return true;
}
increaseAllowance()와 동일하지만, arithmetic underflow를 방지하는 코드가 들어간다.
기본적으로 approve() 함수가 기존의 값을 overwrite하던 방식과 달리,
해당 트랜잭션이 채굴될 시점을 기준으로 +x 만큼, 혹은 -x 만큼 allowance를 수정해준다.
다음의 예시를 살펴보자.
Alice가 Bob에게 30 만큼 increaseAllowance() 하였다.
(token.increaseAllowance(0xBob, 30))
Alice가 10 만큼 allowance를 늘리고자 한다.
(token.increaseAllowance(0xBob, 10))
Bob이 이 트랜잭션이 채굴되기 전, mempool에서 이걸 보았다.
채굴이 되기 전에 이걸 먼저 자기한테 옮기는 트랜잭션을 쏜다.
(token.transferFrom(0xAlice, 0xBob, 30))
Alice의 approve 트랜잭션은 allowance를 0 -> 10 으로 바꾸게 된다.
(Alice는 30 -> 40을 생각했을 수도 있지만, 상관이 없다)
Bob은 추가적으로 10만큼의 token을 더 가져갈 수 있지만, 이는 Alice의 의도에 맞는다.
(만일 기존처럼 approve(0xBob, 40)을 하였다면, Bob은 40을 더 가져가 총 70을 챙겼을 것이다.)
Alice가 allowance를 줄이고자 했던 시나리오이다.
decreaseAllowance를 쓰는 경우를 살펴보자.
Alice가 Bob에게 30 만큼 approve하였다.
(token.increaseAllowance(0xBob, 30))
Alice가 마음이 바뀌었다. allowance를 20으로 줄이고 싶다.
(token.decreaseAllowance(0xBob, 10))
Bob이 이 트랜잭션이 채굴되기 전, mempool에서 이걸 보았다.
채굴이 되기 전에 이걸 먼저 자기한테 옮기는 트랜잭션을 쏜다.
(token.transferFrom(0xAlice, 0xBob, 30))
Alice의 decreaseAllowance 트랜잭션은 underflow로 인해 revert된다.
여기서는 Bob에게
20만큼을 허용하고자 했던 Alice의 의도대로 흘러가지는 않는다.
그치만.... 적어도approve(0xBob, 20)을 했을 경우보다는20이나 손해를 덜 보았다.
(이미 돈을 빼간 Bob에게20만큼 더approve해주는 일은 일어나지 않았다.)
front-running을 고려하여 ERC20의 approve 대신에 increaseAllowance와 decreaseAllowance를 사용하여 allowance를 조절하는 방식에 대해 알아보았다.
increaseAllowance의 경우에는 정확히 owner의 의도대로 동작하게 되었지만,
decreaseAllowance의 경우에는 완벽하게는 아니지만 front-running으로 인한 피해를 어느 정도 막을 수 있게 된다.