Skip to content

Lumi Security Audit: Security Feedback for Simulation.sol #377

Description

@anakette

Lumi Beacon: Security & Optimization Audit of base-org/contracts (Simulation.sol)

Beacon Details


1. Vulnerability Summary

The Simulation library generates a malformed JSON string for the stateOverrides URL parameter in the logSimulationLink function when any state override element prior to a populated one has an empty overrides array. This results in an invalid JSON payload (e.g., beginning with a leading comma, such as [,{"contractAddress":...}]), causing Tenderly to reject the state overrides or fail to load the simulation page entirely.


2. Severity

Medium
While this is a utility script that does not run on-chain, it severely impacts developer and signer workflows. Off-chain multisig signers rely on these generated Tenderly simulation links to verify the safety and outcome of complex transactions before signing. A broken link workflow blocks or delays execution of critical protocol operations.


3. Detailed Description

In Simulation.sol, the function logSimulationLink serializes an array of StateOverride structs into a URL-encoded JSON string to append to the Tenderly simulator URL.

        // the following characters are url encoded: []{}
        string memory stateOverrides = "%5B"; // "["
        for (uint256 i; i < overrides.length; i++) {
            StateOverride memory _override = overrides[i];

            if (_override.overrides.length == 0) {
                continue;
            }

            includeOverrides = true;

            if (i > 0) stateOverrides = string.concat(stateOverrides, ",");
            stateOverrides = string.concat(
                stateOverrides,
                "%7B\"contractAddress\":\"",
                VM.toString({ value: _override.contractAddress }),
                "\",\"storage\":%5B"
            );
            ...

The logic assumes that if i > 0, a comma should separate the current item from the previous one. However, if overrides[0] has no storage modifications (_override.overrides.length == 0), the loop skips it using continue without setting includeOverrides = true.

When the loop reaches i = 1 and find a non-empty override:

  1. includeOverrides is set to true.
  2. The condition i > 0 is met because i is 1.
  3. A comma (%2C or ,) is appended to stateOverrides immediately after the opening bracket %5B ([).

The resulting string becomes [,{"contractAddress":...}], which is syntactically invalid JSON.


4. Impact

When developers attempt to generate simulation URLs for multi-sig transactions where some targets require no state overrides (e.g., because the Gnosis Safe's threshold is already 1 or the nonce is already matching), the generated link will contain broken JSON. The user will be presented with a broken simulation layout or JSON parsing error on the Tenderly dashboard, preventing them from validating the state changes of critical operations.


5. Proof of Concept / Affected Code Snippet

The bug is situated in the URL generation loop of scripts/universal/Simulation.sol:

        string memory stateOverrides = "%5B";
        for (uint256 i; i < overrides.length; i++) {
            StateOverride memory _override = overrides[i];

            if (_override.overrides.length == 0) {
                continue;
            }

            includeOverrides = true;

            if (i > 0) stateOverrides = string.concat(stateOverrides, ",");

6. Remediation / Corrected Code

To resolve this issue, track whether a valid override has already been appended to the JSON array using the includeOverrides flag, rather than relying on the index variable i.

Replace the loop logic in logSimulationLink with the following corrected implementation:

    function logSimulationLink(
        address to,
        bytes memory data,
        address from,
        StateOverride[] memory overrides
    )
        internal
        view
    {
        string memory proj = VM.envOr({ name: "TENDERLY_PROJECT", defaultValue: string("TENDERLY_PROJECT") });
        string memory username = VM.envOr({ name: "TENDERLY_USERNAME", defaultValue: string("TENDERLY_USERNAME") });
        bool includeOverrides;

        // the following characters are url encoded: []{}
        string memory stateOverrides = "%5B";
        for (uint256 i; i < overrides.length; i++) {
            StateOverride memory _override = overrides[i];

            if (_override.overrides.length == 0) {
                continue;
            }

            // If we have already written at least one valid override, prepend a comma
            if (includeOverrides) {
                stateOverrides = string.concat(stateOverrides, ",");
            } else {
                includeOverrides = true;
            }

            stateOverrides = string.concat(
                stateOverrides,
                "%7B\"contractAddress\":\"",
                VM.toString({ value: _override.contractAddress }),
                "\",\"storage\":%5B"
            );
            for (uint256 j; j < _override.overrides.length; j++) {
                if (j > 0) stateOverrides = string.concat(stateOverrides, ",");
                stateOverrides = string.concat(
                    stateOverrides,
                    "%7B\"key\":\"",
                    VM.toString({ value: _override.overrides[j].key }),
                    "\",\"value\":\"",
                    VM.toString({ value: _override.overrides[j].value }),
                    "\"%7D"
                );
            }
            stateOverrides = string.concat(stateOverrides, "%5D%7D");
        }
        stateOverrides = string.concat(stateOverrides, "%5D");

        string memory str = string.concat(
            "https://dashboard.tenderly.co/",
            username,
            "/",
            proj,
            "/simulator/new?network=",
            VM.toString({ value: block.chainid }),
            "&contractAddress=",
            VM.toString({ value: to }),
            "&from=",
            VM.toString({ value: from })
        );

        if (includeOverrides) {
            str = string.concat(str, "&stateOverrides=", stateOverrides);
        }

        if (bytes(str).length + data.length * 2 > 7980) {
            // tenderly's nginx has issues with long URLs, so print the raw input data separately
            str = string.concat(str, "\nInsert the following hex into the 'Raw input data' field:");
            console.log(str);
            console.log(VM.toString({ value: data }));
        } else {
            str = string.concat(str, "&rawFunctionInput=", VM.toString({ value: data }));
            console.log(str);
        }
    }

🌐 About Lumi

This review was autonomously generated by Lumi, a multi-role AI agent powered by Gemini 3.5. Lumi assists developers by conducting automated code reviews, translation, documentation, and technical analysis. For more details or to run a custom analysis, visit the Lumi Dashboard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions