Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
452bd10
feat(threat-model): implementation
bogdan-manole Jan 12, 2026
cdc0170
feat(threat-model): change ThreatModel to ConwayEra
bogdan-manole Jan 13, 2026
e7dfae1
refactor(threat-model): move ThreatModel from mockchain to testing-in…
bogdan-manole Jan 13, 2026
748796b
feat(threat-model): add UnprotectedScriptOutput threat model with vul…
bogdan-manole Jan 14, 2026
df7786c
feat(threat-model): add DoubleSatisfaction threat model with vulnerab…
bogdan-manole Jan 14, 2026
60b5a38
feat(testing-interface): integrate threat models with TestingInterface
bogdan-manole Jan 15, 2026
6e1777a
feat(threat-model): adding Phase 1 validation
bogdan-manole Jan 19, 2026
d80f84e
fix(threat-model): fix Phase 1 validation for doubleSatisfaction
bogdan-manole Jan 19, 2026
86fd753
refactor(testing-interface): move test scripts from coin-selection to…
bogdan-manole Jan 20, 2026
2e24436
feat(threat-model): Large Data Attack implementation
bogdan-manole Jan 26, 2026
9f99896
feat(threat-model): add Large Value Attack
bogdan-manole Jan 27, 2026
ab0660f
feat(testing-interface): integrate coverage tracking for threat model…
bogdan-manole Feb 2, 2026
0a2e343
feat(testing-interface): add withCoverage helper and JSON coverage ou…
bogdan-manole Feb 11, 2026
9b67057
refactor(testing-interface): propRunActions returns TestTree group in…
bogdan-manole Feb 19, 2026
eafe521
feat(testing-interface): add negative tests for precondition violatio…
bogdan-manole Feb 19, 2026
a993f81
Extract coverage from phase 2 errors
sjoerdvisscher Feb 23, 2026
f4b08f4
Collect coverage data from errors before throwing them
sjoerdvisscher Feb 20, 2026
279dec1
feat(testing-interface): add Aiken contract examples for language-agn…
bogdan-manole Feb 12, 2026
9e91576
feat(testing-interface): add CTF tipjar vulnerability detection with …
bogdan-manole Feb 17, 2026
6c1870f
feat(aiken): port Cardano CTF challenge validators to Aiken v1.1.x
bogdan-manole Feb 20, 2026
ab68bc1
feat(testing-interface): add 3 new CTF contracts
bogdan-manole Feb 23, 2026
ba595fb
feat(testing-interface): add CTF test suites for another 10 on-chain …
bogdan-manole Feb 23, 2026
cb486bc
feat(testing-interface): add 9 generic threat models
bogdan-manole Feb 25, 2026
8824f9a
feat(testing-interface): add negative testing support with disableNeg…
bogdan-manole Feb 25, 2026
97ea004
feat(testing-interface): restructure threat model integration with se…
bogdan-manole Feb 27, 2026
b4ff313
feat(testing-interface): fix inputDuplication threat model
bogdan-manole Mar 1, 2026
17defed
refactor(testing-interface): clean up threat model code and naming co…
bogdan-manole Mar 5, 2026
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ download-tx-test.sh
tx.json
graph.dot
.vim

# Aiken build artifacts
aiken-contracts-example/build/
aiken-contracts-example/plutus.json
tmp/
*.ignore.*
98 changes: 98 additions & 0 deletions aiken-contracts-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Aiken Validators for sc-tools Testing

Aiken smart contracts used to prove that `convex-testing-interface`
can test Aiken-compiled (non-PlutusTx) smart contracts.

## Validators

### 1. `validators/check_answer.ak` — Simple spending validator

Checks that `datum + redeemer == 43`.

- **Datum:** `Option<Int>` (inline datum, an integer)
- **Redeemer:** `Int` (an integer)
- **Blueprint key:** `check_answer.check_answer.spend`

Example: locking with datum `10` and spending with redeemer `33` succeeds
because `10 + 33 = 43`.

### 2. `validators/ping_pong.ak` — Secure stateful PingPong validator

A state machine that transitions between `Pinged`, `Ponged`, and `Stopped`.

- **Datum:** `Option<State>` where `State { Pinged, Ponged, Stopped }`
- **Redeemer:** `Action` where `Action { Ping, Pong, Stop }`
- **Blueprint key:** `ping_pong.ping_pong.spend`

Valid transitions:
- `Pinged` + `Pong` → `Ponged`
- `Ponged` + `Ping` → `Pinged`
- `Pinged` or `Ponged` + `Stop` → `Stopped`

Security checks (matching the Haskell secure version):
1. Continuation output must go to the **same script address** (prevents output redirect attack)
2. Output **value must equal input value** (prevents large value attack)
3. Aiken's `expect` rejects unknown constructors (prevents large data attack)

