Concept · Scale-ratio math

Splits without decryption

Percentages the contract can't see become fixed-denominator integers. Scale-ratio is the SDK's bigint primitive.

Splitting an encrypted balance — say, transferring 1/3 of Mira's vesting to a new recipient — looks easy in plaintext: multiply by 1/3. Inside FHEVM, the contract can't see the operands. It can compute FHE.mul(handle, plaintext), but only on integer plaintexts. So the SDK encodes the ratio as a fixed-denominator scaled numerator and lets the contract do the on-chain multiply.

The denominator#

FHE_SPLIT_DENOMINATOR is a public SDK constant equal to 90_090_000 (the LCM of every integer in 1..16 and 10_000), so halves through sixteenths and any basis-point amount land on it with no rounding. The SDK scales the ratio against this denominator, then encrypts the numerator through encryptUint64 and submits it as an externalEuint64 alongside a plaintext uint128 denominator; the contract casts the numerator to euint128 internally for the math. There is no on-chain FHE_SPLIT_DENOMINATOR constant — the denominator is a caller-supplied plaintext, and on chain the contract computes FHE.min(FHE.div(FHE.mul(alloc, N), denominator), alloc).

The helper#

components/split.ts
ts
import { scaleRatio, FHE_SPLIT_DENOMINATOR } from "@tokenops/sdk/fhe";

// 33.3...% split: 1/3 of the original vesting moves to the new recipient.
const ratio = scaleRatio({ numerator: 1n, denominator: 3n });
// ratio === { numerator: 30_030_000n, denominator: 90_090_000n }
// 90_090_000 is divisible by 3, so 1/3 is exact — no rounding.

await manager.splitVesting({
  vestingId,
  newRecipient,
  numerator: ratio.numerator,
  denominator: ratio.denominator,
  preScaled: true, // already scaled — skip the SDK's auto-scale pass
});

// Or skip scaleRatio entirely with the share builder:
//   const ratio = share.fraction(1, 3);  // same { numerator, denominator }

When ratios fail#

scaleRatio itself does not validate that the numerator is ≤ the denominator — scaleRatio({ numerator: 4n, denominator: 3n }) succeeds and returns a ratio above 1. The guard lives in the share.fraction(n, m) builder, which requires n ≤ m (and m in 1..16). On chain, the contract does not revert on over-allocation either: FHE.min silently caps the child allocation to the total. The only on-chain guard on the ratio is a denominator below MIN_SPLIT_DENOMINATOR (10000), which reverts with InvalidSplitDenominator.

See also