smart-contract-security-review
Security review for Scalus/Cardano smart contracts. Analyzes @Compile annotated validators for vulnerabilities like redirect attacks, inexact value validation, missing token verification, integer overflow, and self-dealing. Use when reviewing on-chain code, before deploying validators, or when /security-review is invoked. Requires explicit path argument.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Smart Contract Security Review
Analyze Scalus/Cardano smart contracts for security vulnerabilities.
Target Code Identification
Find on-chain code by searching for:
- Objects/classes with
@Compileannotation - Objects extending
Validator,DataParameterizedValidator, orParameterizedValidator - Objects compiled with
PlutusV3.compile(),PlutusV2.compile(), orPlutusV1.compile()
Search patterns:
grep -rn "@Compile" --include="*.scala" <path>
grep -rn "extends Validator" --include="*.scala" <path>
grep -rn "extends DataParameterizedValidator" --include="*.scala" <path>
Workflow
-
Discovery: Find all
@Compileannotated code in specified path - Classification: Identify validator type (spend/mint/reward/certify/vote/propose)
- Analysis: Check each validator against vulnerability checklist
- False Positive Verification: For each potential issue, verify it's not a false positive
- Reporting: Generate structured report with severity levels (only verified issues)
- Remediation: Use TodoWrite to track issues, fix one-by-one with user confirmation
Vulnerability Checklist
Based on Cardano Developer Portal security guidelines and Scalus-specific patterns.
For detailed patterns and code examples, see references/vulnerabilities.md.
Critical Severity
| ID | Name | Risk | Detection |
|---|---|---|---|
| V001 | Redirect Attack | Funds stolen via output redirection | outputs.at(idx) without address validation |
| V002 | Token/NFT Not Verified | State token excluded | Missing quantityOf on outputs |
| V003 | Inexact Burn/Mint | Extra tokens minted | >= instead of === for quantities |
| V004 | Integer Overflow | Arithmetic overflow | Custom encoding without bounds |
| V005 | Double Satisfaction | Pay once, satisfy many | outputs.exists without unique linking |
High Severity
| ID | Name | Risk | Detection |
|---|---|---|---|
| V006 | Index Validation Missing | Invalid index access | .at(idx) without bounds check |
| V007 | Self-Dealing/Shill Bidding | Price manipulation | No seller/bidder separation |
| V008 | Double Spend via Index | Same UTxO processed twice | Index lists without uniqueness |
| V009 | Inexact Refund Amount | Fund manipulation | >= for refunds instead of === |
| V010 | Other Redeemer Attack | Bypass via different redeemer | Multiple script purposes |
| V011 | Other Token Name Attack | Unauthorized token minting | Policy doesn't check all tokens |
| V012 | Missing UTxO Authentication | Fake UTxO injection | No auth token verification |
| V025 | Oracle Data Validation | Price manipulation, stale data | Oracle data without signature/freshness |
Medium Severity
| ID | Name | Risk | Detection |
|---|---|---|---|
| V013 | Time Handling | Time manipulation | Incorrect interval bounds |
| V014 | Missing Signature | Unauthorized actions | No isSignedBy checks |
| V015 | Datum Mutation | Unauthorized state change | No field comparison |
| V016 | Insufficient Staking Control | Reward redirection | No staking credential check |
| V017 | Arbitrary Datum | Unspendable UTxOs | No datum validation |
| V024 | Parameterization Verification | Script substitution (varies) | ParameterizedValidator with auth params, no token |
Low Severity / Design Issues
| ID | Name | Risk | Detection |
|---|---|---|---|
| V018 | Unbounded Value | UTxO size limit | Unlimited tokens in output |
| V019 | Unbounded Datum | Resource exhaustion | Growing datum size |
| V020 | Unbounded Inputs | TX limit exceeded | Many required UTxOs |
| V021 | UTxO Contention / Concurrency DoS | Bottleneck, DoS | Shared global state, no rate limit |
| V022 | Cheap Spam/Dust | Operation obstruction | No minimum amounts |
| V023 | Locked Value | Permanent lock | Missing exit paths |
False Positive Verification
CRITICAL: Before reporting ANY vulnerability, you MUST verify exploitability by tracing code execution with a concrete attack transaction.
Verification Method: Attack Transaction Tracing
For each potential vulnerability:
-
Construct a concrete attack transaction
- Define specific inputs (UTxOs with concrete values/datums)
- Define the redeemer values
- Define the outputs the attacker would create
- Define signatories
-
Execute the validator logic mentally with this transaction
- Go line-by-line through the validator code
- Track what each variable evaluates to with your attack tx
- Check EVERY
require()statement - does it pass or fail?
-
If ANY require fails, the attack fails → False Positive
-
Only report if ALL requires pass with the attack transaction
Example: V005 Double Satisfaction Verification
Potential vulnerability detected: handlePay uses getAdaFromOutputs(sellerOutputs) without unique linking.
Construct attack transaction:
Inputs:
- EscrowA: 12 ADA, datum={seller=S, buyer=B, escrowAmount=10, initAmount=2}
- EscrowB: 12 ADA, datum={seller=S, buyer=B, escrowAmount=10, initAmount=2}
Outputs:
- 12 ADA to seller S (single output for both!)
- 1 ADA to buyer B
Signatories: [B]
Trace execution for EscrowA:
// Line 57-58:
val contractInputs = txInfo.findOwnInputsByCredential(contractAddress.credential)
// → Returns [EscrowA, EscrowB] (both have same script credential)
val contractBalance = Utils.getAdaFromInputs(contractInputs)
// → 12 + 12 = 24 ADA
// Line 118-120 in handlePay:
require(contractBalance === escrowDatum.escrowAmount + escrowDatum.initializationAmount)
// → require(24 === 10 + 2)
// → require(24 === 12)
// → FAILS! ❌
Result: Attack transaction fails at line 118-120. This is a FALSE POSITIVE.
When to Write a Test
If the attack trace is complex or you're uncertain, write an actual test:
test("V005: Double satisfaction attack should fail") {
// Setup: Create two escrow UTxOs with same seller
val escrowA = createEscrowUtxo(seller = S, buyer = B, amount = 10.ada)
val escrowB = createEscrowUtxo(seller = S, buyer = B, amount = 10.ada)
// Attack: Try to spend both with single output to seller
val attackTx = Transaction(
inputs = List(escrowA, escrowB),
outputs = List(TxOut(sellerAddress, 12.ada)), // Only pay once!
redeemers = Map(escrowA -> Pay, escrowB -> Pay)
)
// Verify: Should this pass or fail?
// If it passes → Real vulnerability
// If it fails → False positive
evaluateValidator(EscrowValidator, escrowA, attackTx) shouldBe failure
}
Verification Checklist
Before reporting, answer these questions:
| Question | Answer Required |
|---|---|
| What is the specific attack transaction? | Inputs, outputs, redeemers, signatories |
| Which line would the attacker exploit? | File:line reference |
| Did you trace through EVERY require in the code path? | Yes/No |
| Does the attack pass ALL requires? | Yes (report) / No (false positive) |
| What value does the attacker gain? | Concrete amount/asset |
Do NOT Report If
- You only found a pattern match without tracing execution
- You haven't constructed a specific attack transaction
- Any
require()in the code path would fail the attack - You're unsure whether the attack works (investigate more or write a test)
Output Format
Use clickable file_path:line_number format for all code locations.
Finding Format
For each vulnerability found, output in this format:
### [SEVERITY] ID: Vulnerability Name
**Location:** `full/path/to/File.scala:LINE`
**Method:** methodName
**Issue:** Brief description of what's wrong
**Vulnerable code** (`full/path/to/File.scala:LINE-LINE`):
```scala
// actual code from file
Fix:
// proposed fix
### Summary Table
At the end, provide a summary with clickable locations:
Summary
| ID | Severity | Location | Issue | Status |
|---|---|---|---|---|
| C-01 | Critical | path/File.scala:123 |
Missing mint validation | Fixed |
| H-01 | High | path/File.scala:87 |
Token not in output | Declined |
| M-01 | Medium | path/File.scala:200 |
Missing signature | False Positive |
False Positives
| ID | Location | Reason |
|---|---|---|
| M-01 | path/File.scala:200 |
Authorization is done via NFT ownership in verifyAuth helper |
Security Grade: A/B/C/D/F
### Location Format Rules
1. Always use full path from project root: `scalus-examples/jvm/src/.../File.scala:123`
2. For ranges use: `File.scala:123-145`
3. For method references: `File.scala:123` (methodName)
4. Make locations clickable by using backticks
## Interactive Workflow
For each finding:
1. Display issue with location and proposed fix
2. Prompt: "Apply fix? [y/n/s/d/f]"
- y: Apply fix, mark completed, verify with `sbtn compile`
- n: Skip, log as "declined"
- s: Skip without logging
- d: Show more details (attack scenario)
- f: Mark as false positive (prompts for reason, logged to summary)
3. After all findings: run `sbtn quick` to verify fixes
4. Generate summary report including:
- Fixed issues
- Declined issues
- False positives with reasons
## Reference
For detailed vulnerability patterns and code examples, see:
- `references/vulnerabilities.md` - Full pattern documentation with Scalus-specific examples
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-02-02
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!