Solidity에서 Low-level call을 하였을 때, return value를 check하지 않을 경우 문제가 생길 수 있는 경우들이 있다.
ERC20 approve과 front running - increaseAllowance(), decreaseAllowance()를 쓰시오 에서 다룬 적이 있는데,
이번 글에서는 문제가 되는 상황들을 한번 살펴보려고 한다.
: transfer
를 사용하면, 사실은 가장 직관적이다. 실패할 경우 revert
를 시켜주므로 가장 깔끔한 방법이다.
하지만 문제는 전달해주는 gas
가 2300
으로 고정되어 있어, receive()
함수에서 자유도가 매우 떨어진다.
따라서 사용하지 않는 것이 좋다.
: transfer
과 call
의 중간 단계인데 정말 아무짝에 쓸모 없는 함수인데 왜 있는지 의문일 정도이다.
transfer
처럼 gas
는 2300
으로 제한되어 있는데, call
처럼 실패하였을 경우 revert
되지도 않는다.
: low-level call에서 사용되는데, tx
의 실패 여부에 따라 true
혹은 false
를 return해준다.
다만 한 가지 주의할 점이 있는데 아래 예시를 보자.
pragma solidity 0.8.18;
contract CallNonExistingAddr {
function test() external {
(bool success, ) = address(0).call(abi.encodeWithSignature("what?"));
require(success); // 통과
}
}
존재하지 않는 주소로 call
을 하였는데, revert
가 생기지 않으면 true
를 return하게 되니, 주의하여야 한다.
receive()
가 없는 contract로 송금receive()
함수가 없는 contract에 송금을 할 경우, 트랜잭션이 실패하게 된다.
하지만 여기서 send
혹은 call
을 사용하고, return value를 체크해주지 않고 만약 contract에서 자산을 withdraw하는 로직이 없다면, 영원히 ETH가 해당 contract에 묶여버리게 된다.
withdraw로직이 있다고 하더라도, 비즈니스로직이 망가질 수 있다.
대출을 받을 때 debtToken을 mint하고 차입자에게 돈을 빌려주는 함수가 있다고 해보자.
escrowNFT.call(abi.encodeWithSignature("mint(address,uint256,uint256)", _recipient, amount, matureTime));
이렇게 low-level call로 함수를 호출하고, return value를 체크하지 않을 경우,
mint함수가 최초에만 실행되고 이후에는 revert되게 된다.
따라서, 차입자는 1번만 debtToken을 mint하고 돈을 계속 무한정 가져갈 수 있게 된다.
function swap(address fromToken, address toToken, uint256 amount) external nonReentrant {
require(isSupported(fromToken, toToken), "one of the tokens (or both) are not supported");
require(amount > 0, "amount should be bigger then 0");
// Check liquidity
uint256 balance = IERC20(toToken).balanceOf(address(this));
require(balance >= amount, "Not enough liquidity");
// Transfer
IERC20(fromToken).transferFrom(msg.sender, address(this), amount);
IERC20(toToken).transfer(msg.sender, amount); value not checked
}
이 예시를 한번 보자.
ERC20의 전송에 대한 함수 구현은 토큰마다 제각각이다.
function transferFrom(address _from, address _to, uint256 _value) public onlyPayloadSize(3 * 32) returns (bool) {
var _allowance = allowed[_from][msg.sender];
if (_value > _allowance) {
return false;
}
uint256 fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
if (_allowance < MAX_UINT) {
allowed[_from][msg.sender] = _allowance.sub(_value);
}
uint256 sendAmount = _value.sub(fee);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
if (fee > 0) {
balances[owner] = balances[owner].add(fee);
Transfer(_from, owner, fee);
}
Transfer(_from, _to, sendAmount);
}
이런 식으로 만약 bool
값만 return을 해주고, 실패 시 revert
를 시키지 않는다면?
위의 예시에서 msg.sender
의 balance가 0이여서 transferFrom
에서 false
가 return되어도 계속 token을 받아올 수 있게 된다.