@@ -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