merca.earth Cadastre Protocol
On-Chain Land Cadastre with Trustless Overlap Prevention on Sui
Yellow Paper v0.8.0
Production Ready — March 2026
Abstract
merca.earth is an on-chain land cadastre protocol for trustless registration, spatial indexing, and overlap prevention of two-dimensional land parcels on the Sui blockchain. The protocol enforces non-overlapping property boundaries through a three-phase collision detection pipeline executed entirely during registration: a hierarchical quadtree spatial index with Morton-coded cells for broadphase culling, axis-aligned bounding box (AABB) filtering for midphase refinement, and the Separating Axis Theorem (SAT) for narrowphase overlap detection. Any parcel that has a positive-area overlap with an existing registered parcel is automatically rejected, providing a protocol-level guarantee of cadastral integrity.
All computation and state reside entirely on-chain, implemented natively in Sui Move. It requires no off-chain components, oracles, or zero-knowledge proofs for its target operating envelope. The protocol supports both simple convex parcels and complex concave (multi-part) parcels through convex decomposition, enabling realistic land property geometries.
Primary use cases: Land registries, property ownership systems, territorial claims in autonomous worlds, and any application requiring exclusive spatial rights.
Table of Contents
1. Notation and Conventions
2. System Model
2.1 Blockchain Layer
2.2 Operating Envelope
2.3 Cadastral Design Philosophy
3. Data Structures
3.1 Fixed-Point Coordinate Representation
3.2 Parcel Object (Multi-Part Support)
3.3 Index and Config
3.4 Capability Objects
3.5 Events
4. Algorithms and Lifecycle
4.1 AABB Computation
4.2 Quadtree Cell Mapping
4.3 Parcel Registration Pipeline
4.4 Overlap Semantics (Touching vs. Overlap)
4.5 Parcel Deletion
4.6 Ownership Transfer
4.7 Parcel Mutations
4.8 IPFS Metadata
5. Computational Complexity Analysis
5.1 Instruction Cost Model
5.2 Empirical Cost Bounds
6. Correctness and Security
6.1 SAT Correctness and Edge Cases
6.2 Quadtree Index Completeness
6.3 Error Handling and Invariant Breaches
Appendix A: Market V2 Mechanics and Cascading Tax
Appendix B: Evidence and Measurements
Appendix C: Roadmap and Future Extensions
Appendix D: Change Log
1. Notation and Conventions
| Symbol | Definition |
|---|---|
| Positive integers | |
| A parcel, composed of one or more convex parts | |
| Vertex set of a convex part of parcel | |
| Number of vertices of a part | |
| Axis-aligned bounding box (AABB) of | |
| The hierarchical quadtree spatial index | |
| Finest-level cell size (fixed-point units) | |
| Fixed-point scaling factor, fixed at | |
| Smallest unit of SUI: | |
| Computation Units in Sui gas metering | |
| Maximum quadtree depth, fixed at 31 | |
| Per-parcel price multiplier in parts per million () |
All coordinates are represented as unsigned 64-bit integers encoding fixed-point values with decimal precision. That is, the real-valued coordinate is stored as .
2. System Model
2.1 Blockchain Layer
The protocol targets Sui mainnet. The relevant properties leveraged by the protocol are:
Object-centric state model: All persistent state is represented as Sui objects. The Spatial Index is a shared object, enabling concurrent multi-party access. Registered parcels are stored inside the shared index state (Index.polygons) and carry a logical owner field used by lifecycle and transfer rules.
Programmable Transaction Blocks (PTB): A single transaction may contain multiple commands executed atomically. The protocol leverages PTBs to perform insertion, spatial queries, and intersection checking atomically.
Gas metering and Storage Refunds: Sui charges for computation and storage. Because cadastral data is large, storage costs dominate registration. Deleting a parcel returns a substantial storage rebate under Sui's storage model. Storage refund mechanics are an implementation detail of the network, but they materially affect lifecycle economics.
Deterministic execution: All Move executions are deterministic. The protocol uses strict integer arithmetic for all collision detection, avoiding floating-point imprecision entirely.
2.2 Operating Envelope
The protocol is optimized for realistic cadastral scenarios. While the protocol allows parameter configuration, empirical validation on testnet yields the following descriptive recommendations for stable operation:
| Parameter | Recommended Range | Practical Limit |
|---|---|---|
| Vertices per convex part | 4–8 | ≤ 12 |
| Total parts per parcel | 1–5 | ≤ 10 |
| Neighbors checked per registration | 3–8 | ≤ 16 |
| Stored quadtree cells per parcel | 1 | 1 |
Parcels fitting within this recommended envelope have measured gross registration costs in the low hundredths of SUI on testnet, with representative examples in Appendix B ranging from roughly 0.013 to 0.039 SUI depending on geometry.
2.3 Cadastral Design Philosophy
merca.earth is designed specifically for non-overlapping spatial registries where data integrity is critical.
Design Decision: Always Enforce Non-Overlapping. The protocol ALWAYS enforces non-overlapping boundaries. This is not configurable. Every parcel registration executes a full collision detection pipeline against all existing parcels in the same spatial region. If any positive-area overlap is detected, the transaction aborts.
Rationale:
- Strong Consistency Guarantee: It is impossible to register overlapping parcels.
- Gas Efficiency: Overlap checking happens exactly once (at registration time) with the cost amortized over the parcel's lifetime, rather than repeatedly checking on every query.
- Data Integrity: Protocol-level enforcement ensures smart contracts built on top of merca.earth never have to arbitrate boundary disputes.
Primary Use Cases:
- Land cadastre and property registries
- Territorial claim systems in autonomous worlds
- Geofencing with exclusive zones (e.g., spectrum allocation, maritime borders)
(Non-goals: Generic spatial indexing for overlapping datasets like heat maps or routing networks. This protocol is strictly for exclusive spatial rights.)
3. Data Structures
The core protocol defines several fundamental structures. The state is represented strictly using the mercatr core package.
3.1 Fixed-Point Coordinate Representation
Because Move lacks native signed integers and floating-point math, all coordinates and geometry use fixed-point arithmetic.
Definition 3.1 (Fixed-Point Coordinate). A coordinate value represents the real value where .
Signed Emulation: For intermediate SAT calculations (cross products, projections), signed values are emulated using a magnitude (u128) and a negative (bool) flag. All comparisons during the collision pipeline are deterministic exact-integer algebraic comparisons.
3.2 Parcel Object (Multi-Part Support)
The primary state object is the Polygon (representing a cadastral Parcel). The protocol supports both simple convex parcels and complex concave (multi-part) parcels through convex decomposition.
A Part represents a single convex polygon boundary:
- Has a sequence of fixed-point and coordinates.
- Caches a local Axis-Aligned Bounding Box (AABB).
- Must have and vertices.
- Rejects edges shorter than the protocol minimum edge length.
- Must be strictly convex (verified by testing cross-product signs of consecutive edges).
A Polygon (Parcel) aggregates 1 to parts and defines the full spatial claim:
- Caches a global AABB (union of all part AABBs).
- Derives a single depth-prefixed Morton key mapping to the quadtree spatial index.
Formal Multipart Validity Rules: To ensure a parcel represents a contiguous, well-formed land claim, it must satisfy strict structural invariants upon registration:
- Interior-Disjointness: No two distinct convex parts within the same parcel may overlap in positive area.
- Exact Shared-Edge Connectivity: The parts must form a connected graph where adjacent parts share exactly identical edges (same vertex coordinates in reverse order).
- Hole-Free Boundary: The outer boundary (edges with exactly one incident part) must form a single continuous, non-intersecting cycle, meaning the parcel contains no topological holes.
- Compactness Floor (Anti-Sliver): To prevent malicious or accidental extreme slivers (which degrade SAT performance), the parcel must pass the implementation's boundary-perimeter compactness proxy: where is the outer-boundary Manhattan perimeter, i.e. the sum of across all boundary edges.
3.3 Index and Config
The Index is a shared object acting as the spatial registry for a given cadastral layer.
cells: A map linking a depth-prefixed Morton code to a list ofIDs residing in that quadtree cell.polygons: A map resolving a parcelIDto its storedPolygonobject.config: AConfigobject specifying the safety limits for this index (e.g., max vertices per part, max parts per parcel, max quadtree depth).
The Index UID also serves as an attachment point for package-internal dynamic fields (e.g., per-parcel IPFS metadata), enabling feature extension without struct migration.
3.4 Capability Objects
Access control is managed via specific capability objects, isolating privileged logic from public exposure:
AdminCap: Minted once at deployment. Used to update configuration parameters, force-remove rogue parcels, and mint lower-level capabilities.LifecycleCap: Required to register new parcels, remove existing parcels, and process standard ownership transfers. This capability is usually held by an upstream market-layer object such asMarketV2to gate the cadastre behind economic rules.TransferCap: Required to bypass normal owner authorization and force an ownership transfer. This empowers forced-buyout markets (like Market V2) to execute sales.
3.5 Events
The protocol emits deterministic events upon state changes.
Index lifecycle events:
Registered: Emitted when a new parcel completes the collision pipeline and is inserted. Includes the natural quadtree depth of the parcel.Removed: Emitted when a parcel is deleted from the index.Transferred: Emitted when parcel ownership changes.
Mutation events:
ParcelReshaped: Emitted when a parcel's geometry is reshaped. Includes old and new area.ParcelsRepartitioned: Emitted when two adjacent parcels are repartitioned.ParcelRetired: Emitted when a parcel is destroyed (split or merge absorb).ParcelSplit: Emitted when a parent parcel is split into children. Includes child ID list.ParcelsMerged: Emitted when two parcels are merged. Includes keep and absorbed IDs.
Metadata events:
MetadataSet: Emitted when IPFS metadata is set or updated on a parcel. Includes polygon_id, owner, CID string, and epoch.MetadataRemoved: Emitted when IPFS metadata is removed from a parcel. Includes polygon_id and owner.
4. Algorithms and Lifecycle
The protocol manages spatial state through a rigorous set of capability-gated lifecycle operations, ensuring structural integrity and enforcing the non-overlapping constraint.
4.1 AABB Computation
An Axis-Aligned Bounding Box (AABB) is computed iteratively during Part creation by scanning the input vertex coordinates to find the minimum and maximum extremes. The union of all part-level AABBs yields the global AABB for the entire Parcel.
4.2 Quadtree Cell Mapping
Each parcel is assigned a single depth-prefixed Morton code corresponding to the smallest quadtree cell that fully encloses the parcel's global AABB. The cell size halves at each increment of depth up to .
To map a parcel to its natural depth:
- The global AABB is evaluated against the spatial grid starting at .
- The grid attempts to subdivide into 4 children. If the AABB crosses a subdivision boundary at depth , the parcel is pinned at depth .
- The resulting Morton code is stored in the parcel's
cellsvector.
4.3 Parcel Registration Pipeline
The registration of a new parcel (index::register) requires a &LifecycleCap capability. During registration, the protocol executes a three-phase collision pipeline to guarantee the new claim does not overlap with any existing parcel.
Phase 1: Broadphase (Quadtree Query)
The index::candidates query collects the IDs of all previously registered parcels residing in quadtree cells that intersect the new parcel's global AABB. This prunes the search space dramatically, finding only nearby neighbors.
Phase 2: Midphase (AABB Filter)
For each candidate returned by the broadphase, a fast AABB intersection test (aabb::intersects) is evaluated against the new parcel's global AABB. Candidates that do not overlap at the AABB level are safely skipped.
Phase 3: Narrowphase (Separating Axis Theorem)
For all remaining midphase survivors, full multi-part SAT collision detection is executed (polygon::intersects). The protocol tests every part of the new parcel against every part of the candidate parcel.
- If ANY positive-area overlap is detected between any two parts, the entire transaction immediately aborts with
EOverlap. - If all tests pass, the parcel is inserted into
index.polygonsand itsIDis added to its natural cell inindex.cells.
4.4 Overlap Semantics: Touching vs. Overlap
The Separating Axis Theorem (SAT) implementation strictly differentiates between touching and overlapping.
- Overlap: Occurs when two polygons intersect and share positive area. The protocol forbids this and will abort.
- Touching: Occurs when two polygons share exactly a boundary edge or a single corner vertex, but their interiors do not intersect (zero shared area). This is permitted.
Because the protocol leverages exact fixed-point integer arithmetic (u128 projections and strict > inequality checks), there is no numerical tolerance () or floating-point imprecision. Boundary touching evaluates cleanly and deterministically as non-overlapping.
4.5 Parcel Deletion
Removing a parcel (index::remove) requires a &LifecycleCap. The operation requires the ID of the parcel. The protocol:
- Looks up the parcel in
index.polygonsand verifies ownership via the current transaction sender context. - Removes the
IDfrom the specific quadtree cell inindex.cells. - Removes the stored parcel record from
index.polygonsand destroys the underlyingPolygonvalue.
Note on storage: Because Sui returns a storage rebate on deletion, removing a parcel reduces long-run storage cost, but the exact refund amount is determined by the network's storage accounting rather than by the protocol itself.
(An administrative bypass, admin::force_remove, requires the AdminCap and invokes the package-internal index::remove_unchecked to forcefully delete any parcel without requiring the owner's signature or LifecycleCap.)
4.6 Ownership Transfer
Transferring ownership of a parcel (index::transfer_ownership) requires a &LifecycleCap and the owner's signature.
However, forced-sale protocols (such as Market V2) utilize a secondary capability, &TransferCap, which empowers the index::force_transfer function. This allows the market layer to bypass the owner's signature entirely and reassign the parcel to a new buyer upon a successful buyout.
4.7 Parcel Mutations
The protocol supports four geometry-mutating operations via the mutations module. All require a &LifecycleCap and enforce strict area conservation and non-overlap invariants.
4.7.1 Reshape Unclaimed (reshape_unclaimed)
Reshapes a single unclaimed parcel while preserving area. The new geometry must fully contain the old geometry (AABB containment and geometric containment). After reshaping, the quadtree cell placement is recomputed.
Invariants enforced:
- New AABB contains old AABB.
- New polygon geometrically contains old polygon.
- No overlaps with other registered parcels.
- Emits
ParcelReshaped(polygon_id, old_area, new_area).
4.7.2 Repartition Adjacent (repartition_adjacent)
Repartitions two adjacent parcels that share an edge. Both parcels receive new geometry while total area is conserved: .
Invariants enforced:
- Parcels share at least one edge (
touches_by_edge). - Total area conserved (exact fixed-point equality).
- New parcels do not overlap each other.
- No overlaps with other registered parcels.
- Emits
ParcelsRepartitioned(a_id, b_id).
4.7.3 Split Replace (split_replace)
Splits a parent parcel into child parcels. The parent is retired and replaced by the children, who inherit the parent's owner.
Invariants enforced:
- Parent area equals sum of children areas (area conservation).
- Children do not overlap each other.
- No overlaps with other registered parcels.
- Emits
ParcelRetired(parent_id)andParcelSplit(parent_id, child_ids).
4.7.4 Merge Keep (merge_keep)
Merges two adjacent parcels owned by the same address. One parcel (keep) receives the merged geometry; the other (absorb) is retired.
Invariants enforced:
- Both parcels have the same owner.
- Parcels share at least one edge.
- Area conserved: .
- No overlaps with other registered parcels.
- Emits
ParcelRetired(absorb_id)andParcelsMerged(keep_id, absorb_id).
4.8 Off-Chain Metadata
The protocol supports attaching an off-chain content identifier (CID, Walrus URI, or other content-addressable reference) to any registered parcel. This enables owners to associate rich off-chain data — property descriptions, imagery, legal documents — with their on-chain parcels without incurring on-chain storage costs for the content itself.
Storage Design: Metadata is stored as a Sui dynamic field on the Index object's UID, keyed by a MetadataKey { polygon_id } struct. This design avoids any migration of existing parcel data and requires no changes to the Polygon struct. Each parcel has at most one metadata entry.
Data Model: A MetadataState record contains:
cid: String— the IPFS content identifier, stored as-given without on-chain validation.updated_epoch: u64— the Sui epoch at which the metadata was last set, providing a freshness signal to off-chain consumers.
Operations:
-
set_metadata(index, polygon_id, cid, ctx): Owner-gated. Sets or overwrites the CID for a parcel. Idempotent — calling twice replaces the previous value via a check-remove-add pattern on the dynamic field. EmitsMetadataSet. Cost: gas only (no protocol fee). -
get_metadata(index, polygon_id): Public read. Returns(String, u64)— the CID and updated epoch. Aborts withEMetadataNotFoundif no metadata has been set. -
has_metadata(index, polygon_id): Public existence check. Returnsbool. -
remove_metadata(index, polygon_id, ctx): Owner-gated. Removes the metadata dynamic field. Aborts withEMetadataNotFoundif no metadata exists. EmitsMetadataRemoved.
Ownership Enforcement: All mutating operations (set_metadata, remove_metadata) verify that the transaction sender matches polygon::owner(index::get(index, polygon_id)). Non-owners are rejected with ENotOwner (6000). Ownership transfers propagate automatically — after a transfer, only the new owner can modify metadata.
Composability: Metadata operations are PTB-composable. A parcel can be registered and its metadata set in a single Programmable Transaction Block by composing index::register → metadata::set_metadata.
Orphaned Metadata: If a parcel is removed from the index while metadata is still attached, the dynamic field persists as an orphan on the Index UID. Callers should invoke remove_metadata before parcel deletion in a PTB to reclaim storage. This is a known limitation; automated cleanup is deferred to future work.
Off-Chain Content Schema: The protocol defines a canonical JSON schema for off-chain metadata content, documented in metadata-schema.md. A conforming document may carry a toponym (name), a reference URI to an SVG visual (svg), a geographic label coordinate (label), an array of points of interest (pois), and a CommonMark description field. Only the schema_version field is required; all others are optional. Content-addressable storage networks such as Walrus (via walrus:// URIs) or IPFS (via ipfs:// URIs) are the recommended storage backends. The on-chain cid field stores any valid URI without scheme validation.
5. Computational Complexity Analysis
Because the protocol operates entirely on-chain, bounding the computational and storage cost of registration is critical for economic viability.
5.1 Instruction Cost Model
The registration pipeline's cost is dominated by the narrowphase collision detection. For a new parcel with parts, executing against AABB-confirmed candidate neighbors, the worst-case number of SAT projection tests is: where is the number of parts in neighbor , and represents the vertex count of a given part.
Because broadphase and midphase cull non-local candidates effectively, stays small even as the total number of registered parcels grows to the hundreds of thousands.
5.2 Empirical Cost Bounds
Rather than relying on unproven asymptotic bounds, the protocol's viability is secured by conservative, measured empirical claims derived directly from Sui testnet execution.
- Storage Dominance: Storage mapping accounts for ~93-96% of the gross registration fee. Computation costs remain relatively flat across standard cadastral geometries.
- Gross Cost Limits: Representative cadastral shapes measured on testnet fall between roughly 0.013 SUI and 0.039 SUI gross, depending on geometry and spatial footprint.
- Lifecycle Economics: Storage rebates materially reduce long-run lifecycle cost, but the exact net cost depends on network-level rebate accounting and should be treated as deployment-dependent.
(See Appendix B for specific testnet data and detailed test scenarios).
6. Correctness and Security
The protocol substitutes complex mathematical proofs with rigorous invariant enforcement, ensuring logical correctness through exhaustive test coverage.
6.1 SAT Correctness and Edge Cases
The Separating Axis Theorem requires absolute convexity for both polygons tested. The protocol enforces this invariant deterministically during Part instantiation by verifying that the cross product of all consecutive edge pairs shares the same sign.
Edge Cases Validated:
- Zero-overlap touching: Exact edge-on-edge contact without interior penetration correctly evaluates to
false(no overlap). - Collinear vertices: Redundant vertices on the same edge are structurally valid but do not break the projection vectors.
- Micro-overlaps: Intersections as small as a single fixed-point unit () are deterministically caught and rejected.
6.2 Quadtree Index Completeness
The broadphase query is designed so it cannot yield false negatives. Because a parcel is pinned to the quadtree depth corresponding to the single cell that fully encloses its global AABB, any query spanning an overlapping region is mathematically guaranteed to intersect that cell at depth or one of its ancestors/descendants. Exhaustive grid-boundary unit tests guarantee no geometric orphans are left behind.
6.3 Error Handling and Invariant Breaches
If a transaction violates any protocol invariant, it aborts cleanly, rolling back all state changes without side effects. The protocol exposes a stable table of error codes.
Core protocol errors (index module):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 4001 | EBadVertices | index | Array vertices, or exceeds limit |
| 4002 | EMismatch | index | xs and ys arrays have different lengths |
| 4005 | ENotFound | index | Queried parcel ID does not exist |
| 4006 | ENotOwner | index | Sender does not own the parcel |
| 4009 | EBadMaxDepth | index | Quadtree max depth exceeds |
| 4010 | ETooManyParts | index | Number of parts exceeds |
| 4012 | EOverlap | index | Core Constraint: Parcel overlaps an existing claim |
| 4013 | EIndexNotEmpty | index | Admin attempted to destroy an index that still holds parcels |
| 4014 | EBadCellSize | index | configured as zero or invalid |
| 4015 | ENotAuthorized | index | Missing or invalid LifecycleCap |
| 4016 | ECoordinateTooLarge | index | Coordinate exceeds representable grid range |
Geometry errors (polygon module):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 2001 | EEmpty | polygon | Empty vertex or part input |
| 2002 | ETooManyParts | polygon | Number of parts exceeds |
| 2003 | ENotConvex | polygon | Part fails strict convexity check |
| 2004 | EBadVertices | polygon | Vertex count out of range |
| 2005 | EMismatch | polygon | xs and ys arrays have different lengths |
| 2006 | EPartOverlap | polygon | Interior disjointness violated within the same parcel |
| 2007 | EInvalidMultipartContact | polygon | Parts share area but not via exact identical edges |
| 2008 | EDisconnectedMultipart | polygon | Parts do not form a single connected graph |
| 2009 | EInvalidBoundary | polygon | Outer boundary contains a topological hole |
| 2010 | EEdgeTooShort | polygon | One or more parcel edges are shorter than the minimum allowed length |
| 2011 | ECompactnessTooLow | polygon | Area-to-perimeter ratio is below the anti-sliver threshold |
| 2012 | EAreaConservationViolation | polygon | Area before and after mutation does not match |
Mutation errors (mutations module):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 5001 | ENotContained | mutations | New geometry does not contain old geometry (reshape) |
| 5002 | EOverlap | mutations | Mutated parcel overlaps another registered parcel |
| 5003 | ESelfRepartition | mutations | Cannot repartition a parcel with itself |
| 5004 | ENotAdjacent | mutations | Parcels do not share an edge (repartition/merge) |
| 5005 | EOwnerMismatch | mutations | Parcels have different owners (merge) |
| 5006 | ESelfMerge | mutations | Cannot merge a parcel with itself |
Metadata errors (metadata module):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 6000 | ENotOwner | metadata | Caller is not the parcel owner |
| 6001 | EMetadataNotFound | metadata | No metadata exists for the queried parcel |
Market V2 errors (market_v2 module):
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 3100 | EInvalidPrice | market_v2 | Computed price is zero or invalid |
| 3101 | EInvalidAreaRange | market_v2 | Level min area exceeds max area |
| 3102 | EDuplicateLevel | market_v2 | Level already registered for this index |
| 3103 | EInvalidRate | market_v2 | Escalation rate out of bounds |
| 3104 | EInvalidFee | market_v2 | Protocol fee out of bounds |
| 3105 | EUnknownIndex | market_v2 | No level configured for this index |
| 3106 | ESelfPurchase | market_v2 | Buyer is already the parcel owner |
| 3107 | EAreaOutOfRange | market_v2 | Parcel area outside level bounds |
| 3108 | EZeroAreaParcel | market_v2 | Parcel has zero computed area |
| 3109 | EInsufficientPayment | market_v2 | Payment coin value below required price |
| 3110 | ENotOwner | market_v2 | Caller does not own the parcel |
| 3111 | ENotRegistered | market_v2 | Parcel not tracked by market |
| 3112 | EZeroAreaSlice | market_v2 | Slice operation captures zero area |
| 3113 | ESameOwnerSlice | market_v2 | Acquire slice requires different owners |
| 3114 | EDifferentOwners | market_v2 | Rebalance/merge requires same owner |
Market V2 tax errors:
| Code | Constant | Module | Breach / Condition |
|---|---|---|---|
| 3200 | ETaxNotInitialized | market_v2 | Tax system not yet initialized |
| 3201 | EBatchTooLarge | market_v2 | Collect batch exceeds MAX_COLLECT_BATCH_SIZE |
| 3202 | EWrongLevel | market_v2 | Parent/child levels are not adjacent |
| 3203 | EBucketNotExpired | market_v2 | Sweep attempted before bucket expiry |
| 3204 | ETaxNotOwner | market_v2 | Caller does not own the polygon for withdrawal |
| 3205 | ENoSelfClaimable | market_v2 | No tax balance available for withdrawal |
| 3206 | EAlreadyInitialized | market_v2 | Tax system already initialized |
| 3207 | EInvalidTaxConfig | market_v2 | Tax level configuration out of bounds |
| 3208 | EUnknownLevel | market_v2 | No tax level configured for this rank |
Appendix A: Market V2 Mechanics
The protocol supports a layered architectural model where the core cadastre is governed by an upstream economic framework. The canonical Market V2 implementation enforces an escalating, forced-sale continuous auction and a cascading, bucketed upstream tax system implemented entirely on-chain.
Forced Buyout Lifecycle:
- A user targets an existing parcel and pays the current algorithmic buyout price.
- The Market contract splits the payment into
85%seller proceeds,7%immediate treasury, and8%deferred hierarchy pool. - The hierarchy pool is deposited into the cascading tax system if tax is initialized and the sold level has a parent; otherwise it is routed directly to treasury.
- Any previously accumulated
self_claimabletax for the seller polygon is auto-withdrawn to the seller before ownership changes. - The Market contract consumes its
TransferCapto invokeindex::force_transfer, reassigning ownership to the buyer. - The polygon's
premium_ppmis advanced using the aggressive resale ladder, andsale_countis incremented.
Price Escalation Formula:
The market tracks a per-parcel premium_ppm (parts per million) together with sale_count. Level tariffs are stored as price_per_km2_mist. The current buyout price is:
where:
premium_ppmis initialized to (1.0×) at registrationsale_countstarts at0
Each buy_full escalates the premium via a hardcoded aggressive resale ladder:
where , ladder(1..64) is the fixed RESALE_LADDER_PPM vector, and ladder(n \ge 65) = 1{,}150{,}000 (1.15× tail).
The premium_ppm model extends naturally to partial operations:
acquire_slice: Price is based on the captured donor area at the donor's current premium. Both receiver and donor keep their existingpremium_ppm.rebalance_slice: Both polygons adopt and .merge_owned: The surviving polygon adopts the area-weighted average premium of both inputs and .expand_unclaimed: Premium is unchanged; the buyer pays only for the added area at the current premium.
All price and split calculations use u128 integer arithmetic internally before truncating back to u64.
A.1 Deferred Tax State
The production Market V2 implementation attaches a dynamic TaxStore to the shared MarketV2 object rather than storing tax state directly in the MarketV2 struct. This preserves Sui upgrade compatibility while keeping the full tax lifecycle on-chain.
The runtime tax state includes:
tax_fund: Balance<SUI>— pooled tax balance backing all deferred claims.version_counters: Table<ID, u64>— per-polygon geometry version used to separate old and new tax buckets after reshapes and repartitions.tax_buckets: Table<TaxBucketKey, TaxBucket>— per-polygon bucketed tax accounting.tax_levels: vector<TaxLevelCfg>— per-level timing configuration for bucket cadence and expiry.self_claimable: Table<ID, u64>— tax already allocated to a polygon owner but not yet withdrawn.polygon_buckets: Table<ID, vector<TaxBucketKey>>— reverse index used for sweep and cleanup.
Each TaxBucketKey is the canonical bucket identity (6 fields):
polygon_id: ID— the polygon this bucket sits on.version: u64— polygon geometry version at accrual time.bucket_id: u64— epoch-window bucket (current_epoch / bucket_epochs).origin_level_rank: u8— level rank of the originating sale (e.g., 0 for block-level).ancestor_distance: u8— cascade step. 1 = initial direct-parent bucket created bytax_accrue; 2+ = forwarded cascade bucket created when a parent collects and forwards upstream.accrual_epoch: u64— epoch when the underlying tax first originated. Inherited through cascades, never reset. Prevents merging buckets from different origination times — two forwarded amounts landing on the same polygon in the same epoch window but originating at different epochs stay separate. This is critical for the anti-retro guard (§A.4).
Each TaxBucket stores:
upstream_total: u64— total MIST deposited into this bucket.total_claimed: u64— MIST already claimed (reserved for partial claims; currently 0 in production).open_epoch: u64— epoch when this bucket instance was opened on its current polygon.accrual_epoch: u64— mirrors the key field for data access.expires_at: u64— deterministic expiry: where is thebucket_id.
Each TaxLevelCfg stores:
retain_upstream_ppmcollector_fee_bpsbucket_epochsexpiry_epochs
In the current production configuration, only bucket_epochs and expiry_epochs are economically active. set_tax_level_cfg(...) zeroes retain_upstream_ppm and collector_fee_bps, retaining them only for layout compatibility with earlier tax iterations.
Delete-after-collect semantics replace the previous EdgeClaim anti-double-claim system: consuming a bucket removes it from both tax_buckets and polygon_buckets, making repeat collection structurally impossible without additional tracking state.
A.2 Tax Accrual on Payment-Bearing Flows
Tax is created on all payment-bearing market flows:
register(...)— initial registration paymentbuy_full(...)— forced buyout paymentexpand_unclaimed(...)— expansion payment for added areaacquire_slice(...)— slice acquisition payment
The runtime rule is:
- Compute the flow-specific payment amount from area,
price_per_km2_mist, andpremium_ppm. - Split payment into an immediate treasury portion and a deferred hierarchy pool.
- If tax is initialized and the sold level has a parent, deposit the hierarchy pool into
tax_fundand credit the corresponding child bucket viatax_accrue(...). - Otherwise route the hierarchy pool directly to treasury.
Important implementation details:
- For
buy_full(...)andacquire_slice(...), the split is85%seller proceeds,7%immediate treasury,8%deferred hierarchy pool. - For
register(...)andexpand_unclaimed(...), there is no displaced seller, so92%goes directly to treasury and8%forms the hierarchy pool. buy_full(...),register(...), andexpand_unclaimed(...)accrue against the target polygon.acquire_slice(...)accrues against the donor polygon, because the donor is the polygon selling area.- Top-level sales/registrations (no parent above the origin rank) route the hierarchy pool directly to treasury rather than opening a bucket.
- If the computed amount is below the minimum bucket threshold (
MIN_BUCKET_AMOUNT = 1000 MIST), the amount is routed directly to treasury instead of creating a dust bucket.
A.3 Versioning and Geometry Changes
Tax collection is version-aware. Geometry-changing operations call tax_close_version(...) so that future accruals use a fresh (polygon_id, version) bucket namespace.
The current implementation closes versions on:
acquire_slice(...)for both receiver and donorexpand_unclaimed(...)rebalance_slice(...)for both polygonsmerge_owned(...)for the surviving polygon
For destructive lifecycle paths, the implementation also sweeps pending child-side tax state:
remove(...)callstax_close_and_sweep(...)merge_owned(...)callstax_close_and_sweep(...)for the absorbed polygon
This ensures that removed or absorbed geometry does not leave orphaned collectible child buckets behind.
A.4 Batched Parent Collection (Cascading Model)
Deferred tax is realized through a cascading direct-parent model. Each sale creates one bucket on the sold polygon. The direct parent collects, keeps a weighted share, and forwards the remainder up the chain by opening a cascade bucket on itself for the next ancestor.
Entry points:
collect_batch(market, parent_index, parent_id, child_index, child_refs, ctx)— accepts pre-builtChildRefstructs.collect_batch_flat(market, parent_index, parent_id, child_index, child_ids, child_versions, child_bucket_ids, ctx)— accepts 3 flat vectors and constructsChildRefinternally.
Batch constraints:
- Bounded by
MAX_COLLECT_BATCH_SIZE(20). - Caller must own
parent_id. - Parent level must be directly above the child level.
Multi-match bucket lookup:
For each (child_id, version, bucket_id) tuple the contract finds ALL matching TaxBucketKey entries in polygon_buckets[child_id] where key.version == version, key.bucket_id == bucket_id, and (key.origin_level_rank + key.ancestor_distance) == parent_level_rank. Each match is processed independently. Previous implementations took only the first match, silently hiding duplicate buckets created by independent cascade paths landing on the same polygon in the same epoch window.
Per-bucket eligibility:
Each matched bucket must pass four checks (failure silently skips the bucket without aborting the batch):
- Bucket exists in
tax_buckets. - Anti-retro guard:
parent.created_epoch <= bucket_key.accrual_epoch— the parent must not have been created in a later epoch than the tax originated. Same-epoch registration is eligible; only later-created parents are ineligible. This prevents retroactive extraction while avoiding false negatives caused by coarse epoch granularity. - Child version matches current
version_counters[child_id]. index::outer_contains_inner(parent, child)confirms full geometric containment.
Cascading split formula:
Given a hierarchy of levels (block → district → city → region → country → continent):
The 4× weight for s = 1 ensures the direct parent receives a significantly larger share than pass-through ancestors. Ghost levels represent hierarchy ranks below the originating sale that cannot participate, diluting each participant's share proportionally.
Delete-after-collect: The consumed bucket is removed from both tax_buckets and polygon_buckets. The bucket itself is the claim token — no EdgeClaim cursor is needed.
Cascade forwarding: If and and :
- A cascade bucket is opened on the parent polygon with
ancestor_distance = s + 1, inheritingorigin_level_rankandaccrual_epochfrom the source bucket. - If a cascade bucket with the same key already exists, the forward amount is merged into it.
open_epochis set to the current epoch;accrual_epochis inherited (never reset).- Emits
TaxBucketOpenedfor newly created cascade buckets.
Terminal forwarding: If but (top level reached) or (dust), the forward amount is routed directly to treasury.
Credits: self_keep is added to self_claimable[parent_id]. Emits TaxCollected per bucket processed.
A.5 Withdrawal and Expiry Sweep
The implementation exposes two terminal paths for deferred balances:
-
withdraw_tax(market, index, polygon_id, ctx)- Callable only by the current polygon owner.
- Withdraws the polygon's
self_claimablebalance fromtax_fund. - Emits
TaxWithdrawn.
-
sweep_expired(market, polygon_id, version, bucket_id, ctx)- Permissionless.
- Requires
current_epoch >= expires_at. - Finds ALL bucket keys matching
(polygon_id, version, bucket_id)and sweeps every match — not just the first. Multiple keys can share the same 3-tuple when independent cascade paths or different accrual epochs produce distinct buckets. - Moves the unclaimed remainder of each bucket from
tax_fundto treasury. - Removes each bucket from
tax_bucketsand prunespolygon_buckets. - Emits
TaxSweptper bucket swept.
This gives the market a complete lifecycle from accrual to parent collection to owner withdrawal or treasury sweep.
A.6 Market V2 Lifecycle Operations
Beyond buy_full, the Market V2 exposes a full set of area-mutation operations that compose the core mutations module with economic rules.
register(market, index, parts_xs, parts_ys, payment, ctx)
Registers a new parcel. Charges area_m2 × price_per_km2_mist / 10^6 at the initial premium (1.0×). Creates PriceState { premium_ppm = 1_000_000, sale_count = 0 }. Splits payment into 92% immediate treasury and 8% hierarchy pool. If tax is initialized and the level has a parent, the hierarchy pool accrues into deferred tax.
expand_unclaimed(market, index, polygon_id, new_parts_xs, new_parts_ys, payment, ctx)
Reshapes an owned parcel to a larger geometry via mutations::reshape_unclaimed. Charges only for the added area: (new_area - old_area) × price_per_km2_mist / 10^6 × premium_ppm / 10^6. Premium is unchanged. Splits payment into 92% immediate treasury and 8% hierarchy pool. Closes the tax version after expansion.
acquire_slice(market, receiver_index, receiver_id, donor_index, donor_id, receiver_xs, receiver_ys, donor_xs, donor_ys, payment, ctx)
Transfers area from a donor parcel (different owner) to a receiver parcel via mutations::repartition_adjacent. The receiver must be owned by the caller; the donor must not be. Charges by captured donor area at the donor's premium. Splits payment into 85% donor proceeds, 7% immediate treasury, and 8% hierarchy pool. Both receiver and donor keep their prior premium_ppm; tax accrues against the donor. Closes versions on both parcels.
rebalance_slice(market, index, a_id, b_id, a_xs, a_ys, b_xs, b_ys, ctx)
Adjusts area between two parcels owned by the same caller via mutations::repartition_adjacent. No payment is required. Both polygons adopt max(premium_a, premium_b) and max(sale_count_a, sale_count_b). Closes versions on both parcels.
merge_owned(market, index, keep_id, absorb_id, merged_xs, merged_ys, ctx)
Merges two adjacent same-owner parcels via mutations::merge_keep. The survivor's premium becomes the area-weighted average of the two premiums; sale_count becomes the max of both inputs. Sweeps all tax buckets from the absorbed polygon. Closes version on the survivor.
remove(market, index, polygon_id, ctx)
Governance/emergency-only removal path. Removes PriceState, sweeps all remaining tax buckets to treasury via tax_close_and_sweep, and calls index::remove through the market's internal LifecycleCap. This is not a public owner-facing deregistration flow.
A.7 Market V2 Events
Market lifecycle events:
MarketV2Registered: Emitted onregister. Includespolygon_id,owner,area_m2,price_paid,immediate_treasury,hierarchy_pool.MarketV2PurchasedFull: Emitted onbuy_full. Includespolygon_id,buyer,seller,price,new_premium_ppm,seller_proceeds,immediate_treasury_fee,hierarchy_pool,sale_count.ParcelExpandedUnclaimed: Emitted onexpand_unclaimed. Includespolygon_id,old_area,new_area,price_paid,immediate_treasury,hierarchy_pool.ParcelSliceAcquired: Emitted onacquire_slice. Includes receiver_id, donor_id.ParcelSliceRebalanced: Emitted onrebalance_slice. Includes a_id, b_id.ParcelOwnedMerged: Emitted onmerge_owned. Includeskeep_id,absorbed_id,premium_ppm.PriceStateUpdated: Emitted whenever a polygon's premium_ppm changes.
Tax events:
TaxBucketOpened { polygon_id, version, bucket_id, origin_level_rank, ancestor_distance, accrual_epoch, amount }— emitted when a new bucket is created (initial accrual or cascade forwarding). Not emitted when an existing bucket receives additional funds.TaxCollected { parent_id, child_id, child_version, bucket_id, origin_level_rank, ancestor_distance, accrual_epoch, amount, collector_fee, self_keep, forward_upstream }— emitted per bucket collected. Onecollect_batchcall may emit multiple events when processing several child buckets.TaxWithdrawn { polygon_id, owner, amount }— emitted when a polygon owner withdrawsself_claimablebalance.TaxSwept { polygon_id, version, bucket_id, origin_level_rank, ancestor_distance, accrual_epoch, remainder }— emitted per expired bucket swept to treasury.TaxVersionClosed { polygon_id, old_version, new_version }— emitted when a polygon's tax version is incremented due to geometry change.
The origin_level_rank, ancestor_distance, and accrual_epoch fields together form the full bucket identity and enable client-side event matching without ambiguity across cascade paths.
These events form the complete auditable surface for market and tax accounting.
A.8 Current Architectural Note
Historically, tax functionality was planned as a separate tax.move implementation module. In the current production code, runtime tax logic and tax data structures live in market_v2.move, while market/sources/tax.move is retained as a compatibility stub. This is an implementation-architecture detail and does not change the normative tax semantics described above.
Harberger-like variants and other alternative tax research remain outside the production V2 specification.
Appendix B: Evidence and Measurements
The protocol's operating envelope and cost models are derived from empirical deployments on the Sui testnet.
Testnet Gas Profiles (Observed):
| Geometry Type | Vertices | Parts | Broadphase Neighbors | Gross Cost (MIST) |
|---|---|---|---|---|
| Simple Square | 4 | 1 | 0 | ~13.3M |
| Hexagon | 6 | 1 | 2 | ~17.6M |
| L-Shape | 8 | 2 | 4 | ~30.4M |
| Complex Stepped | 15 | 5 | 8 | ~32.0M |
Coverage Evidence: The protocol's collision logic is verified by unit integration tests, validating edge cases such as:
- Dense Urban Grids: 25 parcels packed into a configuration without missing overlaps.
- Broadphase Completeness: Stress-testing queries to ensure no geometry fragments bypass midphase filtering.
Appendix C: Roadmap and Future Extensions
Features that fall outside the immediate scope of the V2 production release but remain viable architectural expansions:
- ZK Extension Layer: Pushing SAT narrowphase checking to an off-chain prover to support massive boundary files (e.g., vertices) while retaining on-chain grid indexing.
- Dynamic Grid Resizing: Allowing the root cell size () to expand or subdivide autonomously based on localized parcel density.
- 3D / Volumetric Indexing: Upgrading the 2D AABB and quadtree into an Octree model to support airspace and subterranean mineral rights.
Appendix D: Change Log
- v0.8.0 (Current): Added §4.8 IPFS Metadata — owner-gated off-chain content identifier (CID) attachment via Sui dynamic fields on the Index UID. New
metadata.movemodule withset_metadata,get_metadata,has_metadata,remove_metadata. Metadata errors allocated in the 6000 range. Events:MetadataSet,MetadataRemoved. No migration required for existing parcels. - v0.7.0: Replaced the pre-split ancestor bucket tax model with cascading direct-parent collection. Each sale creates one deferred bucket; parent collects, keeps weighted share, and forwards the remainder up chain via cascade buckets. Added
accrual_epochtoTaxBucketKeyto prevent merging buckets from different origination times. Anti-retro eligibility now requiresparent.created_epoch <= bucket.accrual_epoch, so later-created parents are blocked while same-epoch parents remain eligible.collect_batchprocesses all matching buckets per child ref (multi-match), and delete-after-collect replaces the oldEdgeClaimidempotency cursor. Appendix A also reflects the deployedprice_per_km2_misttariff model, aggressive resale ladder, fixed85/7/8secondary split, and92/8primary-expansion split. - v0.6.0: Added §4.7 Parcel Mutations (reshape, repartition, split, merge). Replaced legacy
sale_countprice formula with canonicalpremium_ppmmodel including partial escalation semantics. Documented all payment-bearing tax accrual points (register, buy_full, expand_unclaimed, acquire_slice). Added Market V2 lifecycle operations (§A.6) and full event catalogue (§A.7). Expanded error code tables to cover polygon, mutations, and market_v2 modules. FixedEBadMaxDepthlimit from 20 to 31. Added mutation events to §3.5. - v0.5.0: Reframed as merca.earth Cadastre Protocol. Upgraded concave/multi-part support from "future" to "core". Replaced theoretical cost formulas with conservative testnet-driven evidence. Isolated Market V2 mechanics from the spatial core spec. Formally defined touching vs. overlap semantics and strict polygon compactness invariants.
- v0.1.0: Initial research draft (formerly "GeoSui"). Proposed basic grid + SAT integration and outlined theoretical broadphase gas costs.