Skip to main content
Bridging a token between two chains usually means the two sides have different numeric representations of the “same” asset — 18 decimals on EVM, 7 on Stellar, 6 on some SPL tokens. ProofBridge handles this inline: both chains agree on the token decimals at order-signing time, assert them against the token contracts at settlement time, and do the scaling arithmetic in a shared primitive so the two sides never drift.

The OrderParams now carries decimals

Every order the user signs commits to two new fields: Both are bound into the EIP-712 digest. The signer isn’t just authorising an amount — they’re authorising an amount at a specific decimal scaling. Switching either value invalidates the signature.

params.amount is always in order-chain units

By convention, params.amount is the raw unit amount on the order chain. For an 18-decimal ERC-20, that’s wei. For a 7-decimal Stellar asset, it’s the base unit shown in Horizon. When the order lands on the ad chain, AdManager computes the ad-side amount internally:
This runs through the shared DecimalScaling primitive (EVM library + proofbridge-core::decimal_scaling on Stellar) before any pool accounting or token transfer. The order chain only ever sees the raw params.amount; the ad chain only ever sees the scaled adAmount.

Worked example: 18-decimal ↔ 7-decimal

Say a bridger is moving 1 ETH from an 18-decimal chain and matching it against an ad denominated in a 7-decimal token on Stellar.
And the reverse direction — 100 whole units of a 7-decimal token matched against an 18-decimal ad:
In both cases, params.amount is what the bridger signs, what lands in the OrderPortal, and what rides through the Merkle tree and the ZK proof.

On-chain decimals must match the signed decimals

Both portals defend against a signer lying about decimals. At validateOrder time:
  • OrderPortal asserts params.orderDecimals == token.decimals() for the deposited token (with wNativeToken used for ETH / XLM wrappers).
  • AdManager asserts params.adDecimals == token.decimals() for the ad-side token.
A mismatch reverts before any scaling or transfer runs. This catches two classes of bug: a signer who copies a template with the wrong decimals, and a subtle “token has been re-deployed with different decimals” drift.

Guardrails

  • MAX_DECIMALS = 30 — rejects absurd values at the boundary and keeps the 10^diff factor well inside uint256 headroom.
  • Scale-up is overflow-checked — Solidity 0.8 reverts on overflow by default, and the Stellar implementation uses explicit checked arithmetic.
  • Scale-down reverts on non-exact division — if the raw amount doesn’t divide evenly when mapping from higher to lower decimals, the settlement reverts. Silent truncation would change the economic value of the order, so it’s treated as a signed-amount error and surfaced to the user before any money moves.

Error surface

New errors emitted when a decimal invariant is violated: EVM
  • DecimalScaling__DecimalsOutOfRange(uint8)orderDecimals or adDecimals exceeds MAX_DECIMALS.
  • DecimalScaling__NonExactDownscale(uint256 amount, uint8 fromDec, uint8 toDec) — scale-down would truncate.
  • DecimalScaling__DecimalsMismatch(uint8 expected, uint8 provided) — raised by assertMatchesOnChain when the signed decimals don’t equal the token contract’s decimals().
Stellar (proofbridge-core::errors)
  • order_decimals_mismatch
  • ad_decimals_mismatch
  • Equivalent range / non-exact-downscale errors inside the decimal_scaling module.

From 1-to-1 bridging to cross-asset trades (exploratory)

Today’s scaling is a pure decimal re-basing between two representations of the same underlying asset — 1 ETH ↔ 1 wETH, 1 XLM ↔ 1 wXLM. The signed-order surface stays small and the settlement math stays auditable. Once the current roadmap is delivered, extending routes to quote cross-asset rates is a natural follow-up: a single order could bridge and swap in one step (for example ETH → USDC on Stellar). The signed OrderParams would gain a rate field, DecimalScaling.scale would gain a rate-aware overload, and ad creators would either set their own rates or opt into a shared rate-balancing layer. Nothing in the current design forecloses this direction — but it’s not part of the current plan.