We add a most recent contribution field to a synthetic account record and calculate its value correctly. The response object contains 13 units but the next read returns zero because the storage writer still copies only the fields it knew before the change.

Our independent example models a Solidity style separation between an engine's memory record and a vault's explicit storage setter. It uses a small JavaScript Map and named fields. The record and all quantities are constructed for this article and describe no deployed system.

A memory update is not a storage update

A Solidity storage reference can modify persistent state directly while a memory value represents a separate copy. When a contract reads a record from another contract it receives returned data and must make another authorized call to persist changes through the downstream interface.

That second boundary often contains a manual list of assignments. Adding a struct field updates the type but does not automatically update a setter that names each field individually. The compiler can accept both sides because leaving a field unchanged is valid code even when it violates the intended business transition.

function contribute(id, amount, write) {
  if (amount <= 0n) throw new Error("positive contribution required");
  const record = read(id);
  record.total += amount;
  record.recent = amount;
  write(id, record);
  return record;
}

The broken writer intentionally omits the added field. Its caller sees a locally correct result so a test asserting only the function's return value passes. A fresh read through the storage interface exposes the missing assignment.

Test the round trip through the public boundary

Our fixture first writes using the old serializer and proves that the returned record differs from the persisted record. It then uses the complete writer and reads through the storage interface again. The test checks both the cumulative total and the most recent quantity after a second contribution.

This is a contract test for the boundary between producer and persistence. The producer owns the transition that calculates the next values. The storage contract owns what it accepts and stores. Both contracts must agree on the meaning of each field as well as its type and ordering.

We need a fresh read after the write because a caller holding a correct memory object can continue computing from that object and hide an incomplete persistence operation until another transaction arrives through a different entry point and reconstructs its state from the fields that actually survived the previous transaction.

That delayed symptom can make the next operation look broken even though the original loss happened earlier. Capturing the smallest write and read sequence gives the regression a specific boundary to defend.

Decide what existing records mean

An added field starts at a default value for existing records unless a migration populates it. Zero can mean no previous contribution or unknown history and those meanings may require different behavior. A schema change should define which interpretation the application uses.

A version marker can make that distinction explicit for an offchain store. In an upgradeable Solidity contract the storage layout itself is another constraint. Adding a field to a struct used in arrays or other storage arrangements requires a layout review and cannot be justified by a passing JavaScript round trip.

Return value recent=13; incomplete storage recent=0
6 assertions passed

Our fixture uses a fixed serializer to illustrate field omission. Replacing it with a broad object spread reduces manual copying in JavaScript but does not solve every boundary. Unknown fields might need rejection and independently deployed contracts still require compatible ABI definitions.

Preserve authorization while changing the record

A complete setter must remain restricted to authorized callers. Making the storage contract accept arbitrary replacement records avoids an omitted assignment only by introducing a different trust problem. The storage layer should validate the scope of the update and maintain invariants that callers must not bypass.

We add a rejected negative contribution and verify that the stored record stays unchanged. In a Solidity suite we also test an unauthorized writer and confirm transaction rollback if a later validation fails. The fixture has no EVM and explicitly checks its input before mutation.

Two contribution transactions followed by a fresh storage read provide a compact persistence regression. Derive the expected final record from the accepted sequence so a cached response or an intermediate memory object cannot accidentally supply the answer the test was meant to verify.

Sources and examples