Skip to content

Commit 8f79c51

Browse files
committed
refactor(EchidnaERC20CommerceEscrowWrapper): update testing configuration and contract invariants
- Enhanced the Echidna testing configuration to include absolute paths for OpenZeppelin remapping, improving compatibility and clarity. - Updated the `echidna_*` functions in the `EchidnaERC20CommerceEscrowWrapper` contract to track token supply accurately and ensure proper state validation. - Deprecated outdated solver configurations in the Echidna config file for better alignment with the latest version. - Improved comments for clarity on the purpose and functionality of invariants, ensuring better understanding of the testing framework.
1 parent 1292244 commit 8f79c51

File tree

4 files changed

+41
-30
lines changed

4 files changed

+41
-30
lines changed

.github/workflows/security-echidna.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,16 @@ jobs:
117117
echo "Test limit: ${{ steps.mode.outputs.TEST_LIMIT }}"
118118
echo "Timeout: ${{ steps.mode.outputs.TIMEOUT }}s"
119119
120+
# Get absolute path to OpenZeppelin for remapping
121+
OZ_PATH="$(cd ../.. && pwd)/node_modules/@openzeppelin/"
122+
120123
echidna src/contracts/test/EchidnaERC20CommerceEscrowWrapper.sol \
121124
--contract EchidnaERC20CommerceEscrowWrapper \
122125
--config echidna.config.yml \
123126
--test-limit ${{ steps.mode.outputs.TEST_LIMIT }} \
124127
--timeout ${{ steps.mode.outputs.TIMEOUT }} \
125128
--format text \
129+
--crytic-args "--solc-remaps @openzeppelin/=$OZ_PATH" \
126130
| tee reports/security/echidna-report.txt
127131
128132
ECHIDNA_EXIT=${PIPESTATUS[0]}

packages/smart-contracts/.slither.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"exclude_low": false,
66
"exclude_medium": false,
77
"exclude_high": false,
8-
"solc_remaps": ["@openzeppelin=node_modules/@openzeppelin"],
8+
"solc_remaps": ["@openzeppelin=../../node_modules/@openzeppelin"],
99
"solc_args": "--optimize --optimize-runs 200",
1010
"exclude_dependencies": true,
1111
"json": "-",

packages/smart-contracts/echidna.config.yml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ testLimit: 100000 # Number of test sequences to run (increase for deeper testing
66
testMode: property # Test mode: assertion, property, or overflow
77
timeout: 300 # Timeout in seconds (5 minutes for CI, increase locally)
88

9-
# Solver configuration
10-
solver: cvc5 # SMT solver: cvc5, z3, or bitwuzla
11-
solverTimeout: 20 # Solver timeout in seconds
9+
# Solver configuration (deprecated in Echidna 2.x, kept for backwards compatibility)
10+
# solver: cvc5 # SMT solver: cvc5, z3, or bitwuzla
11+
# solverTimeout: 20 # Solver timeout in seconds
1212

1313
# Coverage and corpus settings
1414
coverage: true # Enable coverage-guided fuzzing
@@ -22,12 +22,12 @@ seqLen: 15 # Sequence length per test
2222
shrinkLimit: 5000 # Number of shrinking attempts
2323
dictFreq: 0.40 # Dictionary usage frequency
2424

25-
# Gas settings
26-
gasLimit: 12000000 # Gas limit per transaction
27-
maxGasPerBlock: 12000000 # Maximum gas per block
25+
# Gas settings (deprecated in Echidna 2.x)
26+
# gasLimit: 12000000 # Gas limit per transaction
27+
# maxGasPerBlock: 12000000 # Maximum gas per block
2828

2929
# Property testing
30-
checkAsserts: true # Check Solidity assertions
30+
# checkAsserts: true # Check Solidity assertions (deprecated in Echidna 2.x)
3131
estimateGas: true # Estimate gas usage
3232
maxValue: 100000000000000000000 # Max ETH to send (100 ETH)
3333

@@ -45,9 +45,8 @@ filterBlacklist: true # Filter out blacklisted functions
4545
filterFunctions: [] # Functions to exclude from testing
4646

4747
# Compilation settings
48-
solcArgs: '--optimize --optimize-runs 200 --allow-paths .'
49-
solcRemaps: ['@openzeppelin/=node_modules/@openzeppelin/']
50-
crytic-export-dir: 'crytic-export'
48+
# Note: In Echidna 2.x, compiler options should be passed via --crytic-args, not in config
49+
# Example: --crytic-args "--solc-remaps @openzeppelin/=/path/to/node_modules/@openzeppelin/"
5150
# CI-specific overrides (can be overridden via command line)
5251
# For faster CI: --test-limit 50000 --timeout 180
5352
# For thorough local testing: --test-limit 500000 --timeout 3600

