Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/interface/IGnosisSafe.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity ^0.8.25;

@coderabbitai coderabbitai Bot May 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify pragma style in reviewed source files.
rg -nP '^\s*pragma solidity\s+' src/interface/IGnosisSafe.sol src/lib/LibProdSafes.sol

Repository: S01-Issuer/st0x.deploy

Length of output: 175


Use exact Solidity pin for this source interface.

IGnosisSafe is under src/** but uses ^0.8.25. Repository rules require exact pinning (=0.8.25) for source interfaces to keep builds deterministic.

Suggested fix
-pragma solidity ^0.8.25;
+pragma solidity =0.8.25;

Per coding guidelines, interfaces and contracts in src/**/*.sol must use exact version pinning (=0.8.25); caret pinning (^0.8.25) is reserved for library files.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pragma solidity ^0.8.25;
pragma solidity =0.8.25;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/interface/IGnosisSafe.sol` at line 3, The file's pragma uses caret
pinning (^0.8.25) which violates the repository rule requiring exact compiler
pins for source interfaces; update the pragma in IGnosisSafe (the interface
declaration symbol IGnosisSafe) to use an exact pin (=0.8.25) instead of ^0.8.25
so the source under src/** builds deterministically.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining this one. Repo convention for ABI interface declarations is ^0.8.25 — see src/interface/IStoxUnifiedDeployerV1.sol and src/interface/IStoxWrappedTokenVaultBeaconSetDeployerV1.sol, both pure interface files under src/** using caret pinning. CLAUDE.md states =0.8.25 for contracts/scripts and ^0.8.25 for libraries; interfaces compile as ABI fragments (no runtime bytecode), aligning them with the library bucket. ICorporateActionsV1.sol uses =0.8.25 but it's tied tightly to the diamond deployment and behaves more like a contract. IGnosisSafe is a third-party ABI shim consumed by libraries, so ^0.8.25 is the right pin.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hardyjosh, understood — thanks for the thorough explanation. The distinction makes sense: interfaces with no runtime bytecode align with the library bucket and use ^0.8.25, while contracts and scripts that produce deployable bytecode get =0.8.25. I'll apply this correctly in future reviews.

( ˘ ³˘)♥ 🐇


✏️ Learnings added
Learnt from: hardyjosh
Repo: S01-Issuer/st0x.deploy PR: 189
File: src/interface/IGnosisSafe.sol:3-3
Timestamp: 2026-05-20T17:00:44.584Z
Learning: In the S01-Issuer/st0x.deploy repository, pragma pinning follows this convention (documented in CLAUDE.md):
- Contracts and scripts (deployable bytecode) use exact pinning: `pragma solidity =0.8.25;`
- Interfaces and libraries (no runtime bytecode / ABI fragments) use caret pinning: `pragma solidity ^0.8.25;`
Solidity interface files under `src/interface/` that are pure ABI shims (e.g., IGnosisSafe.sol, IStoxUnifiedDeployerV1.sol, IStoxWrappedTokenVaultBeaconSetDeployerV1.sol) correctly use `^0.8.25`. An exception is `ICorporateActionsV1.sol`, which uses `=0.8.25` because it is tightly coupled to diamond deployment and behaves more like a contract.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


/// @title IGnosisSafe
/// @notice Minimal interface for interacting with a Safe (formerly Gnosis Safe)
/// proxy. Pinned at the surface required by the multisig threshold migration
/// tooling: read-only owner/threshold/version/storage introspection, the
/// canonical transaction hash builder, and the privileged `changeThreshold`
/// mutator (self-call only — guarded by the Safe's own signature verification
/// in production; reachable in tests via `vm.prank`).
/// @dev This interface intentionally omits the rest of the Safe ABI (sigs,
/// owner management, module management, fallback handler config, etc.) — the
/// migration tooling only needs the functions declared here. Extending this
/// interface in future migrations is preferred over re-declaring functions
/// locally per script.
///
/// All function signatures match the canonical Safe v1.4.1 L2 source:
/// https://github.com/safe-global/safe-contracts/tree/v1.4.1/contracts
interface IGnosisSafe {
/// @notice Allows the Safe (self-call only) to update the number of owner
/// signatures required to confirm a transaction.
/// @param threshold The new threshold. Must be `> 0` and `<= owners.length`
/// as enforced by the Safe's OwnerManager. Reverts otherwise.
function changeThreshold(uint256 threshold) external;

/// @notice Returns the current signature threshold.
/// @return The number of owner signatures required to execute a transaction.
function getThreshold() external view returns (uint256);

/// @notice Returns the current owner set in Safe-internal linked-list
/// order (sentinel is `address(0x1)`).
/// @return The list of owner addresses for this Safe.
function getOwners() external view returns (address[] memory);

/// @notice Returns the current Safe transaction nonce. Used as the `_nonce`
/// argument to `getTransactionHash` when computing the next executable
/// transaction's hash off-chain.
/// @return The next nonce that will be consumed by `execTransaction`.
function nonce() external view returns (uint256);

/// @notice Paginated module enumeration. Walks the modules linked list
/// starting from `start` (use sentinel `address(0x1)` to enumerate from
/// the head).
/// @param start The cursor address. Pass `address(0x1)` to begin at the
/// head of the modules linked list.
/// @param pageSize The maximum number of modules to return in this call.
/// @return array The modules page (length up to `pageSize`).
/// @return next The next cursor address. Equal to `start` (sentinel) when
/// the list is empty, or a module address to feed back into a subsequent
/// call when paging is incomplete.
function getModulesPaginated(address start, uint256 pageSize)
external
view
returns (address[] memory array, address next);

/// @notice Computes the canonical EIP-712 transaction hash for the supplied
/// transaction inputs. This is the hash that owners must sign for
/// `execTransaction` to accept the bundle.
/// @dev We delegate hashing to the live Safe instance rather than
/// recomputing locally — this binds the produced hash to the Safe's
/// declared domain separator and prevents drift between off-chain tooling
/// and on-chain verification.
/// @param to Destination of the inner transaction.
/// @param value Native value forwarded to `to`.
/// @param data Calldata forwarded to `to`.
/// @param operation `0` for `CALL`, `1` for `DELEGATECALL`.
/// @param safeTxGas Gas forwarded to the inner call.
/// @param baseGas Fixed gas costs added on top of `safeTxGas` for refund.
/// @param gasPrice Refund gas price (zero for non-refunded executions).
/// @param gasToken Token used for the refund (zero address for native).
/// @param refundReceiver Refund receiver (zero address for `tx.origin`).
/// @param _nonce Safe nonce that this transaction will consume.
/// @return The Safe transaction hash.
function getTransactionHash(
address to,
uint256 value,
bytes calldata data,
uint8 operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address refundReceiver,
uint256 _nonce
) external view returns (bytes32);

/// @notice Reads raw storage from the Safe proxy. Used to assert
/// well-known Safe storage slots (singleton, fallback handler, guard)
/// without going through accessor functions that could be shadowed by a
/// malicious fallback.
/// @param offset Starting storage slot index.
/// @param length Number of slots to read.
/// @return The concatenated 32-byte words read from storage.
function getStorageAt(uint256 offset, uint256 length) external view returns (bytes memory);

/// @notice Returns the Safe implementation's version string. For Safe
/// v1.4.1 the canonical value is `"1.4.1"`.
/// @return The version string declared by the Safe singleton.
/// @dev `VERSION` is upper-case to match the canonical Safe ABI selector;
/// renaming would break ABI compatibility with the live deployment.
//slither-disable-next-line naming-convention
function VERSION() external view returns (string memory);
}
118 changes: 118 additions & 0 deletions src/lib/LibProdSafes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity ^0.8.25;

/// @title LibProdSafes
/// @notice Production Safe constants for the ST0x token-owner multisig on
/// Base. Pinned addresses, codehashes, slot values, and the expected owner
/// set (currently 4 owners) are derived from live on-chain state and the
/// canonical Safe deployment manifest, then re-asserted from fork tests so
/// that drift between this file and reality trips CI rather than slipping
/// into a migration script.
/// @dev Scope: the multisig threshold migration raises this Safe from
/// 1-of-4 to 3-of-4. (Originally specced as 1-of-5 -> 3-of-5; the owner
/// roster was reduced to four on 2026-05-18 via the `RemovedOwner` event
/// at block 46156528, before the threshold migration was executed.)
/// @dev Sources:
/// - Safe v1.4.1 L2 singleton & proxy: github.com/safe-global/safe-deployments
/// under `src/assets/v1.4.1/safe_l2.json` (chainId 8453 entry). Both the
/// singleton address and the proxy bytecode are deterministic across the
/// Safe L2 deployment, so the proxy codehash below is also constant.
/// - ST0x Safe address & live state read on Base on 2026-05-20 via
/// `cast call`. The owner set, threshold, and storage-slot pins below
/// match the post-removal state. `StoxProdV2.t.sol::testProdDeployBaseV2`
/// exercises these against an unpinned head fork (via
/// `LibSafeInvariants.assertAll`) so the next CI run catches any further
/// drift; see that test for why `LibTestProd.PROD_TEST_BLOCK_NUMBER_BASE`
/// is not reused.
library LibProdSafes {
/// @notice Safe v1.4.1 L2 singleton (master copy) address on Base.
/// Verified by reading proxy storage slot `0x0` of
/// `STOX_TOKEN_OWNER_SAFE` and matching against the
/// `safe-deployments` manifest.
address constant SAFE_V1_4_1_L2_SINGLETON = 0x29fcB43b46531BcA003ddC8FCB67FFE91900C762;

/// @notice Runtime codehash of a Safe v1.4.1 proxy on Base. Equal to
/// `extcodehash(STOX_TOKEN_OWNER_SAFE)` and to every other v1.4.1 L2
/// proxy pointing at `SAFE_V1_4_1_L2_SINGLETON`. Pinning this codehash
/// guards against the Safe address being replaced by an EOA-controlled
/// contract or a fake proxy pointing at a malicious singleton.
bytes32 constant SAFE_V1_4_1_L2_PROXY_CODEHASH = 0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000;

/// @notice Expected `VERSION()` string from a Safe v1.4.1 singleton.
string constant SAFE_V1_4_1_VERSION = "1.4.1";

/// @notice Runtime codehash of the Safe v1.4.1 L2 singleton bytecode at
/// `SAFE_V1_4_1_L2_SINGLETON`. Pinning this guards against an attacker
/// who replaces the bytecode at the singleton address (e.g. via
/// `SELFDESTRUCT` + re-create) while preserving the proxy codehash.
/// Without this pin, every implementation-backed accessor on the Safe
/// (`VERSION()`, `getOwners()`, `getThreshold()`, etc.) is mediated by
/// untrusted code at the singleton address. Asserting this codehash
/// before any of those reads closes that gap.
/// @dev Computed via `keccak256(eth_getCode(SAFE_V1_4_1_L2_SINGLETON))`
/// on Base on 2026-05-20.
bytes32 constant SAFE_V1_4_1_L2_SINGLETON_CODEHASH =
0xb1f926978a0f44a2c0ec8fe822418ae969bd8c3f18d61e5103100339894f81ff;

/// @notice CompatibilityFallbackHandler v1.4.1 address on Base. Verified
/// against the live Safe's fallback handler storage slot. Pinned so a
/// swapped-in malicious handler that shadows view selectors via
/// fallback can be detected by `assertBaseSafeInvariants`.
/// @dev Source: github.com/safe-global/safe-deployments
/// `src/assets/v1.4.1/compatibility_fallback_handler.json` (chainId
/// 8453 entry). Cross-checked on Base on 2026-05-20.
address constant SAFE_V1_4_1_COMPATIBILITY_FALLBACK_HANDLER = 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99;

/// @notice The Safe that owns every ST0x receipt vault on Base. Subject
/// of the threshold migration (1 -> 3, against the current 4-owner
/// roster).
/// https://basescan.org/address/0xe70d821f3462A074E63b42D0aac6523faAe1D611
address constant STOX_TOKEN_OWNER_SAFE = 0xe70d821f3462a074e63b42d0AaC6523faAe1d611;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
from eth_utils import to_checksum_address
addr = "0xe70d821f3462a074e63b42d0aac6523faae1d611"
print("EIP-55:", to_checksum_address(addr))
print("In source: 0xe70d821f3462a074e63b42d0AaC6523faAe1d611")
print("In docs:   0xe70d821f3462A074E63b42D0aac6523faAe1D611")
PY

Repository: S01-Issuer/st0x.deploy

Length of output: 188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Constant in src/lib/LibProdSafes.sol =="
sed -n '60,90p' src/lib/LibProdSafes.sol | nl -ba | sed -n '8,20p'

echo
echo "== Compute EIP-55 checksums =="
python3 - <<'PY'
import sys, subprocess

# Try eth_utils first; if missing, install it.
try:
    from eth_utils import to_checksum_address
except Exception:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "eth-utils"])
    from eth_utils import to_checksum_address

addr_lower = "0xe70d821f3462a074e63b42d0aac6523faae1d611"

source_literal = "0xe70d821f3462a074e63b42d0AaC6523faAe1d611"
docs_literal   = "0xe70d821f3462a074e63b42D0aac6523faAe1d611"
pr_desc_literal = "0xe70d821f3462A074E63b42D0aac6523faAe1d611"

print("checksummed:", to_checksum_address(addr_lower))
print("matches source_literal:", to_checksum_address(addr_lower) == source_literal)
print("matches docs_literal:  ", to_checksum_address(addr_lower) == docs_literal)
print("matches pr_desc_literal:", to_checksum_address(addr_lower) == pr_desc_literal)
PY

Repository: S01-Issuer/st0x.deploy

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Constant context in src/lib/LibProdSafes.sol (lines 60-90) =="
awk 'NR>=60 && NR<=90 {printf "%5d:%s\n", NR, $0}' src/lib/LibProdSafes.sol

echo
echo "== Compute EIP-55 checksums =="
python3 - <<'PY'
import sys, subprocess
try:
    from eth_utils import to_checksum_address
except Exception:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "eth-utils"])
    from eth_utils import to_checksum_address

addr_lower = "0xe70d821f3462a074e63b42d0aac6523faAe1d611".lower()
# Normalize to lowercase hex without checksum issues
addr_lower = addr_lower.lower()

source_literal = "0xe70d821f3462a074e63b42d0AaC6523faAe1d611"
docs_literal   = "0xe70d821f3462A074E63b42D0aac6523faAe1d611"  # placeholder if docs comment differs; will compare below
pr_desc_literal = "0xe70d821f3462A074E63b42D0aac6523faAe1d611"

# Also compute from the address without depending on mixed-case spellings:
addr_only_lower = addr_lower

checksummed = to_checksum_address(addr_only_lower)
print("checksummed:", checksummed)

def matches(lit):
    try:
        return to_checksum_address(lit.lower()) == lit
    except Exception:
        return False

print("matches source_literal:", matches(source_literal))
print("matches docs_literal:   ", matches(docs_literal))
print("matches pr_desc_literal:", matches(pr_desc_literal))
PY

Repository: S01-Issuer/st0x.deploy

Length of output: 2540


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import sys, subprocess

target_lower = "0xe70d821f3462a074e63b42d0aac6523faae1d611"

source_literal = "0xe70d821f3462a074e63b42d0AaC6523faAe1d611"
basescan_literal = "0xe70d821f3462A074E63b42D0aac6523faAe1D611"

# Try to find a keccak256 implementation
def keccak256(data: bytes) -> bytes:
    # 1) eth_hash
    try:
        from eth_hash.auto import keccak
        return keccak(data)
    except Exception:
        pass

    # 2) web3
    try:
        from web3 import Web3
        return Web3.keccak(data)
    except Exception:
        pass

    # 3) pycryptodome
    try:
        from Crypto.Hash import keccak as _keccak
        k = _keccak.new(digest_bits=256)
        k.update(data)
        return k.digest()
    except Exception:
        pass

    # 4) sha3 (sometimes provides keccak)
    try:
        import sha3
        k = sha3.keccak_256()
        k.update(data)
        return k.digest()
    except Exception:
        pass

    return None

k = keccak256(target_lower[2:].encode())
if k is None:
    print("No keccak256 implementation available in this environment.")
    print("Cannot compute EIP-55 checksum without a keccak256 library.")
    sys.exit(1)

hexaddr = target_lower[2:]
hash_hex = k.hex()

# EIP-55: for each hex char, uppercase if corresponding hash nibble >= 8
checksummed = "0x"
for i, ch in enumerate(hexaddr):
    if ch.isalpha():
        checksummed += ch.upper() if int(hash_hex[i], 16) >= 8 else ch.lower()
    else:
        checksummed += ch

print("computed EIP-55:", checksummed)
print("source_literal matches:", checksummed == source_literal)
print("basescan_literal matches:", checksummed == basescan_literal)
PY

Repository: S01-Issuer/st0x.deploy

Length of output: 185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Tool availability =="
command -v cast || true
command -v forge || true
command -v node || true
command -v npm || true
python3 -m pip --version || true

ADDR_LOWER="0xe70d821f3462a074e63b42d0aac6523faae1d611"
SOURCE_LITERAL="0xe70d821f3462a074e63b42d0AaC6523faAe1d611"
BASESCAN_LITERAL="0xe70d821f3462A074E63b42D0aac6523faAe1d611"

echo
echo "== Try cast (Foundry) =="
if command -v cast >/dev/null 2>&1; then
  CHECKSUM="$(cast to-checksum-address "$ADDR_LOWER")"
  echo "cast checksum: $CHECKSUM"
  echo "matches source literal: $([ "$CHECKSUM" = "$SOURCE_LITERAL" ] && echo yes || echo no)"
  echo "matches basescan literal: $([ "$CHECKSUM" = "$BASESCAN_LITERAL" ] && echo yes || echo no)"
fi

echo
echo "== Try Node ethers =="
if command -v node >/dev/null 2>&1; then
  node - <<'NODE' || true
const addr = process.env.ADDR_LOWER;
try {
  const { getAddress } = require("ethers");
  const cs = getAddress(addr);
  console.log("ethers checksum:", cs);
  console.log("matches source literal:", cs === process.env.SOURCE_LITERAL);
  console.log("matches basescan literal:", cs === process.env.BASESCAN_LITERAL);
} catch (e) {
  console.log("ethers not available:", e.message);
  process.exit(0);
}
NODE
fi

echo
echo "== Try pip install eth-utils and compute =="
python3 - <<'PY' || true
import sys, subprocess
addr = "0xe70d821f3462a074e63b42d0aac6523faae1d611"
SOURCE_LITERAL="0xe70d821f3462a074e63b42d0AaC6523faAe1d611"
BASESCAN_LITERAL="0xe70d821f3462A074E63b42D0aac6523faAe1D611"

try:
    from eth_utils import to_checksum_address
except Exception:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "eth-utils"])
    from eth_utils import to_checksum_address

cs = to_checksum_address(addr)
print("eth-utils checksum:", cs)
print("matches source literal:", cs == SOURCE_LITERAL)
print("matches basescan literal:", cs == BASESCAN_LITERAL)
PY

Repository: S01-Issuer/st0x.deploy

Length of output: 2137


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import sys, subprocess

addr = "0xe70d821f3462a074e63b42d0aac6523faae1d611"
source_literal = "0xe70d821f3462a074e63b42d0AaC6523faAe1d611"
basescan_literal = "0xe70d821f3462A074E63b42D0aac6523faAe1d611"

def ensure():
    try:
        from eth_utils import to_checksum_address
        return to_checksum_address
    except Exception:
        # Install keccak backend first, then eth-utils
        subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "eth-hash[pycryptodome]"])
        subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "eth-utils"])
        from eth_utils import to_checksum_address
        return to_checksum_address

to_checksum_address = ensure()
cs = to_checksum_address(addr)

print("computed EIP-55:", cs)
print("matches source literal:", cs == source_literal)
print("matches basescan literal:", cs == basescan_literal)
PY

Repository: S01-Issuer/st0x.deploy

Length of output: 1803


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import sys, subprocess, importlib

def run(cmd):
    print("RUN:", " ".join(cmd), file=sys.stderr)
    subprocess.check_call(cmd)

# Try installing keccak backend + eth-utils
run([sys.executable, "-m", "pip", "install", "--no-input", "pycryptodome"])
run([sys.executable, "-m", "pip", "install", "--no-input", "eth-hash[pycryptodome]"])
run([sys.executable, "-m", "pip", "install", "--no-input", "eth-utils"])

for mod in ["eth_utils", "eth_hash"]:
    importlib.import_module(mod)
print("imports ok", file=sys.stderr)

from eth_utils import to_checksum_address
addr = "0xe70d821f3462a074e63b42d0aac6523faae1d611"
source_literal = "0xe70d821f3462a074e63b42d0AaC6523faAe1d611"
basescan_literal = "0xe70d821f3462A074E63b42D0aac6523faAe1d611"

cs = to_checksum_address(addr)
print("computed EIP-55:", cs)
print("matches source literal:", cs == source_literal)
print("matches basescan literal:", cs == basescan_literal)
PY

Repository: S01-Issuer/st0x.deploy

Length of output: 1777


Fix EIP-55 casing mismatch for STOX_TOKEN_OWNER_SAFE comment

STOX_TOKEN_OWNER_SAFE’s mixed-case literal is the correct EIP-55 form: 0xe70d821f3462a074e63b42d0AaC6523faAe1d611 (matches the source constant on line 71). The mixed-case address in the BaseScan URL comment on line 70 (0xe70d821f3462A074E63b42D0aac6523faAe1D611) does not match the EIP-55 checksum—update the comment to the correct casing (or use all-lowercase) to keep it consistent.

🧰 Tools
🪛 Betterleaks (1.2.0)

[high] 71-71: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/LibProdSafes.sol` at line 71, The BaseScan URL comment next to the
STOX_TOKEN_OWNER_SAFE constant contains a differently-cased address that does
not match the EIP-55 checksum used by the constant; update that comment so the
address casing matches the STOX_TOKEN_OWNER_SAFE constant
(0xe70d821f3462a074e63b42d0AaC6523faAe1d611) or convert the comment address to
all-lowercase for consistency; ensure you edit the comment adjacent to the
STOX_TOKEN_OWNER_SAFE declaration and do not change the constant itself.


