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
97 changes: 97 additions & 0 deletions rules/auditability/interfaces/detect-missing-contract-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
export interface MissingInterfaceViolation {
type: string;
line: number;
description: string;
recommendation: string;
}

export interface MissingContractInterfaceResult {
detected: boolean;
violations: MissingInterfaceViolation[];
message: string;
suggestion: string;
}

interface DirectInteractionPattern {
type: string;
pattern: RegExp;
description: string;
recommendation: string;
}

const INTERACTION_PATTERNS: DirectInteractionPattern[] = [
{
type: "direct-abi-encode",
pattern:
/abi\.encodeWithSignature|abi\.encodeWithSelector|abi\.encodeCall/g,
description:
"Direct ABI encoding bypasses type safety and interface validation.",
recommendation:
"Define and import an interface (e.g., IERC20) instead of using raw abi.encodeWithSignature calls.",
},
{
type: "raw-address-cast",
pattern: /(?:I[A-Z]\w*)?\(\s*address\s*\(/g,
description:
"Casting an address to a contract type without a proper interface may hide interface mismatches.",
recommendation:
"Use a well-defined interface and cast to the interface type, not raw address.",
},
{
type: "inline-interface",
pattern: /interface\s+\w+\s*\{[^}]*\bfunction\b[^}]*\}\s*$/gm,
description:
"Inline or local interface definitions may lead to duplication and maintenance issues.",
recommendation:
"Extract interfaces into shared modules for reuse and consistency across the codebase.",
},
];

function lineNumber(code: string, index: number): number {
let line = 1;
for (let i = 0; i < index; i++) {
if (code[i] === "\n") line++;
}
return line;
}

export function detectMissingContractInterface(
code: string,
): MissingContractInterfaceResult {
const violations: MissingInterfaceViolation[] = [];

for (const {
type,
pattern,
description,
recommendation,
} of INTERACTION_PATTERNS) {
const re = new RegExp(pattern.source, "g");
let match: RegExpExecArray | null;
while ((match = re.exec(code)) !== null) {
violations.push({
type,
line: lineNumber(code, match.index),
description,
recommendation,
});
}
}

if (violations.length === 0) {
return {
detected: false,
violations: [],
message: "No missing contract interface issues detected.",
suggestion: "",
};
}

return {
detected: true,
violations,
message: `Detected ${violations.length} instance(s) of missing or improper interface usage.`,
suggestion:
"Always use well-defined interfaces for external contract interactions to improve readability and safety.",
};
}
96 changes: 96 additions & 0 deletions rules/security/external-calls/detect-unsafe-low-level-calls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
export interface UnsafeLowLevelCall {
type: string;
line: number;
description: string;
recommendation: string;
}

export interface UnsafeLowLevelCallsResult {
detected: boolean;
calls: UnsafeLowLevelCall[];
message: string;
suggestion: string;
}

interface LowLevelCallPattern {
type: string;
pattern: RegExp;
description: string;
recommendation: string;
}

