๐Ÿ‘“Examples

Understanding the Examples: In this section, we dive into practical instances of reentrancy vulnerabilities discovered in actual bug bounty reports, Code Arena analyses, and more. By examining these real-life scenarios, you'll attain a richer, more tangible comprehension of reentrancy attacks and their potential repercussions. This understanding will serve to bolster your smart contract auditing capabilities in a substantive and effective manner.

Example 1: ERC721 _checkOnERC721Received re-entrancy

1. The Core Functionality:

The smart contract in question seems to be a kind of bank or vault that holds XDEFI tokens. Users can "lock" their XDEFI tokens in this contract for some duration and receive a unique token (an NFT) in return, representing their deposit.

2. The Vulnerable Functions:

_safeMint Function: This function mints (or creates) a new token. However, before completing, it checks if the recipient can handle receiving this type of token through the _checkOnERC721Received function.

_checkOnERC721Received Function: This function checks if the receiving address (of the minted token) is a contract and if it can properly handle the incoming token. If it is a contract, it will call the onERC721Received function on that contract.

lock and _lock Functions: These functions handle the process of locking XDEFI tokens and creating the unique token (NFT) in return.

updateDistribution Function: This function updates the distribution of rewards based on the amount of XDEFI in the contract.

3. The Problem:

Reentrancy Vulnerability: After locking XDEFI tokens and before updating the total deposited amount (totalDepositedXDEFI), an external contract (a potentially malicious one) can be triggered due to the _checkOnERC721Received function. If this external contract then tries to interact again with our main contract, it might catch it in an inconsistent state because totalDepositedXDEFI hasn't been updated yet.

In simpler terms, think of it like a bank teller who gives out money to a customer but gets interrupted to perform another task before noting down the transaction. If this interruption can be manipulated, the customer might be able to exploit this situation.

Impact: An attacker can artificially inflate the _pointsPerUnit variable, making it abnormally large. This would then allow them to claim a lot more rewards than they should get. Moreover, they can potentially "unlock" and withdraw more assets than they deposited.

4. Mitigation:

To protect the smart contract from this vulnerability, a reentrancy guard or modifier should be added to functions. This ensures that once a function is being executed, it cannot be interrupted or "re-entered" until it's completed its tasks. It's like a bank teller saying, "Please wait for your current transaction to complete before initiating another one."

Example 2: Double Re-entrancy guard

When writing smart contracts, ensuring their security is of the utmost importance. In Solidity, the nonReentrant modifier is often used as a protective measure against reentrancy attacks. However, using it without a clear understanding can lead to unexpected behavior, as demonstrated in the example below.

function mint(uint256 amount) public nonReentrant override {
    mintTo(amount, msg.sender);
}

function mintTo(uint256 amount, address to) public nonReentrant override {
    require(auction.auctionOngoing() == false);
    // ... Rest of the function implementation ...
}

In the code snippet above, we see that the mint function has a nonReentrant modifier, and it calls the mintTo function which also has a nonReentrant modifier. The issue here is that when the mint function is called, it will set the _notEntered state variable (used in a typical nonReentrant implementation) to false. Then, when it calls the mintTo function, this function will immediately fail because of its own nonReentrant check, seeing that _notEntered is false.

Effectively, this double use of nonReentrant makes the mint function unusable, as it will always result in an error when attempting to invoke the mintTo function.

Recommendation:

To avoid such malfunction and ensure that the contract behaves as expected, one should avoid stacking or nesting the nonReentrant modifier.

The corrected code should be:

function mint(uint256 amount) public override {
    mintTo(amount, msg.sender);
}

function mintTo(uint256 amount, address to) public nonReentrant override {
    require(auction.auctionOngoing() == false);
    // ... Rest of the function implementation ...
}

By removing the nonReentrant modifier from the mint function, we ensure that the guard is only applied once when mintTo is executed, allowing the functions to proceed without unnecessary errors.

Conclusion:

While guards like nonReentrant are powerful tools in a Solidity developer's toolkit, it's crucial to understand their underlying mechanism and use them appropriately. Misuse can lead to undesired behavior and potential vulnerabilities. Always ensure you know the consequences of the modifiers and other security mechanisms you apply.

Example 3: Reentrancy Vulnerability in sponsor() function

Security in smart contracts is a matter of utmost importance. Ensuring that functions are protected from malicious attacks is critical. One such vulnerability often overlooked is reentrancy attacks, and in this spotlight, we'll look at a real-world example of such an oversight.

Problematic Code:

Here is the problematic sponsor() function from the Vault.sol contract:

function sponsor(uint256 _amount, uint256 _lockedUntil)
    external
    override(IVaultSponsoring)
{
    if (_lockedUntil == 0)
        _lockedUntil = block.timestamp + MIN_SPONSOR_LOCK_DURATION;
    else
        require(
            _lockedUntil >= block.timestamp + MIN_SPONSOR_LOCK_DURATION,
            "Vault: lock time is too small"
        );

    uint256 tokenId = depositors.mint(
        _msgSender(),
        _amount,
        0,
        _lockedUntil
    );

    emit Sponsored(tokenId, _amount, _msgSender(), _lockedUntil);

    totalSponsored += _amount;
    _transferAndCheckUnderlying(_msgSender(), _amount);
}

The vulnerability arises due to the depositors.mint() function, which has a callback to the msg.sender. Since there are state updates (totalSponsored variable update and Sponsored event emission) after the external call to depositors.mint(), a malicious actor can exploit this to reenter the sponsor() function.

Effectively, an attacker can exploit this vulnerability by ensuring the totalSponsored amount is updated only once, even after calling mint() several times. The same issue will affect the Sponsored event, which will be emitted only once after the multiple reentries.

Recommendation:

To safeguard against this vulnerability, a reentrancy guard should be added to the sponsor() function. The typical pattern involves adding a state variable like _status which is checked at the beginning and end of the function to prevent reentrancy:

modifier nonReentrant() {
    require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
    _status = _ENTERED;
    _;
    _status = _NOT_ENTERED;
}

Now, add this nonReentrant modifier to the sponsor() function:

function sponsor(uint256 _amount, uint256 _lockedUntil)
    external
    override(IVaultSponsoring)
    nonReentrant
{
    // ... Rest of the function implementation ...
}

Conclusion:

Reentrancy vulnerabilities can lead to unexpected and unintended consequences if not adequately addressed. Developers should always be cautious about the order of state updates and external calls in their smart contracts and utilize tools like the reentrancy guard to ensure robust security.

Last updated