codehawks-2023-07-codehawks-escrow-contract-m01
[M-01] Fee-on-transfer tokens aren’t supported
Summary
Fee-on-transfer 토큰을 지원하지 않는다.
Keyword
fee-on-transfer, erc20, business logic vul
Vulnerability
Fee-on-transfer 토큰이란 transfer를 호출할 때마다 수수료를 떼는 특수한 ERC20이다. 수수료는 보내는 유저에게서 추가로 떼어가는 토큰도 있고, 보내고자 하는 양에서 수수료를 떼고 남은 양을 보내는 토큰도 있다. 후자의 경우 Fee-on-transfer 토큰을 지원한다면 떼이는 수수료보다 적은 양의 토큰을 보낸다면 언더플로우로 인해 revert될 가능성이 있다. 또한 실제로 받는 토큰은 transfer를 요청한 토큰의 양보다 적은 양이 될 수 있다.
Fee-on-transfer 토큰의 대표적인 예는 USDT 토큰이다.
function transfer(address _to, uint _value) public onlyPayloadSize(2 * 32) {
uint fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
uint sendAmount = _value.sub(fee);
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
if (fee > 0) {
balances[owner] = balances[owner].add(fee);
Transfer(msg.sender, owner, fee);
}
Transfer(msg.sender, _to, sendAmount);
}EscrowFactory 컨트랙트에서 Escrow 컨트랙트를 배포할 때 ERC20의 주소를 입력받아 초기화에 이용한다. 이 때 ERC20은 어떤 토큰이든 가능하다. 따라서 Fee-on-transfer 토큰을 이용하는 시나리오도 충분히 가능하다.
tokenContract.safeTransferFrom(msg.sender, computedAddress, price);
Escrow escrow = new Escrow{salt: salt}(
price,
tokenContract,
msg.sender,
seller,
arbiter,
arbiterFee
);Escrow 컨트랙트를 배포하기 전 먼저 배포될 주소를 계산한 후, tokenContract.safeTransferFrom(msg.sender, computedAddress, price)를 통해 토큰을 price만큼 에치해둔다. 하지만 Fee-on-transfer 토큰의 경우 transfer 과정에 수수료를 뜯어가 실제로 Escrow 컨트랙트에 예치되는 토큰은 price - fee 만큼이 된다.
Escrow 컨트랙트의 생성자에서는 토큰이 미리 예치되어 있는지 확인한다. 이 때, tokenContract.balanceOf(address(this))는 price - fee이므로 생성자에서 revert 된다.
constructor(
uint256 price,
IERC20 tokenContract,
address buyer,
address seller,
address arbiter,
uint256 arbiterFee
) {
...
if (tokenContract.balanceOf(address(this)) < price) revert Escrow__MustDeployWithTokenBalance();
...
}따라서 USDT와 같은 Fee-on-transfer 토큰은 이 Escrow 컨트랙트를 이용할 수 없다.
Impact
Fee-on-transfer 토큰을 서비스에 이용할 수 없다.
Mitigation
Escrow 생성자 파라미터로 price 대신 fee를 제한, 실제로 예치된 토큰 양을 넘긴다.
Escrow escrow = new Escrow{salt: salt}(
- price,
+ tokenContract.balanceOf(computedAddress),
tokenContract,
msg.sender,
seller,
arbiter,
arbiterFee
);Memo
프로젝트에서 특정 토큰만 다루겠다고 명시하지 않는다면 Fee-on-transfer 토큰이 지원되는지 확인하자. 여러 ERC20을 포괄적으로 다루는 프로젝트에서는 이를 고려해야 한다.
이 컨테스트에서는 WETH, USDC, LINK, DAI 등의 토큰이 사용된다고 했지만, USDT같은 토큰도 대중적이므로 이를 엑셉한 것 같다.
tags: bughunting, codehawks, smart contract, solidity, erc20, fee-on-transfer token, business-logic-vul, severity medium