Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
df2c6ec
Merge pull request #119 from Blockchain-Powered-eSIM/main
ManulParihar Aug 18, 2026
e33b156
Add the custom errors for USD pricing and settlement records
ManulParihar Aug 28, 2026
dfd09c0
Price data bundles in USD cents and record how each one was paid for
ManulParihar Aug 28, 2026
a9e7c7f
Require a non-zero cents ceiling when the registry is initialised
ManulParihar Aug 28, 2026
1f498f8
Deploy the payment adapter and wire it to the registry
ManulParihar Aug 28, 2026
a2957ea
Make USD cents the only price ceiling
ManulParihar Aug 28, 2026
46a2b53
Move the test suite onto cents, bytes32 ids and payment references
ManulParihar Aug 28, 2026
2aa87ea
Refresh the gas baselines for the cents pricing path
ManulParihar Aug 28, 2026
09fe5ab
Cut the comments back to plain language
ManulParihar Aug 28, 2026
0cf13c3
Test the currency table, cent conversion and payment references
ManulParihar Aug 28, 2026
017fb19
Hold settled purchases to the same price ceiling as bought ones
ManulParihar Aug 28, 2026
96c0836
Test settled purchases, the ordering guard and reference reuse
ManulParihar Aug 28, 2026
54a4261
Cap how many decimals a currency may be registered with
ManulParihar Aug 28, 2026
dd8d7f4
Fuzz the cent conversion over every price and every currency
ManulParihar Aug 28, 2026
c970653
Add a payment handler and the two payment invariants
ManulParihar Aug 28, 2026
8fde15f
Verify the currency table and the payment references
ManulParihar Aug 28, 2026
38b1ddc
Record the prover job the adapter rules were verified at
ManulParihar Aug 28, 2026
4d29fb6
Measure the payment path and refresh the gas baselines
ManulParihar Aug 28, 2026
0be794f
Baseline the adapter's storage and pin the packed slots
ManulParihar Aug 28, 2026
8beadd0
Record what a deployed proxy reads after the ceiling moved
ManulParihar Aug 28, 2026
d441737
Register the two currencies at configure time
ManulParihar Aug 28, 2026
5ffb733
Drop declarations nothing reaches
ManulParihar Aug 28, 2026
cf69073
Let an eSIM wallet pull tokens from its device wallet
ManulParihar Aug 28, 2026
50e8526
Move tokens to the vault from the payment adapter
ManulParihar Aug 28, 2026
2fcb98c
Buy a data bundle with an ERC-20
ManulParihar Aug 28, 2026
efe580f
Give the suite real tokens to move
ManulParihar Aug 28, 2026
ecc382c
Cover pulling, settling and buying with a token
ManulParihar Aug 28, 2026
b2bf754
Drive the token path through the invariant campaign
ManulParihar Aug 28, 2026
918e666
Sweep settle across every price and currency
ManulParihar Aug 28, 2026
1575bdc
Measure the token path and refresh the baselines
ManulParihar Aug 28, 2026
8ebcf70
Regenerate the contract docs
ManulParihar Aug 28, 2026
d4587b3
Act on the static analysis of the token path
ManulParihar Aug 28, 2026
07714bd
Say spending rather than pulling ETH where one flag covers both
ManulParihar Aug 28, 2026
5c2e019
Correct two comments on the token path
ManulParihar Aug 28, 2026
7314ca9
Hand the payment adapter to the timelock too
ManulParihar Aug 28, 2026
d2ce4dc
Check the adapter's owner in the fork rehearsal
ManulParihar Aug 28, 2026
f56c107
Put the payment adapter in the README
ManulParihar Aug 28, 2026
d00c3ae
Say what settle really does with a caller-declared amount
ManulParihar Aug 28, 2026
6605df8
Record the prover run behind the settle rules
ManulParihar Aug 29, 2026
07bede1
Refresh the gas baseline after the guard reorder
ManulParihar Aug 29, 2026
cbd64b7
Give the rehearsal the config the deploy actually reads
ManulParihar Aug 29, 2026
fd6bdbe
Let hardhat run without a deployer key
ManulParihar Aug 29, 2026
4d452c0
Regenerate the contract docs
ManulParihar Aug 29, 2026
abea3ec
Say which key hardhat reads
ManulParihar Aug 29, 2026
e351a98
Retire the ETH purchase path
ManulParihar Aug 29, 2026
3065956
Move the tests onto the token path
ManulParihar Aug 29, 2026
00c65ab
Drop the ETH pull rule from the wallet spec
ManulParihar Aug 29, 2026
7047573
Refresh the baselines and docs after the removal
ManulParihar Aug 29, 2026
6fecc16
Bring the README up to date with the token path
ManulParihar Aug 29, 2026
6ff7168
Let a run point the scripts at a different record
ManulParihar Aug 29, 2026
c4deeae
Split the deploy config checks from reading the environment
ManulParihar Aug 29, 2026
926d394
Cover the deploy scripts and their failure branches
ManulParihar Aug 29, 2026
aaa21b8
Run the deploy script tests in CI
ManulParihar Aug 29, 2026
a794193
Keep the rehearsal out of the real deployment record
ManulParihar Aug 29, 2026
7b757f8
Document the deploy script suite and why it runs alone
ManulParihar Aug 29, 2026
cfdb046
Name the two test files after what they now cover
ManulParihar Aug 29, 2026
f6ffdc6
Fix two event name typos to match sibling naming
ManulParihar Aug 29, 2026
c908b86
Update stale payment wording in comments
ManulParihar Aug 29, 2026
b722f4f
Remove dead debug output and unused imports from the test harness
ManulParihar Aug 29, 2026
fe644a6
Tidy formatting and comment phrasing in a few mocks
ManulParihar Aug 29, 2026
f0f17bb
Harden the two payment paths against replay, front-running and short …
ManulParihar Aug 31, 2026
ff88e9f
Say what the standby flag and the price ceiling actually bound
ManulParihar Aug 31, 2026
55313ce
Hold the funded lazy deployment while the protocol is paused
ManulParihar Aug 31, 2026
81c18ac
Say what the ownership handover event does and does not prove
ManulParihar Aug 31, 2026
6c26c1a
Say why the history backfill is not held to the price ceiling
ManulParihar Aug 31, 2026
07a64af
Add Echidna and Medusa stateful fuzz suite
ManulParihar Aug 31, 2026
5047324
Merge dev: the v0.8 Base Sepolia deployment record and its timelock h…
ManulParihar Aug 31, 2026
eb548f4
Trim comments that only restated the code beneath them
ManulParihar Sep 1, 2026
8796b20
Fix false AlreadyDeployed revert in fork rehearsal
ManulParihar Sep 1, 2026
ba77f71
Fix NatSpec gaps: missing tags, wrong return name, missing inheritdoc
ManulParihar Sep 1, 2026
af6e70f
Add missing @notice on constructors and @return on simple getters
ManulParihar Sep 1, 2026
cc41080
Remove stray character breaking compilation in ESIMWallet.sol
ManulParihar Sep 1, 2026
32b6b69
Regenerate NatSpec docs to match recent contract comment fixes
ManulParihar Sep 1, 2026
00c321d
Remove funding.json
ManulParihar Sep 1, 2026
a5564cf
Fix stale README doc links and interfaces description
ManulParihar Sep 1, 2026
1c2e964
Update stale test count in README
ManulParihar Sep 1, 2026
77fcb2e
Updated storage layout for Registry
ManulParihar Sep 1, 2026
7b70d50
Merge pull request #120 from Blockchain-Powered-eSIM/upgrade/payment-…
ManulParihar Sep 1, 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
1,068 changes: 598 additions & 470 deletions .gas-snapshot

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ jobs:
forge test --via-ir -vvv
id: test

# The deploy scripts, which the fork rehearsal covers better but which it cannot run here:
# that needs an RPC key and a live anvil. These reach the failure and resume branches
# instead. They run one at a time because they share one record file and one process
# environment, so `--threads 1` is not optional.
- name: Run deploy script tests
run: |
FOUNDRY_PROFILE=scripts forge test --via-ir --threads 1 -vvv
id: script-test

# Fuzz and invariant runs draw a fresh seed each time, and the malleable twin case verifies a
# signature generated offchain per run, so all three report a different figure every time.
# They are left out of the baseline rather than given a tolerance, so what remains is checked
Expand Down
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ node_modules/
artifacts/
cache/
out/
out-fuzz/
forge-cache/

# Written by every `forge script --broadcast` run. The deployment record kept on purpose is
# deployments/address.json, which the scripts write and later scripts read.
broadcast/

# Scratch records the deploy script tests write. They live here because fs_permissions grants the
# scripts write access to this directory and nowhere else.
deployments/.test-*.json
lcov.info
.DS_Store

Expand All @@ -19,3 +24,8 @@ lcov.info
# Editor settings are per developer. The remappings inside are generated from remappings.txt
# anyway, so a stale copy here is worse than no copy.
.vscode/

# What the fizz skill generates: contract metadata, entry-point selection, cost estimates,
# coverage targets, campaign reports. Regenerated by the skill, kept local rather than committed.
fizz_data/
crytic-export/
145 changes: 145 additions & 0 deletions PROPERTIES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Properties