const LOW_LEVEL_PATTERNS: LowLevelCallPattern[] = [
{
type: "raw-call",
pattern: /\.call\s*\(/g,
description:
"Low-level .call() bypasses safety checks and may propagate errors silently.",
recommendation:
"Use higher-level abstractions (e.g. ISomeInterface) instead of raw .call(). Always check the return value.",
},
{
type: "unchecked-call-result",
pattern: /\.call\s*\{[^}]*\}\s*\([^;]*\);(?!\s*require)/g,
description:
"Raw .call() result is not checked. Failed calls will not revert the transaction.",
recommendation:
"Use require(success) after .call() or use the ReentrancyGuard pattern when interacting with external contracts.",
},
{
type: "delegatecall-usage",
pattern: /\.delegatecall\s*\(/g,
description:
"delegatecall() executes code in the caller context and may corrupt state.",
recommendation:
"Avoid delegatecall() where possible. When necessary, ensure the target contract is trusted and properly validated.",
},
];

function lineNumber(code: string, index: number): number {
let line = 1;
for (let i = 0; i < index; i++) {
if (code[i] === "\n") line++;
}
return line;
}

export function detectUnsafeLowLevelCalls(
code: string,
): UnsafeLowLevelCallsResult {
const calls: UnsafeLowLevelCall[] = [];

for (const {
type,
pattern,
description,
recommendation,
} of LOW_LEVEL_PATTERNS) {
const re = new RegExp(pattern.source, "g");
let match: RegExpExecArray | null;
while ((match = re.exec(code)) !== null) {
calls.push({
type,
line: lineNumber(code, match.index),
description,
recommendation,
});
}
}

if (calls.length === 0) {
return {
detected: false,
calls: [],
message: "No unsafe low-level calls detected.",
suggestion: "",
};
}

return {
detected: true,
calls,
message: `Detected ${calls.length} unsafe low-level call(s): ${[...new Set(calls.map((c) => c.type))].join(", ")}.`,
suggestion:
"Use Solidity higher-level interfaces or safe wrappers. Always validate return values from external calls.",
};
}
97 changes: 97 additions & 0 deletions rules/security/math/detect-unsafe-integer-downcasting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
export interface UnsafeDowncast {
type: string;
line: number;
description: string;
recommendation: string;
}

export interface UnsafeIntegerDowncastingResult {
detected: boolean;
downcasts: UnsafeDowncast[];
message: string;
suggestion: string;
}

interface DowncastPattern {
type: string;
pattern: RegExp;
description: string;
recommendation: string;
}

const DOWNCAST_PATTERNS: DowncastPattern[] = [
{
type: "explicit-narrowing-cast",
pattern: /\b(?:uint\d+\(|int\d+\()\s*[a-zA-Z_]\w*\s*\)/g,
description:
"Explicit integer downcast may silently truncate value if the source exceeds the target range.",
recommendation:
"Use OpenZeppelin SafeCast or add an explicit bounds check before downcasting.",
},
{
type: "solidity-downcast",
pattern:
/\buint(?:8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128)\s*\(/g,
description:
"Downcasting to smaller unsigned integer types may truncate high-order bits without warning.",
recommendation:
"Validate the value fits in the target type using SafeCast library or manual range checks.",
},
{
type: "int-downcast",
pattern: /\bint(?:8|16|24|32|40|48|56|64|72|80|88|96|104|112|120)\s*\(/g,
description:
"Downcasting to smaller signed integer types may silently truncate or produce unexpected negative values.",
recommendation:
"Use safe casting utilities and verify the value is within the valid range of the target type.",
},
];

function lineNumber(code: string, index: number): number {
let line = 1;
for (let i = 0; i < index; i++) {
if (code[i] === "\n") line++;
}
return line;
}

export function detectUnsafeIntegerDowncasting(
code: string,
): UnsafeIntegerDowncastingResult {
const downcasts: UnsafeDowncast[] = [];

for (const {
type,
pattern,
description,
recommendation,
} of DOWNCAST_PATTERNS) {
const re = new RegExp(pattern.source, "g");
let match: RegExpExecArray | null;
while ((match = re.exec(code)) !== null) {
downcasts.push({
type,
line: lineNumber(code, match.index),
description,
recommendation,
});
}
}

if (downcasts.length === 0) {
return {
detected: false,
downcasts: [],
message: "No unsafe integer downcasting detected.",
suggestion: "",
};
}

return {
detected: true,
downcasts,
message: `Detected ${downcasts.length} unsafe integer downcast(s).`,
suggestion:
"Use SafeCast from OpenZeppelin or add bounds checks before downcasting to prevent silent truncation.",
};
}
88 changes: 88 additions & 0 deletions rules/security/tokens/detect-improper-erc20-approval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
export interface ImproperApproval {
type: string;
line: number;
description: string;
recommendation: string;
}

export interface ImproperERC20ApprovalResult {
detected: boolean;
approvals: ImproperApproval[];
message: string;
suggestion: string;
}

interface ApprovalPattern {
type: string;
pattern: RegExp;
description: string;
recommendation: string;
}

const APPROVAL_PATTERNS: ApprovalPattern[] = [
{
type: "direct-approve-overwrite",
pattern: /\.approve\([^)]*\)/g,
description:
"Direct approve() call may overwrite existing allowance, enabling race conditions.",
recommendation:
"Use increaseAllowance() / decreaseAllowance() instead of approve() to prevent front-running attacks.",
},
{
type: "unchecked-approval-return",
pattern: /approve\([^)]*\);(?!\s*\{)/g,
description:
"approve() return value is not checked. The call may fail silently.",
recommendation:
"Check the boolean return value of approve() using require() or a safe wrapper like safeApprove().",
},
];

function lineNumber(code: string, index: number): number {
let line = 1;
for (let i = 0; i < index; i++) {
if (code[i] === "\n") line++;
}
return line;
}

export function detectImproperERC20Approval(
code: string,
): ImproperERC20ApprovalResult {
const approvals: ImproperApproval[] = [];

for (const {
type,
pattern,
description,
recommendation,
} of APPROVAL_PATTERNS) {
const re = new RegExp(pattern.source, "g");
let match: RegExpExecArray | null;
while ((match = re.exec(code)) !== null) {
approvals.push({
type,
line: lineNumber(code, match.index),
description,
recommendation,
});
}
}

if (approvals.length === 0) {
return {
detected: false,
approvals: [],
message: "No improper ERC20 approval patterns detected.",
suggestion: "",
};
}

return {
detected: true,
approvals,
message: `Detected ${approvals.length} improper ERC20 approval pattern(s).`,
suggestion:
"Replace direct approve() with increaseAllowance() / decreaseAllowance() and always check return values.",
};
}
Loading
Loading