You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add real cross-implementation diff and genuine migration verification (#226) (#745)
Issue #226 asked for differential testing between contract
implementations, but the existing framework (commit 0169685) only
diffed hello-world against a second instance of itself, despite its
own doc comment claiming to compare "hello-world vs lending". This
strengthens both real gaps found on review:
- Add a ContractAdapter trait and a genuine LendingAdapter (wrapping
the separate `lending` contract crate) in diff_harness.rs, and a
new hello_world_vs_lending_test.rs that runs the same deposit/
zero-amount/position-reflects-balance checks across both
implementations. Borrow/repay/withdraw are intentionally NOT
cross-diffed — hello-world borrows against existing collateral,
while lending's borrow atomically deposits new collateral and
borrows in one call, requiring collateral_amount > 0 every time.
Forcing a 1:1 comparison there would misrepresent what's tested;
documented under "Known Structural Differences".
- Replace the placebo migration test (which only re-created a client
handle to an untouched contract) with a real one: lending's own
migration_verification_test.rs now drives the actual UpgradeManager
governance lifecycle (propose -> approve -> queue timelock ->
advance ledger -> execute, and separately execute -> rollback) and
asserts a live lending position is untouched by it. Neither
hello-world nor lending actually swaps WASM code anywhere in this
repo (UpgradeManager only tracks an approved hash/version in
storage), so a true WASM-swap migration test isn't feasible without
a separate compiled .wasm artifact and build step -- documented as
a known limitation rather than faked.
- Fix an unrelated but blocking bug found while touching this crate:
hello-world/src/tests/mod.rs declared four prop-test modules
(prop_arithmetic_test, prop_interest_test, prop_liquidation_test,
prop_deposit_test) whose source files were never created for this
crate -- a hard compile error blocking the entire crate, including
the differential tests. Removed the phantom declarations (the real
equivalents already exist in the `lending` crate under different
names).
- Wire the new tests into the CI differential-test step.
Note: this environment has no Rust/cargo toolchain, so none of this
could be compiled or run locally -- reviewed carefully against
existing, working test code in both crates instead. CI will be the
first real compile.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: stellar-lend/contracts/hello-world/DIFFERENTIAL_TEST_REPORT.md
+34-9Lines changed: 34 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,28 +2,36 @@
2
2
3
3
## What This Tests
4
4
5
-
Differential testing runs the **same inputs against two independent contract instances** and asserts their outputs are identical. This catches subtle behavioral regressions that unit tests miss — especially after upgrades or refactors.
5
+
Differential testing runs the **same inputs against two independent contract implementations** and asserts their outputs are identical. This catches subtle behavioral regressions that unit tests miss — especially after upgrades or refactors.
6
+
7
+
Two flavors are covered:
8
+
9
+
1.**Same implementation, two instances** (`differential_test.rs`) — regression guard: two fresh `hello-world` instances must always agree.
10
+
2.**Genuinely different implementations** (`hello_world_vs_lending_test.rs`) — compares `hello-world` against the separate `lending` contract crate via a shared `ContractAdapter` trait.
|`src/tests/hello_world_vs_lending_test.rs`| Cross-implementation comparison: `hello-world` vs `lending` (deposit only — see below) |
19
+
|`src/tests/migration_verification_test.rs`| hello-world: storage read-back sanity across a re-created client handle (weak — see "Known Structural Differences") |
20
+
|`../lending/src/migration_verification_test.rs`| lending: drives the *real*`UpgradeManager` governance lifecycle (propose → approve → queue timelock → execute/rollback) and confirms it doesn't disturb lending's own application state |
14
21
15
22
## Running Locally
16
23
17
24
```bash
18
25
cd stellar-lend
19
-
# All differential tests
20
26
cargo test --package hello-world --lib tests::differential_test -- --nocapture
27
+
cargo test --package hello-world --lib tests::hello_world_vs_lending_test -- --nocapture
21
28
cargo test --package hello-world --lib tests::migration_verification_test -- --nocapture
29
+
cargo test --package stellarlend-lending --lib migration_verification_test -- --nocapture
22
30
```
23
31
24
32
## How Divergences Are Reported
25
33
26
-
If two instances return different results for the same input, the test panics with:
34
+
If two instances/implementations return different results for the same input, the test panics with:
27
35
28
36
```
29
37
[DIVERGENCE] deposit: v1=Ok(true) v2=Err(())
@@ -36,13 +44,30 @@ If two instances return different results for the same input, the test panics wi
36
44
|---|---|
37
45
| Non-deterministic behavior | Ledger timestamp pinned via `env.ledger().set_timestamp()` before each test |
38
46
| State-dependent outputs | Tests run full sequences: deposit → borrow → repay → check position |
39
-
| Zero-amount inputs | Explicit test asserting both instances reject consistently |
40
-
| Storage layout across upgrades |`migration_verification_test.rs` reads raw storage keys via `env.as_contract()`|
47
+
| Zero-amount inputs | Explicit tests asserting both same-implementation instances*and* both cross-implementation contracts reject consistently |
48
+
| Storage layout across upgrades |`migration_verification_test.rs`(hello-world) reads raw storage keys via `env.as_contract()`; `migration_verification_test.rs` (lending) drives the real `UpgradeManager` state machine including its 48h timelock|
41
49
| Multiple users | Multi-user migration test with 5 users and distinct amounts |
50
+
| Performance differences | Not covered — no benchmark comparison between implementations yet |
51
+
52
+
## Known Structural Differences (why some operations aren't cross-implementation-diffed)
53
+
54
+
`hello-world` and `lending` have genuinely different domain models, discovered while building the cross-implementation harness:
55
+
56
+
-**Borrow**: `hello-world.borrow_asset` borrows against previously-deposited collateral. `lending.borrow` is atomic — it deposits new collateral *and* borrows in the same call, and rejects `collateral_amount <= 0`. There is no way to call it "borrow only, against existing collateral" the way hello-world does, so a literal same-inputs comparison would require synthetically inventing a matching action for one side, which would test the harness's own workaround rather than the contracts. **Not compared.**
57
+
-**Asset model**: `hello-world` takes `asset: Option<Address>` (single/native asset). `lending` requires `asset: Address` on every call (multi-asset). The `ContractAdapter` trait picks one fixed `Address` per adapter instance to keep `deposit` comparable.
58
+
-**Position shape**: `hello-world::Position { collateral, debt }` vs `lending::UserPositionSummary { collateral_balance, debt_balance, collateral_value, debt_value, health_factor }`. Only the two directly-equivalent raw balance fields are compared; value/health-factor fields depend on an oracle neither adapter configures.
59
+
60
+
If `lending`'s API changes to make borrow/repay/withdraw genuinely comparable (e.g. a non-atomic borrow-against-existing-collateral entry point is added), extend `ContractAdapter` and `hello_world_vs_lending_test.rs` accordingly.
61
+
62
+
## Known Limitation: No Real WASM-Swap Migration Test
63
+
64
+
Neither `hello-world` nor `lending` currently exposes a real "upgrade this contract's code" entry point — `UpgradeManager` (`common/src/upgrade.rs`, used by `lending`/`amm`/`bridge`) only tracks an *approved* WASM hash + version in storage; it never calls Soroban's `env.deployer().update_current_contract_wasm(..)`. Genuinely testing storage-layout survival across a real code upgrade would require compiling and checking in a separate `.wasm` artifact for an "old" version and loading it via `env.register_contract_wasm(..)` — a build-pipeline addition, not a test-code change, and out of scope here. `lending/src/migration_verification_test.rs` instead verifies the real, available claim: driving the actual governance lifecycle to completion (and to rollback) does not disturb a live position. hello-world's own `migration_verification_test.rs` is weaker (it only re-creates a client handle to an untouched contract) because hello-world doesn't integrate `UpgradeManager` at all.
65
+
66
+
Separately, `scripts/migration-simulator` and `environments/migration-sandbox` already provide real migration dry-run tooling against a forked network — that's operational tooling, not part of this Rust unit-test suite.
42
67
43
68
## CI Integration
44
69
45
-
Differential tests run as a dedicated CI step in `.github/workflows/ci-cd.yml` and upload `differential-test-report.txt` as an artifact on every push/PR. A failure here means a behavioral regression was introduced.
70
+
Differential and migration-verification tests (both crates) run as a dedicated CI step in `.github/workflows/ci-cd.yml` and upload `differential-test-report.txt` as an artifact on every push/PR. A failure here means a behavioral regression was introduced.
0 commit comments