/// @notice The current expected threshold for `STOX_TOKEN_OWNER_SAFE`.
/// Updated by the threshold-migration PR family once live execution
/// lands: scripts and the post-migration pin both treat this constant
/// as the canonical current truth, so the value bumps from `1` to `3`
/// in the same PR that records the live post-execution state.
uint256 constant STOX_TOKEN_OWNER_SAFE_THRESHOLD = 1;

/// @notice Owner #1 of `STOX_TOKEN_OWNER_SAFE`. Order matches
/// `getOwners()` (Safe-internal linked-list order).
address constant STOX_TOKEN_OWNER_SAFE_OWNER_1 = 0x19f95a84aa1C48A2c6a7B2d5de164331c86D030C;

/// @notice Owner #2 of `STOX_TOKEN_OWNER_SAFE`. Order matches
/// `getOwners()` (Safe-internal linked-list order).
address constant STOX_TOKEN_OWNER_SAFE_OWNER_2 = 0x8f6bF4A948Af2Fc74eE34982C4435a7C013D1A52;

/// @notice Owner #3 of `STOX_TOKEN_OWNER_SAFE`. Order matches
/// `getOwners()` (Safe-internal linked-list order). This slot previously
/// held the now-removed owner `0x691AcCd4...`; after the 2026-05-18
/// `RemovedOwner` event at block 46156528 the linked list shifted up
/// and what was owner #4 became owner #3.
address constant STOX_TOKEN_OWNER_SAFE_OWNER_3 = 0x91E2AF6Ee6bc5d0f7AA1644Bb94957932629d2DB;