What the Echidna and Medusa campaign in `test/fizz/` asserts about the protocol.

Each entry carries a **Guarantee**: `SHOULD-HOLD` means docs, a single-writer guard or an exact
identity say it must be true, and a violation is a bug. `EXPLORATORY` means it was inferred, and a
violation is a lead for a person to judge rather than a confirmed defect.

Run them with `medusa fuzz` or `echidna . --contract FuzzTester --config echidna.yaml`.

## Global

Checked after every call in a sequence. These iterate the wallets the campaign has built, so they
stay O(n) in the population and never O(n squared).

- [x] **GL-01 — an eSIM wallet's purchase history is append-only, entry for entry.**
Once an entry is seen at index `i`, its id, price and settlement never change again, and the list
never shortens. SHOULD-HOLD: all three writers push and nothing indexes or deletes. This one is
here because the repo's own notes record a mutation that made a purchase overwrite entry zero and
nothing in the suite caught it, unit tests included.

- [x] **GL-02 — the history copy cursor never runs backwards or past the end.**
`historyEntriesCopied[id]` never decreases, and never exceeds the stored history length reached
through the live device redirect. SHOULD-HOLD: I-8, single writer bounded by `outstanding`.

- [x] **GL-03 — the deployment cursor never runs backwards or past the end.**
`eSIMWalletsDeployed[deviceId]` never decreases and never exceeds that device's associated
identifier count. SHOULD-HOLD: I-9, both writers clamped by `_boundedBatchSize`.

- [x] **GL-04 — a device's identifier list freezes once it has a wallet.**
After `isDeviceIdentifierAlreadyUsed` first reads true, the associated identifier list never
changes length again. SHOULD-HOLD: the only two writers refuse a deployed device. This is the
mechanism GL-03 depends on, checked separately so a break localises.

- [x] **GL-05 — an eSIM wallet's registration is never revoked.**
Once `isESIMWalletValid[w]` is non-zero it never returns to zero; only the named holder changes.
SHOULD-HOLD: I-6, `bindESIMWallet` is the sole writer and no path zeroes it.

- [x] **GL-06 — a spent payment reference never un-spends.**
For any `(wallet, reference)` the campaign has spent, `usedPaymentReferences` stays true, across
an adapter rotation. SHOULD-HOLD: I-4 as corrected, one guarded writer on `Registry`.

- [x] **GL-07 — one P256 key names at most one device wallet.**
For every device wallet, the key index resolves its current key back to that same wallet.
SHOULD-HOLD: I-5, rotation deletes the old hash before writing the new one.

- [x] **GL-08 — the registry's copy of a wallet's owner key matches the wallet's own.**
SHOULD-HOLD: rotation writes both in one transaction, so they cannot split.

- [x] **GL-09 — a device identifier names one wallet, and that wallet agrees.**
SHOULD-HOLD: `_updateDeviceWalletInfo` is the sole writer and refuses an identifier already taken.
Reachable because both deploy routes draw from a shared pool of contested identifiers.

- [x] **GL-10 — an eSIM identifier is claimed once and never reassigned.**
SHOULD-HOLD: `_claimESIMIdentifier` refuses a second claim.

- [x] **GL-11 — the ceiling always binds, and nothing was ever written over it.**
The registry default is never zero, since zero reads as "no ceiling" everywhere a cap is consumed.
No purchase was ever accepted priced above the ceiling in force at the moment it was written.
SHOULD-HOLD: I-1, I-11, E-1. The per-entry half is checked at write time rather than against
today's cap, because the owner may lower the cap afterwards and that does not make an earlier
purchase illegal.

- [x] **GL-12 — settlement token is never created or destroyed.**
`totalSupply` equals the sum of every balance the harness can hold it in: the fuzzer itself, the
actors, every device wallet, every eSIM wallet, both adapters and both vaults. SHOULD-HOLD: the
mock is a plain ERC-20 with no fee or rebase, so this is an accounting identity as long as no
value escapes the accounted set.

