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#
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.