TW
Tripwire
Find contracts. Test them. Review real vulns.

Analysis Run

019d5666-7238-704a-97b4-9ab40e4f0ac7

failed
Chain ID
1
Address
Block Number
Created
Sat, Apr 4, 2026 2:50 AM
Review Summary

What survived the funnel

Open this run to answer one question: is this real attacker-relevant behavior with real impact, or just normal contract plumbing?

Confirmed
0
Validated
0
Source
Verified
Review Backing
Present
Best current signal
Untrusted DELEGATECALL target reachable
crit 0 high 0 proxy context implementation known
Review Checklist
1. Read the findings table first. Ignore raw jobs until the result looks real.
2. Confirm attacker control, not just the existence of a sink.
3. Confirm money or privilege impact.
4. Keep it if the behavior is real. Kill it if this is normal proxy, admin, or upgrade plumbing.

Processing Jobs

Status & attempts
ID Type Status Attempts Heartbeat
42904 artifact.fetch complete 1 / 3 1 week ago
42905 proxy.resolve complete 1 / 3 1 week ago
42906 tool.cast_disasm complete 1 / 3 1 week ago
42907 analysis.bundle complete 1 / 3 1 week ago
42908 capability.graph complete 1 / 3 1 week ago
42909 detector.run complete 1 / 3 1 week ago
42910 validation.fork complete 1 / 3 1 week ago
43000 report.consolidate complete 1 / 6 1 week ago
43001 tool.slither failed 2 / 2 1 week ago

Artifact

Runtime bytecode: available
Creation TX:
Deployer:
Proxy: yes confirmed
Verified Source: yes
Proxy evidence
{
    "method": "etherscan+onchain",
    "status": "confirmed",
    "etherscan_hint": {
        "source": "etherscan",
        "etherscan": {
            "proxy_flag": true,
            "implementation": "0xa8372d6ff00d48a25baa1af16d6a86c936708f4e"
        }
    },
    "implementation": "0xa8372d6ff00d48a25baa1af16d6a86c936708f4e",
    "onchain_method": "eip1967"
}

Slither

tool.slither
Status
failed slither failed (exit 1): 'forge clean' running (wd: /tmp/slither-hs3ywvxx) 'forge config --json' running 'forge build --build-info --skip ./test/** ./script/** --force' running (wd: /tmp/slither-hs3ywvxx) 'forge' returned non-zero exit code 1 Error: Encountered invalid solc version in lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol: No solc version exists that matches the version requirement: ^0.8.22 Traceback (most recent call last): File "/var/www/tripwire/.venv/bin/slither", line 10, in <module> sys.exit(main()) ~~~~^^ File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 776, in main main_impl(all_detector_classes=detectors, all_printer_classes=printers) ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 882, in main_impl ) = process_all(filename, args, detector_classes, printer_classes) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 96, in process_all compilations = compile_all(target, **vars(args)) File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 722, in compile_all compilations.append(CryticCompile(target, **kwargs)) ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^ File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 211, in __init__ self._compile(**kwargs) ~~~~~~~~~~~~~^^^^^^^^^^ File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 633, in _compile self._platform.compile(self, **kwargs) ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^ File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/platform/foundry.py", line 102, in compile hardhat_like_parsing( ~~~~~~~~~~~~~~~~~~~~^ crytic_compile, str(self._target), build_directory, str(self._project_root) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/platform/hardhat.py", line 52, in hardhat_like_parsing raise InvalidCompilation(txt) crytic_compile.platform.exceptions.InvalidCompilation: Compilation failed. Can you run build command? /tmp/slither-hs3ywvxx/out/build-info is not a directory.
Attempts
2 / 2
Findings
0
Solc
View stderr
Traceback (most recent call last):
  File "/var/www/tripwire/worker/worker.py", line 198, in main
    payload = dispatch_job(job_type, job_id, job_meta, run_data, config, laravel_client)
  File "/var/www/tripwire/worker/worker.py", line 114, in dispatch_job
    return slither_run.run(job_id, job_meta, run_data, config, log, laravel_client)
           ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/www/tripwire/worker/jobs/slither_run.py", line 1084, in run
    stdout_text, stderr_text, result_json = _run_slither(
                                            ~~~~~~~~~~~~^
        workspace,
        ^^^^^^^^^^
    ...<4 lines>...
        target_path=target_path,
        ^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/var/www/tripwire/worker/jobs/slither_run.py", line 958, in _run_slither
    raise RuntimeError(f"slither failed (exit {result.returncode}): {stderr_text or stdout_text}")
RuntimeError: slither failed (exit 1): 'forge clean' running (wd: /tmp/slither-hs3ywvxx)
'forge config --json' running
'forge build --build-info --skip ./test/** ./script/** --force' running (wd: /tmp/slither-hs3ywvxx)
'forge' returned non-zero exit code 1
Error: Encountered invalid solc version in lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol: No solc version exists that matches the version requirement: ^0.8.22
Traceback (most recent call last):
  File "/var/www/tripwire/.venv/bin/slither", line 10, in <module>
    sys.exit(main())
             ~~~~^^
  File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 776, in main
    main_impl(all_detector_classes=detectors, all_printer_classes=printers)
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 882, in main_impl
    ) = process_all(filename, args, detector_classes, printer_classes)
        ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/www/tripwire/.venv/lib/python3.13/site-packages/slither/__main__.py", line 96, in process_all
    compilations = compile_all(target, **vars(args))
  File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 722, in compile_all
    compilations.append(CryticCompile(target, **kwargs))
                        ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 211, in __init__
    self._compile(**kwargs)
    ~~~~~~~~~~~~~^^^^^^^^^^
  File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/crytic_compile.py", line 633, in _compile
    self._platform.compile(self, **kwargs)
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/platform/foundry.py", line 102, in compile
    hardhat_like_parsing(
    ~~~~~~~~~~~~~~~~~~~~^
        crytic_compile, str(self._target), build_directory, str(self._project_root)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/var/www/tripwire/.venv/lib/python3.13/site-packages/crytic_compile/platform/hardhat.py", line 52, in hardhat_like_parsing
    raise InvalidCompilation(txt)
crytic_compile.platform.exceptions.InvalidCompilation: Compilation failed. Can you run build command?
/tmp/slither-hs3ywvxx/out/build-info is not a directory.

Codex

report.consolidate
Job Status
complete
Attempts
1 / 6
Findings
2
No findings?
no
Summary
The implementation has two meaningful issues: withdrawal signatures are not domain-separated by bridge instance, and deposits account for the requested amount instead of the actual tokens received.
View stdout
{"no_findings":false,"summary":"The implementation has two meaningful issues: withdrawal signatures are not domain-separated by bridge instance, and deposits account for the requested amount instead of the actual tokens received.","findings":[{"severity":"medium","category":"auth","title":"Withdraw signatures can be replayed across bridge deployments on the same chain","exploit_scenario":"Validators sign a withdrawal for one L1 predicate instance. Because the signed digest is only `keccak256(chainid, data)`, the same signatures remain valid on any other predicate deployment on the same chain that uses the same validator registry and token mapping. An attacker can submit the packet to a second funded bridge instance and withdraw the same amount there as well, since `orderExecuted[from][orderId]` is tracked per contract.","recommendation":"Include `address(this)` (or another unique bridge-domain identifier) in the signed message, and ideally bind signatures to a validator-set checkpoint/epoch as well.","evidence":"`syncWithdraw` builds `messageHash = keccak256(abi.encodePacked(block.chainid, _data))` and `_checkValidatorSignatures` verifies signatures against that hash only. No contract address, proxy address, or deployment-specific domain separator is included.","locations":["contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:72","contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:86","contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:169"]},{"severity":"medium","category":"other","title":"Fee-on-transfer tokens break bridge accounting and can drain pooled liquidity","exploit_scenario":"If governance maps an ERC20 that charges transfer fees or otherwise credits less than `_amount`, `deposit` still emits a deposit for the full `_amount` after `safeTransferFrom`. Off-chain relayers will typically mint/credit the full amount on L2, allowing the depositor to later withdraw more on L1 than this contract actually received. The shortfall is paid from other users' liquidity, or the bridge becomes insolvent for that token.","recommendation":"Measure the actual balance delta before and after `safeTransferFrom` and emit/credit that received amount instead of the user-supplied `_amount`. Alternatively, explicitly reject non-standard / fee-on-transfer tokens.","evidence":"`_initiateDeposit` transfers `_amount` in, but never checks the post-transfer balance. It immediately emits `DepositToken(..., _amount)` based on the requested amount rather than the actual tokens received.","locations":["contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:155","contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:161","contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:165"]}]}
View stderr
OpenAI Codex v0.118.0 (research preview)
--------
workdir: /tmp/codex-nv9d2bwk
model: gpt-5.4
provider: openai
approval: never
sandbox: read-only
reasoning effort: high
reasoning summaries: none
session id: 019d56a8-fdc9-7321-b1a2-c7b383e4fd4d
--------
user
Context: The user requested the proxy at 0x63105ee97bfb22dfe23033b3b14a4f8fed121ee9.
Audit the implementation at 0xa8372d6ff00d48a25baa1af16d6a86c936708f4e.
Findings should refer to the implementation unless explicitly about proxy mechanics.

You are a security auditor. Audit this Solidity codebase for exploitable vulnerabilities.
Focus on: reentrancy, access control, upgradeability/proxy issues, signature validation, oracle manipulation,
accounting/invariant breaks, external call patterns, unchecked return values, and dangerous delegatecall.

Scope rules:
- You MUST fully read `AUDIT_BUNDLE.txt` before concluding. It contains the complete contents of all first-party Solidity sources in this workspace.
- Treat `lib/` and `@openzeppelin/` as third-party dependencies; skip them unless there is a concrete misuse angle in first-party code.
- If there are only dependency files and no first-party sources, set no_findings=true and explain in summary.

Output MUST be valid JSON matching the provided schema.
- If you find nothing meaningful: set no_findings=true and findings=[].
- Otherwise: set no_findings=false and include findings with severity/category/title and file/line locations when possible.

Output constraints (IMPORTANT):
- severity MUST be exactly one of: info, low, medium, high, critical (lowercase).
- category MUST be exactly one of: access_control, reentrancy, math, oracle, upgradeability, auth, other.
Keep summary short and concrete.
warning: Codex could not find system bubblewrap on PATH. Please install bubblewrap with your package manager. Codex will use the vendored bubblewrap in the meantime.
exec
/bin/bash -lc 'pwd && rg --files' in /tmp/codex-nv9d2bwk
 succeeded in 0ms:
/tmp/codex-nv9d2bwk
codex-output-schema.json
AUDIT_BUNDLE.txt
@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol
contracts/bridge/v2/core/common/IBridgeRegistry.sol
@openzeppelin/contracts-upgradeable/interfaces/draft-IERC1822Upgradeable.sol
contracts/bridge/v2/core/L1/predicates/IL1Predicate.sol
contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol
@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol
@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol
@openzeppelin/contracts-upgradeable/utils/StorageSlotUpgradeable.sol
@openzeppelin/contracts-upgradeable/proxy/beacon/IBeaconUpgradeable.sol
contracts/bridge/v2/libs/ValidatorSet.sol
contracts/bridge/v2/libs/Create2.sol
@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol
@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol
@openzeppelin/contracts/utils/Strings.sol
@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol
@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol
@openzeppelin/contracts/token/ERC20/IERC20.sol
@openzeppelin/contracts/utils/Address.sol
@openzeppelin/contracts/utils/Context.sol
@openzeppelin/contracts/utils/cryptography/ECDSA.sol
@openzeppelin/contracts/utils/math/Math.sol
@openzeppelin/contracts/token/ERC20/ERC20.sol
@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol

  → Inspect workspace and fully read AUDIT_BUNDLE.txt to identify first-party contracts and proxy/implementation context
  • Analyze first-party Solidity code for exploitable issues in scoped risk areas
  • Produce JSON report with concrete findings tied to implementation code
exec
/bin/bash -lc "wc -l AUDIT_BUNDLE.txt && sed -n '1,260p' AUDIT_BUNDLE.txt" in /tmp/codex-nv9d2bwk
 succeeded in 0ms:
436 AUDIT_BUNDLE.txt
This file concatenates the full contents of first-party Solidity sources for auditing.
Do not treat dependency code under lib/ or @openzeppelin/ as first-party.

================================================================================
FILE: contracts/bridge/v2/core/L1/predicates/IL1Predicate.sol
================================================================================
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

// LightLink 2023
interface IL1Predicate {
  /* Events */
  function l2TokenBytecodeHash() external view returns (bytes32);

  function mapToken(address _l1Token) external;
}

================================================================================
FILE: contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol
================================================================================
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../../../libs/Create2.sol";
import "./IL1Predicate.sol";
import "../../common/IBridgeRegistry.sol";

// LightLink 2023
contract L1ERC20Predicate is
  Initializable, //
  UUPSUpgradeable,
  ReentrancyGuardUpgradeable,
  IL1Predicate
{
  using ECDSA for bytes32;
  using SafeERC20 for IERC20;

  // variables
  bool public isPaused;
  address public bridgeRegistry;
  address public l2Predicate;
  bytes32 public l2TokenBytecodeHash;
  mapping(address => address) public l1ToL2Gateway;

  mapping(address => uint256) public counter;
  mapping(address => mapping(uint256 => bool)) public orderExecuted;
  mapping(address => mapping(uint256 => mapping(address => bool))) public isConfirmed;

  event TokenMapped(bytes message);
  event DepositToken(bytes message);
  event WithdrawToken(bytes32 messageHash);

  modifier requireMultisig() {
    require(msg.sender == IBridgeRegistry(bridgeRegistry).getMultisig(), "Multisig required");
    _;
  }

  modifier notPaused() {
    require(!isPaused, "Paused");
    _;
  }

  receive() external payable {
    revert("Not supported");
  }

  function recovery(address[] memory _tokens, address _to) external requireMultisig {
    for (uint256 i = 0; i < _tokens.length; i++) {
      address token = _tokens[i];
      uint256 _amount = IERC20(token).balanceOf(address(this));
      if (_amount > 0) {
        IERC20(token).safeTransfer(_to, _amount);
      }
    }
  }

  function deposit(address _l1Token, uint256 _amount) external virtual {
    _initiateDeposit(_l1Token, msg.sender, msg.sender, _amount);
  }

  // verified
  function depositTo(address _l1Token, address _to, uint256 _amount) external virtual {
    _initiateDeposit(_l1Token, msg.sender, _to, _amount);
  }

  function syncWithdraw(
    address[] memory _currentValidators,
    bytes[] memory _signatures,
    // transaction data
    bytes memory _data
  ) external nonReentrant notPaused {
    (address from, uint256 orderId, address l1Token, address l2Token, address to, uint256 amount) = abi.decode(_data, (address, uint256, address, address, address, uint256));
    require(l2Predicate != address(0), "Not implemented");
    require(amount > 0, "Not enough amount");
    require(to != address(0), "Invalid address");
    require(!orderExecuted[from][orderId], "Order already executed");
    require(_currentValidators.length == _signatures.length, "Input mismatch");
    require(l1ToL2Gateway[l1Token] == l2Token, "Invalid token gateway");

    bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, _data));
    _checkValidatorSignatures(
      from,
      orderId,
      _currentValidators,
      _signatures,
      // Get hash of the transaction batch and checkpoint
      messageHash,
      IBridgeRegistry(bridgeRegistry).consensusPowerThreshold()
    );

    orderExecuted[from][orderId] = true;

    IERC20(l1Token).safeTransfer(to, amount);

    emit WithdrawToken(messageHash);
  }

  function initialize(address _registry, address _l2Predicate) public initializer {
    __ReentrancyGuard_init();
    __L1ERC20Predicate_init(_registry, _l2Predicate);
  }

  function modifyL2TokenBytecodeHash(bytes32 _l2TokenBytecodeHash) public requireMultisig {
    l2TokenBytecodeHash = _l2TokenBytecodeHash;
  }

  function toggleIsPaused(bool _status) public requireMultisig {
    isPaused = _status;
  }

  function modifyL2Predicate(address _l2Predicate) public requireMultisig {
    l2Predicate = _l2Predicate;
  }

  function mapToken(address _l1Token) public requireMultisig {
    // check if token is already mapped
    require(l2Predicate != address(0x0), "Invalid L2 pair");
    require(l1ToL2Gateway[_l1Token] == address(0x0), "Already mapped");

    string memory name;
    try ERC20(_l1Token).name() returns (string memory _name) {
      name = _name;
    } catch {}
    string memory symbol;
    try ERC20(_l1Token).symbol() returns (string memory _symbol) {
      symbol = _symbol;
    } catch {}
    uint8 decimals = ERC20(_l1Token).decimals();

    // compute child token address before deployment using create2
    bytes32 salt = keccak256(abi.encodePacked(_l1Token));
    address l2Token = Create2.computeAddress(salt, l2TokenBytecodeHash, l2Predicate);

    // add into mapped tokens
    l1ToL2Gateway[_l1Token] = l2Token;

    emit TokenMapped(abi.encode(msg.sender, counter[msg.sender], _l1Token, name, symbol, decimals));
    counter[msg.sender]++;
  }

  function __L1ERC20Predicate_init(address _registry, address _l2Predicate) internal {
    bridgeRegistry = _registry;
    l2Predicate = _l2Predicate;
    l2TokenBytecodeHash = 0xb9ae7ba14d826de669be54f7c79008181b430f21bd3ff90dac8cce1e60ae88a9;
  }

  function _authorizeUpgrade(address) internal override requireMultisig {}

  function _initiateDeposit(address _l1Token, address _from, address _to, uint256 _amount) internal notPaused {
    require(l2Predicate != address(0), "Not implemented");
    require(_amount > 0, "Not enough amount");
    require(_to != address(0), "Invalid address");
    require(l1ToL2Gateway[_l1Token] != address(0), "Not supported");

    IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);

    uint256 counter_ = counter[_from];

    emit DepositToken(abi.encode(_from, counter_, _l1Token, l1ToL2Gateway[_l1Token], _to, _amount));
    counter[_from]++;
  }

  function _checkValidatorSignatures(
    address _from, //
    uint256 _orderId,
    address[] memory _currentValidators,
    bytes[] memory _signatures,
    bytes32 _messageHash,
    uint256 _powerThreshold
  ) private {
    uint256 cumulativePower = 0;

    for (uint256 i = 0; i < _currentValidators.length; i++) {
      address signer = _messageHash.toEthSignedMessageHash().recover(_signatures[i]);
      require(signer == _currentValidators[i], "Validator signature does not match.");
      require(IBridgeRegistry(bridgeRegistry).validValidator(signer), "Invalid validator");
      require(!isConfirmed[_from][_orderId][signer], "No duplicate validator");

      // prevent double-signing attacks
      isConfirmed[_from][_orderId][signer] = true;

      // Sum up cumulative power
      cumulativePower += IBridgeRegistry(bridgeRegistry).getPower(signer);

      // Break early to avoid wasting gas
      if (cumulativePower > _powerThreshold) {
        break;
      }
    }

    // Check that there was enough power
    require(cumulativePower >= _powerThreshold, "Submitted validator set signatures do not have enough power.");
    // Success
  }
}

================================================================================
FILE: contracts/bridge/v2/core/common/IBridgeRegistry.sol
================================================================================
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "../../libs/ValidatorSet.sol";

// LightLink 2023
interface IBridgeRegistry {
  event ConsensusPowerThresholdModified(uint256 _amount);
  event ValidatorsModifed(address[] _accounts, uint256[] _powers);
  event ValidatorsRemoved(address[] _accounts);
  event MutisigModified(address _multisig);
  event SystemVerifierModified(address _systemVerifier);

  /* Views */
  function consensusPowerThreshold() external view returns (uint256);

  function validValidator(address) external view returns (bool);

  function getPower(address) external view returns (uint256);

  function getValidators() external view returns (ValidatorSet.Validator[] memory);

  function getMultisig() external view returns (address);

  function getSystemVerifier() external view returns (address);

  /* Actions */
  function modifyConsensusPowerThreshold(uint256 _amount) external;

  function modifyValidators(address[] memory _accounts, uint256[] memory _powers) external;

  function removeValidators(address[] memory _accounts) external;

  function modifySystemVerifier(address _systemVerifier) external;
}


exec
/bin/bash -lc "sed -n '261,520p' AUDIT_BUNDLE.txt" in /tmp/codex-nv9d2bwk
 succeeded in 0ms:
================================================================================
FILE: contracts/bridge/v2/libs/Create2.sol
================================================================================
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/Create2.sol)

pragma solidity 0.8.19;

// LightLink 2023
/**
 * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer.
 * `CREATE2` can be used to compute in advance the address where a smart
 * contract will be deployed, which allows for interesting new mechanisms known
 * as 'counterfactual interactions'.
 *
 * See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more
 * information.
 */
library Create2 {
  function createClone(bytes32 _salt, address _target) internal returns (address _result) {
    bytes20 _targetBytes = bytes20(_target);

    assembly {
      let clone := mload(0x40)
      mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
      mstore(add(clone, 0x14), _targetBytes)
      mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
      _result := create2(0, clone, 0x37, _salt)
    }

    require(_result != address(0), "Create2: Failed on minimal deploy");
  }

  function createClone2(bytes32 _salt, bytes memory _bytecode) internal returns (address _result) {
    assembly {
      _result := create2(0, add(_bytecode, 0x20), mload(_bytecode), _salt)

      if iszero(extcodesize(_result)) {
        revert(0, 0)
      }
    }

    require(_result != address(0), "Create2: Failed on deploy");
  }

  /**
   * @dev Deploys a contract using `CREATE2`. The address where the contract
   * will be deployed can be known in advance via {computeAddress}.
   *
   * The bytecode for a contract can be obtained from Solidity with
   * `type(contractName).creationCode`.
   *
   * Requirements:
   *
   * - `bytecode` must not be empty.
   * - `salt` must have not been used for `bytecode` already.
   * - the factory must have a balance of at least `amount`.
   * - if `amount` is non-zero, `bytecode` must have a `payable` constructor.
   */
  function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) {
    require(address(this).balance >= amount, "Create2: insufficient balance");
    require(bytecode.length != 0, "Create2: bytecode length is zero");
    /// @solidity memory-safe-assembly
    assembly {
      addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
    }
    require(addr != address(0), "Create2: Failed on deploy");
  }

  /**
   * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the
   * `bytecodeHash` or `salt` will result in a new destination address.
   */
  function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
    return computeAddress(salt, bytecodeHash, address(this));
  }

  /**
   * @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at
   * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}.
   */
  function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address addr) {
    /// @solidity memory-safe-assembly
    assembly {
      let ptr := mload(0x40) // Get free memory pointer

      // |                   | ↓ ptr ...  ↓ ptr + 0x0B (start) ...  ↓ ptr + 0x20 ...  ↓ ptr + 0x40 ...   |
      // |-------------------|---------------------------------------------------------------------------|
      // | bytecodeHash      |                                                        CCCCCCCCCCCCC...CC |
      // | salt              |                                      BBBBBBBBBBBBB...BB                   |
      // | deployer          | 000000...0000AAAAAAAAAAAAAAAAAAA...AA                                     |
      // | 0xFF              |            FF                                                             |
      // |-------------------|---------------------------------------------------------------------------|
      // | memory            | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC |
      // | keccak(start, 85) |            ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ |

      mstore(add(ptr, 0x40), bytecodeHash)
      mstore(add(ptr, 0x20), salt)
      mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes
      let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff
      mstore8(start, 0xff)
      addr := keccak256(start, 85)
    }
  }
}

================================================================================
FILE: contracts/bridge/v2/libs/ValidatorSet.sol
================================================================================
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

// LightLink 2023
library ValidatorSet {
  struct Validator {
    address addr;
    uint256 power;
  }

  struct Record {
    Validator[] values;
    mapping(address => uint256) indexes; // value to index
  }

  function add(Record storage _record, address _value, uint256 _power) internal {
    if (contains(_record, _value)) return; // exist
    _record.values.push(Validator(_value, _power));
    _record.indexes[_value] = _record.values.length;
  }

  function modify(Record storage _record, address _value, uint256 _power) internal {
    if (!contains(_record, _value)) {
      add(_record, _value, _power);
      return;
    }
    uint256 valueIndex = _record.indexes[_value];
    _record.values[valueIndex - 1].power = _power;
  }

  function remove(Record storage _record, address _value) internal {
    uint256 valueIndex = _record.indexes[_value];
    if (valueIndex == 0) return; // removed non-exist value
    uint256 toDeleteIndex = valueIndex - 1; // dealing with out of bounds
    uint256 lastIndex = _record.values.length - 1;
    if (lastIndex != toDeleteIndex) {
      Validator memory lastvalue = _record.values[lastIndex];
      _record.values[toDeleteIndex] = lastvalue;
      _record.indexes[lastvalue.addr] = valueIndex; // Replace lastvalue's index to valueIndex
    }
    _record.values.pop();
    _record.indexes[_value] = 0; // set to 0
  }

  function contains(Record storage _record, address _value) internal view returns (bool) {
    return _record.indexes[_value] != 0;
  }

  function size(Record storage _record) internal view returns (uint256) {
    return _record.values.length;
  }

  function at(Record storage _record, uint256 _index) internal view returns (Validator memory) {
    return _record.values[_index];
  }

  function indexOf(Record storage _record, address _value) internal view returns (bool, uint256) {
    if (!contains(_record, _value)) return (false, 0);
    return (true, _record.indexes[_value] - 1);
  }

  function getPower(Record storage _record, address _value) internal view returns (uint256) {
    if (!contains(_record, _value)) return 0;
    return _record.values[_record.indexes[_value] - 1].power;
  }
}


exec
/bin/bash -lc "nl -ba contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol | sed -n '1,220p'" in /tmp/codex-nv9d2bwk
 succeeded in 0ms:
     1	// SPDX-License-Identifier: MIT
     2	pragma solidity 0.8.19;
     3	
     4	import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
     5	import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
     6	import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
     7	import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
     8	import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
     9	import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
    10	import "../../../libs/Create2.sol";
    11	import "./IL1Predicate.sol";
    12	import "../../common/IBridgeRegistry.sol";
    13	
    14	// LightLink 2023
    15	contract L1ERC20Predicate is
    16	  Initializable, //
    17	  UUPSUpgradeable,
    18	  ReentrancyGuardUpgradeable,
    19	  IL1Predicate
    20	{
    21	  using ECDSA for bytes32;
    22	  using SafeERC20 for IERC20;
    23	
    24	  // variables
    25	  bool public isPaused;
    26	  address public bridgeRegistry;
    27	  address public l2Predicate;
    28	  bytes32 public l2TokenBytecodeHash;
    29	  mapping(address => address) public l1ToL2Gateway;
    30	
    31	  mapping(address => uint256) public counter;
    32	  mapping(address => mapping(uint256 => bool)) public orderExecuted;
    33	  mapping(address => mapping(uint256 => mapping(address => bool))) public isConfirmed;
    34	
    35	  event TokenMapped(bytes message);
    36	  event DepositToken(bytes message);
    37	  event WithdrawToken(bytes32 messageHash);
    38	
    39	  modifier requireMultisig() {
    40	    require(msg.sender == IBridgeRegistry(bridgeRegistry).getMultisig(), "Multisig required");
    41	    _;
    42	  }
    43	
    44	  modifier notPaused() {
    45	    require(!isPaused, "Paused");
    46	    _;
    47	  }
    48	
    49	  receive() external payable {
    50	    revert("Not supported");
    51	  }
    52	
    53	  function recovery(address[] memory _tokens, address _to) external requireMultisig {
    54	    for (uint256 i = 0; i < _tokens.length; i++) {
    55	      address token = _tokens[i];
    56	      uint256 _amount = IERC20(token).balanceOf(address(this));
    57	      if (_amount > 0) {
    58	        IERC20(token).safeTransfer(_to, _amount);
    59	      }
    60	    }
    61	  }
    62	
    63	  function deposit(address _l1Token, uint256 _amount) external virtual {
    64	    _initiateDeposit(_l1Token, msg.sender, msg.sender, _amount);
    65	  }
    66	
    67	  // verified
    68	  function depositTo(address _l1Token, address _to, uint256 _amount) external virtual {
    69	    _initiateDeposit(_l1Token, msg.sender, _to, _amount);
    70	  }
    71	
    72	  function syncWithdraw(
    73	    address[] memory _currentValidators,
    74	    bytes[] memory _signatures,
    75	    // transaction data
    76	    bytes memory _data
    77	  ) external nonReentrant notPaused {
    78	    (address from, uint256 orderId, address l1Token, address l2Token, address to, uint256 amount) = abi.decode(_data, (address, uint256, address, address, address, uint256));
    79	    require(l2Predicate != address(0), "Not implemented");
    80	    require(amount > 0, "Not enough amount");
    81	    require(to != address(0), "Invalid address");
    82	    require(!orderExecuted[from][orderId], "Order already executed");
    83	    require(_currentValidators.length == _signatures.length, "Input mismatch");
    84	    require(l1ToL2Gateway[l1Token] == l2Token, "Invalid token gateway");
    85	
    86	    bytes32 messageHash = keccak256(abi.encodePacked(block.chainid, _data));
    87	    _checkValidatorSignatures(
    88	      from,
    89	      orderId,
    90	      _currentValidators,
    91	      _signatures,
    92	      // Get hash of the transaction batch and checkpoint
    93	      messageHash,
    94	      IBridgeRegistry(bridgeRegistry).consensusPowerThreshold()
    95	    );
    96	
    97	    orderExecuted[from][orderId] = true;
    98	
    99	    IERC20(l1Token).safeTransfer(to, amount);
   100	
   101	    emit WithdrawToken(messageHash);
   102	  }
   103	
   104	  function initialize(address _registry, address _l2Predicate) public initializer {
   105	    __ReentrancyGuard_init();
   106	    __L1ERC20Predicate_init(_registry, _l2Predicate);
   107	  }
   108	
   109	  function modifyL2TokenBytecodeHash(bytes32 _l2TokenBytecodeHash) public requireMultisig {
   110	    l2TokenBytecodeHash = _l2TokenBytecodeHash;
   111	  }
   112	
   113	  function toggleIsPaused(bool _status) public requireMultisig {
   114	    isPaused = _status;
   115	  }
   116	
   117	  function modifyL2Predicate(address _l2Predicate) public requireMultisig {
   118	    l2Predicate = _l2Predicate;
   119	  }
   120	
   121	  function mapToken(address _l1Token) public requireMultisig {
   122	    // check if token is already mapped
   123	    require(l2Predicate != address(0x0), "Invalid L2 pair");
   124	    require(l1ToL2Gateway[_l1Token] == address(0x0), "Already mapped");
   125	
   126	    string memory name;
   127	    try ERC20(_l1Token).name() returns (string memory _name) {
   128	      name = _name;
   129	    } catch {}
   130	    string memory symbol;
   131	    try ERC20(_l1Token).symbol() returns (string memory _symbol) {
   132	      symbol = _symbol;
   133	    } catch {}
   134	    uint8 decimals = ERC20(_l1Token).decimals();
   135	
   136	    // compute child token address before deployment using create2
   137	    bytes32 salt = keccak256(abi.encodePacked(_l1Token));
   138	    address l2Token = Create2.computeAddress(salt, l2TokenBytecodeHash, l2Predicate);
   139	
   140	    // add into mapped tokens
   141	    l1ToL2Gateway[_l1Token] = l2Token;
   142	
   143	    emit TokenMapped(abi.encode(msg.sender, counter[msg.sender], _l1Token, name, symbol, decimals));
   144	    counter[msg.sender]++;
   145	  }
   146	
   147	  function __L1ERC20Predicate_init(address _registry, address _l2Predicate) internal {
   148	    bridgeRegistry = _registry;
   149	    l2Predicate = _l2Predicate;
   150	    l2TokenBytecodeHash = 0xb9ae7ba14d826de669be54f7c79008181b430f21bd3ff90dac8cce1e60ae88a9;
   151	  }
   152	
   153	  function _authorizeUpgrade(address) internal override requireMultisig {}
   154	
   155	  function _initiateDeposit(address _l1Token, address _from, address _to, uint256 _amount) internal notPaused {
   156	    require(l2Predicate != address(0), "Not implemented");
   157	    require(_amount > 0, "Not enough amount");
   158	    require(_to != address(0), "Invalid address");
   159	    require(l1ToL2Gateway[_l1Token] != address(0), "Not supported");
   160	
   161	    IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);
   162	
   163	    uint256 counter_ = counter[_from];
   164	
   165	    emit DepositToken(abi.encode(_from, counter_, _l1Token, l1ToL2Gateway[_l1Token], _to, _amount));
   166	    counter[_from]++;
   167	  }
   168	
   169	  function _checkValidatorSignatures(
   170	    address _from, //
   171	    uint256 _orderId,
   172	    address[] memory _currentValidators,
   173	    bytes[] memory _signatures,
   174	    bytes32 _messageHash,
   175	    uint256 _powerThreshold
   176	  ) private {
   177	    uint256 cumulativePower = 0;
   178	
   179	    for (uint256 i = 0; i < _currentValidators.length; i++) {
   180	      address signer = _messageHash.toEthSignedMessageHash().recover(_signatures[i]);
   181	      require(signer == _currentValidators[i], "Validator signature does not match.");
   182	      require(IBridgeRegistry(bridgeRegistry).validValidator(signer), "Invalid validator");
   183	      require(!isConfirmed[_from][_orderId][signer], "No duplicate validator");
   184	
   185	      // prevent double-signing attacks
   186	      isConfirmed[_from][_orderId][signer] = true;
   187	
   188	      // Sum up cumulative power
   189	      cumulativePower += IBridgeRegistry(bridgeRegistry).getPower(signer);
   190	
   191	      // Break early to avoid wasting gas
   192	      if (cumulativePower > _powerThreshold) {
   193	        break;
   194	      }
   195	    }
   196	
   197	    // Check that there was enough power
   198	    require(cumulativePower >= _powerThreshold, "Submitted validator set signatures do not have enough power.");
   199	    // Success
   200	  }
   201	}

exec
/bin/bash -lc "nl -ba contracts/bridge/v2/core/common/IBridgeRegistry.sol | sed -n '1,200p'" in /tmp/codex-nv9d2bwk
 succeeded in 0ms:
     1	// SPDX-License-Identifier: MIT
     2	pragma solidity 0.8.19;
     3	import "../../libs/ValidatorSet.sol";
     4	
     5	// LightLink 2023
     6	interface IBridgeRegistry {
     7	  event ConsensusPowerThresholdModified(uint256 _amount);
     8	  event ValidatorsModifed(address[] _accounts, uint256[] _powers);
     9	  event ValidatorsRemoved(address[] _accounts);
    10	  event MutisigModified(address _multisig);
    11	  event SystemVerifierModified(address _systemVerifier);
    12	
    13	  /* Views */
    14	  function consensusPowerThreshold() external view returns (uint256);
    15	
    16	  function validValidator(address) external view returns (bool);
    17	
    18	  function getPower(address) external view returns (uint256);
    19	
    20	  function getValidators() external view returns (ValidatorSet.Validator[] memory);
    21	
    22	  function getMultisig() external view returns (address);
    23	
    24	  function getSystemVerifier() external view returns (address);
    25	
    26	  /* Actions */
    27	  function modifyConsensusPowerThreshold(uint256 _amount) external;
    28	
    29	  function modifyValidators(address[] memory _accounts, uint256[] memory _powers) external;
    30	
    31	  function removeValidators(address[] memory _accounts) external;
    32	
    33	  function modifySystemVerifier(address _systemVerifier) external;
    34	}

exec
/bin/bash -lc "nl -ba contracts/bridge/v2/libs/ValidatorSet.sol | sed -n '1,200p'" in /tmp/codex-nv9d2bwk
 succeeded in 0ms:
     1	// SPDX-License-Identifier: MIT
     2	pragma solidity 0.8.19;
     3	
     4	// LightLink 2023
     5	library ValidatorSet {
     6	  struct Validator {
     7	    address addr;
     8	    uint256 power;
     9	  }
    10	
    11	  struct Record {
    12	    Validator[] values;
    13	    mapping(address => uint256) indexes; // value to index
    14	  }
    15	
    16	  function add(Record storage _record, address _value, uint256 _power) internal {
    17	    if (contains(_record, _value)) return; // exist
    18	    _record.values.push(Validator(_value, _power));
    19	    _record.indexes[_value] = _record.values.length;
    20	  }
    21	
    22	  function modify(Record storage _record, address _value, uint256 _power) internal {
    23	    if (!contains(_record, _value)) {
    24	      add(_record, _value, _power);
    25	      return;
    26	    }
    27	    uint256 valueIndex = _record.indexes[_value];
    28	    _record.values[valueIndex - 1].power = _power;
    29	  }
    30	
    31	  function remove(Record storage _record, address _value) internal {
    32	    uint256 valueIndex = _record.indexes[_value];
    33	    if (valueIndex == 0) return; // removed non-exist value
    34	    uint256 toDeleteIndex = valueIndex - 1; // dealing with out of bounds
    35	    uint256 lastIndex = _record.values.length - 1;
    36	    if (lastIndex != toDeleteIndex) {
    37	      Validator memory lastvalue = _record.values[lastIndex];
    38	      _record.values[toDeleteIndex] = lastvalue;
    39	      _record.indexes[lastvalue.addr] = valueIndex; // Replace lastvalue's index to valueIndex
    40	    }
    41	    _record.values.pop();
    42	    _record.indexes[_value] = 0; // set to 0
    43	  }
    44	
    45	  function contains(Record storage _record, address _value) internal view returns (bool) {
    46	    return _record.indexes[_value] != 0;
    47	  }
    48	
    49	  function size(Record storage _record) internal view returns (uint256) {
    50	    return _record.values.length;
    51	  }
    52	
    53	  function at(Record storage _record, uint256 _index) internal view returns (Validator memory) {
    54	    return _record.values[_index];
    55	  }
    56	
    57	  function indexOf(Record storage _record, address _value) internal view returns (bool, uint256) {
    58	    if (!contains(_record, _value)) return (false, 0);
    59	    return (true, _record.indexes[_value] - 1);
    60	  }
    61	
    62	  function getPower(Record storage _record, address _value) internal view returns (uint256) {
    63	    if (!contains(_record, _value)) return 0;
    64	    return _record.values[_record.indexes[_value] - 1].power;
    65	  }
    66	}

web search: 
web search: 
exec
/bin/bash -lc "sed -n '1,220p' @openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol" in /tmp/codex-nv9d2bwk
 succeeded in 0ms:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (proxy/utils/UUPSUpgradeable.sol)

pragma solidity ^0.8.0;

import "../../interfaces/draft-IERC1822Upgradeable.sol";
import "../ERC1967/ERC1967UpgradeUpgradeable.sol";
import "./Initializable.sol";

/**
 * @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an
 * {ERC1967Proxy}, when this contract is set as the implementation behind such a proxy.
 *
 * A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is
 * reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing
 * `UUPSUpgradeable` with a custom implementation of upgrades.
 *
 * The {_authorizeUpgrade} function must be overridden to include access restriction to the upgrade mechanism.
 *
 * _Available since v4.1._
 */
abstract contract UUPSUpgradeable is Initializable, IERC1822ProxiableUpgradeable, ERC1967UpgradeUpgradeable {
    function __UUPSUpgradeable_init() internal onlyInitializing {
    }

    function __UUPSUpgradeable_init_unchained() internal onlyInitializing {
    }
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment
    address private immutable __self = address(this);

    /**
     * @dev Check that the execution is being performed through a delegatecall call and that the execution context is
     * a proxy contract with an implementation (as defined in ERC1967) pointing to self. This should only be the case
     * for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a
     * function through ERC1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to
     * fail.
     */
    modifier onlyProxy() {
        require(address(this) != __self, "Function must be called through delegatecall");
        require(_getImplementation() == __self, "Function must be called through active proxy");
        _;
    }

    /**
     * @dev Check that the execution is not being performed through a delegate call. This allows a function to be
     * callable on the implementing contract but not through proxies.
     */
    modifier notDelegated() {
        require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall");
        _;
    }

    /**
     * @dev Implementation of the ERC1822 {proxiableUUID} function. This returns the storage slot used by the
     * implementation. It is used to validate the implementation's compatibility when performing an upgrade.
     *
     * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
     * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
     * function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier.
     */
    function proxiableUUID() external view virtual override notDelegated returns (bytes32) {
        return _IMPLEMENTATION_SLOT;
    }

    /**
     * @dev Upgrade the implementation of the proxy to `newImplementation`.
     *
     * Calls {_authorizeUpgrade}.
     *
     * Emits an {Upgraded} event.
     */
    function upgradeTo(address newImplementation) external virtual onlyProxy {
        _authorizeUpgrade(newImplementation);
        _upgradeToAndCallUUPS(newImplementation, new bytes(0), false);
    }

    /**
     * @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call
     * encoded in `data`.
     *
     * Calls {_authorizeUpgrade}.
     *
     * Emits an {Upgraded} event.
     */
    function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual onlyProxy {
        _authorizeUpgrade(newImplementation);
        _upgradeToAndCallUUPS(newImplementation, data, true);
    }

    /**
     * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
     * {upgradeTo} and {upgradeToAndCall}.
     *
     * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}.
     *
     * ```solidity
     * function _authorizeUpgrade(address) internal override onlyOwner {}
     * ```
     */
    function _authorizeUpgrade(address newImplementation) internal virtual;

    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[50] private __gap;
}

exec
/bin/bash -lc "sed -n '1,220p' @openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol" in /tmp/codex-nv9d2bwk
 succeeded in 0ms:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (proxy/ERC1967/ERC1967Upgrade.sol)

pragma solidity ^0.8.2;

import "../beacon/IBeaconUpgradeable.sol";
import "../../interfaces/draft-IERC1822Upgradeable.sol";
import "../../utils/AddressUpgradeable.sol";
import "../../utils/StorageSlotUpgradeable.sol";
import "../utils/Initializable.sol";

/**
 * @dev This abstract contract provides getters and event emitting update functions for
 * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots.
 *
 * _Available since v4.1._
 *
 * @custom:oz-upgrades-unsafe-allow delegatecall
 */
abstract contract ERC1967UpgradeUpgradeable is Initializable {
    function __ERC1967Upgrade_init() internal onlyInitializing {
    }

    function __ERC1967Upgrade_init_unchained() internal onlyInitializing {
    }
    // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1
    bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;

    /**
     * @dev Storage slot with the address of the current implementation.
     * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is
     * validated in the constructor.
     */
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    /**
     * @dev Emitted when the implementation is upgraded.
     */
    event Upgraded(address indexed implementation);

    /**
     * @dev Returns the current implementation address.
     */
    function _getImplementation() internal view returns (address) {
        return StorageSlotUpgradeable.getAddressSlot(_IMPLEMENTATION_SLOT).value;
    }

    /**
     * @dev Stores a new address in the EIP1967 implementation slot.
     */
    function _setImplementation(address newImplementation) private {
        require(AddressUpgradeable.isContract(newImplementation), "ERC1967: new implementation is not a contract");
        StorageSlotUpgradeable.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
    }

    /**
     * @dev Perform implementation upgrade
     *
     * Emits an {Upgraded} event.
     */
    function _upgradeTo(address newImplementation) internal {
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);
    }

    /**
     * @dev Perform implementation upgrade with additional setup call.
     *
     * Emits an {Upgraded} event.
     */
    function _upgradeToAndCall(
        address newImplementation,
        bytes memory data,
        bool forceCall
    ) internal {
        _upgradeTo(newImplementation);
        if (data.length > 0 || forceCall) {
            _functionDelegateCall(newImplementation, data);
        }
    }

    /**
     * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
     *
     * Emits an {Upgraded} event.
     */
    function _upgradeToAndCallUUPS(
        address newImplementation,
        bytes memory data,
        bool forceCall
    ) internal {
        // Upgrades from old implementations will perform a rollback test. This test requires the new
        // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing
        // this special case will break upgrade paths from old UUPS implementation to new ones.
        if (StorageSlotUpgradeable.getBooleanSlot(_ROLLBACK_SLOT).value) {
            _setImplementation(newImplementation);
        } else {
            try IERC1822ProxiableUpgradeable(newImplementation).proxiableUUID() returns (bytes32 slot) {
                require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID");
            } catch {
                revert("ERC1967Upgrade: new implementation is not UUPS");
            }
            _upgradeToAndCall(newImplementation, data, forceCall);
        }
    }

    /**
     * @dev Storage slot with the admin of the contract.
     * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is
     * validated in the constructor.
     */
    bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

    /**
     * @dev Emitted when the admin account has changed.
     */
    event AdminChanged(address previousAdmin, address newAdmin);

    /**
     * @dev Returns the current admin.
     */
    function _getAdmin() internal view returns (address) {
        return StorageSlotUpgradeable.getAddressSlot(_ADMIN_SLOT).value;
    }

    /**
     * @dev Stores a new address in the EIP1967 admin slot.
     */
    function _setAdmin(address newAdmin) private {
        require(newAdmin != address(0), "ERC1967: new admin is the zero address");
        StorageSlotUpgradeable.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
    }

    /**
     * @dev Changes the admin of the proxy.
     *
     * Emits an {AdminChanged} event.
     */
    function _changeAdmin(address newAdmin) internal {
        emit AdminChanged(_getAdmin(), newAdmin);
        _setAdmin(newAdmin);
    }

    /**
     * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy.
     * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor.
     */
    bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

    /**
     * @dev Emitted when the beacon is upgraded.
     */
    event BeaconUpgraded(address indexed beacon);

    /**
     * @dev Returns the current beacon.
     */
    function _getBeacon() internal view returns (address) {
        return StorageSlotUpgradeable.getAddressSlot(_BEACON_SLOT).value;
    }

    /**
     * @dev Stores a new beacon in the EIP1967 beacon slot.
     */
    function _setBeacon(address newBeacon) private {
        require(AddressUpgradeable.isContract(newBeacon), "ERC1967: new beacon is not a contract");
        require(
            AddressUpgradeable.isContract(IBeaconUpgradeable(newBeacon).implementation()),
            "ERC1967: beacon implementation is not a contract"
        );
        StorageSlotUpgradeable.getAddressSlot(_BEACON_SLOT).value = newBeacon;
    }

    /**
     * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does
     * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that).
     *
     * Emits a {BeaconUpgraded} event.
     */
    function _upgradeBeaconToAndCall(
        address newBeacon,
        bytes memory data,
        bool forceCall
    ) internal {
        _setBeacon(newBeacon);
        emit BeaconUpgraded(newBeacon);
        if (data.length > 0 || forceCall) {
            _functionDelegateCall(IBeaconUpgradeable(newBeacon).implementation(), data);
        }
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function _functionDelegateCall(address target, bytes memory data) private returns (bytes memory) {
        require(AddressUpgradeable.isContract(target), "Address: delegate call to non-contract");

        // solhint-disable-next-line avoid-low-level-calls
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return AddressUpgradeable.verifyCallResult(success, returndata, "Address: low-level delegate call failed");
    }

    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[50] private __gap;
}

exec
/bin/bash -lc "sed -n '1,220p' @openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol" in /tmp/codex-nv9d2bwk
 succeeded in 0ms:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.1) (proxy/utils/Initializable.sol)

pragma solidity ^0.8.2;

import "../../utils/AddressUpgradeable.sol";

/**
 * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
 * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
 * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
 * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
 *
 * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
 * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
 * case an upgrade adds a module that needs to be initialized.
 *
 * For example:
 *
 * [.hljs-theme-light.nopadding]
 * ```
 * contract MyToken is ERC20Upgradeable {
 *     function initialize() initializer public {
 *         __ERC20_init("MyToken", "MTK");
 *     }
 * }
 * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
 *     function initializeV2() reinitializer(2) public {
 *         __ERC20Permit_init("MyToken");
 *     }
 * }
 * ```
 *
 * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
 * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
 *
 * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
 * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
 *
 * [CAUTION]
 * ====
 * Avoid leaving a contract uninitialized.
 *
 * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
 * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
 * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
 *
 * [.hljs-theme-light.nopadding]
 * ```
 * /// @custom:oz-upgrades-unsafe-allow constructor
 * constructor() {
 *     _disableInitializers();
 * }
 * ```
 * ====
 */
abstract contract Initializable {
    /**
     * @dev Indicates that the contract has been initialized.
     * @custom:oz-retyped-from bool
     */
    uint8 private _initialized;

    /**
     * @dev Indicates that the contract is in the process of being initialized.
     */
    bool private _initializing;

    /**
     * @dev Triggered when the contract has been initialized or reinitialized.
     */
    event Initialized(uint8 version);

    /**
     * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
     * `onlyInitializing` functions can be used to initialize parent contracts.
     *
     * Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a
     * constructor.
     *
     * Emits an {Initialized} event.
     */
    modifier initializer() {
        bool isTopLevelCall = !_initializing;
        require(
            (isTopLevelCall && _initialized < 1) || (!AddressUpgradeable.isContract(address(this)) && _initialized == 1),
            "Initializable: contract is already initialized"
        );
        _initialized = 1;
        if (isTopLevelCall) {
            _initializing = true;
        }
        _;
        if (isTopLevelCall) {
            _initializing = false;
            emit Initialized(1);
        }
    }

    /**
     * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
     * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
     * used to initialize parent contracts.
     *
     * A reinitializer may be used after the original initialization step. This is essential to configure modules that
     * are added through upgrades and that require initialization.
     *
     * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
     * cannot be nested. If one is invoked in the context of another, execution will revert.
     *
     * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
     * a contract, executing them in the right order is up to the developer or operator.
     *
     * WARNING: setting the version to 255 will prevent any future reinitialization.
     *
     * Emits an {Initialized} event.
     */
    modifier reinitializer(uint8 version) {
        require(!_initializing && _initialized < version, "Initializable: contract is already initialized");
        _initialized = version;
        _initializing = true;
        _;
        _initializing = false;
        emit Initialized(version);
    }

    /**
     * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
     * {initializer} and {reinitializer} modifiers, directly or indirectly.
     */
    modifier onlyInitializing() {
        require(_initializing, "Initializable: contract is not initializing");
        _;
    }

    /**
     * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
     * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
     * to any version. It is recommended to use this to lock implementation contracts that are designed to be called
     * through proxies.
     *
     * Emits an {Initialized} event the first time it is successfully executed.
     */
    function _disableInitializers() internal virtual {
        require(!_initializing, "Initializable: contract is initializing");
        if (_initialized < type(uint8).max) {
            _initialized = type(uint8).max;
            emit Initialized(type(uint8).max);
        }
    }

    /**
     * @dev Returns the highest version that has been initialized. See {reinitializer}.
     */
    function _getInitializedVersion() internal view returns (uint8) {
        return _initialized;
    }

    /**
     * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
     */
    function _isInitializing() internal view returns (bool) {
        return _initializing;
    }
}

Read the entire audit bundle and analyzed the first-party bridge predicate implementation for signature handling, accounting, access control, and proxy-specific risks. I identified concrete issues in signature domain separation and token accounting.
  ✓ Inspect workspace and fully read AUDIT_BUNDLE.txt to identify first-party contracts and proxy/implementation context
  ✓ Analyze first-party Solidity code for exploitable issues in scoped risk areas
  → Produce JSON report with concrete findings tied to implementation code
codex
{"no_findings":false,"summary":"The implementation has two meaningful issues: withdrawal signatures are not domain-separated by bridge instance, and deposits account for the requested amount instead of the actual tokens received.","findings":[{"severity":"medium","category":"auth","title":"Withdraw signatures can be replayed across bridge deployments on the same chain","exploit_scenario":"Validators sign a withdrawal for one L1 predicate instance. Because the signed digest is only `keccak256(chainid, data)`, the same signatures remain valid on any other predicate deployment on the same chain that uses the same validator registry and token mapping. An attacker can submit the packet to a second funded bridge instance and withdraw the same amount there as well, since `orderExecuted[from][orderId]` is tracked per contract.","recommendation":"Include `address(this)` (or another unique bridge-domain identifier) in the signed message, and ideally bind signatures to a validator-set checkpoint/epoch as well.","evidence":"`syncWithdraw` builds `messageHash = keccak256(abi.encodePacked(block.chainid, _data))` and `_checkValidatorSignatures` verifies signatures against that hash only. No contract address, proxy address, or deployment-specific domain separator is included.","locations":["contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:72","contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:86","contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:169"]},{"severity":"medium","category":"other","title":"Fee-on-transfer tokens break bridge accounting and can drain pooled liquidity","exploit_scenario":"If governance maps an ERC20 that charges transfer fees or otherwise credits less than `_amount`, `deposit` still emits a deposit for the full `_amount` after `safeTransferFrom`. Off-chain relayers will typically mint/credit the full amount on L2, allowing the depositor to later withdraw more on L1 than this contract actually received. The shortfall is paid from other users' liquidity, or the bridge becomes insolvent for that token.","recommendation":"Measure the actual balance delta before and after `safeTransferFrom` and emit/credit that received amount instead of the user-supplied `_amount`. Alternatively, explicitly reject non-standard / fee-on-transfer tokens.","evidence":"`_initiateDeposit` transfers `_amount` in, but never checks the post-transfer balance. It immediately emits `DepositToken(..., _amount)` based on the requested amount rather than the actual tokens received.","locations":["contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:155","contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:161","contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:165"]}]}
tokens used
43,959
Metadata
{
    "codex": {
        "ran_at": "2026-04-04T04:05:29.408507+00:00",
        "returncode": 0
    },
    "project": {
        "mode": "source",
        "file_count": 23,
        "import_count": 26,
        "first_party_contracts_count": 5
    },
    "summary": "The implementation has two meaningful issues: withdrawal signatures are not domain-separated by bridge instance, and deposits account for the requested amount instead of the actual tokens received.",
    "raw_final": {
        "summary": "The implementation has two meaningful issues: withdrawal signatures are not domain-separated by bridge instance, and deposits account for the requested amount instead of the actual tokens received.",
        "findings": [
            {
                "title": "Withdraw signatures can be replayed across bridge deployments on the same chain",
                "category": "auth",
                "evidence": "`syncWithdraw` builds `messageHash = keccak256(abi.encodePacked(block.chainid, _data))` and `_checkValidatorSignatures` verifies signatures against that hash only. No contract address, proxy address, or deployment-specific domain separator is included.",
                "severity": "medium",
                "locations": [
                    "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:72",
                    "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:86",
                    "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:169"
                ],
                "recommendation": "Include `address(this)` (or another unique bridge-domain identifier) in the signed message, and ideally bind signatures to a validator-set checkpoint/epoch as well.",
                "exploit_scenario": "Validators sign a withdrawal for one L1 predicate instance. Because the signed digest is only `keccak256(chainid, data)`, the same signatures remain valid on any other predicate deployment on the same chain that uses the same validator registry and token mapping. An attacker can submit the packet to a second funded bridge instance and withdraw the same amount there as well, since `orderExecuted[from][orderId]` is tracked per contract."
            },
            {
                "title": "Fee-on-transfer tokens break bridge accounting and can drain pooled liquidity",
                "category": "other",
                "evidence": "`_initiateDeposit` transfers `_amount` in, but never checks the post-transfer balance. It immediately emits `DepositToken(..., _amount)` based on the requested amount rather than the actual tokens received.",
                "severity": "medium",
                "locations": [
                    "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:155",
                    "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:161",
                    "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:165"
                ],
                "recommendation": "Measure the actual balance delta before and after `safeTransferFrom` and emit/credit that received amount instead of the user-supplied `_amount`. Alternatively, explicitly reject non-standard / fee-on-transfer tokens.",
                "exploit_scenario": "If governance maps an ERC20 that charges transfer fees or otherwise credits less than `_amount`, `deposit` still emits a deposit for the full `_amount` after `safeTransferFrom`. Off-chain relayers will typically mint/credit the full amount on L2, allowing the depositor to later withdraw more on L1 than this contract actually received. The shortfall is paid from other users' liquidity, or the bridge becomes insolvent for that token."
            }
        ],
        "no_findings": false
    },
    "no_findings": false,
    "proxy_context": {
        "proxy_run_id": "019d5666-7238-704a-97b4-9ab40e4f0ac7",
        "proxy_address": "0x63105ee97bfb22dfe23033b3b14a4f8fed121ee9",
        "implementation_run_id": "019d5667-1339-71d8-a320-f4112d024afe",
        "implementation_address": "0xa8372d6ff00d48a25baa1af16d6a86c936708f4e"
    },
    "schema_version": 1
}

Opcode Heuristics

tool.cast_disasm
Delegatecall
1
Selfdestruct
0
CREATE2
0
CALL-family (heavy)
1
EXT*/BALANCE
1
Total opcodes
101
Flags
delegatecall_present proxy_like_runtime
View cast disassembly output
00000000: PUSH1 0x80
00000002: PUSH1 0x40
00000004: MSTORE
00000005: CALLDATASIZE
00000006: PUSH1 0x10
00000008: JUMPI
00000009: PUSH1 0x0e
0000000b: PUSH1 0x13
0000000d: JUMP
0000000e: JUMPDEST
0000000f: STOP
00000010: JUMPDEST
00000011: PUSH1 0x0e
00000013: JUMPDEST
00000014: PUSH1 0x1f
00000016: PUSH1 0x1b
00000018: PUSH1 0x21
0000001a: JUMP
0000001b: JUMPDEST
0000001c: PUSH1 0x65
0000001e: JUMP
0000001f: JUMPDEST
00000020: JUMP
00000021: JUMPDEST
00000022: PUSH1 0x00
00000024: PUSH1 0x60
00000026: PUSH32 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
00000047: SLOAD
00000048: PUSH20 0xffffffffffffffffffffffffffffffffffffffff
0000005d: AND
0000005e: SWAP1
0000005f: JUMP
00000060: JUMPDEST
00000061: SWAP1
00000062: POP
00000063: SWAP1
00000064: JUMP
00000065: JUMPDEST
00000066: CALLDATASIZE
00000067: PUSH1 0x00
00000069: DUP1
0000006a: CALLDATACOPY
0000006b: PUSH1 0x00
0000006d: DUP1
0000006e: CALLDATASIZE
0000006f: PUSH1 0x00
00000071: DUP5
00000072: GAS
00000073: DELEGATECALL
00000074: RETURNDATASIZE
00000075: PUSH1 0x00
00000077: DUP1
00000078: RETURNDATACOPY
00000079: DUP1
0000007a: DUP1
0000007b: ISZERO
0000007c: PUSH1 0x83
0000007e: JUMPI
0000007f: RETURNDATASIZE
00000080: PUSH1 0x00
00000082: RETURN
00000083: JUMPDEST
00000084: RETURNDATASIZE
00000085: PUSH1 0x00
00000087: REVERT
00000088: INVALID
00000089: LOG2
0000008a: PUSH5 0x6970667358
00000090: UNKNOWN(0x22)
00000091: SLT
00000092: KECCAK256
00000093: UNKNOWN(0x27)
00000094: UNKNOWN(0x4E)
00000095: UNKNOWN(0x2E)
00000096: UNKNOWN(0xD9)
00000097: SWAP14
00000098: TLOAD
00000099: UNKNOWN(0xBF)
0000009a: EXTCODESIZE
0000009b: MLOAD
0000009c: UNKNOWN(0x2C)
0000009d: UNKNOWN(0xB6)
0000009e: LOG2
0000009f: UNKNOWN(0xB4)
000000a0: NUMBER
000000a1: RETURN
000000a2: PUSH8 0x609dddc30d75aebd
000000ab: SWAP8
000000ac: CALLVALUE
000000ad: STATICCALL
000000ae: BYTE
000000af: UNKNOWN(0xBC)
000000b0: DUP6
000000b1: DUP4
000000b2: UNKNOWN(0xD9)
000000b3: PUSH5 0x736f6c6343
000000b9: STOP
000000ba: ADDMOD
000000bb: SGT
000000bc: STOP
000000bd: CALLER

Findings

validated = confirmed for call sinks; sink observed for others
Severity Category Tool Title Confidence Validated Confirmed Details
critical upgradeability detector Untrusted DELEGATECALL target reachable 70 no no
view
trace_id: 0xecc6f0251a0258ad492700a035f3902e3aea3ae762afb158cce5d5b4ba2e1c2d
call_targets: 0xa8372d6ff00d48a25baa1af16d6a86c936708f4e
target_varies: no
classification: constant_target
validation_json
{
    "sink": "DELEGATECALL",
    "errors": 0,
    "status": "sink_reached",
    "attempts": 1,
    "trace_id": "0xecc6f0251a0258ad492700a035f3902e3aea3ae762afb158cce5d5b4ba2e1c2d",
    "confirmed": false,
    "trace_mode": "callTracer",
    "call_targets": [
        "0xa8372d6ff00d48a25baa1af16d6a86c936708f4e"
    ],
    "matched_probe": null,
    "target_varies": false,
    "classification": "constant_target"
}
witness_json
{
    "notes": "heuristic witness; entrypoints are best-effort selectors with zeroed calldata",
    "constraints": [],
    "entrypoints": [
        {
            "calldata": "0x",
            "selector": "fallback",
            "calldata_variants": [
                "0x"
            ]
        }
    ]
}
evidence_json
{
    "examples": [
        {
            "pc": 115,
            "opcode": "DELEGATECALL",
            "returndata_checked": null,
            "value_expression_category": "n/a",
            "target_expression_category": "computed"
        }
    ],
    "guards_detected": [],
    "delegatecall_count": 1,
    "untrusted_delegatecall_count": 1
}
medium upgradeability cast DELEGATECALL present 80 no
view
evidence_json
{
    "notes": "delegatecall enables execution in caller context; common in proxies/upgradeable patterns",
    "delegatecall_count": 1
}
medium auth codex Withdraw signatures can be replayed across bridge deployments on the same chain 75 no
view
evidence_json
{
    "evidence": "`syncWithdraw` builds `messageHash = keccak256(abi.encodePacked(block.chainid, _data))` and `_checkValidatorSignatures` verifies signatures against that hash only. No contract address, proxy address, or deployment-specific domain separator is included.",
    "locations": [
        "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:72",
        "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:86",
        "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:169"
    ],
    "recommendation": "Include `address(this)` (or another unique bridge-domain identifier) in the signed message, and ideally bind signatures to a validator-set checkpoint/epoch as well.",
    "exploit_scenario": "Validators sign a withdrawal for one L1 predicate instance. Because the signed digest is only `keccak256(chainid, data)`, the same signatures remain valid on any other predicate deployment on the same chain that uses the same validator registry and token mapping. An attacker can submit the packet to a second funded bridge instance and withdraw the same amount there as well, since `orderExecuted[from][orderId]` is tracked per contract."
}
medium other codex Fee-on-transfer tokens break bridge accounting and can drain pooled liquidity 75 no
view
evidence_json
{
    "evidence": "`_initiateDeposit` transfers `_amount` in, but never checks the post-transfer balance. It immediately emits `DepositToken(..., _amount)` based on the requested amount rather than the actual tokens received.",
    "locations": [
        "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:155",
        "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:161",
        "contracts/bridge/v2/core/L1/predicates/L1ERC20Predicate.sol:165"
    ],
    "recommendation": "Measure the actual balance delta before and after `safeTransferFrom` and emit/credit that received amount instead of the user-supplied `_amount`. Alternatively, explicitly reject non-standard / fee-on-transfer tokens.",
    "exploit_scenario": "If governance maps an ERC20 that charges transfer fees or otherwise credits less than `_amount`, `deposit` still emits a deposit for the full `_amount` after `safeTransferFrom`. Off-chain relayers will typically mint/credit the full amount on L2, allowing the depositor to later withdraw more on L1 than this contract actually received. The shortfall is paid from other users' liquidity, or the bridge becomes insolvent for that token."
}
info upgradeability cast Likely proxy-like runtime 60 no
view
evidence_json
{
    "notes": "short runtime with SLOAD and DELEGATECALL resembles minimal proxy/upgradeable pattern",
    "sload_count": 1,
    "total_opcodes": 101,
    "delegatecall_count": 1
}