- [x] **GL-13 — only the path that moved the money may say it did.**
The count of `Settlement.DeviceWallet` entries in a wallet's history equals the number of
successful `buyDataBundleWithToken` calls against it. SHOULD-HOLD: G-31, G-43, E-1. Every other
writer refuses that tag.

- [x] **GL-14 — the currency table stays inside its own bounds.**
Every registered asset has decimals in `[2, 36]`, and a symbol once registered never returns to
unregistered. SHOULD-HOLD: I-2, I-3, `_writeAsset` is the sole writer.

## Specific

Asserted inside the handler that makes the call, where a before-and-after comparison is the point.

- [x] **SP-01 — a purchase moves value between four addresses and creates none.**
Across one `buyDataBundleWithToken`, the combined balance of the device wallet, the eSIM wallet,
the adapter and the vault is unchanged, and the adapter's own balance ends exactly where it
started. SHOULD-HOLD: `spent + refunded == amountIn` by construction, so nothing rests on the
adapter.

- [x] **SP-02 — what the adapter spends matches an independently written formula.**
The settled amount equals `price * 10**decimals / 100` computed in the harness rather than read
back from the adapter. SHOULD-HOLD: catches a reordering of the multiply and divide that a
self-consistency check cannot.

- [x] **SP-03 — the quote is exact, monotonic, and agrees with settlement.**
`amount * 100 == price * 10**decimals` with no remainder, a higher price never quotes lower, and
`settle` spends exactly what `quote` returned for the same inputs. SHOULD-HOLD: decimals are
bounded at or above 2, so the division by 100 never truncates. X-1 covers the third part.

- [x] **SP-04 — a wallet lands where the factory said it would.**
Both factories' counterfactual address equals the address actually deployed for the same inputs.
SHOULD-HOLD: the prediction and the deployment encode the same salt and init code.

- [x] **SP-05 — a handover does not carry the old owner's ceiling.**
After `acceptOwnershipTransfer`, `priceCapUSDCents` reads zero, so the incoming owner starts on
the registry default. SHOULD-HOLD: `_secureTransferOwnership` resets it. Worth asserting because
the outgoing owner controls that cap right up to the moment it hands the wallet over.

- [x] **SP-06 — accepting ownership does not rewrite the registry association.**
`acceptOwnershipTransfer` leaves `isESIMWalletValid` naming whoever last bound the wallet.
SHOULD-HOLD: X-2. The divergence is deliberate, which is why authorization reads live `owner()`.

- [x] **SP-07 — raising or clearing standby never touches the registration.**
A call that changes `isESIMWalletOnStandby` leaves `isESIMWalletValid` alone. SHOULD-HOLD: the
two mappings are independent. This is a direct regression net for a defect this repo already
shipped once, where the flag was derived from the association. The inverse property, that one
implies the other, would be wrong.

- [x] **SP-08 — releasing and re-adding a wallet never hands back spend rights.**
A removal clears both the validity flag and funds access; a later re-add restores validity and
clears standby but leaves funds access false. SHOULD-HOLD: G-21 refuses a grant at bind time, so
access can only come from a separate owner-signed call.

- [x] **SP-09 — a deployed but unregistered device wallet can do nothing.**
Between `createAccount` and `postCreateAccount` the wallet holds code, is not valid, does not
claim its identifier, and cannot deploy an eSIM wallet. SHOULD-HOLD: `createAccount` writes no
external storage.

- [x] **SP-10 — a pause stops every path that moves value.**
While paused, `pullToken`, `buyDataBundleWithToken`, `recordSettledPurchase` and the lazy funded
deployment all revert. SHOULD-HOLD: each carries the check. The last one only recently gained it.
The two lazy siblings that move no ETH are deliberately not in this list.

## Not asserted, and why

- **Anything needing a WebAuthn signature.** `validateUserOp`, `isValidSignature` and the challenge
binding need a P256 assertion produced through `vm.ffi`, which the fuzzers cannot call. Owner-gated
calls are reached by impersonating the wallet instead. The signature path is covered by the
Foundry differential and shape suites and by a fork test through the deployed EntryPoint.
- **Native ETH conservation.** Several handlers top up a caller with `vm.deal` before a payable
call, because a pranked call is debited from the pranked account. That manufactures ETH by
cheatcode, so a sum-conservation property over ETH would be false by construction and would say
nothing about the protocol.
- **Round trips over the conversion.** There is no units-back-to-cents function, so there is no
inverse to compose with.
- **Anything about shares, liquidity, liquidation or TVL.** None of it exists here.
Loading
Loading