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
147 changes: 147 additions & 0 deletions libs/engine/analyzers/solidity-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,26 @@ export class SolidityAnalyzer extends BaseAnalyzer implements Analyzer {
typical: 2000,
},
},
{
id: 'sol-013',
name: 'Unsafe Timestamp Dependency',
description: 'Detects use of block.timestamp or now in control flow and warns when critical logic depends on manipulable timestamps.',
severity: Severity.HIGH,
category: 'security',
enabled: true,
tags: ['security', 'timestamp', 'blockchain', 'time-dependency'],
documentationUrl: 'https://docs.gasguard.dev/rules/sol-013',
},
{
id: 'sol-014',
name: 'Insecure tx.origin Authentication',
description: 'Detects tx.origin usage in authentication or authorization checks and suggests using msg.sender instead.',
severity: Severity.HIGH,
category: 'security',
enabled: true,
tags: ['security', 'authentication', 'tx-origin', 'msg-sender'],
documentationUrl: 'https://docs.gasguard.dev/rules/sol-014',
},
{
id: 'sol-012',
name: 'Missing Event Emission',
Expand Down Expand Up @@ -316,6 +336,46 @@ export class SolidityAnalyzer extends BaseAnalyzer implements Analyzer {
},
})));
}

// Rule: sol-013 - Unsafe Timestamp Dependency
if (this.isRuleEnabled('sol-013', config)) {
const timestampDependencies = this.detectTimestampDependencies(code);
findings.push(...timestampDependencies.map(location => ({
ruleId: 'sol-013',
message: location.message,
severity: this.getRuleSeverity('sol-013', config),
location: {
file: filePath,
startLine: location.startLine,
endLine: location.endLine,
},
suggestedFix: {
description: 'Avoid using block.timestamp or now for critical control flow. Use a secure timelock, on-chain delay based on block.number, or an oracle-backed time source.',
codeSnippet: 'require(block.timestamp >= unlockTime, "Too early"); // avoid relying on timestamp for security-critical decisions',
documentationUrl: 'https://docs.gasguard.dev/rules/sol-013',
},
})));
}

// Rule: sol-014 - Insecure tx.origin Authentication
if (this.isRuleEnabled('sol-014', config)) {
const txOriginFindings = this.detectTxOriginUsage(code);
findings.push(...txOriginFindings.map(location => ({
ruleId: 'sol-014',
message: location.message,
severity: this.getRuleSeverity('sol-014', config),
location: {
file: filePath,
startLine: location.startLine,
endLine: location.endLine,
},
suggestedFix: {
description: 'Use msg.sender for authentication and authorization checks instead of tx.origin to prevent phishing and contract-based attacks.',
codeSnippet: 'require(msg.sender == owner, "Unauthorized");',
documentationUrl: 'https://docs.gasguard.dev/rules/sol-014',
},
})));
}

// Rule: sol-008 - Unsafe External Calls
if (this.isRuleEnabled('sol-008', config)) {
Expand Down Expand Up @@ -609,6 +669,93 @@ export class SolidityAnalyzer extends BaseAnalyzer implements Analyzer {
return findings;
}

private detectTimestampDependencies(
code: string,
): Array<{ startLine: number; endLine: number; message: string }> {
const findings: Array<{ startLine: number; endLine: number; message: string }> = [];
const lines = code.split('\n');
let inBlockComment = false;

const timestampPattern = /\b(?:block\.timestamp|now)\b/;
const eventEmissionPattern = /\bemit\b/;

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();

if (trimmed.startsWith('/*') || trimmed.startsWith('/**')) {
inBlockComment = true;
}
if (inBlockComment) {
if (trimmed.includes('*/')) {
inBlockComment = false;
}
continue;
}

if (trimmed.startsWith('//')) {
continue;
}

if (!timestampPattern.test(line)) {
continue;
}

if (eventEmissionPattern.test(line)) {
continue;
}

findings.push({
startLine: i + 1,
endLine: i + 1,
message:
'Unsafe reliance on block.timestamp/now detected. Block timestamps are manipulable by miners, so avoid using them for critical control flow.',
});
}

return findings;
}

private detectTxOriginUsage(
code: string,
): Array<{ startLine: number; endLine: number; message: string }> {
const findings: Array<{ startLine: number; endLine: number; message: string }> = [];
const lines = code.split('\n');
let inBlockComment = false;

const txOriginPattern = /\btx\.origin\b/;

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();

if (trimmed.startsWith('/*') || trimmed.startsWith('/**')) {
inBlockComment = true;
}
if (inBlockComment) {
if (trimmed.includes('*/')) {
inBlockComment = false;
}
continue;
}

if (trimmed.startsWith('//')) {
continue;
}

if (txOriginPattern.test(line)) {
findings.push({
startLine: i + 1,
endLine: i + 1,
message:
'Insecure tx.origin authentication detected. Use msg.sender instead of tx.origin for authorization checks.',
});
}
}

return findings;
}

private detectInsecureFallbackFunctions(code: string): Array<{ startLine: number; endLine: number }> {
const findings: Array<{ startLine: number; endLine: number }> = [];
const lines = code.split('\n');
Expand Down
92 changes: 92 additions & 0 deletions tests/rules/solidity-rules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,98 @@ contract SecureBank is ReentrancyGuard {
});
});

describe('sol-013: Unsafe Timestamp Dependency', () => {
it('should detect unsafe reliance on block.timestamp and now in critical logic', async () => {
const code = `
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract TimeSensitive {
uint256 public deadline;

function bid() external {
require(block.timestamp <= deadline, "Auction expired");
// bidding logic
}

function recordStart() external {
uint256 startTime = now;
// start logic
}
}
`;

const result = await analyzer.analyze(code, 'time-sensitive.sol');
RuleAssertions.assertHasFinding(result.findings, 'sol-013');
RuleAssertions.assertFindingSeverity(result.findings, 'sol-013', 'high');
});

it('should not flag block.timestamp when used only for event emission', async () => {
const code = `
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract LogTimestamp {
event Timestamped(uint256 timestamp);

function log() external {
emit Timestamped(block.timestamp);
}
}
`;

const result = await analyzer.analyze(code, 'timestamp-log.sol');
RuleAssertions.assertNotHasFinding(result.findings, 'sol-013');
});

it('should detect insecure tx.origin authentication usage', async () => {
const code = `
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract OriginAuth {
address public owner;

constructor() {
owner = msg.sender;
}

function adminAction() external {
require(tx.origin == owner, "Not authorized");
// sensitive logic
}
}
`;

const result = await analyzer.analyze(code, 'origin-auth.sol');
RuleAssertions.assertHasFinding(result.findings, 'sol-014');
RuleAssertions.assertFindingMessage(result.findings, 'sol-014', 'tx.origin');
});

it('should not flag msg.sender authentication', async () => {
const code = `
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SenderAuth {
address public owner;

constructor() {
owner = msg.sender;
}

function adminAction() external {
require(msg.sender == owner, "Not authorized");
// sensitive logic
}
}
`;

const result = await analyzer.analyze(code, 'sender-auth.sol');
RuleAssertions.assertNotHasFinding(result.findings, 'sol-014');
});
});

describe('Rule Assertions', () => {
it('should provide helpful assertion messages', async () => {
const code = `
Expand Down
Loading