Beanstalk Notion
Beanstalk Notion
/
🪲
Bug Reports
/
BIC Notes
/
📄
Report #31740
📄

Report #31740

Report Date
May 26, 2024
Status
Closed
Payout

Lack of Validation on DiamondCutFacet.sol contract

‣
Report Info

Report ID

#31740

Report type

Smart Contract

Has PoC?

Yes

Target

https://etherscan.io/address/0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5

Impacts

Permanent freezing of funds

Description

The diamondCut function in the provided smart contract lacks adequate validation checks, allowing critical functions to be removed, replaced, or added maliciously. This vulnerability could lead to the contract becoming unusable, funds being permanently frozen, or unauthorized control over the contract.

Vulnerability Details

The diamondCut function allows the owner to add, replace, or remove functions from the contract. However, there are no checks to prevent the removal of critical functions, such as those required for transferring ownership or handling funds. Additionally, there are no validations to ensure that the functions being added or replaced are not malicious. Here is the problematic function:

This function checks for valid facet addresses, ensures critical functions are not removed, and validates the delegate call to _init.

Impact Details

The impact of this vulnerability can be severe, including:

  • Permanent Freezing of Funds: If critical functions that manage fund transfers are removed or replaced incorrectly, users may be unable to access or withdraw their funds.
  • Ownership Hijacking: Replacing the ownership transfer function with a malicious one can allow an attacker to take over the contract.
  • Functionality Disruption: Removing or replacing essential functions can lead to the contract becoming non-functional, disrupting all services dependent on it.

These issues can result in significant financial losses and loss of trust in the contract and associated services.

References

  • [EIP-2535 Diamond Standard](https://eips.ethereum.org/EIPS/eip-2535)
  • [Solidity Security Considerations](https://docs.soliditylang.org/en/v0.7.6/security-considerations.html)
  • [Delegatecall Security Risks](https://consensys.github.io/smart-contract-best-practices/known_attacks/#delegatecall)

Proof of concept

Scenario: Removal of Critical Functions

Let's demonstrate how the lack of validation in the diamondCut function can lead to the removal of a critical function, such as setContractOwner, which can subsequently freeze the contract's ability to change ownership.

Step-by-Step Proof of Concept

  1. Setup the Contract:
    • Deploy the DiamondCutFacet contract along with the necessary library and interface contracts.
  2. Attempt to Remove setContractOwner:
    • Use the diamondCut function to remove the setContractOwner function from the contract.

Deployment Script

First, deploy the necessary contracts.

Proof of Concept Script

This script demonstrates how an attacker can remove the setContractOwner function.

Execution

  1. Deploy DiamondCutFacet Contract:
    • Deploy the DiamondCutFacet contract to the blockchain.
  2. Deploy Attack Contract:
    • Deploy the AttackDiamondCut contract to the blockchain.
  3. Remove setContractOwner Function:
    • Call removeSetContractOwner function from the AttackDiamondCut contract, passing the address of the DiamondCutFacet contract.

Expected Outcome

After executing the proof of concept:

  • The setContractOwner function will be removed from the DiamondCutFacet contract.
  • The owner will no longer be able to transfer ownership of the contract.
  • This demonstrates how the lack of validation in the diamondCut function can lead to permanent loss of critical functionality, effectively freezing the contract.

Mitigation

To mitigate this risk, ensure that the diamondCut function includes validation checks to prevent the removal or replacement of critical functions. The enhanced contract code provided earlier adds these necessary checks.

References

  • [EIP-2535 Diamond Standard](https://eips.ethereum.org/EIPS/eip-2535)
  • [Solidity Security Considerations](https://docs.soliditylang.org/en/v0.7.6/security-considerations.html)
  • [Delegatecall Security Risks](https://consensys.github.io/smart-contract-best-practices/known_attacks/#delegatecall)

Immunefi Response

We have reviewed your submission, but unfortunately, we are closing the report for the following reasons:
  • The submission contains the output of an automated scanner without demonstrating that it is a valid issue.
  • The submission lacks the required information regarding the vulnerability's impact on the reported asset.

As per the bug bounty program's policy, we require all submissions to be accompanied by a Proof of Concept (PoC) that demonstrates the vulnerability's existence and impact. Since the submission doesn't provide any proof of the vulnerability's existence, we have decided to close it.

Please note that the project will receive a report of the closed submission and may choose to re-open it, but they are not obligated to do so.

function diamondCut(
    FacetCut[] calldata _diamondCut,
    address _init,
    bytes calldata _calldata
) external override {
    LibDiamond.enforceIsContractOwner();
    _validateDiamondCut(_diamondCut);
    _validateDelegateCall(_init, _calldata);
    LibDiamond.diamondCut(_diamondCut, _init, _calldata);
}

function _validateDiamondCut(FacetCut[] calldata _diamondCut) internal view {
    for (uint256 i = 0; i < _diamondCut.length; i++) {
        IDiamondCut.FacetCutAction action = _diamondCut[i].action;
        address facetAddress = _diamondCut[i].facetAddress;
        bytes4[] memory functionSelectors = _diamondCut[i].functionSelectors;

        require(facetAddress != address(0) || action == IDiamondCut.FacetCutAction.Remove, "DiamondCutFacet: Invalid facet address");

        for (uint256 j = 0; j < functionSelectors.length; j++) {
            bytes4 selector = functionSelectors[j];

            if (action == IDiamondCut.FacetCutAction.Remove) {
                _ensureNotCriticalFunction(selector);
            } else if (action == IDiamondCut.FacetCutAction.Replace) {
                _ensureNotReplacingWithMaliciousFunction(facetAddress, selector);
            }
        }
    }
}

function _ensureNotCriticalFunction(bytes4 selector) internal pure {
    bytes4[] memory criticalSelectors = new bytes4[](2);
    criticalSelectors[0] = this.diamondCut.selector;
    criticalSelectors[1] = LibDiamond.setContractOwner.selector;

    for (uint256 i = 0; i < criticalSelectors.length; i++) {
        require(selector != criticalSelectors[i], "DiamondCutFacet: Cannot remove critical function");
    }
}

function _ensureNotReplacingWithMaliciousFunction(address facetAddress, bytes4 selector) internal view {
    uint256 codeSize;
    assembly {
        codeSize := extcodesize(facetAddress)
    }
    require(codeSize > 0, "DiamondCutFacet: New facet address has no code");
}

function _validateDelegateCall(address _init, bytes calldata _calldata) internal view {
    require(_init != address(0), "DiamondCutFacet: _init address cannot be zero");
    if (_calldata.length > 0) {
        uint256 codeSize;
        assembly {
            codeSize := extcodesize(_init)
        }
        require(codeSize > 0, "DiamondCutFacet: _init address has no code");
    }
}
// SPDX-License-Identifier: MIT

pragma solidity ^0.7.6;

import {IDiamondCut} from "../../interfaces/IDiamondCut.sol";
import {LibDiamond} from "../../libraries/LibDiamond.sol";

contract DiamondCutFacet is IDiamondCut {
    function diamondCut(
        FacetCut[] calldata _diamondCut,
        address _init,
        bytes calldata _calldata
    ) external override {
        LibDiamond.enforceIsContractOwner();
        _validateDiamondCut(_diamondCut);
        _validateDelegateCall(_init, _calldata);
        LibDiamond.diamondCut(_diamondCut, _init, _calldata);
    }

    function _validateDiamondCut(FacetCut[] calldata _diamondCut) internal view {
        for (uint256 i = 0; i < _diamondCut.length; i++) {
            IDiamondCut.FacetCutAction action = _diamondCut[i].action;
            address facetAddress = _diamondCut[i].facetAddress;
            bytes4[] memory functionSelectors = _diamondCut[i].functionSelectors;

            require(facetAddress != address(0) || action == IDiamondCut.FacetCutAction.Remove, "DiamondCutFacet: Invalid facet address");

            for (uint256 j = 0; j < functionSelectors.length; j++) {
                bytes4 selector = functionSelectors[j];

                if (action == IDiamondCut.FacetCutAction.Remove) {
                    _ensureNotCriticalFunction(selector);
                } else if (action == IDiamondCut.FacetCutAction.Replace) {
                    _ensureNotReplacingWithMaliciousFunction(facetAddress, selector);
                }
            }
        }
    }

    function _ensureNotCriticalFunction(bytes4 selector) internal pure {
        bytes4[] memory criticalSelectors = new bytes4[](2);
        criticalSelectors[0] = this.diamondCut.selector;
        criticalSelectors[1] = LibDiamond.setContractOwner.selector;

        for (uint256 i = 0; i < criticalSelectors.length; i++) {
            require(selector != criticalSelectors[i], "DiamondCutFacet: Cannot remove critical function");
        }
    }

    function _ensureNotReplacingWithMaliciousFunction(address facetAddress, bytes4 selector) internal view {
        uint256 codeSize;
        assembly {
            codeSize := extcodesize(facetAddress)
        }
        require(codeSize > 0, "DiamondCutFacet: New facet address has no code");
    }

    function _validateDelegateCall(address _init, bytes calldata _calldata) internal view {
        require(_init != address(0), "DiamondCutFacet: _init address cannot be zero");
        if (_calldata.length > 0) {
            uint256 codeSize;
            assembly {
                codeSize := extcodesize(_init)
            }
            require(codeSize > 0, "DiamondCutFacet: _init address has no code");
        }
    }
}
// SPDX-License-Identifier: MIT

pragma solidity ^0.7.6;

import {IDiamondCut} from "../../interfaces/IDiamondCut.sol";
import {LibDiamond} from "../../libraries/LibDiamond.sol";

contract AttackDiamondCut {
    function removeSetContractOwner(DiamondCutFacet diamondCutFacet) external {
        IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1);
        bytes4[] memory functionSelectors = new bytes4[](1);
        functionSelectors[0] = LibDiamond.setContractOwner.selector;

        cut[0] = IDiamondCut.FacetCut({
            facetAddress: address(0), // address(0) for removal
            action: IDiamondCut.FacetCutAction.Remove,
            functionSelectors: functionSelectors
        });

        diamondCutFacet.diamondCut(cut, address(0), "");
    }
}