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
25 changes: 25 additions & 0 deletions contracts/security/GasOptimizedReentrancyGuard.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title GasOptimizedReentrancyGuard
/// @notice Reentrancy guard using 1/2 sentinel values so re-entry checks always
/// hit a warm storage slot (2,900 gas) instead of a cold 0->1 write (20,000 gas).
abstract contract GasOptimizedReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;

uint256 private _status;

error ReentrantCall();

constructor() {
_status = _NOT_ENTERED;
}

modifier nonReentrant() {
if (_status == _ENTERED) revert ReentrantCall();
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
41 changes: 41 additions & 0 deletions rules/g003_abstract_visibility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! Closes #650: Rule G003 - flag public variables in abstract/internal contracts.
//! Starter line-scan implementation; full AST-based scoping is a follow-up.

pub struct PublicVarFinding {
pub variable_name: String,
}

/// Scans declaration lines inside an `abstract contract` body for `public` state vars.
pub fn detect_public_vars_in_abstract(declaration_lines: &[&str]) -> Vec<PublicVarFinding> {
let mut findings = Vec::new();
for line in declaration_lines {
let trimmed = line.trim();
if trimmed.contains("public") && trimmed.ends_with(';') && !trimmed.contains('(') {
if let Some(name) = trimmed.split_whitespace().last() {
findings.push(PublicVarFinding {
variable_name: name.trim_end_matches(';').to_string(),
});
}
}
}
findings
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn flags_public_state_variable() {
let lines = ["uint256 public totalSupply;"];
let findings = detect_public_vars_in_abstract(&lines);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].variable_name, "totalSupply");
}

#[test]
fn ignores_internal_variable() {
let lines = ["uint256 internal totalSupply;"];
assert!(detect_public_vars_in_abstract(&lines).is_empty());
}
}
41 changes: 41 additions & 0 deletions rules/g006_payable_admin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! Closes #651: Rule G006 - suggest `payable` on admin-only functions.
//! Starter regex-based implementation; full bytecode-check verification is a follow-up.

pub struct PayableAdminFinding {
pub function_name: String,
}

const ADMIN_MODIFIERS: [&str; 2] = ["onlyOwner", "onlyAdmin"];

/// Scans a function signature line for an admin modifier that lacks `payable`.
pub fn detect_missing_payable(function_signature: &str, function_name: &str) -> Option<PayableAdminFinding> {
let has_admin_modifier = ADMIN_MODIFIERS.iter().any(|m| function_signature.contains(m));
let has_payable = function_signature.contains("payable");
if has_admin_modifier && !has_payable {
return Some(PayableAdminFinding { function_name: function_name.to_string() });
}
None
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn flags_admin_function_without_payable() {
let sig = "function withdraw() external onlyOwner {";
assert!(detect_missing_payable(sig, "withdraw").is_some());
}

#[test]
fn ignores_already_payable_function() {
let sig = "function withdraw() external payable onlyOwner {";
assert!(detect_missing_payable(sig, "withdraw").is_none());
}

#[test]
fn ignores_non_admin_function() {
let sig = "function deposit() external {";
assert!(detect_missing_payable(sig, "deposit").is_none());
}
}
23 changes: 23 additions & 0 deletions scripts/compare-gas-snapshots.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Closes #649: compare forge gas snapshots against the baseline branch.
# Starter script; wiring this into a .github/workflows/gas-diff.yml job is a follow-up
# (requires a token with the `workflow` scope to push CI workflow file changes).
set -euo pipefail

BASELINE_REF="${1:-main}"
SNAPSHOT_FILE=".gas-snapshot"
BASELINE_FILE="$(mktemp)"

git show "${BASELINE_REF}:${SNAPSHOT_FILE}" > "${BASELINE_FILE}" 2>/dev/null || {
echo "No baseline snapshot found on ${BASELINE_REF}; skipping diff." >&2
exit 0
}

forge snapshot --check --diff "${BASELINE_FILE}" || true

echo "## Gas Snapshot Diff (vs ${BASELINE_REF})"
echo '```diff'
diff -u "${BASELINE_FILE}" "${SNAPSHOT_FILE}" || true
echo '```'

rm -f "${BASELINE_FILE}"
Loading