chainlink-audit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/config.js +77 -0
- package/dist/index.js +82 -0
- package/dist/reporters/html.js +372 -0
- package/dist/reporters/json.js +3 -0
- package/dist/reporters/markdown.js +27 -0
- package/dist/reporters/text.js +27 -0
- package/dist/rules/automation.js +102 -0
- package/dist/rules/ccip.js +163 -0
- package/dist/rules/data-feeds.js +127 -0
- package/dist/rules/functions-cre.js +93 -0
- package/dist/rules/helpers.js +43 -0
- package/dist/rules/index.js +12 -0
- package/dist/rules/vrf.js +99 -0
- package/dist/scanner.js +133 -0
- package/dist/types.js +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { extractFunctionBody, firstLineMatching, makeFinding } from "./helpers.js";
|
|
2
|
+
function hasAutomation(content) {
|
|
3
|
+
return /(AutomationCompatibleInterface|KeeperCompatibleInterface|function\s+checkUpkeep\b|function\s+performUpkeep\b)/.test(content);
|
|
4
|
+
}
|
|
5
|
+
export const automationRules = [
|
|
6
|
+
{
|
|
7
|
+
metadata: {
|
|
8
|
+
ruleId: "CL-AUTO-001",
|
|
9
|
+
product: "automation",
|
|
10
|
+
severity: "high",
|
|
11
|
+
title: "Potential performUpkeep without condition revalidation",
|
|
12
|
+
description: "performUpkeep appears not to revalidate checkUpkeep conditions.",
|
|
13
|
+
},
|
|
14
|
+
scan(context) {
|
|
15
|
+
if (!hasAutomation(context.content))
|
|
16
|
+
return [];
|
|
17
|
+
const body = extractFunctionBody(context.content, "performUpkeep");
|
|
18
|
+
if (!body)
|
|
19
|
+
return [];
|
|
20
|
+
const revalidates = /(require|revert|UpkeepNotNeeded|checkUpkeep)/.test(body) &&
|
|
21
|
+
/(block\.timestamp|lastExecutedAt|interval|upkeepNeeded|balance|paused)/.test(body);
|
|
22
|
+
if (revalidates)
|
|
23
|
+
return [];
|
|
24
|
+
return [
|
|
25
|
+
makeFinding({
|
|
26
|
+
context,
|
|
27
|
+
ruleId: this.metadata.ruleId,
|
|
28
|
+
severity: this.metadata.severity,
|
|
29
|
+
confidence: "medium",
|
|
30
|
+
line: firstLineMatching(context.lines, /performUpkeep/),
|
|
31
|
+
title: this.metadata.title,
|
|
32
|
+
description: "Potential issue: anyone can call performUpkeep, so it must independently validate that upkeep is still needed.",
|
|
33
|
+
risk: "Attackers or stale Automation calls may trigger premature or invalid state transitions.",
|
|
34
|
+
recommendation: "Revalidate all critical checkUpkeep conditions inside performUpkeep before changing state.",
|
|
35
|
+
}),
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
metadata: {
|
|
41
|
+
ruleId: "CL-AUTO-002",
|
|
42
|
+
product: "automation",
|
|
43
|
+
severity: "medium",
|
|
44
|
+
title: "Potential stale performData trust",
|
|
45
|
+
description: "performUpkeep decodes performData without an obvious freshness or consistency check.",
|
|
46
|
+
},
|
|
47
|
+
scan(context) {
|
|
48
|
+
if (!hasAutomation(context.content))
|
|
49
|
+
return [];
|
|
50
|
+
const body = extractFunctionBody(context.content, "performUpkeep");
|
|
51
|
+
if (!/abi\.decode\s*\(\s*performData/.test(body))
|
|
52
|
+
return [];
|
|
53
|
+
const validatesPerformData = /(StalePerformData|scheduledAt\s*==|nonce|epoch|deadline|lastExecutedAt|require|revert)/.test(body);
|
|
54
|
+
if (validatesPerformData)
|
|
55
|
+
return [];
|
|
56
|
+
return [
|
|
57
|
+
makeFinding({
|
|
58
|
+
context,
|
|
59
|
+
ruleId: this.metadata.ruleId,
|
|
60
|
+
severity: this.metadata.severity,
|
|
61
|
+
confidence: "low",
|
|
62
|
+
line: firstLineMatching(context.lines, /abi\.decode\s*\(\s*performData/),
|
|
63
|
+
title: this.metadata.title,
|
|
64
|
+
description: "Potential issue: performData generated earlier may be replayed after state conditions change.",
|
|
65
|
+
risk: "Stale performData can execute the wrong task, skip newer state, or trigger duplicate work.",
|
|
66
|
+
recommendation: "Bind performData to current state with a nonce, scheduled timestamp, epoch, or equivalent consistency check.",
|
|
67
|
+
}),
|
|
68
|
+
];
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
metadata: {
|
|
73
|
+
ruleId: "CL-AUTO-003",
|
|
74
|
+
product: "automation",
|
|
75
|
+
severity: "low",
|
|
76
|
+
title: "Potential missing pause or emergency control for upkeep",
|
|
77
|
+
description: "Automation-compatible logic has no obvious pause/emergency path.",
|
|
78
|
+
},
|
|
79
|
+
scan(context) {
|
|
80
|
+
if (!hasAutomation(context.content))
|
|
81
|
+
return [];
|
|
82
|
+
if (!/function\s+performUpkeep\b/.test(context.content))
|
|
83
|
+
return [];
|
|
84
|
+
const hasPause = /(Pausable|whenNotPaused|paused|pause\(|emergency|guardian)/i.test(context.content);
|
|
85
|
+
if (hasPause)
|
|
86
|
+
return [];
|
|
87
|
+
return [
|
|
88
|
+
makeFinding({
|
|
89
|
+
context,
|
|
90
|
+
ruleId: this.metadata.ruleId,
|
|
91
|
+
severity: this.metadata.severity,
|
|
92
|
+
confidence: "low",
|
|
93
|
+
line: firstLineMatching(context.lines, /performUpkeep|checkUpkeep/),
|
|
94
|
+
title: this.metadata.title,
|
|
95
|
+
description: "Potential issue: critical upkeep logic may not have an emergency stop mechanism.",
|
|
96
|
+
risk: "A faulty upkeep or bad offchain configuration may continue changing state until code/configuration is upgraded.",
|
|
97
|
+
recommendation: "Consider pause, guardian, or emergency controls for high-impact upkeep logic and document forwarder assumptions.",
|
|
98
|
+
}),
|
|
99
|
+
];
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { countMatches, extractFunctionBody, firstLineMatching, makeFinding } from "./helpers.js";
|
|
2
|
+
function ccipBody(content) {
|
|
3
|
+
return extractFunctionBody(content, "_ccipReceive") || extractFunctionBody(content, "ccipReceive");
|
|
4
|
+
}
|
|
5
|
+
function hasCcipReceiver(content) {
|
|
6
|
+
return /(function\s+_ccipReceive\b|function\s+ccipReceive\b|is\s+CCIPReceiver)/.test(content);
|
|
7
|
+
}
|
|
8
|
+
export const ccipRules = [
|
|
9
|
+
{
|
|
10
|
+
metadata: {
|
|
11
|
+
ruleId: "CL-CCIP-001",
|
|
12
|
+
product: "ccip",
|
|
13
|
+
severity: "high",
|
|
14
|
+
title: "Potential CCIP receive without source chain validation",
|
|
15
|
+
description: "_ccipReceive appears to lack sourceChainSelector validation.",
|
|
16
|
+
},
|
|
17
|
+
scan(context) {
|
|
18
|
+
if (!hasCcipReceiver(context.content))
|
|
19
|
+
return [];
|
|
20
|
+
const body = ccipBody(context.content) || context.content;
|
|
21
|
+
const validates = /sourceChainSelector/.test(body) && /(require|revert|allowed|trusted|allowlist)/i.test(body);
|
|
22
|
+
if (validates)
|
|
23
|
+
return [];
|
|
24
|
+
return [
|
|
25
|
+
makeFinding({
|
|
26
|
+
context,
|
|
27
|
+
ruleId: this.metadata.ruleId,
|
|
28
|
+
severity: this.metadata.severity,
|
|
29
|
+
confidence: "medium",
|
|
30
|
+
line: firstLineMatching(context.lines, /(_ccipReceive|ccipReceive)/),
|
|
31
|
+
title: this.metadata.title,
|
|
32
|
+
description: "Potential issue: messages from unexpected source chains may be accepted.",
|
|
33
|
+
risk: "Cross-chain spoofing or misrouted messages can trigger unauthorized state changes.",
|
|
34
|
+
recommendation: "Validate message.sourceChainSelector against an explicit allowlist before decoding payloads or mutating state.",
|
|
35
|
+
}),
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
metadata: {
|
|
41
|
+
ruleId: "CL-CCIP-002",
|
|
42
|
+
product: "ccip",
|
|
43
|
+
severity: "high",
|
|
44
|
+
title: "Potential CCIP receive without source sender validation",
|
|
45
|
+
description: "_ccipReceive appears to lack trusted sender validation.",
|
|
46
|
+
},
|
|
47
|
+
scan(context) {
|
|
48
|
+
if (!hasCcipReceiver(context.content))
|
|
49
|
+
return [];
|
|
50
|
+
const body = ccipBody(context.content) || context.content;
|
|
51
|
+
const validates = /(message\.sender|sender)/.test(body) && /(require|revert|allowed|trusted|allowlist)/i.test(body);
|
|
52
|
+
if (validates)
|
|
53
|
+
return [];
|
|
54
|
+
return [
|
|
55
|
+
makeFinding({
|
|
56
|
+
context,
|
|
57
|
+
ruleId: this.metadata.ruleId,
|
|
58
|
+
severity: this.metadata.severity,
|
|
59
|
+
confidence: "medium",
|
|
60
|
+
line: firstLineMatching(context.lines, /(_ccipReceive|ccipReceive)/),
|
|
61
|
+
title: this.metadata.title,
|
|
62
|
+
description: "Potential issue: messages from untrusted source contracts may be accepted.",
|
|
63
|
+
risk: "An attacker may spoof business instructions if sender validation is missing or incomplete.",
|
|
64
|
+
recommendation: "Decode message.sender and validate it against the trusted sender for the specific source chain.",
|
|
65
|
+
}),
|
|
66
|
+
];
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
metadata: {
|
|
71
|
+
ruleId: "CL-CCIP-003",
|
|
72
|
+
product: "ccip",
|
|
73
|
+
severity: "high",
|
|
74
|
+
title: "Potential missing CCIP router validation",
|
|
75
|
+
description: "Receiver entrypoint does not appear to validate msg.sender/router.",
|
|
76
|
+
},
|
|
77
|
+
scan(context) {
|
|
78
|
+
if (!hasCcipReceiver(context.content))
|
|
79
|
+
return [];
|
|
80
|
+
const validatesRouter = /msg\.sender\s*(!=|==)\s*(router|s_router|i_router|address\(router\))|InvalidRouter|onlyRouter/i.test(context.content);
|
|
81
|
+
if (validatesRouter || /is\s+CCIPReceiver/.test(context.content))
|
|
82
|
+
return [];
|
|
83
|
+
return [
|
|
84
|
+
makeFinding({
|
|
85
|
+
context,
|
|
86
|
+
ruleId: this.metadata.ruleId,
|
|
87
|
+
severity: this.metadata.severity,
|
|
88
|
+
confidence: "medium",
|
|
89
|
+
line: firstLineMatching(context.lines, /(ccipReceive|_ccipReceive)/),
|
|
90
|
+
title: this.metadata.title,
|
|
91
|
+
description: "Potential issue: direct calls to the receiver may bypass CCIP router trust assumptions.",
|
|
92
|
+
risk: "If the entrypoint is public/external and router validation is absent, fabricated messages may be processed.",
|
|
93
|
+
recommendation: "Ensure only the expected CCIP router can invoke message handling, either through CCIPReceiver or explicit msg.sender checks.",
|
|
94
|
+
}),
|
|
95
|
+
];
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
metadata: {
|
|
100
|
+
ruleId: "CL-CCIP-004",
|
|
101
|
+
product: "ccip",
|
|
102
|
+
severity: "medium",
|
|
103
|
+
title: "Potential unsafe CCIP payload decoding",
|
|
104
|
+
description: "Receiver decodes message data without obvious defensive checks.",
|
|
105
|
+
},
|
|
106
|
+
scan(context) {
|
|
107
|
+
if (!hasCcipReceiver(context.content))
|
|
108
|
+
return [];
|
|
109
|
+
const body = ccipBody(context.content) || context.content;
|
|
110
|
+
if (!/abi\.decode\s*\(\s*message\.data/.test(body))
|
|
111
|
+
return [];
|
|
112
|
+
const defensiveChecks = /(message\.data\.length|try\s+this|catch|InvalidPayload|Payload|version|schema)/i.test(body);
|
|
113
|
+
if (defensiveChecks)
|
|
114
|
+
return [];
|
|
115
|
+
return [
|
|
116
|
+
makeFinding({
|
|
117
|
+
context,
|
|
118
|
+
ruleId: this.metadata.ruleId,
|
|
119
|
+
severity: this.metadata.severity,
|
|
120
|
+
confidence: "low",
|
|
121
|
+
line: firstLineMatching(context.lines, /abi\.decode\s*\(\s*message\.data/),
|
|
122
|
+
title: this.metadata.title,
|
|
123
|
+
description: "Potential issue: malformed payloads may revert or be decoded under the wrong schema.",
|
|
124
|
+
risk: "Unexpected decoding failures can block message processing or route invalid business instructions.",
|
|
125
|
+
recommendation: "Validate payload schema/version/length or isolate decoding failures so they can be handled manually.",
|
|
126
|
+
}),
|
|
127
|
+
];
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
metadata: {
|
|
132
|
+
ruleId: "CL-CCIP-005",
|
|
133
|
+
product: "ccip",
|
|
134
|
+
severity: "medium",
|
|
135
|
+
title: "Potential tightly coupled CCIP receiver logic",
|
|
136
|
+
description: "Receiver appears to execute business logic directly without an obvious graceful failure path.",
|
|
137
|
+
},
|
|
138
|
+
scan(context) {
|
|
139
|
+
if (!hasCcipReceiver(context.content))
|
|
140
|
+
return [];
|
|
141
|
+
const body = ccipBody(context.content);
|
|
142
|
+
if (!body)
|
|
143
|
+
return [];
|
|
144
|
+
const businessSignals = countMatches(body, [/transfer/i, /mint/i, /burn/i, /swap/i, /deposit/i, /withdraw/i, /credit/i, /call\s*\{/]);
|
|
145
|
+
const gracefulFailure = /(try|catch|failedMessages|manual|retry|recover|queue)/i.test(context.content);
|
|
146
|
+
if (businessSignals < 1 || gracefulFailure)
|
|
147
|
+
return [];
|
|
148
|
+
return [
|
|
149
|
+
makeFinding({
|
|
150
|
+
context,
|
|
151
|
+
ruleId: this.metadata.ruleId,
|
|
152
|
+
severity: this.metadata.severity,
|
|
153
|
+
confidence: "low",
|
|
154
|
+
line: firstLineMatching(context.lines, /(_ccipReceive|ccipReceive)/),
|
|
155
|
+
title: this.metadata.title,
|
|
156
|
+
description: "Potential issue: receiver business logic appears coupled to CCIP execution without a visible retry/manual path.",
|
|
157
|
+
risk: "A revert in message handling may require manual execution or leave cross-chain state inconsistent.",
|
|
158
|
+
recommendation: "Separate validation/decoding from business logic and add a tested graceful failure or manual recovery path where needed.",
|
|
159
|
+
}),
|
|
160
|
+
];
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { firstLineMatching, hasAny, makeFinding } from "./helpers.js";
|
|
2
|
+
export const dataFeedRules = [
|
|
3
|
+
{
|
|
4
|
+
metadata: {
|
|
5
|
+
ruleId: "CL-DF-001",
|
|
6
|
+
product: "data-feeds",
|
|
7
|
+
severity: "medium",
|
|
8
|
+
title: "Potential Data Feed read without freshness validation",
|
|
9
|
+
description: "latestRoundData() appears to be used without an obvious stale price check.",
|
|
10
|
+
},
|
|
11
|
+
scan(context) {
|
|
12
|
+
if (!/\.latestRoundData\s*\(/.test(context.content))
|
|
13
|
+
return [];
|
|
14
|
+
const hasFreshnessCheck = /updatedAt/.test(context.content) &&
|
|
15
|
+
/(block\.timestamp|maxStaleness|stale|heartbeat|updatedAt\s*!=\s*0)/i.test(context.content);
|
|
16
|
+
if (hasFreshnessCheck)
|
|
17
|
+
return [];
|
|
18
|
+
return [
|
|
19
|
+
makeFinding({
|
|
20
|
+
context,
|
|
21
|
+
ruleId: this.metadata.ruleId,
|
|
22
|
+
severity: this.metadata.severity,
|
|
23
|
+
confidence: "medium",
|
|
24
|
+
line: firstLineMatching(context.lines, /\.latestRoundData\s*\(/),
|
|
25
|
+
title: this.metadata.title,
|
|
26
|
+
description: "Potential issue: the feed answer may be accepted even when updatedAt is zero or older than the intended heartbeat.",
|
|
27
|
+
risk: "Stale oracle data can lead to incorrect pricing, unsafe liquidations, incorrect collateral valuation, or stale settlement.",
|
|
28
|
+
recommendation: "Validate updatedAt != 0 and enforce a feed-specific max staleness threshold before using the answer.",
|
|
29
|
+
}),
|
|
30
|
+
];
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
metadata: {
|
|
35
|
+
ruleId: "CL-DF-002",
|
|
36
|
+
product: "data-feeds",
|
|
37
|
+
severity: "high",
|
|
38
|
+
title: "Potential missing positive answer check",
|
|
39
|
+
description: "latestRoundData() appears to be used without requiring answer > 0.",
|
|
40
|
+
},
|
|
41
|
+
scan(context) {
|
|
42
|
+
if (!/\.latestRoundData\s*\(/.test(context.content))
|
|
43
|
+
return [];
|
|
44
|
+
const hasPositiveAnswerCheck = hasAny(context.content, [
|
|
45
|
+
/answer\s*>\s*0/,
|
|
46
|
+
/price\s*>\s*0/,
|
|
47
|
+
/oracleAnswer\s*>\s*0/,
|
|
48
|
+
/InvalidOracleAnswer/,
|
|
49
|
+
]);
|
|
50
|
+
if (hasPositiveAnswerCheck)
|
|
51
|
+
return [];
|
|
52
|
+
return [
|
|
53
|
+
makeFinding({
|
|
54
|
+
context,
|
|
55
|
+
ruleId: this.metadata.ruleId,
|
|
56
|
+
severity: this.metadata.severity,
|
|
57
|
+
confidence: "medium",
|
|
58
|
+
line: firstLineMatching(context.lines, /\.latestRoundData\s*\(/),
|
|
59
|
+
title: this.metadata.title,
|
|
60
|
+
description: "Potential issue: a zero or negative feed answer may be cast to uint256 or used as a valid price.",
|
|
61
|
+
risk: "Non-positive oracle answers can break accounting, cause reverts, or create extreme normalized prices after casting.",
|
|
62
|
+
recommendation: "Require answer > 0 before casting or using the price.",
|
|
63
|
+
}),
|
|
64
|
+
];
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
metadata: {
|
|
69
|
+
ruleId: "CL-DF-003",
|
|
70
|
+
product: "data-feeds",
|
|
71
|
+
severity: "low",
|
|
72
|
+
title: "Possible hardcoded Data Feed decimals",
|
|
73
|
+
description: "Code appears to assume 8 feed decimals or scale by a hardcoded 1e10 factor.",
|
|
74
|
+
},
|
|
75
|
+
scan(context) {
|
|
76
|
+
if (!/(AggregatorV3Interface|latestRoundData|\.decimals\s*\()/i.test(context.content))
|
|
77
|
+
return [];
|
|
78
|
+
const hardcodedDecimals = /(1e8|10\s*\*\*\s*8|100000000|\*\s*1e10|1e10)/;
|
|
79
|
+
if (!hardcodedDecimals.test(context.content))
|
|
80
|
+
return [];
|
|
81
|
+
return [
|
|
82
|
+
makeFinding({
|
|
83
|
+
context,
|
|
84
|
+
ruleId: this.metadata.ruleId,
|
|
85
|
+
severity: this.metadata.severity,
|
|
86
|
+
confidence: "low",
|
|
87
|
+
line: firstLineMatching(context.lines, hardcodedDecimals),
|
|
88
|
+
title: this.metadata.title,
|
|
89
|
+
description: "Potential issue: not all Chainlink feeds use the same decimals, and migrations can invalidate implicit assumptions.",
|
|
90
|
+
risk: "Incorrect normalization can misprice assets by orders of magnitude.",
|
|
91
|
+
recommendation: "Read feed.decimals() or document why the specific feed decimals are safe for this deployment.",
|
|
92
|
+
}),
|
|
93
|
+
];
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
metadata: {
|
|
98
|
+
ruleId: "CL-DF-004",
|
|
99
|
+
product: "data-feeds",
|
|
100
|
+
severity: "medium",
|
|
101
|
+
title: "Potential missing L2 sequencer uptime check",
|
|
102
|
+
description: "Repository appears to target L2 and use Data Feeds without an obvious Sequencer Uptime Feed check.",
|
|
103
|
+
},
|
|
104
|
+
scan(context) {
|
|
105
|
+
if (!context.repoSignals.targetsL2)
|
|
106
|
+
return [];
|
|
107
|
+
if (!/\.latestRoundData\s*\(/.test(context.content))
|
|
108
|
+
return [];
|
|
109
|
+
const hasSequencerCheck = /(SequencerUptime|sequencer|gracePeriod|L2Sequencer)/i.test(context.content);
|
|
110
|
+
if (hasSequencerCheck)
|
|
111
|
+
return [];
|
|
112
|
+
return [
|
|
113
|
+
makeFinding({
|
|
114
|
+
context,
|
|
115
|
+
ruleId: this.metadata.ruleId,
|
|
116
|
+
severity: this.metadata.severity,
|
|
117
|
+
confidence: "low",
|
|
118
|
+
line: firstLineMatching(context.lines, /\.latestRoundData\s*\(/),
|
|
119
|
+
title: this.metadata.title,
|
|
120
|
+
description: "Potential issue: L2 sequencer downtime can make recent-looking price data unsafe immediately after recovery.",
|
|
121
|
+
risk: "Protocols may execute using stale or unfair prices around sequencer outages.",
|
|
122
|
+
recommendation: "For L2 deployments, check the Chainlink Sequencer Uptime Feed and enforce a post-recovery grace period.",
|
|
123
|
+
}),
|
|
124
|
+
];
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { firstLineMatching, hasAny, makeFinding } from "./helpers.js";
|
|
2
|
+
function hasFunctions(content) {
|
|
3
|
+
return /(FunctionsClient|FunctionsRequest|sendRequest|fulfillRequest|DONHostedSecrets|sourceCode|secrets)/i.test(content);
|
|
4
|
+
}
|
|
5
|
+
export const functionsCreRules = [
|
|
6
|
+
{
|
|
7
|
+
metadata: {
|
|
8
|
+
ruleId: "CL-FN-001",
|
|
9
|
+
product: "functions-cre",
|
|
10
|
+
severity: "info",
|
|
11
|
+
title: "Chainlink Functions usage detected",
|
|
12
|
+
description: "Functions integration detected; migration and CRE considerations should be documented.",
|
|
13
|
+
},
|
|
14
|
+
scan(context) {
|
|
15
|
+
if (!hasFunctions(context.content))
|
|
16
|
+
return [];
|
|
17
|
+
if (/\bCRE\b|migration|sunset/i.test(context.content))
|
|
18
|
+
return [];
|
|
19
|
+
return [
|
|
20
|
+
makeFinding({
|
|
21
|
+
context,
|
|
22
|
+
ruleId: this.metadata.ruleId,
|
|
23
|
+
severity: this.metadata.severity,
|
|
24
|
+
confidence: "medium",
|
|
25
|
+
line: firstLineMatching(context.lines, /(FunctionsClient|FunctionsRequest|sendRequest)/i),
|
|
26
|
+
title: this.metadata.title,
|
|
27
|
+
description: "Potential issue: legacy Functions assumptions may be undocumented.",
|
|
28
|
+
risk: "Operational migration, DON behavior, billing, or maintenance assumptions may be missed during audit or deployment.",
|
|
29
|
+
recommendation: "Add an explicit Functions/CRE migration note and document current Chainlink guidance for this integration.",
|
|
30
|
+
}),
|
|
31
|
+
];
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
metadata: {
|
|
36
|
+
ruleId: "CL-FN-002",
|
|
37
|
+
product: "functions-cre",
|
|
38
|
+
severity: "medium",
|
|
39
|
+
title: "Potential unsafe secrets or external API assumptions",
|
|
40
|
+
description: "Functions-style code appears to reference secrets or API calls without clear handling assumptions.",
|
|
41
|
+
},
|
|
42
|
+
scan(context) {
|
|
43
|
+
if (!hasFunctions(context.content))
|
|
44
|
+
return [];
|
|
45
|
+
const secretsOrApi = /(apiKey|secret|Authorization|fetch\s*\(|axios|http)/i.test(context.content);
|
|
46
|
+
const documented = /(DONHostedSecrets|encryptedSecrets|rateLimit|quota|throttle|secretVersion)/i.test(context.content);
|
|
47
|
+
if (!secretsOrApi || documented)
|
|
48
|
+
return [];
|
|
49
|
+
return [
|
|
50
|
+
makeFinding({
|
|
51
|
+
context,
|
|
52
|
+
ruleId: this.metadata.ruleId,
|
|
53
|
+
severity: this.metadata.severity,
|
|
54
|
+
confidence: "low",
|
|
55
|
+
line: firstLineMatching(context.lines, /(apiKey|secret|Authorization|fetch\s*\(|axios|http)/i),
|
|
56
|
+
title: this.metadata.title,
|
|
57
|
+
description: "Potential issue: secrets or external API limits may not be handled safely.",
|
|
58
|
+
risk: "Leaked credentials, throttled APIs, or nondeterministic external responses can break request fulfillment.",
|
|
59
|
+
recommendation: "Use supported encrypted/DON-hosted secrets and document API quotas, throttling, and failure behavior.",
|
|
60
|
+
}),
|
|
61
|
+
];
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
metadata: {
|
|
66
|
+
ruleId: "CL-FN-003",
|
|
67
|
+
product: "functions-cre",
|
|
68
|
+
severity: "low",
|
|
69
|
+
title: "Potential missing Functions timeout/error handling assumptions",
|
|
70
|
+
description: "Functions request/fulfillment flow lacks obvious timeout or error handling documentation.",
|
|
71
|
+
},
|
|
72
|
+
scan(context) {
|
|
73
|
+
if (!hasFunctions(context.content))
|
|
74
|
+
return [];
|
|
75
|
+
const handlesErrors = hasAny(context.content, [/err\b/i, /error/i, /timeout/i, /failed/i, /retry/i]);
|
|
76
|
+
if (handlesErrors)
|
|
77
|
+
return [];
|
|
78
|
+
return [
|
|
79
|
+
makeFinding({
|
|
80
|
+
context,
|
|
81
|
+
ruleId: this.metadata.ruleId,
|
|
82
|
+
severity: this.metadata.severity,
|
|
83
|
+
confidence: "low",
|
|
84
|
+
line: firstLineMatching(context.lines, /(sendRequest|fulfillRequest|Functions)/i),
|
|
85
|
+
title: this.metadata.title,
|
|
86
|
+
description: "Potential issue: request failure, timeout, or malformed response handling is not obvious.",
|
|
87
|
+
risk: "Protocol state may remain unresolved or accept incomplete offchain results.",
|
|
88
|
+
recommendation: "Document and test timeout, error, malformed response, and retry assumptions.",
|
|
89
|
+
}),
|
|
90
|
+
];
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function firstLineMatching(lines, pattern) {
|
|
2
|
+
const index = lines.findIndex((line) => pattern.test(line));
|
|
3
|
+
return index === -1 ? 1 : index + 1;
|
|
4
|
+
}
|
|
5
|
+
export function hasAny(content, patterns) {
|
|
6
|
+
return patterns.some((pattern) => pattern.test(content));
|
|
7
|
+
}
|
|
8
|
+
export function extractFunctionBody(content, functionName) {
|
|
9
|
+
const match = new RegExp(`function\\s+${functionName}\\b`).exec(content);
|
|
10
|
+
if (!match)
|
|
11
|
+
return "";
|
|
12
|
+
const open = content.indexOf("{", match.index);
|
|
13
|
+
if (open === -1)
|
|
14
|
+
return "";
|
|
15
|
+
let depth = 0;
|
|
16
|
+
for (let index = open; index < content.length; index++) {
|
|
17
|
+
const char = content[index];
|
|
18
|
+
if (char === "{")
|
|
19
|
+
depth++;
|
|
20
|
+
if (char === "}")
|
|
21
|
+
depth--;
|
|
22
|
+
if (depth === 0)
|
|
23
|
+
return content.slice(open + 1, index);
|
|
24
|
+
}
|
|
25
|
+
return content.slice(open + 1);
|
|
26
|
+
}
|
|
27
|
+
export function countMatches(content, patterns) {
|
|
28
|
+
return patterns.reduce((count, pattern) => count + (pattern.test(content) ? 1 : 0), 0);
|
|
29
|
+
}
|
|
30
|
+
export function makeFinding(input) {
|
|
31
|
+
return {
|
|
32
|
+
ruleId: input.ruleId,
|
|
33
|
+
severity: input.severity,
|
|
34
|
+
confidence: input.confidence,
|
|
35
|
+
file: input.context.file,
|
|
36
|
+
line: input.line,
|
|
37
|
+
title: input.title,
|
|
38
|
+
description: input.description,
|
|
39
|
+
risk: input.risk,
|
|
40
|
+
recommendation: input.recommendation,
|
|
41
|
+
manualReviewRequired: input.manualReviewRequired ?? true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { automationRules } from "./automation.js";
|
|
2
|
+
import { ccipRules } from "./ccip.js";
|
|
3
|
+
import { dataFeedRules } from "./data-feeds.js";
|
|
4
|
+
import { functionsCreRules } from "./functions-cre.js";
|
|
5
|
+
import { vrfRules } from "./vrf.js";
|
|
6
|
+
export const rules = [
|
|
7
|
+
...dataFeedRules,
|
|
8
|
+
...ccipRules,
|
|
9
|
+
...vrfRules,
|
|
10
|
+
...automationRules,
|
|
11
|
+
...functionsCreRules,
|
|
12
|
+
];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { countMatches, extractFunctionBody, firstLineMatching, makeFinding } from "./helpers.js";
|
|
2
|
+
function hasVrf(content) {
|
|
3
|
+
return /(VRFConsumerBase|VRFCoordinator|function\s+fulfillRandomWords\b|requestRandomWords)/.test(content);
|
|
4
|
+
}
|
|
5
|
+
export const vrfRules = [
|
|
6
|
+
{
|
|
7
|
+
metadata: {
|
|
8
|
+
ruleId: "CL-VRF-001",
|
|
9
|
+
product: "vrf",
|
|
10
|
+
severity: "high",
|
|
11
|
+
title: "Potential VRF callback without requestId tracking",
|
|
12
|
+
description: "fulfillRandomWords appears to lack requestId validation.",
|
|
13
|
+
},
|
|
14
|
+
scan(context) {
|
|
15
|
+
if (!/function\s+fulfillRandomWords\b/.test(context.content))
|
|
16
|
+
return [];
|
|
17
|
+
const body = extractFunctionBody(context.content, "fulfillRandomWords") || context.content;
|
|
18
|
+
const validates = /requestId/.test(body) &&
|
|
19
|
+
/(pendingRequests|requestStatus|requests)\s*\[?\s*requestId/.test(context.content) &&
|
|
20
|
+
/(require|revert|UnknownRequest)/.test(body);
|
|
21
|
+
if (validates)
|
|
22
|
+
return [];
|
|
23
|
+
return [
|
|
24
|
+
makeFinding({
|
|
25
|
+
context,
|
|
26
|
+
ruleId: this.metadata.ruleId,
|
|
27
|
+
severity: this.metadata.severity,
|
|
28
|
+
confidence: "medium",
|
|
29
|
+
line: firstLineMatching(context.lines, /fulfillRandomWords/),
|
|
30
|
+
title: this.metadata.title,
|
|
31
|
+
description: "Potential issue: unknown or duplicate VRF fulfillments may mutate state.",
|
|
32
|
+
risk: "Incorrect request correlation can break fairness, overwrite randomness, or settle the wrong user/action.",
|
|
33
|
+
recommendation: "Track request IDs before requesting randomness and reject unknown or duplicate fulfillments.",
|
|
34
|
+
}),
|
|
35
|
+
];
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
metadata: {
|
|
40
|
+
ruleId: "CL-VRF-002",
|
|
41
|
+
product: "vrf",
|
|
42
|
+
severity: "medium",
|
|
43
|
+
title: "Potential heavy or reverting VRF callback",
|
|
44
|
+
description: "fulfillRandomWords appears to contain business logic that may revert or exceed callback gas.",
|
|
45
|
+
},
|
|
46
|
+
scan(context) {
|
|
47
|
+
if (!/function\s+fulfillRandomWords\b/.test(context.content))
|
|
48
|
+
return [];
|
|
49
|
+
const body = extractFunctionBody(context.content, "fulfillRandomWords");
|
|
50
|
+
if (!body)
|
|
51
|
+
return [];
|
|
52
|
+
const riskySignals = countMatches(body, [/for\s*\(/, /while\s*\(/, /\.call\s*\(/, /\.transfer\s*\(/, /\.send\s*\(/, /require\s*\(/, /revert\s+/]);
|
|
53
|
+
if (riskySignals === 0)
|
|
54
|
+
return [];
|
|
55
|
+
return [
|
|
56
|
+
makeFinding({
|
|
57
|
+
context,
|
|
58
|
+
ruleId: this.metadata.ruleId,
|
|
59
|
+
severity: this.metadata.severity,
|
|
60
|
+
confidence: "low",
|
|
61
|
+
line: firstLineMatching(context.lines, /fulfillRandomWords/),
|
|
62
|
+
title: this.metadata.title,
|
|
63
|
+
description: "Potential issue: VRF callback contains logic that may revert, call externally, or become gas-heavy.",
|
|
64
|
+
risk: "Failed VRF callbacks can block randomness delivery and leave protocol actions unresolved.",
|
|
65
|
+
recommendation: "Keep fulfillRandomWords minimal, avoid external calls/unbounded loops, and test callback gas limits.",
|
|
66
|
+
}),
|
|
67
|
+
];
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
metadata: {
|
|
72
|
+
ruleId: "CL-VRF-003",
|
|
73
|
+
product: "vrf",
|
|
74
|
+
severity: "medium",
|
|
75
|
+
title: "Potential VRF request state not tracked",
|
|
76
|
+
description: "Randomness requests appear without obvious pre-fulfillment state tracking.",
|
|
77
|
+
},
|
|
78
|
+
scan(context) {
|
|
79
|
+
if (!/requestRandomWords\s*\(/.test(context.content))
|
|
80
|
+
return [];
|
|
81
|
+
const tracksBeforeFulfillment = /(pendingRequests|requestStatus|requests)\s*\[.*\]\s*=/.test(context.content);
|
|
82
|
+
if (tracksBeforeFulfillment)
|
|
83
|
+
return [];
|
|
84
|
+
return [
|
|
85
|
+
makeFinding({
|
|
86
|
+
context,
|
|
87
|
+
ruleId: this.metadata.ruleId,
|
|
88
|
+
severity: this.metadata.severity,
|
|
89
|
+
confidence: "low",
|
|
90
|
+
line: firstLineMatching(context.lines, /requestRandomWords\s*\(/),
|
|
91
|
+
title: this.metadata.title,
|
|
92
|
+
description: "Potential issue: request lifecycle state is not visibly committed when randomness is requested.",
|
|
93
|
+
risk: "State may be manipulated between request and fulfillment or callbacks may not map to the intended action.",
|
|
94
|
+
recommendation: "Store request metadata before or immediately after requesting randomness and validate it in the callback.",
|
|
95
|
+
}),
|
|
96
|
+
];
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
];
|