/// @notice Owner #4 of `STOX_TOKEN_OWNER_SAFE`. Order matches
/// `getOwners()` (Safe-internal linked-list order). Formerly owner #5
/// before the 2026-05-18 roster reduction.
address constant STOX_TOKEN_OWNER_SAFE_OWNER_4 = 0xBF8a5DE7BaAFaD46495217d467F43ae305cb900f;

/// @notice Returns the expected owner set for `STOX_TOKEN_OWNER_SAFE` in
/// the exact order returned by `getOwners()` against an unpinned Base
/// head fork (the live-state pin lives in
/// `StoxProdV2.t.sol::testProdDeployBaseV2`, which selects head rather
/// than pinning to a historical block so the next CI run catches any
/// further drift). Provided as a helper because Solidity 0.8 cannot
/// express a file-scope `constant address[]` and declaring the array
/// as `immutable` is contract-scoped only.
/// @return The four owners of the ST0x token-owner Safe in
/// `getOwners()` order.
function expectedOwners() internal pure returns (address[] memory) {
address[] memory owners = new address[](4);
owners[0] = STOX_TOKEN_OWNER_SAFE_OWNER_1;
owners[1] = STOX_TOKEN_OWNER_SAFE_OWNER_2;
owners[2] = STOX_TOKEN_OWNER_SAFE_OWNER_3;
owners[3] = STOX_TOKEN_OWNER_SAFE_OWNER_4;
return owners;
}
}
Loading