Skip to content

Commit 367bfc5

Browse files
committed
standards: add integration hardening and docs
1 parent 7e35587 commit 367bfc5

24 files changed

Lines changed: 12433 additions & 3 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Standards Hardening
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
property_cases:
7+
description: Property-test cases per invariant
8+
required: false
9+
default: "8192"
10+
data_driver_cases:
11+
description: Forge data-driver fuzz cases
12+
required: false
13+
default: "2048"
14+
schedule:
15+
- cron: "17 2 * * *"
16+
17+
jobs:
18+
hardening:
19+
name: Standards property and ABI fuzzing
20+
runs-on: core
21+
timeout-minutes: 240
22+
env:
23+
STANDARDS_PROPTEST_CASES: ${{ github.event.inputs.property_cases || '8192' }}
24+
STANDARDS_PROPTEST_MAX_SHRINK_ITERS: "16384"
25+
STANDARDS_DATA_DRIVER_FUZZ_CASES: ${{ github.event.inputs.data_driver_cases || '2048' }}
26+
STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS: "4096"
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: dsherret/rust-toolchain-file@v1
30+
- run: rustup target add wasm32-unknown-unknown
31+
- run: make standards-hardening

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
STANDARDS_WASM_CONTRACTS := standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/multisig_controller standards/examples/proxy_counter
1+
STANDARDS_EXAMPLES := standards/examples/authorization_counter standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/multisig_controller standards/examples/proxy_counter
2+
STANDARDS_TEST_HELPERS := standards/examples/moonlight_call_router
3+
STANDARDS_WASM_CONTRACTS := $(STANDARDS_EXAMPLES) $(STANDARDS_TEST_HELPERS)
24
LEGACY_SUBDIRS := tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn
35
STANDARDS_PROPTEST_CASES ?= 8192
46
STANDARDS_PROPTEST_MAX_SHRINK_ITERS ?= 16384
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Dusk Contract Standards Audit Packet
2+
3+
This document is the auditor-facing entry point for the Dusk-native standards
4+
layer. It captures the review scope, security assumptions, invariant matrix,
5+
validation commands, and known residual risks for this branch.
6+
7+
## Scope
8+
9+
In scope:
10+
11+
- `standards/dusk-contract-standards/src/**`
12+
- `standards/examples/authorization_counter`
13+
- `standards/examples/drc20_roles_pausable`
14+
- `standards/examples/drc721_collection`
15+
- `standards/examples/multisig_controller`
16+
- `standards/examples/proxy_counter`
17+
- `scripts/dusk-contract-standards-local-smoke.sh`
18+
- `standards/dusk-contract-standards/tests/**`
19+
20+
Out of scope:
21+
22+
- Dusk protocol consensus, wallet, node, prover, and VM internals.
23+
- Forge macro implementation correctness, except where the generated
24+
data-driver and Wasm artifacts are exercised through this repository's tests.
25+
- Economic policy choices such as exact upgrade delays, role assignment
26+
procedures, and marketplace royalty enforcement.
27+
- Compatibility with Ethereum/OpenZeppelin ABIs where Dusk-native semantics
28+
deliberately diverge.
29+
30+
## Security Model
31+
32+
The standards layer assumes:
33+
34+
- `CallContext::current()` correctly reports observed Moonlight root callers
35+
through `public_sender`, including normal transfer-contract entrypoint
36+
execution, and reports immediate contract callers for nested calls.
37+
- Phoenix calls do not expose a stable caller identity. Phoenix authorization
38+
must therefore be an explicit Schnorr signature over an `AuthorizedAction`.
39+
- Dusk signature primitives verify according to their upstream implementations.
40+
- `abi::self_id()`, `abi::block_height()`, `abi::emit`, and contract-call
41+
routing are provided correctly by the host.
42+
- Client encoders use the same schema and byte serialization as the generated
43+
Forge data-drivers.
44+
45+
The layer does not assume:
46+
47+
- an Ethereum-like `msg.sender` for Phoenix;
48+
- delegatecall-style proxy storage behavior;
49+
- off-chain signatures are safe without chain/contract/domain/action/payload
50+
binding;
51+
- zero principals are valid owners, recipients, spenders, operators, or admins.
52+
53+
## Invariant Matrix
54+
55+
| ID | Invariant | Primary Coverage |
56+
| --- | --- | --- |
57+
| AUTH-1 | Signed actions bind chain id, contract, domain, action id, payload hash, nonce, principal, and expiry before nonce/replay consumption. | `tests/primitives.rs`, `tests/properties.rs`, VM test, local-node smoke |
58+
| AUTH-2 | Rejected envelope, signature, expiry, role, owner, admin, and replay-key cases do not advance nonce/replay state. | property negative matrices, VM test, local-node smoke |
59+
| AUTH-3 | Phoenix authorization proves control of a Schnorr key principal and never relies on observed caller identity. | primitives, properties, signed auth example, local-node smoke |
60+
| AUTH-4 | Moonlight owners can authorize by observed root caller or signed BLS action; contract owners only by observed immediate contract caller. Transfer-contract entrypoint calls map to `public_sender`, while nested calls do not. | primitives, reference contracts, VM test, `core::context` unit tests |
61+
| MULTISIG-1 | Threshold multisig requires distinct owner quorum and rejects duplicate signers before nonce/replay consumption. | primitives |
62+
| MULTISIG-2 | Observed Moonlight/contract owners can count toward quorum; Phoenix owners require signed action approvals. | primitives |
63+
| MULTISIG-3 | Multisig owner and threshold maintenance requires current quorum and rejected changes leave state unchanged. | primitives |
64+
| MULTISIG-4 | Standalone controller proposal/confirmation requires distinct owners, rejects non-owners and duplicate confirmations without nonce movement, expires stale proposals, tombstones executed ids, and clears stale pending operations after authority-removing changes. | primitives, properties, VM test |
65+
| MULTISIG-5 | A contract-owned proxy/admin path can be controlled by a 2-of-3 standalone multisig controller through observed inter-contract caller context, while direct owner calls to the governed target fail. | VM test, local-node smoke |
66+
| ACCESS-1 | Role grants/revokes are admin-gated, reject zero accounts, and emit typed events when state changes. | primitives, DRC20 reference, data-driver event decoding |
67+
| ACCESS-2 | Owner and owner-set initialization reject zero principals atomically. | primitives, properties |
68+
| PAUSE-1 | Reference pausable DRC20/DRC721 pause all balance-changing operations. | primitives, VM test, local-node smoke |
69+
| PAUSE-2 | Approvals remain available while paused and do not move balances or ownership. | VM test, local-node smoke |
70+
| TOKEN20-1 | DRC20 total supply equals the modeled sum of balances after arbitrary operation sequences. | property state-machine model |
71+
| TOKEN20-2 | DRC20 transfer, transfer-from, allowance, mint, burn, cap, and vote checkpoint failures are atomic. | primitives, properties |
72+
| TOKEN721-1 | DRC721 owner, balance, approval, operator, enumerable, and total-supply views remain internally consistent. | property state-machine model |
73+
| TOKEN721-2 | DRC721 mint, transfer, burn, approval, operator, and royalty failures are atomic. | primitives, properties |
74+
| ROYALTY-1 | Royalty receivers cannot be zero, basis points cannot exceed 10,000, and overflow-prone quotes do not mutate state. | primitives, properties |
75+
| NONCE-1 | Nonce domains are independent, monotonic, import/export safe, and atomic on rejection. | primitives, properties |
76+
| REPLAY-1 | Replay keys cannot be reused for the same principal and import/export is monotonic. | primitives, properties |
77+
| PROXY-1 | Upgrade admin rejects zero admin/implementation and binds signed admin actions to exact payloads. | primitives, VM test, local-node smoke |
78+
| PROXY-2 | Prepare, activate, cancel, rollback, and rollback-finalization obey delay/window state transitions and emit events. | primitives, properties, data-driver event decoding |
79+
| TIMELOCK-1 | Timelock scheduling/execution/cancellation follows delay and predecessor constraints. | primitives, properties |
80+
| TIMELOCK-2 | Controller minimum-delay changes are self-governed through scheduled operations, not direct arbitrary admin mutation. | primitives, properties |
81+
| REENTRANCY-1 | Scoped guard rejects nested entry and resets after panics. | primitives |
82+
| ABI-1 | Forge data-driver schemas load for every reference contract. | `tests/data_driver_fuzz.rs` |
83+
| ABI-2 | Function inputs JSON-encode, decode, and re-encode consistently; malformed and mutated payloads do not crash. | `tests/data_driver_fuzz.rs` |
84+
| ABI-3 | Function outputs and typed events decode from rkyv and mutated output/event payloads do not crash readers. | `tests/data_driver_fuzz.rs` |
85+
| NODE-1 | Real local Rusk deployment accepts valid signed calls and rejects replay, bad payload, expired action, pause, and proxy replay cases while preserving expected state. | `scripts/dusk-contract-standards-local-smoke.sh` |
86+
87+
## Reproducible Validation
88+
89+
Run the audit-grade validation pass:
90+
91+
```sh
92+
./scripts/dusk-contract-standards-audit-grade.sh
93+
```
94+
95+
Default intensity:
96+
97+
- `STANDARDS_PROPTEST_CASES=8192`
98+
- `STANDARDS_PROPTEST_MAX_SHRINK_ITERS=16384`
99+
- `STANDARDS_DATA_DRIVER_FUZZ_CASES=4096`
100+
- `STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS=8192`
101+
102+
For a funded local-node run, start Rusk and provide wallet/node settings:
103+
104+
```sh
105+
RUN_LOCAL_NODE_SMOKE=1 \
106+
RUSK_URL=http://127.0.0.1:18080 \
107+
RUSK_WALLET_BIN=/path/to/rusk-wallet \
108+
WALLET_DIR=target/rusk-local-smoke/wallet \
109+
WALLET_RESTORE_FILE=/path/to/wallet.dat \
110+
./scripts/dusk-contract-standards-audit-grade.sh
111+
```
112+
113+
For dependency advisory scanning, install `cargo-audit` and opt in:
114+
115+
```sh
116+
RUN_CARGO_AUDIT=1 ./scripts/dusk-contract-standards-audit-grade.sh
117+
```
118+
119+
## Focused Reviews
120+
121+
- `docs/dusk-contract-standards-multisig-controller-review.md`
122+
123+
## Auditor Checklist
124+
125+
Reviewers should focus on:
126+
127+
- whether the Dusk principal model is the right abstraction for Moonlight,
128+
Phoenix, and contract callers;
129+
- whether every public signed path uses action-bound helpers before nonce
130+
consumption;
131+
- whether single-owner admin paths should be replaced with
132+
`ThresholdMultisig` composition or standalone `MultisigController`
133+
ownership before production deployment;
134+
- whether payload-hash construction is unambiguous and domain separated enough
135+
for downstream wallets;
136+
- whether pause semantics match product/security expectations;
137+
- whether upgrade and timelock policies are sufficient for production systems;
138+
- whether any event is missing for an indexer-relevant state change;
139+
- whether generated data-driver schemas are acceptable as the client/wallet
140+
ABI source of truth.
141+
142+
## Residual Risks
143+
144+
The branch is audit-ready, not audit-complete. Remaining risk areas:
145+
146+
- independent client encoders may still disagree with Forge data-drivers unless
147+
they are tested differentially;
148+
- Dusk runtime host-function behavior is assumed rather than proven here;
149+
- no formal verification is included for arithmetic or state machines;
150+
- dependency advisory scanning is optional unless `cargo-audit` is installed;
151+
- local-node smoke is deterministic and adversarial for defined invariants, but
152+
it is not a long-running network chaos test.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Dusk Contract Standards Hardening Track
2+
3+
This branch is a separate hardening track for the Dusk-native standards layer.
4+
It keeps implementation work close to the contracts, but treats security
5+
validation as a first-class deliverable rather than a final smoke test.
6+
7+
## Current Scope
8+
9+
The first hardening pass focuses on invariants that should hold regardless of
10+
the composing contract:
11+
12+
- signed authorization must bind contract, domain, action id, payload hash,
13+
nonce, principal, and expiry before nonce/replay state is consumed;
14+
- Phoenix signed authorization must preserve nonce and replay-key state across
15+
all rejected envelope, signature, expiry, policy, and replay-key cases;
16+
- owner, role, and upgrade-admin checks must not consume a valid signer nonce
17+
when the signer is not authorized for the policy being checked;
18+
- threshold multisig checks must verify distinct owner quorum before consuming
19+
any signer nonce or replay key;
20+
- mixed owner-set checks must follow the same no-consume-on-unauthorized rule;
21+
- failed token operations must not leave partial native state behind;
22+
- DRC20 total supply must equal the modeled sum of balances for all touched
23+
accounts;
24+
- DRC20 allowance movement must match the reference model exactly;
25+
- DRC721 total supply must equal the modeled number of live token ids;
26+
- DRC721 owner, approval, operator, balance, and enumerable views must remain
27+
internally consistent after arbitrary operation sequences;
28+
- nonce import/export and replay-key import/export must be monotonic,
29+
idempotent, and atomic on rejected nonce consumption;
30+
- voting checkpoints must reject non-monotonic writes without partially
31+
updating account or total-supply checkpoints;
32+
- royalty registry mutations and overflow-prone royalty quotes must leave
33+
registry state unchanged on rejection;
34+
- supply-cap changes must reject cap reductions below current supply and
35+
overflow-prone mints without moving the configured cap;
36+
- owner-set initialization and timelock-controller maintenance operations must
37+
validate before mutating durable state;
38+
- the reserved zero principal must not be accepted as an actor, recipient,
39+
spender, minter, burner, or operator where that would create ownership or
40+
authorization state.
41+
42+
## Added Validation
43+
44+
`standards/dusk-contract-standards/tests/properties.rs` adds property-based
45+
state-machine tests for:
46+
47+
- action-bound authorization rejection cases;
48+
- Phoenix action-bound authorization rejection and replay-key preservation
49+
cases;
50+
- mixed owner sets;
51+
- access-control role/admin mutation;
52+
- timelock scheduling, execution, cancellation, and delay mutation;
53+
- timelock-controller self-governed delay changes and malformed maintenance
54+
payload rejection;
55+
- upgrade-admin prepare, activate, cancel, rollback, and finalization flows;
56+
- nonce/replay state import, consumption, and rejection;
57+
- two-step ownership;
58+
- DRC20 supply caps and voting-unit checkpoints;
59+
- DRC20 accounting and allowance flows;
60+
- DRC721 ownership, approval, operator, balance, and enumerable views;
61+
- DRC721 royalty registry mutation and quote behavior.
62+
63+
The tests generate random operation sequences, run them against the real
64+
primitive and an independent model, and assert after each operation that:
65+
66+
- success/failure matches the model;
67+
- successful calls produce the exact expected state;
68+
- failing calls leave the pre-call state unchanged.
69+
70+
The saved proptest regression file keeps the self-transfer case that caught a
71+
DRC20 accounting bug during this pass.
72+
73+
The extended invariant pass also found and fixed:
74+
75+
- a `VotingUnits::move_units` atomicity bug where an account checkpoint could
76+
be written before a later total-supply checkpoint rejected the operation;
77+
- an `OwnerSet::init` atomicity bug where a later invalid owner could leave
78+
earlier owners in the set;
79+
- a `TimelockController::execute_min_delay_change` atomicity bug where a
80+
malformed payload could mark an operation done before being rejected.
81+
82+
## Commands
83+
84+
Run the hardening tests with:
85+
86+
```sh
87+
cargo test -p dusk-contract-standards
88+
cargo test -p dusk-contract-standards --test properties
89+
```
90+
91+
Run the long randomized hardening loop with:
92+
93+
```sh
94+
start=$(date +%s)
95+
end=$((start + 14400))
96+
while [ "$(date +%s)" -lt "$end" ]; do
97+
PROPTEST_CASES=2048 PROPTEST_MAX_SHRINK_ITERS=8192 \
98+
cargo test -p dusk-contract-standards --test properties
99+
done
100+
```
101+
102+
On April 26, 2026, this loop ran for 14,404 seconds and completed 367 full
103+
property-suite iterations without a failure.
104+
105+
On April 26, 2026, the extended 16-test property suite also passed a
106+
4,096-case focused run:
107+
108+
```sh
109+
PROPTEST_CASES=4096 PROPTEST_MAX_SHRINK_ITERS=8192 \
110+
cargo test -p dusk-contract-standards --test properties
111+
```
112+
113+
The property suite now also honors `STANDARDS_PROPTEST_CASES` and
114+
`STANDARDS_PROPTEST_MAX_SHRINK_ITERS`, which lets CI run longer without
115+
editing the test source:
116+
117+
```sh
118+
STANDARDS_PROPTEST_CASES=8192 STANDARDS_PROPTEST_MAX_SHRINK_ITERS=16384 \
119+
cargo test -p dusk-contract-standards --test properties
120+
```
121+
122+
Forge data-driver ABI fuzzing covers JSON-to-rkyv input encoding, input
123+
decoding, roundtrips, malformed JSON, bad shapes, unknown functions, output
124+
decoding, typed event decoding, and mutated input/output/event payloads for the
125+
five standards reference contracts:
126+
127+
```sh
128+
make standards-data-drivers
129+
STANDARDS_DATA_DRIVER_FUZZ_CASES=2048 \
130+
cargo test -p dusk-contract-standards --test data_driver_fuzz -- --ignored
131+
```
132+
133+
The `standards-hardening` workflow runs these longer property and data-driver
134+
fuzz jobs on demand and nightly.
135+
136+
Run the audit-grade validation script with:
137+
138+
```sh
139+
./scripts/dusk-contract-standards-audit-grade.sh
140+
```
141+
142+
See `docs/dusk-contract-standards-audit.md` for the auditor-facing scope,
143+
threat model, invariant matrix, and residual-risk register.
144+
145+
Run the full standards validation pass with:
146+
147+
```sh
148+
cargo fmt
149+
cargo test -p dusk-contract-standards
150+
cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \
151+
-p authorization-counter \
152+
-p drc20-roles-pausable \
153+
-p drc721-collection \
154+
-p multisig-controller \
155+
-p proxy-counter \
156+
--features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract
157+
cargo test -p dusk-contract-standards --test examples_vm -- --ignored
158+
make standards-data-drivers
159+
cargo test -p dusk-contract-standards --test data_driver_fuzz -- --ignored
160+
cargo clippy -p dusk-contract-standards --all-targets -- -D warnings
161+
```
162+
163+
## Next Research Items
164+
165+
The next hardening layer should add differential tests against independent
166+
client encoders, formal or semi-formal arithmetic/state-machine proofs, and
167+
local-node scenario tests that intentionally mix successful transactions with
168+
rejected transactions across block boundaries over longer runs.

0 commit comments

Comments
 (0)