Our synthetic game has one character in a room and a movement command that proposes a second room. The rule calculation is easy. The more interesting question is which component may commit the new location and how it proves that the player still owns the character when the transition executes.

We build a small independent model of Solidity game state. Its rooms and character labels are invented and no world layout or deployed contract architecture is represented. The fixture isolates ownership checks and revision checks so adding another action does not require trusting it to rewrite every field.

Make the action propose a bounded change

A rule function reads the current state and produces a proposed change. The state writer validates that the request is authorized for the specific character and transition. Separating those jobs lets us inspect the authority boundary without giving every action unrestricted access to the complete world state.

In the fixture a movement proposal contains the character identifier and expected revision along with source and destination rooms. The commit function checks ownership and verifies that the recorded state still matches the proposal. Only then does it update the location and increase the revision.

function commitMove(caller, proposal) {
  const current = world.get(proposal.id);
  if (caller !== current.owner) throw new Error("owner");
  if (proposal.revision !== current.revision || proposal.from !== current.room)
    throw new Error("stale proposal");
  if (proposal.from === proposal.to) throw new Error("same room");
  world.set(proposal.id, {
    ...current,
    room: proposal.to,
    revision: current.revision + 1,
  });
}

The revision is an optimistic concurrency token. It prevents a command calculated from an old state from overwriting a newer move. On the EVM transactions execute sequentially but a player can still submit a transaction based on a view that changed before their transaction is mined.

Ownership is evaluated when the command executes

An interface can display a move button for the current owner and still be wrong by the time the transaction executes. An ERC721 character may change owners between the read and the transaction. The authoritative check therefore belongs in the execution path.

A location contract calling a rule contract also changes the immediate Solidity caller. The architecture needs to distinguish the player who requested an action from the contract forwarding it. Passing an original caller parameter is useful only when the receiving contract authenticates the forwarder allowed to supply that parameter.

We constrain the state writer to a declared transition because an action that calculates a valid destination should not gain permission to replace a character's owner or inventory and a component boundary becomes useful only when the permissions crossing it are narrower than the complete state that the component happens to be able to read.

The fixture does not implement a forwarding protocol. It receives a symbolic caller directly and keeps the trust assumption explicit. Contract integration tests must restore that missing boundary and reject calls that spoof the original player.

Shared state needs consistent invariants

A movement system may maintain both a character to location lookup and a list of characters in each location. Updating one representation without the other produces contradictory answers from two otherwise reasonable read methods. The writer needs to update those views as one state transition.

Our fixture stores one canonical location so the test stays small. If a second index is introduced then the model should assert that membership in the location list agrees with the canonical location after every move and after every rejected command.

Unauthorized and stale moves rejected; revisions advance on commits
8 assertions passed

The tests reject a nonowner and a stale proposal. We also simulate an ownership change before a proposed move is committed and verify that the previous owner no longer has permission. The successful movement increases the revision exactly once.

Modularity changes the review surface

Separating storage and rules introduces more interfaces and permission checks. It does not automatically make the system easier to secure. An action registry or replaceable rule implementation adds governance authority that needs explicit limits and tests for unauthorized replacement.

For Solidity we test external calls made during an action and review whether another action can observe a partially committed state. A reentrancy guard on one function does not constrain a separate entry point by itself. The state invariants must hold across the complete set of reachable calls.

An inventory transfer between characters is a useful test of the same authority boundary. Define who may initiate it and commit the debit and credit together so no action can remove an item from one inventory without completing its authorized destination change.

Sources and examples