팀 프로젝트를 위해 Contract를 어떻게 구조를 잡을 것인가에 대해 이야기하다가 approve 함수에 대해 이야기를 나누게 되었습니다. Approve를 걸어놓는 금액을 max로 설정하는 것에 대한 이야기였는데, 이전에 Openzeppelin 코드를 살펴본 기억을 토대로 approve의 amount는 owner의 balanceOf 값을 넘지 못하지 않나 하는 생각이 들었습니다.
(transferFrom을 진행할 때에 balanceOf 값이 넘는 amount는 트랜잭션이 성공하지 못하기 때문에 당연히 allowance도 balanceOf 값과 유관하게 설정해야 한다고 생각하고 있었던 듯합니다.)
그래서 다시 Openzeppelin의 ERC20 코드를 살펴 보았는데, 해당 코드에서는 놀랍게도 approve를 진행할 때에 owner의 balanceOf 값과는 무관하게 토큰 amount를 지정할 수 있게 되어있었습니다. 추가로 Max값으로 allowance를 지정하는 것 역시 가능했었습니다.
물론 ERC20 표준에만 어긋나지 않는 선에서 custom해서 사용 가능하지만, 그래도 많이 쓰이고 있는 Openzeppelin에서 이렇게 구현했다는 점에 의의를 두고 정리해보았습니다.
ERC20의 approve 메서드
ERC20은 대체 가능한 토큰 즉, Fungible Token의 표준이다. 이렇게 표준이 존재하기 때문에 이더리움 네트워크 상에서는 ERC20 토큰끼리의 호환성을 보장할 수 있게 된다. ERC20 표준에서는 정의해야 하는 여러 method들이 존재하는데, approve도 이 중 하나이다.
(EIP20 문서에서 이를 확인할 수 있다.)
function approve(address _spender, uint256 _value) public returns (bool success)
approve는 파라미터로 _spender와 _value를 입력받는데, approve를 호출한 사람이 소유한 토큰 중 _value만큼을 _spender가 인출할 수 있는 권한을 지니게 된다. 이때 msg.sender가 _spender에게 인출 권한을 준 수량인 _value는 allowance라고 부른다. approve를 여러 번 호출하면, 이 allownace를 계속 덮어쓰면서 갱신할 수 있다. 이후, _spender는 msg.sender의 토큰 중 _value 이하만큼을 transferFrom 메서드를 통해 다른 유저에게 전송할 수 있다.
Openzeppelin의 ERC20 구현
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
사실, approve 함수 자체의 구현은 굉장히 간단한 편이다. 실제 approve를 진행한다고 볼 수 있는 로직들은 모두 internal인 _approve에서 진행된다고 볼 수 있다. 우선은 address의 owner를 _msgSender로 지정하고(_msgSender는 해당 tx를 호출한 address를 의미) _approve(owner, opender, amount)를 호출한다.
function _approve(
address owner,
address spender,
uint256 amount
) internal virtual
{
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
_approve에서는 실제로 allowance 값을 바꾸어 준 후, approve가 일어났다는 의미로 Approval 이벤트를 발생시킨다. 이때, owner나 spender의 address에 대해서는 유효성 검사를 하지만, amount에 대한 유효성 검사는 따로 진행하지 않는다. 이는 EIP20에도 approve에서 amount 값에 대해 제한하고 있거나 따로 언급을 한 내용이 없기 때문인 것 같다.
즉, 유저는 자신의 balanceOf 보다 더 큰 금액을 spender에게 인출 권한을 줄 수 있는 것이다.
앞서 포스팅을 진행하기 전에, Approve를 걸어놓는 금액을 max로 설정하는 것에 대한 이야기를 나누었다고 언급했었다. 이와 관련된 내용은 _spendAllowance에서 살펴볼 수 있었다.
_spendAllownace는 owner - spender의 allowance의 값을 줄여주는 역할을 하는데, 이는 owner의 토큰 중 일부(혹은 전부)를 spender가 다른 주소로 전송한 경우 allowance가 줄어들어야 하기 때문에 존재한다. 즉, transferFrom에서 이를 호출한다.
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
transferFrom은 위와 같이 구현되어 있는데, _msgSender()를 spender로 설정한 후 순차적으로 allowance를 줄여준 후 token을 to 주소에게 전송한다. transferFrom도 실제 토큰을 전송하는 로직은 internal인 _transfer에서 실행한다.
function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
_spendAllownace를 살펴 보면, if문으로 currentAllownace가 type(uint256).max인지 확인하는 구문이 있다.
이 조건문이 true가 된다면, currentAllowance 즉, owner가 spender에게 걸어놓은 양이 type(uint256).max가 아니라면 _approve 함수를 다시 호출하여 현재 transfer가 일어날 만큼의 양만큼 줄어든 양으로 다시 allowance를 바꾸어준다. 이때, type(uint256).max 값은 2**256-1을 의미하며 solidity에서 나타낼 수 있는 가장 큰 uint256 값이다.
만약, currentAllownace가 type(uint256).max라면 _approve를 다시 호출하지 않는데, 이는 currentAllowance가 계속 max 값으로 유지된다는 것을 의미한다. 이렇게 max 값으로 한 번 지정하게 되면, 다시 approve를 호출하지 않는 이상 spender가 owner의 토큰을 무한정 전송할 수 있게 된다.
사실, 유저들이 토큰 거래를 하면서 모든 토큰의 컨트랙트를 살펴본 후에 거래하는 것이 아니기 때문에 approve 함수는 유저의 지갑을 공격하기 굉장히 좋은 지점인 것 같다. 토큰 거래를 하기 이전, 혹은 토큰에 대해서 approve를 진행하기 전에는 해당 사이트가 스캠 사이트인지 아닌지 서칭해 본 후에 진행하는 것이 좋을 것 같다.
'Blockchain' 카테고리의 다른 글
Smart contract의 code size를 줄이는 방법 (0) | 2023.04.10 |
---|---|
Hardhat을 통해 Polygon Test network에서 swap test 진행하기 (0) | 2023.04.04 |
Ethers.js를 이용해서 Event logging하기 (0) | 2023.03.29 |
Solidity Optimizer의 runs 옵션 이해하기 (3) | 2023.02.27 |
ERC20의 approve()과 front-running (0) | 2022.11.28 |
댓글