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

Report #31739

Report Date
May 26, 2024
Status
Closed
Payout

Unbound Gas Consumption on the Diamond.sol Contract

‣
Report Info

Report ID

#31739

Report type

Smart Contract

Has PoC?

Yes

Target

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

Impacts

Unbounded gas consumption

Description

The Diamond contract contains potential unbound gas consumption issues, particularly in its fallback function and dynamic array operations. If these issues are exploited in production, they can lead to out-of-gas errors, making the contract unusable and causing transaction failures, which can disrupt contract functionality and result in financial losses.

Vulnerability Details

Fallback Function with Unbounded Delegatecall

The fallback function in the Diamond contract uses delegatecall to execute functions from various facets. The gas consumption of these delegatecall operations is unbounded, depending on the implementation of the called functions, which can lead to excessive gas usage.

Code:

Dynamic Array Operations

Functions like addFunctions, replaceFunctions, and removeFunctions operate on dynamic arrays, such as facetFunctionSelectors. If these arrays grow too large, the operations on them can consume unbounded gas.

Code:

Unbounded Loop in diamondCut

The diamondCut function iterates over the _diamondCut array, and the size of this array can lead to unbounded gas consumption.

Code:

Impact Details

Severity

The potential impact of these vulnerabilities includes:

  • Transaction Failures: Unbounded gas consumption can cause transactions to run out of gas, leading to transaction failures.
  • Contract Downtime: Continuous failures may render the contract unusable, affecting all its users.
  • Financial Losses: Failed transactions can lead to financial losses due to gas fees and disrupted contract operations.

References

  • [EIP-2535 Diamond Standard](https://eips.ethereum.org/EIPS/eip-2535)
  • [Solidity Assembly Documentation](https://docs.soliditylang.org/en/v0.7.6/assembly.html)
  • [Solidity Gas and Fees](https://docs.soliditylang.org/en/v0.7.6/introduction-to-smart-contracts.html#gas-and-fees)

Proof of concept

To demonstrate the unbounded gas consumption issue in the Diamond contract, we will create a proof-of-concept (PoC) scenario where a function call in the fallback function causes excessive gas usage. We will simulate this scenario by implementing a facet function that performs a gas-intensive operation, and then invoke it through the Diamond contract's fallback function.

Step-by-Step PoC

  1. Deploy the Diamond Contract.
  2. Deploy a Facet with a Gas-Intensive Function.
  3. Add the Facet to the Diamond Contract.
  4. Invoke the Gas-Intensive Function through the Diamond Contract.

Code Implementation

1. Diamond.sol

This is the Diamond contract provided initially:

2. GasIntensiveFacet.sol

This facet contains a gas-intensive function.

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

contract GasIntensiveFacet {
    function gasIntensiveOperation() external {
        uint sum = 0;
        for (uint i = 0; i < 100000; i++) {
            sum += i;
        }
    }
}

3. Deployment and Interaction Script

Use Hardhat or Truffle to deploy the contracts and interact with them.

Running the PoC

  1. Setup Hardhat or Truffle: Ensure you have Hardhat or Truffle configured in your project.
  2. Compile the Contracts: Compile the Diamond and GasIntensiveFacet contracts.
  3. Deploy the Contracts: Run the deploy.js script to deploy the contracts and add the facet.
  4. Invoke the Gas-Intensive Function: The script will invoke the gasIntensiveOperation function through the Diamond contract.

Conclusion

This PoC demonstrates how unbounded gas consumption can occur in the Diamond contract. The GasIntensiveFacet contains a function that performs a gas-intensive operation. When this function is added to the Diamond contract and invoked through the fallback function, it consumes a significant amount of gas, potentially leading to out-of-gas errors. This highlights the need for proper gas management and optimization in smart contract design.

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.

fallback() external payable {
    LibDiamond.DiamondStorage storage ds;
    bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION;
    assembly {
        ds.slot := position
    }
    address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;
    require(facet != address(0), "Diamond: Function does not exist");
    assembly {
        calldatacopy(0, 0, calldatasize())
        let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
        returndatacopy(0, 0, returndatasize())
        switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
    }
}
function addFunctions(address _facetAddress, bytes4[] memory _functionSelectors) internal {
    require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut");
    DiamondStorage storage ds = diamondStorage();
    require(_facetAddress != address(0), "LibDiamondCut: Add facet cant be address(0)");
    uint16 selectorPosition = uint16(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length);
    if (selectorPosition == 0) {
        enforceHasContractCode(_facetAddress, "LibDiamondCut: New facet has no code");
        ds.facetFunctionSelectors[_facetAddress].facetAddressPosition = uint16(ds.facetAddresses.length);
        ds.facetAddresses.push(_facetAddress);
    }
    for (uint256 selectorIndex; selectorIndex < _functionSelectors.length; selectorIndex++) {
        bytes4 selector = _functionSelectors[selectorIndex];
        address oldFacetAddress = ds.selectorToFacetAndPosition[selector].facetAddress;
        require(oldFacetAddress == address(0), "LibDiamondCut: Cant add function that already exists");
        ds.facetFunctionSelectors[_facetAddress].functionSelectors.push(selector);
        ds.selectorToFacetAndPosition[selector].facetAddress = _facetAddress;
        ds.selectorToFacetAndPosition[selector].functionSelectorPosition = selectorPosition;
        selectorPosition++;
    }
}
function diamondCut(
    IDiamondCut.FacetCut[] memory _diamondCut,
    address _init,
    bytes memory _calldata
) internal {
    for (uint256 facetIndex; facetIndex < _diamondCut.length; facetIndex++) {
        IDiamondCut.FacetCutAction action = _diamondCut[facetIndex].action;
        if (action == IDiamondCut.FacetCutAction.Add) {
            addFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
        } else if (action == IDiamondCut.FacetCutAction.Replace) {
            replaceFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
        } else if (action == IDiamondCut.FacetCutAction.Remove) {
            removeFunctions(_diamondCut[facetIndex].facetAddress, _diamondCut[facetIndex].functionSelectors);
        } else {
            revert("LibDiamondCut: Incorrect FacetCutAction");
        }
    }
    emit DiamondCut(_diamondCut, _init, _calldata);
    initializeDiamondCut(_init, _calldata);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
pragma experimental ABIEncoderV2;

import {LibDiamond} from "../libraries/LibDiamond.sol";
import {DiamondCutFacet} from "./facets/DiamondCutFacet.sol";
import {DiamondLoupeFacet} from "./facets/DiamondLoupeFacet.sol";
import {OwnershipFacet} from "./facets/OwnershipFacet.sol";
import {AppStorage} from "./AppStorage.sol";
import {IERC165} from "../interfaces/IERC165.sol";
import {IDiamondCut} from "../interfaces/IDiamondCut.sol";
import {IDiamondLoupe} from "../interfaces/IDiamondLoupe.sol";
import {IERC173} from "../interfaces/IERC173.sol";

contract Diamond {
    AppStorage internal s;

    receive() external payable {}

    constructor(address _contractOwner) {
        LibDiamond.setContractOwner(_contractOwner);
        LibDiamond.addDiamondFunctions(
            address(new DiamondCutFacet()),
            address(new DiamondLoupeFacet()),
            address(new OwnershipFacet())
        );
    }

    fallback() external payable {
        LibDiamond.DiamondStorage storage ds;
        bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION;
        assembly {
            ds.slot := position
        }
        address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress;
        require(facet != address(0), "Diamond: Function does not exist");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
                case 0 {
                    revert(0, returndatasize())
                }
                default {
                    return(0, returndatasize())
                }
        }
    }
}
// deploy.js
const { ethers } = require("hardhat");

async function main() {
  // Deploy the Diamond contract
  const Diamond = await ethers.getContractFactory("Diamond");
  const diamond = await Diamond.deploy(ethers.constants.AddressZero);
  await diamond.deployed();
  console.log("Diamond deployed to:", diamond.address);

  // Deploy the GasIntensiveFacet contract
  const GasIntensiveFacet = await ethers.getContractFactory("GasIntensiveFacet");
  const gasIntensiveFacet = await GasIntensiveFacet.deploy();
  await gasIntensiveFacet.deployed();
  console.log("GasIntensiveFacet deployed to:", gasIntensiveFacet.address);

  // Add the GasIntensiveFacet to the Diamond contract
  const diamondCut = [
    {
      facetAddress: gasIntensiveFacet.address,
      action: 0, // Add
      functionSelectors: [gasIntensiveFacet.interface.getSighash("gasIntensiveOperation")]
    }
  ];

  const diamondCutFacet = await ethers.getContractAt("DiamondCutFacet", diamond.address);
  await diamondCutFacet.diamondCut(diamondCut, ethers.constants.AddressZero, "0x");

  // Call the gas-intensive function through the Diamond contract
  console.log("Calling gas-intensive function...");
  const tx = await diamond.gasIntensiveOperation();
  await tx.wait();
  console.log("Gas-intensive function called successfully");
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});