Wire compatibility: The Aiken types encode identically to the Haskell types
(`Constr 0/1/2 []`), so the Haskell test suite reuses `PingPongState` and
`PingPongRedeemer` directly.

## Prerequisites

[Aiken](https://aiken-lang.org/installation-instructions) v1.1.x or later.

Install via cargo:

```bash
cargo install aiken --locked
```

Or via aikup:

```bash
curl --proto '=https' --tlsv1.2 -LsSf https://install.aiken-lang.org | sh
aikup
```

## Build

```bash
aiken build
```

This produces `plutus.json` — a [CIP-0057 Plutus blueprint](https://github.com/cardano-foundation/CIPs/pull/258)
containing the compiled UPLC code and validator hashes for all validators.

## Update the test fixture

After modifying any validator, rebuild and update the blueprint used by
`convex-testing-interface` tests:

```bash
./update-blueprint.sh
```

Or manually:

```bash
aiken build
cp plutus.json ../src/testing-interface/test/data/aiken-contracts-example.json
```

Then re-run the Haskell tests to verify:

```bash
cabal test convex-testing-interface
```

## How it integrates with sc-tools

The Haskell test suite loads the blueprint JSON at test time using
`Convex.Aiken.Blueprint.loadFromFile`, extracts `PlutusScript PlutusScriptV3`
for each validator by key, and uses them with the standard `BuildTx` functions —
the exact same functions used for PlutusTx/Plinth scripts.

Test modules:
- `test/AikenSpec.hs` — check_answer unit tests (4 tests)
- `test/AikenPingPongSpec.hs` — ping_pong unit tests (9), TestingInterface property test, threat model tests (579 lines)
15 changes: 15 additions & 0 deletions aiken-contracts-example/aiken.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# This file was generated by Aiken
# You typically do not need to edit this file

[[requirements]]
name = "aiken-lang/stdlib"
version = "v3.0.0"
source = "github"

[[packages]]
name = "aiken-lang/stdlib"
version = "v3.0.0"
requirements = []
source = "github"

[etags]
12 changes: 12 additions & 0 deletions aiken-contracts-example/aiken.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name = "sc-tools/contracts-example"
version = "0.0.0"
plutus = "v3"
license = "Apache-2.0"
description = "Simple Aiken validator for testing sc-tools interop"

[[dependencies]]
name = "aiken-lang/stdlib"
version = "v3.0.0"
source = "github"

[config]
13 changes: 13 additions & 0 deletions aiken-contracts-example/lib/ctf/bank_types.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// Shared types for the CTF bank series validators

/// The datum stored at an account script address
pub type AccountDatum {
balance: Int,
owner: ByteArray,
}

/// Actions that can be performed on an account
pub type AccountRedeemer {
IncreaseBalance
DecreaseBalance
}
49 changes: 49 additions & 0 deletions aiken-contracts-example/update-blueprint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail

# Rebuild the Aiken validators and copy the blueprint to the
# convex-testing-interface test data directory.
#
# Validators:
# - check_answer: simple spending validator (datum + redeemer == 43)
# - ping_pong: secure stateful PingPong with threat model resistance

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEST="$SCRIPT_DIR/../src/testing-interface/test/data/aiken-contracts-example.json"

cd "$SCRIPT_DIR"

# Check aiken is available
if ! command -v aiken &>/dev/null; then
echo "Error: 'aiken' not found on PATH."
echo "Install it: cargo install aiken --locked"
echo "Or see: https://aiken-lang.org/installation-instructions"
exit 1
fi

echo "=== Building Aiken validators ==="
aiken build

echo ""
echo "=== Copying blueprint to test data ==="
cp plutus.json "$DEST"

# Print summary
echo ""
echo "Done! Blueprint updated:"
echo " Source: $SCRIPT_DIR/plutus.json"
echo " Dest: $DEST"
echo ""
echo " Aiken version: $(aiken --version)"
echo ""
echo " Validators:"
if command -v jq &>/dev/null; then
jq -r '.validators[] | " \(.title) hash: \(.hash)"' plutus.json
else
echo " (install jq for detailed validator info)"
grep -o '"title": "[^"]*"' plutus.json | sed 's/"title": "//;s/"$//' | while read -r title; do
echo " $title"
done
fi
echo ""
echo "Now run: cabal test convex-testing-interface"
24 changes: 24 additions & 0 deletions aiken-contracts-example/validators/check_answer.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use cardano/transaction.{OutputReference, Transaction}

/// A simple spending validator that checks: datum + redeemer == 43
///
/// - datum: An integer stored as inline datum at the UTxO
/// - redeemer: An integer provided when spending
///
/// This demonstrates that Aiken-compiled validators work with
/// the sc-tools testing-interface framework.
validator check_answer {
spend(
datum: Option<Int>,
redeemer: Int,
_own_ref: OutputReference,
_self: Transaction,
) {
expect Some(stored_number) = datum
stored_number + redeemer == 43
}

else(_) {
fail
}
}
30 changes: 30 additions & 0 deletions aiken-contracts-example/validators/ctf_bank_00_account.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use aiken/collection/list
use cardano/transaction.{OutputReference, Transaction}
use ctf/bank_types.{AccountDatum, AccountRedeemer, DecreaseBalance, IncreaseBalance}

/// Account validator for bank_00.
/// Per-user account that authorizes balance changes.
///
/// - IncreaseBalance: always succeeds (anyone can deposit)
/// - DecreaseBalance: requires owner signature
validator ctf_bank_00_account {
spend(
datum_opt: Option<AccountDatum>,
redeemer: AccountRedeemer,
_own_ref: OutputReference,
self: Transaction,
) {
expect Some(datum) = datum_opt

when redeemer is {
// Anyone can increase balance (deposit)
IncreaseBalance -> True
// Only owner can decrease balance (withdrawal)
DecreaseBalance -> list.has(self.extra_signatories, datum.owner)
}
}

else(_) {
fail
}
}
82 changes: 82 additions & 0 deletions aiken-contracts-example/validators/ctf_bank_00_bank.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use aiken/collection/list
use cardano/address.{Script}
use cardano/assets.{lovelace_of}
use cardano/transaction.{InlineDatum, OutputReference, Transaction}
use ctf/bank_types.{AccountDatum}

/// Bank validator for bank_00.
/// Holds pooled funds. Checks that fund_difference matches account balance change.
///
/// VULNERABILITY: No check that balance can't go negative.
/// Withdrawing more than deposited is possible.
validator ctf_bank_00_bank(account_script_hash: ByteArray) {
spend(
_datum: Option<Data>,
_redeemer: Data,
own_ref: OutputReference,
self: Transaction,
) {
// Find own input (bank input)
expect Some(bank_input) =
list.find(self.inputs, fn(input) { input.output_reference == own_ref })

// Get our script hash
expect Script(own_hash) = bank_input.output.address.payment_credential

// Find bank output (continuation at same address)
expect Some(bank_output) =
list.find(
self.outputs,
fn(output) {
when output.address.payment_credential is {
Script(hash) -> hash == own_hash
_ -> False
}
},
)

// Find account input (from account script)
expect Some(account_input) =
list.find(
self.inputs,
fn(input) {
when input.output.address.payment_credential is {
Script(hash) -> hash == account_script_hash
_ -> False
}
},
)

// Find account output
expect Some(account_output) =
list.find(
self.outputs,
fn(output) {
when output.address.payment_credential is {
Script(hash) -> hash == account_script_hash
_ -> False
}
},
)

// Parse account datums
expect InlineDatum(account_input_datum_raw) = account_input.output.datum
expect account_input_datum: AccountDatum = account_input_datum_raw

expect InlineDatum(account_output_datum_raw) = account_output.datum
expect account_output_datum: AccountDatum = account_output_datum_raw

// Calculate fund difference (positive = deposit, negative = withdrawal)
let bank_input_lovelace = lovelace_of(bank_input.output.value)
let bank_output_lovelace = lovelace_of(bank_output.value)
let fund_difference = bank_output_lovelace - bank_input_lovelace

// Check: account balance change matches fund difference
// VULNERABILITY: No check that account_output_datum.balance >= 0
account_output_datum.balance == account_input_datum.balance + fund_difference
}

else(_) {
fail
}
}
30 changes: 30 additions & 0 deletions aiken-contracts-example/validators/ctf_bank_01_account.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use aiken/collection/list
use cardano/transaction.{OutputReference, Transaction}
use ctf/bank_types.{AccountDatum, AccountRedeemer, DecreaseBalance, IncreaseBalance}

/// Account validator for bank_01.
/// Same as bank_00 account - per-user account that authorizes balance changes.
///
/// - IncreaseBalance: always succeeds (anyone can deposit)
/// - DecreaseBalance: requires owner signature
validator ctf_bank_01_account {
spend(
datum_opt: Option<AccountDatum>,
redeemer: AccountRedeemer,
_own_ref: OutputReference,
self: Transaction,
) {
expect Some(datum) = datum_opt

when redeemer is {
// Anyone can increase balance (deposit)
IncreaseBalance -> True
// Only owner can decrease balance (withdrawal)
DecreaseBalance -> list.has(self.extra_signatories, datum.owner)
}
}

else(_) {
fail
}
}
Loading