Skip to content

Panic 0x11 under --via-ir with optimizer enabled on a subtraction guarded by a ternary #17032

Description

@jorge-tb

Description

With --via-ir and the optimizer both enabled, a call to OpenZeppelin's Checkpoints.upperLookupRecent reverts with Panic(0x11) (arithmetic underflow). The same source compiled with either flag alone, or with neither, returns the correct value.

The only subtraction reachable in that call is index - 1 in the final statement:
return index == 0 ? 0 : _unsafeAccess(self._checkpoints, index - 1)._value;

In the failing case index is 0, so the ternary should short-circuit and the subtraction should never be evaluated. The underflow therefore occurs on a path the source makes unreachable.

Expected: getPastVotes returns 0 for a timepoint before the account's first checkpoint. Actual: Panic(0x11).

Reproduced on 0.8.36 and 0.8.37.

Environment

  • Compiler version: 0.8.37 (also reproduces on 0.8.36)
  • Compilation pipeline: IR (via_ir = true); required together with optimizer = true
  • Target EVM version: osaka
  • Framework/IDE: Foundry 1.7.1
  • EVM execution environment: revm, via forge test
  • Operating system: Ubuntu on WSL2

Steps to Reproduce
Minimal reproduction: https://github.com/jorge-tb/solidity-via-ir-optimization-erc20votes-bug-repro

A minimal ERC-20 with ERC20Votes and timestamp clock mode. One mint, one self-delegation, then a getPastVotes query one second before the delegation timestamp.

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

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";
import {ERC6372Utils} from "@openzeppelin/contracts/utils/ERC6372Utils.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

contract Minimal is ERC20, EIP712, ERC20Votes {
    constructor() ERC20("VIA_IR", "VIR") EIP712("VIA_IR", "1") {}

    function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
        super._update(from, to, value);
    }

    function mint(uint256 amount) external {
        _mint(msg.sender, amount);
    }

    function clock() public view override returns (uint48) {
        return Time.timestamp();
    }

    function CLOCK_MODE() public view override returns (string memory) {
        return ERC6372Utils.timestampClockMode(clock);
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.36;

import {Test} from "forge-std/Test.sol";
import {Minimal} from "../src/Minimal.sol";

contract MinimalTest is Test {
    Minimal minimal;
    address stakeholder;
    uint256 immutable totalTokens = 100;

    function setUp() public {
        stakeholder = makeAddr("stakeholder");

        minimal = new Minimal();
        vm.prank(stakeholder);
        minimal.mint(totalTokens);
    }

    function test_GetPastVotes_ReturnsZeroBeforeDelegation() public {
        uint256 delegationTimestamp = block.timestamp + 1 days;

        vm.warp(delegationTimestamp);
        vm.prank(stakeholder);
        minimal.delegate(stakeholder);

        vm.warp(delegationTimestamp + 1 days);
        assertEq(minimal.getPastVotes(stakeholder, delegationTimestamp - 1), 0);
        assertEq(minimal.getPastVotes(stakeholder, delegationTimestamp), totalTokens);
    }
}

Result matrix

via_ir optimizer Result
false false passes
false true passes
true false passes
true true Panic(0x11)

forge test passes with via_ir = false, and with via_ir = true and the optimizer disabled. It fails with both enabled, at every optimizer_runs value tested: 1, 200, 2000, 20000, 20000000.

Dependency: openzeppelin-contracts 5.7.0 (commit cab19933c33c2ad1d4c7a84864a3601dddfd16f3).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions