019d5666-7238-704a-97b4-9ab40e4f0ac7
Open this run to answer one question: is this real attacker-relevant behavior with real impact, or just normal contract plumbing?
| 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 |
{
"method": "etherscan+onchain",
"status": "confirmed",
"etherscan_hint": {
"source": "etherscan",
"etherscan": {
"proxy_flag": true,
"implementation": "0xa8372d6ff00d48a25baa1af16d6a86c936708f4e"
}
},
"implementation": "0xa8372d6ff00d48a25baa1af16d6a86c936708f4e",
"onchain_method": "eip1967"
}
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.
{"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"]}]}
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
{
"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
}
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
| Severity | Category | Tool | Title | Confidence | Validated | Confirmed | Details |
|---|---|---|---|---|---|---|---|
| critical | upgradeability | detector | Untrusted DELEGATECALL target reachable | 70 | no | no |
viewtrace_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 | — |
viewevidence_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 | — |
viewevidence_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 | — |
viewevidence_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 | — |
viewevidence_json{
"notes": "short runtime with SLOAD and DELEGATECALL resembles minimal proxy/upgradeable pattern",
"sload_count": 1,
"total_opcodes": 101,
"delegatecall_count": 1
}
|