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:
FieldTypeMeaning
orderDecimalsuint8Decimals of the token on the order chain (where the bridger deposits).
adDecimalsuint8Decimals of the token on the ad chain (where the ad creator provides liquidity).
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:
adAmount = params.amount × 10^(adDecimals − orderDecimals)
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.
orderDecimals = 18
adDecimals    = 7
params.amount = 1 × 10¹⁸              # 1 whole ETH, in wei
adAmount      = params.amount × 10^(7 − 18)
              = 10¹⁸ / 10¹¹
              = 10⁷                   # 1 whole unit of the 7-decimal token
And the reverse direction — 100 whole units of a 7-decimal token matched against an 18-decimal ad:
adAmount      = 100 × 10⁷ = 10⁹
params.amount = adAmount × 10^(18 − 7)
              = 10²⁰                  # 100 whole units of the 18-decimal token
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.