Project

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

Unrestricted External Call in callExternalContract Function Allows Full System Compromise and Financial Asset Drain by Malicious Actor

Summary

The unrestricted external call capability in the MembershipERC1155 contract poses a critical security vulnerability that could lead to complete compromise of token security and financial assets. Since the contract manages membership tokens and profit distribution, a malicious actor with OWP_FACTORY_ROLE could exploit this to drain funds, manipulate token balances, or execute unauthorized operations through carefully crafted external calls. The lack of contract and function whitelisting means any contract address and any function signature can be called, effectively bypassing the carefully designed access controls and financial safeguards present in the rest of the contract.

The issue stems from the callExternalContract function implementing a generic delegatecall pattern without proper boundaries on what can be executed. While the function is protected by OWP_FACTORY_ROLE, this single authorization check is insufficient given the broad capabilities granted. The implementation allows for arbitrary execution of any function on any external contract, opening attack vectors where a malicious actor could potentially:

  1. Execute malicious code that reenters the contract during profit distribution

  2. Call sensitive functions on connected protocol contracts

  3. Upgrade proxy implementations if connected to upgradeable contracts

  4. Manipulate token balances through unauthorized minting or burning

  5. Drain accumulated profits before legitimate distribution

The vulnerability is particularly severe because the contract handles financial assets and membership governance, where any compromise could affect both asset security and organizational control.

Vulnerable Code

https://github.com/Cyfrin/2024-11-one-world/blob/main/contracts/dao/tokens/MembershipERC1155.sol#L218

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

Scenario

Consider a scenario where the contract is part of a DAO's membership system. A malicious actor who gains OWP_FACTORY_ROLE could craft a call to an external contract that they control. This contract could then execute a series of actions like:

  1. Call back into profit distribution functions multiple times through reentrancy

  2. Execute state changes during an ongoing profit distribution

  3. Manipulate token balances during critical operations

  4. Call sensitive admin functions on connected protocol contracts

The attack becomes possible because the current implementation places no restrictions on which contracts can be called or which functions can be executed, making the OWP_FACTORY_ROLE effectively equivalent to complete admin control over the entire system.

Recommended Fix

// Add storage for whitelisted contracts and function signatures
contract MembershipERC1155 is ERC1155Upgradeable, AccessControlUpgradeable, IMembershipERC1155 {
// Whitelist mappings
mapping(address => bool) public whitelistedContracts;
mapping(address => mapping(bytes4 => bool)) public whitelistedFunctions;
// Events for tracking whitelist changes
event ContractWhitelisted(address indexed contractAddress, bool status);
event FunctionWhitelisted(address indexed contractAddress, bytes4 indexed functionSig, bool status);
// Whitelist management functions
function setContractWhitelist(address contractAddress, bool status)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
require(contractAddress != address(0), "Invalid address");
require(Address.isContract(contractAddress), "Not a contract");
whitelistedContracts[contractAddress] = status;
emit ContractWhitelisted(contractAddress, status);
}
function setFunctionWhitelist(
address contractAddress,
bytes4 functionSig,
bool status
)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
require(whitelistedContracts[contractAddress], "Contract not whitelisted");
whitelistedFunctions[contractAddress][functionSig] = status;
emit FunctionWhitelisted(contractAddress, functionSig, status);
}
// Secured external call function
function callExternalContract(address contractAddress, bytes memory data)
external
payable
onlyRole(OWP_FACTORY_ROLE)
nonReentrant
returns (bytes memory)
{
require(contractAddress != address(0), "Invalid contract address");
require(Address.isContract(contractAddress), "Target must be a contract");
require(whitelistedContracts[contractAddress], "Contract not whitelisted");
bytes4 functionSig = bytes4(data[:4]);
require(whitelistedFunctions[contractAddress][functionSig], "Function not whitelisted");
// Execute the call and capture success
(bool success, bytes memory returndata) = contractAddress.call{value: msg.value}(data);
require(success, "External call failed");
return returndata;
}
}

This implementation adds several critical security improvements:

  1. Contract whitelist management with admin-only control

  2. Function signature whitelist for approved contracts

  3. Reentrancy protection using nonReentrant modifier

  4. Proper validation of contract addresses

  5. Events for tracking whitelist changes and maintaining transparency

  6. Granular control over which functions can be called on which contracts

The whitelisting system allows the DAO to explicitly define which contracts and functions are allowed, significantly reducing the attack surface while maintaining necessary functionality. Consider also implementing:

  1. Timelock for whitelist changes

  2. Multi-signature requirements for whitelist modifications

  3. Additional validation of function parameters

  4. Maximum value limits for payable calls

  5. Emergency pause mechanism for external calls

Updates

Lead Judging Commences

0xbrivan2 Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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