-
Notifications
You must be signed in to change notification settings - Fork 1
feat(safe): add IGnosisSafe interface and LibProdSafes constants for token-owner Safe #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f148997
962c121
7fdcc0b
ae5733a
6ae8b5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
|
|
||
| /// @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); | ||
| } | ||
| 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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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")
PYRepository: 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)
PYRepository: 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))
PYRepository: 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)
PYRepository: 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)
PYRepository: 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)
PYRepository: 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)
PYRepository: S01-Issuer/st0x.deploy Length of output: 1777 Fix EIP-55 casing mismatch for
🧰 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 |
||
|
|
||
| /// @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; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: S01-Issuer/st0x.deploy
Length of output: 175
Use exact Solidity pin for this source interface.
IGnosisSafeis undersrc/**but uses^0.8.25. Repository rules require exact pinning (=0.8.25) for source interfaces to keep builds deterministic.Suggested fix
Per coding guidelines, interfaces and contracts in
src/**/*.solmust use exact version pinning (=0.8.25); caret pinning (^0.8.25) is reserved for library files.📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
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— seesrc/interface/IStoxUnifiedDeployerV1.solandsrc/interface/IStoxWrappedTokenVaultBeaconSetDeployerV1.sol, both pure interface files undersrc/**using caret pinning. CLAUDE.md states=0.8.25for contracts/scripts and^0.8.25for libraries; interfaces compile as ABI fragments (no runtime bytecode), aligning them with the library bucket.ICorporateActionsV1.soluses=0.8.25but it's tied tightly to the diamond deployment and behaves more like a contract.IGnosisSafeis a third-party ABI shim consumed by libraries, so^0.8.25is the right pin.There was a problem hiding this comment.
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