출처 - 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으로 인한 피해를 어느 정도 막을 수 있게 된다.