code4rena-2023-06-angle-protocol-h01
[H-01] Possible reentrancy during redemption/swap
Summary
담보 토큰이 ERC777인 경우 redeem/swap으로 담보 토큰을 받을 시 tokensReceived 훅을 통해 재진입이 가능하고, 재진입을 통해 담보 해제를 요청하면 실제보다 더 많은 담보 토큰을 돌려받을 수 있다.
Keyword
erc777, reentrancy, theft, stablecoin, defi
Vulnerability
- contracts/transmuter/facets/Redeemer.sol#L102-L136
- contracts/transmuter/facets/Swapper.sol#L176-L222
Redeemer 컨트랙트에서 담보 토큰을 맡기고 agToken을 얻을 수 있고, redeem 기능을 이용하면 agToken을 burn하며 담보 토큰을 되돌려받을 수 있다. redeem을 호출하는 엔트리포인트는 여러개 있지만, 모두 내부적으로 _redeem를 호출하며 이 함수에 핵심 로직이 있다.
function _redeem(
uint256 amount,
address to,
uint256 deadline,
uint256[] memory minAmountOuts,
address[] memory forfeitTokens
) internal returns (address[] memory tokens, uint256[] memory amounts) {
...
(tokens, amounts, subCollateralsTracker) = _quoteRedemptionCurve(amount);
...
IAgToken(ts.agToken).burnSelf(amount, msg.sender);
address[] memory collateralListMem = ts.collateralList;
uint256 indexCollateral;
for (uint256 i; i < amounts.length; ++i) {
if (amounts[i] < minAmountOuts[i]) revert TooSmallAmountOut();
// If a token is in the `forfeitTokens` list, then it is not sent as part of the redemption process
if (amounts[i] > 0 && LibHelpers.checkList(tokens[i], forfeitTokens) < 0) {
Collateral storage collatInfo = ts.collaterals[collateralListMem[indexCollateral]];
if (collatInfo.onlyWhitelisted > 0 && !LibWhitelist.checkWhitelist(collatInfo.whitelistData, to))
revert NotWhitelisted();
if (collatInfo.isManaged > 0)
LibManager.release(tokens[i], to, amounts[i], collatInfo.managerData.config);
else IERC20(tokens[i]).safeTransfer(to, amounts[i]);
}
if (subCollateralsTracker[indexCollateral] - 1 <= i) ++indexCollateral;
}
emit Redeemed(amount, tokens, amounts, forfeitTokens, msg.sender, to);
}돌려받을 담보 토큰의 수는 _quoteRedemptionCurve 함수를 통해 계산된다.
function _quoteRedemptionCurve(
uint256 amountBurnt
)
internal
view
returns (address[] memory tokens, uint256[] memory balances, uint256[] memory subCollateralsTracker)
{
...
(collatRatio, stablecoinsIssued, tokens, balances, subCollateralsTracker) = LibGetters.getCollateralRatio();
...
uint256 balancesLength = balances.length;
for (uint256 i; i < balancesLength; ++i) {
// The amount given for each token in reserves does not depend on the price of the tokens in reserve:
// it is a proportion of the balance for each token computed as the ratio between the stablecoins
// burnt relative to the amount of stablecoins issued.
// If the protocol is over-collateralized, the amount of each token given is inversely proportional
// to the collateral ratio.
balances[i] = collatRatio >= BASE_9
? (amountBurnt * balances[i] * (uint64(yRedemptionCurveMem[yRedemptionCurveMem.length - 1]))) /
(stablecoinsIssued * collatRatio)
: (amountBurnt * balances[i] * penaltyFactor) / (stablecoinsIssued * BASE_9);
}
}받을 담보 토큰 수는 balances[i] = collatRatio >= BASE_9 ? (amountBurnt * balances[i] * (uint64(yRedemptionCurveMem[yRedemptionCurveMem.length - 1]))) / (stablecoinsIssued * collatRatio) : (amountBurnt * balances[i] * penaltyFactor) / (stablecoinsIssued * BASE_9); 로 계산되며, 계산에 사용된 balances[i]에는 컨트랙트에 예치된 담보 토큰 수가 있다.
이제 취약점에 대하여 생각해보자. _redeem 함수에서는 담보 토큰이 ERC777일 가능성을 고려하지 않았다. IERC20(tokens[i]).safeTransfer(to, amounts[i])로 토큰을 직접 전송하거나 LibManager.release를 호출해 토큰을 이동할 때, 이 토큰이 ERC777이라면 토큰 수신자 측의 tokensReceived 훅이 실행될 수 있다. 이를 이용해 재진입이 가능하고 이를 이용해 더 많은 담보를 가져올 수 있다.
다음과 같은 시나리오로 공격이 가능하다.
- 2개의 담보 colA와 colB 토큰을 사용한다고 가정하자. Transmuter 컨트랙트는 colA와 colB토큰을 각각 1000개씩 가지고 있다. Alice는 20개의 agToken을 가지고 있다.
- Alice가 10개의 agToken을 주며
redeem을 호출하면 10개의 colA와 colB를 받아야 한다. - colA 토큰은 ERC777이라고 가정하자. colA 토큰을 먼저 받고,
tokensReceived훅을 이용하여redeem(10)를 재호출한다. - 2번째
redeem에서, 총 담보 양은 colA = 990, colB = 1000인 상태이다. colB가 아직 첫번째redeem에서 보내지지 않았기 때문이다. - Alice는 2번째
redeem에서 원래 받아야하는 양보다 더 많은 담보를 상환 받는다.
동일하게, ERC777로 인한 재진입 이슈가 swap 에서도 일어날 수 있을 것이다.
Impact
원래보다 많은 양의 담보 토큰을 상환받음
Mitigation
_redeem과 _swap 함수에 재진입 가드를 추가
tags: bughunting, angle protocol, smart contract, solidity, stablecoin, defi, reentrancy, erc777, erc20, severity high