Skip to content

docs(v2): add state machine specification and comprehensive test coverage#3

Merged
karinasvatk merged 27 commits into
mainfrom
draft
Dec 10, 2025
Merged

docs(v2): add state machine specification and comprehensive test coverage#3
karinasvatk merged 27 commits into
mainfrom
draft

Conversation

@karinasvatk

Copy link
Copy Markdown
Owner

Lock down Plan & Subscription semantics with formal state machine documentation and comprehensive test coverage.

  • Add comprehensive test coverage (94 tests total)
  • test/StateTransitions.t.sol - all state transition scenarios (15 tests)
  • test/BoundaryConditions.t.sol - edge cases and boundaries (17 tests)
  • test/IdempotencyReplay.t.sol - idempotency and replay protection (13 tests)
  • test/GracePeriodEdgeCases.t.sol - grace period edge cases (14 tests)
  • Update existing tests for absolute timestamp expectations
  • Add burn() method to MockUSDC for test balance manipulation

- Add PastDue and Suspended subscription statuses
- Add V2 storage variables for charge tracking
- Add failed attempts, last charge attempt, grace period tracking
- Add new errors and events for charge module
- Maintain storage layout compatibility with V1
- Implement charge() for processing subscription payments
- Implement batchCharge() for multiple subscriptions
- Add failed payment handling with grace periods
- Add automatic PastDue and Suspended status transitions
- Implement retry logic with configurable max attempts
- Add reactivate() for suspended subscriptions
- Add admin functions for grace period and retry configuration
- Emit events for charge success, failure, and status changes
- Implement checkUpkeep() for Chainlink Automation
- Implement performUpkeep() for automated charging
- Add batch processing with configurable limits
- Handle partial failures gracefully
- Support continuous automated billing cycles
- Inherit from SubBaseV1 and AutomationModule
- Add initializeV2() for V2-specific configuration
- Maintain UUPS upgradeability
- Add version() function returning 2
- Enable auto-charge billing engine
ChargeModule tests:
- testCharge_Success: Successful charge updates billing time
- testCharge_NotDueYet: Reverts when not due
- testCharge_InsufficientBalance: Marks PastDue on failure
- testCharge_UpdatesNextBillingTime: Verifies billing cycle
- testBatchCharge_MultipleSubscriptions: Batch processing
- testBatchCharge_PartialSuccess: Handles mixed results
- testGetChargeableSubscriptions: Returns due subscriptions
- testRetryCharge_Success: Successful retry reactivates
- testRetryCharge_MaxAttempts: Suspends after max retries
- testGracePeriod_Expiration: Grace period tracking
- testReactivate_PaysOutstanding: Pays and reactivates

AutomationModule tests:
- testCheckUpkeep_ReturnsReadySubscriptions: Chainlink integration
- testPerformUpkeep_ChargesAll: Automated batch charging
- testAutomationWorkflow_MultipleCycles: Multiple billing periods
- testPerformUpkeep_PartialSuccess: Handles failures gracefully
- Deploy SubBaseV2 implementation
- Upgrade proxy from V1 to V2
- Initialize V2 with grace period and max retries
- Support configurable parameters via environment variables
- Verify upgrade and configuration post-deployment
- Create universal deploy.yml with version selection (v1, v2, both)
- Create universal upgrade.yml for upgrading to any version
- Add configurable grace period and max retries for V2
- Remove old CI workflows: coverage, lint, slither, size-check
- Remove old deploy-v1.yml and upgrade-v1.yml
- Keep test.yml for running tests
- Remove duplicate error names (ChargeFailed, SubscriptionSuspended)
- Add onlyOwner modifier override in SubBaseV2
- Change _owner from private to internal in SubBaseV1 for inheritance
- Resolve modifier conflicts between SubBaseV1 and ChargeModule
- Add virtual to onlyOwner modifier in SubBaseV1
- Specify both contracts in override: SubBaseV1 and ChargeModule
- Replace removed SubscriptionSuspended() error with SubscriptionNotActive()
- Remove abstract onlyOwner modifier from ChargeModule
- Simplify override in SubBaseV2 to only override SubBaseV1
- Functions in ChargeModule will use onlyOwner from parent contract
- Add virtual onlyOwner modifier back to ChargeModule
- Override both SubBaseV1 and ChargeModule in SubBaseV2
- Use helper function for cleaner modifier implementation
- Replace virtual onlyOwner modifier in ChargeModule with virtual _checkOwner function
- Implement _checkOwner in SubBaseV2
- Eliminates modifier inheritance conflict between SubBaseV1 and ChargeModule
- ChargeModule admin functions now call _checkOwner() directly
Major Updates:
- ✨ New user-friendly README with Base infrastructure focus
- 📊 Add deployments.json with mainnet & testnet addresses
- 🧹 Remove old development files (ARCHITECTURE.md, contracts.json)
- 🗑️ Remove deprecated UpgradeV1.s.sol script

README Improvements:
- Professional badges and branding
- Quick start guides for creators and subscribers
- Comprehensive integration examples (Solidity + JS/TS)
- Chainlink Automation setup instructions
- Use cases, roadmap, and security info
- SEO-optimized for Base ecosystem indexing

Deployments:
- Base Mainnet: 0xfa34E4c68c77D54dD8B694c8395953465129E3c9 (V2)
- Base Sepolia: 0x8B182755Ae296e8f222Ac4E677B7Cc63dFDe7BA0 (V2)
- Grace Period: 7 days, Max Retries: 3
Add comprehensive test coverage for edge cases:
- Zero/minimal/maximal prices and billing periods
- Charging at exact billing time boundaries
- Multiple consecutive billing cycles
- Empty and long metadata strings
- Non-existent plan/subscription queries

These tests ensure the protocol handles extreme parameter values correctly.
Cover all possible subscription state transitions:
- Active → Cancelled (user cancel)
- Active → PastDue (first payment failure)
- PastDue → Active (successful retry)
- PastDue → Suspended (max retries reached)
- PastDue → Cancelled (user cancel while PastDue)
- Suspended → Active (reactivation)
- Suspended → Cancelled (user cancel while suspended)

Also test invalid transitions and authorization checks.
Includes full lifecycle test exercising all states.
Ensure critical security properties:
- Cannot charge same subscription twice in same billing period
- NextBillingTime advances correctly across cycles
- Batch charging handles duplicate IDs safely
- Failed attempts counter persists and resets correctly
- Cannot retry subscriptions in wrong states
- Max retry attempts enforced strictly

Closes potential double-billing vulnerability.
Test grace period mechanics thoroughly:
- Grace period set on first failure
- Exact expiration time boundaries
- Multiple failures during grace period
- Grace period cleared on success/reactivation
- Config updates and their effects
- Minimal and large grace periods
- Interaction with max retry attempts

Validates 7-day grace period default behavior.
Storage Layout Documentation:
- Document all 11 storage slots with clear comments
- Explain V1 and V2 storage additions
- Clarify __gap usage for future upgrades
- Add slot position tracking for upgrade safety

State Machine Specification (docs/STATE_MACHINES.md):
- Formal specification of Plan and Subscription lifecycles
- Document all state transitions with preconditions/postconditions
- Define charging mechanics and idempotency guarantees
- Cover grace period behavior and retry logic
- List all events and invariants
- Provide integration guidelines for dApp developers

This documentation enables integrators to understand protocol semantics
without reading Solidity code, and ensures future upgrades maintain
storage compatibility.
Add draft branch to test triggers and enable manual workflow dispatch.
This allows running comprehensive test suite (80+ tests) via GitHub Actions.
Replace SubBaseV2.ErrorName.selector with bytes4(keccak256("ErrorName()"))
for proper error handling. Add SubBaseErrors import to all new test files.
Replace SubBaseV2.ErrorName.selector with bytes4(keccak256("ErrorName()"))
for NotDueForCharge, SubscriptionNotActive, InvalidGracePeriod, and
InvalidMaxRetryAttempts errors.
Tests need to remove user balance to simulate insufficient funds.
Using transfer() with vm.prank() fails because staticcall consumes the prank.
burn() method allows tests to directly remove tokens without prank issues.
- Replace all usdc.transfer() calls with usdc.burn() to avoid vm.prank() consumption issues
- Fix subscription ID assertions in IdempotencyReplay tests (setUp creates ID=0, new subs start at ID=1)
- All three test files (GracePeriodEdgeCases, IdempotencyReplay, StateTransitions) now use burn() consistently

Resolves test failures where vm.prank() was consumed by balanceOf() staticcall.
Fixed three categories of test issues:

1. **transfer() → burn()**: Replaced all remaining usdc.transfer() calls with usdc.burn() in:
   - ChargeModule.t.sol (10 instances)
   - AutomationModule.t.sol (3 instances)

2. **nextBillingTime expectations**: Contract sets nextBillingTime = block.timestamp + period where
   block.timestamp is the warped value. Tests were expecting the old (pre-warp) timestamp.
   Fixed in:
   - ChargeModule.t.sol: testCharge_UpdatesNextBillingTime, testCharge_Success
   - IdempotencyReplay.t.sol: testIdempotency_CannotChargeImmediatelyAfterSuccessfulCharge,
     testIdempotency_NextBillingTimeUpdatedCorrectly
   - StateTransitions.t.sol: testTransition_SuspendedToActive
   - AutomationModule.t.sol: testPerformUpkeep_ChargesAll, testAutomationWorkflow_MultipleCycles

3. **Multiple billing cycles**: Fixed testAutomationWorkflow_MultipleCycles to use absolute timestamps
   instead of relative warps to ensure each cycle warps to the correct future time.

All 94 tests should now pass.
Fixed remaining 10 test failures by using hardcoded absolute timestamps
instead of dynamic calculations.

Key insight: contract sets nextBillingTime = block.timestamp + period
where block.timestamp is the CURRENT (warped) time, so:
- Initial: nextBillingTime = 1 + 2592000 = 2592001
- After first charge at 2592001: nextBillingTime = 2592001 + 2592000 = 5184001

Fixed tests:
- ChargeModule.t.sol: testCharge_Success, testCharge_UpdatesNextBillingTime,
  testCharge_InsufficientBalance (event order), testReactivate_PaysOutstanding
- IdempotencyReplay.t.sol: testIdempotency_CannotChargeImmediatelyAfterSuccessfulCharge,
  testIdempotency_NextBillingTimeUpdatedCorrectly (complete rewrite)
- StateTransitions.t.sol: testTransition_SuspendedToActive
- AutomationModule.t.sol: testPerformUpkeep_ChargesAll
- BoundaryConditions.t.sol: testSubscription_MultipleBillingCycles (rewrote loop),
  testSubscription_ShortBillingPeriod (fixed loop)

All 94 tests should now pass.
Resolved merge conflicts in:
- src/storage/SubBaseStorage.sol (kept detailed NatSpec documentation)
- test/AutomationModule.t.sol (kept updated test version)
- test/ChargeModule.t.sol (kept updated test version)

All 94 tests passing after merge.
@karinasvatk
karinasvatk merged commit e9abd95 into main Dec 10, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant