code4rena-2021-06-pooltogether-m06
[M-06] YieldSourcePrizePool_canAwardExternal does not work
Summary
YieldSourcePrizePool._canAwardExternal 함수는 ib 토큰을 리워드로 사용하는 것을 막기 위한 함수이지만 실제로 ib 토큰인지 제대로 확인하지 못해 막지 못한다.(고 주장했지만 내 생각엔 틀린듯.)
Keyword
bug, logic flaw
Vulnerability
_canAwardExternal 함수의 용도는 복권 당첨금을 ControlledToken이 아닌 다른 토큰으로 주고싶을 때, 해당 토큰이 상금으로 사용해도 되는 토큰인지를 확인하는 것이다. abstract 컨트랙트인 PrizePool 컨트랙트에는 virtual로 정의되어 있으며, 이를 상속하는 컨트랙트에서 구현해야 한다.
function transferExternalERC20(
address to,
address externalToken,
uint256 amount
)
external override
onlyPrizeStrategy
{
if (_transferOut(to, externalToken, amount)) {
emit TransferredExternalERC20(to, externalToken, amount);
}
}
function _transferOut(
address to,
address externalToken,
uint256 amount
)
internal
returns (bool)
{
require(_canAwardExternal(externalToken), "PrizePool/invalid-external-token");
if (amount == 0) {
return false;
}
IERC20Upgradeable(externalToken).safeTransfer(to, amount);
return true;
}
function _canAwardExternal(address _externalToken) internal virtual view returns (bool);
YieldSourcePrizePool는 PrizePool를 상속한 구현이다. YieldSourcePrizePool._canAwardExternal 함수의 주석을 보면, 상금으로 사용할 외부 토큰이 YieldSource에 토큰을 예치하며 받은 ib토큰이면 안 된다는 룰을 명시한다. YieldSource 컨트랙트 자체가 ERC20으로, Underlying 토큰을 예치하면 ib(interest-bearing) 토큰을 발행해준다. (토큰화되지 않은 YieldSource 컨트랙트도 있긴 하다.)
즉, YieldSourcePrizePool._canAwardExternal 함수는 _externalToken가 YieldSource의 ib 토큰인지 확인해야 한다.
/// @notice Determines whether the passed token can be transferred out as an external award.
/// @dev Different yield sources will hold the deposits as another kind of token: such a Compound's cToken. The
/// prize strategy should not be allowed to move those tokens.
/// @param _externalToken The address of the token to check
/// @return True if the token may be awarded, false otherwise
function _canAwardExternal(address _externalToken) internal override view returns (bool) {
return _externalToken != address(yieldSource);
}하지만 yieldSource 변수에 저장된 주소는 이자 농사를 요청하는 엔트리포인트 컨트랙트 주소이지, ib 토큰(interest-bearing 토큰)의 주소는 아니다.(라고 주장했다.) 따라서 이 함수는 주석에서 설명하는 것처럼 동작하지 못하는 경우가 있다.
Impact
_canAwardExternal 함수에서 ib 토큰 주소와의 비교를 해내지 못한다. ib 토큰을 복권 당첨 리워드로 사용할 수 있다.
Mitigation
interestToken() 같은 함수를 만들어 return _externalToken != yieldSource.interestToken(); 와 같이 비교해야 한다.
Memo
주최자 측은 이 함수는 레거시이며, 모든 yieldSource가 토큰화되지도 않았으므로 그다지 위험하지 않다고 반응했다.(Badger와 Sushi yieldSource 컨트랙트는 토큰화되지 않았다.) 다만 이 함수가 virtual이므로 꼭 구현해야 하기 때문에 yieldSource가 토큰화되었다고 가정하고 구현했다고 하였다.
내 생각에는 버그헌터가 코드 의도를 잘못 이해한 것 같다. 토큰화되지 않은 컨트랙트와 비교하는 것은 불필요하긴 하지만, ib 토큰을 사용하는 토큰화된 yieldSource 컨트랙트는 그 자체가 ib 토큰(ERC20)이기 때문에, address(yieldSource)와 비교해도 무방하다. 토큰화되지 않은 yieldSource의 경우 _canAwardExternal를 확인할 필요도 없다.
tags: bughunting, pooltogether, smart contract, solidity, logic flaw, severity medium