packages/smart-contracts/src/contracts/test/EchidnaERC20CommerceEscrowWrapper.sol

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ contract EchidnaERC20CommerceEscrowWrapper {
5050
uint256 public totalReclaimed;
5151
uint256 public totalRefunded;
5252

53+
// Track supply for invariant checking
54+
uint256 private supply;
55+
5356
// Test accounts
5457
address public constant PAYER = address(0x1000);
5558
address public constant MERCHANT = address(0x2000);
@@ -72,6 +75,9 @@ contract EchidnaERC20CommerceEscrowWrapper {
7275
// We mint initial tokens but will mint more as needed in drivers
7376
token.mint(address(this), 10000000 ether);
7477

78+
// Track initial supply
79+
supply = token.totalSupply();
80+
7581
// Pre-approve wrapper and feeProxy for efficiency
7682
// Individual operations will also approve mockEscrow as needed
7783
token.approve(address(wrapper), type(uint256).max);
@@ -105,6 +111,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
105111
// In Echidna, this contract IS the caller, so we use address(this) for all roles
106112
// Ensure this contract has tokens and has approved escrow
107113
token.mint(address(this), amount);
114+
supply += amount; // Track minted tokens
108115
token.approve(address(mockEscrow), amount);
109116

110117
// Authorize payment (this contract acts as payer, we use different addresses for merchant/operator)
@@ -185,6 +192,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
185192

186193
// Setup: Mint tokens and approve escrow
187194
token.mint(address(this), amount);
195+
supply += amount; // Track minted tokens
188196
token.approve(address(mockEscrow), amount);
189197

190198
try
@@ -240,6 +248,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
240248

241249
// Setup: Give this contract (operator) tokens for refund and approve
242250
token.mint(address(this), refundAmount);
251+
supply += refundAmount; // Track minted tokens
243252
// Note: refund flow pulls from operator, so we need approval on wrapper
244253
token.approve(address(wrapper), refundAmount);
245254

@@ -271,7 +280,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
271280
// ============================================
272281
/// @notice Invariant: Fees can never exceed the capture amount
273282
/// @dev This ensures merchant always receives non-negative amount
274-
function echidna_fee_never_exceeds_capture() public view returns (bool) {
283+
function echidna_fee_never_exceeds_capture() public returns (bool) {
275284
// For any valid feeBps (0-10000), fee should never exceed captureAmount
276285
uint256 captureAmount = 1000 ether;
277286
for (uint16 feeBps = 0; feeBps <= 10000; feeBps += 100) {
@@ -285,7 +294,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
285294

286295
/// @notice Invariant: Fee basis points validation works correctly
287296
/// @dev feeBps > 10000 should always revert
288-
function echidna_invalid_fee_bps_reverts() public view returns (bool) {
297+
function echidna_invalid_fee_bps_reverts() public returns (bool) {
289298
// This is a pure mathematical invariant - fee calculation should never overflow
290299
// For any valid feeBps (0-10000), (amount * feeBps) / 10000 should be <= amount
291300
// For invalid feeBps (>10000), the contract should revert in capturePayment
@@ -301,7 +310,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
301310
// ============================================
302311
/// @notice Invariant: Fee calculation cannot cause underflow
303312
/// @dev merchantAmount = captureAmount - feeAmount should always be >= 0
304-
function echidna_no_underflow_in_merchant_payment() public view returns (bool) {
313+
function echidna_no_underflow_in_merchant_payment() public returns (bool) {
305314
uint256 captureAmount = 1000 ether;
306315
// Test various fee percentages
307316
for (uint16 feeBps = 0; feeBps <= 10000; feeBps += 500) {
@@ -344,18 +353,17 @@ contract EchidnaERC20CommerceEscrowWrapper {
344353
// ============================================
345354
// INVARIANT 4: Accounting Bounds
346355
// ============================================
347-
/// @notice Invariant: Total supply of test token should never decrease (except explicit burns)
348-
/// @dev Detects any unexpected token loss
349-
function echidna_token_supply_never_decreases() public view returns (bool) {
356+
/// @notice Invariant: Total supply of test token should never decrease
357+
/// @dev Detects any unexpected token loss (supply only increases via mints, never burns)
358+
function echidna_token_supply_never_decreases() public returns (bool) {
350359
uint256 currentSupply = token.totalSupply();
351-
// Supply should be at least the initial minted amount
352-
uint256 minExpectedSupply = 30000000 ether; // 3 accounts * 10M each
353-
return currentSupply >= minExpectedSupply;
360+
// Supply should equal our tracked supply (we only mint, never burn)
361+
return currentSupply == supply;
354362
}
355363

356364
/// @notice Invariant: Wrapper contract should never hold tokens permanently
357365
/// @dev All tokens should either be in escrow or returned
358-
function echidna_wrapper_not_token_sink() public view returns (bool) {
366+
function echidna_wrapper_not_token_sink() public returns (bool) {
359367
// The wrapper itself should not accumulate tokens
360368
// (tokens go to escrow, merchant, or fee receiver)
361369
uint256 wrapperBalance = token.balanceOf(address(wrapper));
@@ -369,13 +377,13 @@ contract EchidnaERC20CommerceEscrowWrapper {
369377

370378
/// @notice Invariant: Total captured should never exceed total authorized
371379
/// @dev This ensures we can't capture more than we've authorized
372-
function echidna_captured_never_exceeds_authorized() public view returns (bool) {
380+
function echidna_captured_never_exceeds_authorized() public returns (bool) {
373381
return totalCaptured <= totalAuthorized;
374382
}
375383

376384
/// @notice Invariant: Fee calculation in practice never causes underflow
377385
/// @dev Merchant should always receive a non-negative amount
378-
function echidna_merchant_receives_nonnegative() public view returns (bool) {
386+
function echidna_merchant_receives_nonnegative() public returns (bool) {
379387
// Check merchant's balance never decreases inappropriately
380388
// Merchant balance should be >= 0 (trivially true for uint256, but checks for logic errors)
381389
uint256 merchantBalance = token.balanceOf(MERCHANT);
@@ -384,7 +392,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
384392

385393
/// @notice Invariant: Fee receiver accumulates fees correctly
386394
/// @dev Fee receiver should only get tokens from fee payments
387-
function echidna_fee_receiver_only_gets_fees() public view returns (bool) {
395+
function echidna_fee_receiver_only_gets_fees() public returns (bool) {
388396
// Fee receiver balance should be reasonable relative to total captures
389397
uint256 feeReceiverBalance = token.balanceOf(FEE_RECEIVER);
390398
// Fees can't exceed all captured amounts (max 100% fee)
@@ -393,7 +401,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
393401

394402
/// @notice Invariant: Token conservation law
395403
/// @dev Total supply should equal sum of all account balances
396-
function echidna_token_conservation() public view returns (bool) {
404+
function echidna_token_conservation() public returns (bool) {
397405
uint256 supply = token.totalSupply();
398406
uint256 accountedFor = token.balanceOf(address(this)) +
399407
token.balanceOf(PAYER) +
@@ -409,7 +417,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
409417

410418
/// @notice Invariant: Escrow should not hold tokens after operations complete
411419
/// @dev Tokens should flow through escrow, not accumulate
412-
function echidna_escrow_not_token_sink() public view returns (bool) {
420+
function echidna_escrow_not_token_sink() public returns (bool) {
413421
uint256 escrowBalance = token.balanceOf(address(mockEscrow));
414422
// Escrow may hold tokens temporarily, but shouldn't accumulate excessively
415423
// Allow up to total authorized amount (worst case all authorized, none captured/voided)
@@ -422,15 +430,15 @@ contract EchidnaERC20CommerceEscrowWrapper {
422430

423431
/// @notice Invariant: Payment reference counter only increases
424432
/// @dev Counter should be monotonically increasing
425-
function echidna_payment_ref_counter_monotonic() public view returns (bool) {
433+
function echidna_payment_ref_counter_monotonic() public returns (bool) {
426434
// Counter should never decrease
427435
// We track this implicitly - if counter decreased, we'd have collisions
428436
return paymentRefCounter >= 0; // Always true, but documents the property
429437
}
430438

431439
/// @notice Invariant: Mock escrow state consistency
432440
/// @dev For any payment, capturableAmount + refundableAmount should have sensible bounds
433-
function echidna_escrow_state_consistent() public view returns (bool) {
441+
function echidna_escrow_state_consistent() public returns (bool) {
434442
// Check a few recent payments for state consistency
435443
if (paymentRefCounter == 0) return true;
436444

@@ -461,7 +469,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
461469

462470
/// @notice Invariant: Operator authorization is respected
463471
/// @dev Only designated operators should be able to capture/void
464-
function echidna_operator_authorization_enforced() public view returns (bool) {
472+
function echidna_operator_authorization_enforced() public returns (bool) {
465473
// This is enforced by modifiers in the wrapper
466474
// We verify the modifier exists by checking operator field is set
467475
if (paymentRefCounter == 0) return true;
@@ -481,7 +489,7 @@ contract EchidnaERC20CommerceEscrowWrapper {
481489

482490
/// @notice Invariant: Fee basis points are validated
483491
/// @dev Captures with invalid feeBps should always revert
484-
function echidna_fee_bps_validation_enforced() public view returns (bool) {
492+
function echidna_fee_bps_validation_enforced() public returns (bool) {
485493
// This property is enforced by the wrapper's InvalidFeeBps check
486494
// We test it by ensuring our driver respects the bounds
487495
// The wrapper should never allow feeBps > 10000

0 commit comments

Comments
 (0)