import { concatHex, keccak256, numberToHex, type Address, type Hex } from 'viem' import { describe, expect, test } from 'vite-plus/test' import * as Campaigns from './Campaigns.js' import * as Projection from './Projection.js' const distributor = '0x0000000000000000000000000000000000000002' const campaignId = `0x${'00'.repeat(12)}${'11'.repeat(20)}` as Hex describe('schema.Config', () => { test('accepts the minimal boost configuration', () => { expect( Campaigns.schema.Config.parse({ boostRewards: { endTimestamp: 1_800, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, perUserPrincipalCapAssets: '25000000000', startTimestamp: 300, targetAnnualRateBps: 700, totalPrincipalCapAssets: '10000000000000', treasury: '0x0000000000000000000000000000000000000004', }, }), ).toMatchInlineSnapshot(` { "boostRewards": { "endTimestamp": 1800, "excludedAddresses": [], "funding": { "wallet": "0x0000000000000000000000000000000000000003", }, "intervalSeconds": 300, "perUserPrincipalCapAssets": "25000000000", "startTimestamp": 300, "targetAnnualRateBps": 700, "totalPrincipalCapAssets": "10000000000000", "treasury": "0x0000000000000000000000000000000000000004", }, } `) }) test('accepts partial final intervals and rejects empty, interval-less, and decreasing-cap documents', () => { const boost = { endTimestamp: 1_800, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, perUserPrincipalCapAssets: '101', startTimestamp: 300, targetAnnualRateBps: 700, totalPrincipalCapAssets: '100', treasury: '0x0000000000000000000000000000000000000004', } expect([ Campaigns.schema.Config.safeParse({}).success, Campaigns.schema.Config.safeParse({ boostRewards: { ...boost, endTimestamp: 1_801, perUserPrincipalCapAssets: '100' }, }).success, Campaigns.schema.Config.safeParse({ boostRewards: { ...boost, endTimestamp: 599, perUserPrincipalCapAssets: '100' }, }).success, Campaigns.schema.Config.safeParse({ boostRewards: boost }).success, ]).toStrictEqual([false, true, false, false]) }) test('accepts independent aligned schedules and rejects different accounting grids', () => { const boostRewards = { endTimestamp: 1_800, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, perUserPrincipalCapAssets: '100', startTimestamp: 300, targetAnnualRateBps: 700, totalPrincipalCapAssets: '100', treasury: '0x0000000000000000000000000000000000000004', } const targetYield = { annualRate: { bps: 500 }, endTimestamp: 2_400, funding: [{ remainder: true, wallet: '0x0000000000000000000000000000000000000005' }], intervalSeconds: 300, startTimestamp: 600, } expect([ Campaigns.schema.Config.safeParse({ boostRewards, targetYield, }).success, Campaigns.schema.Config.safeParse({ boostRewards, targetYield: { ...targetYield, startTimestamp: 601 }, }).success, Campaigns.schema.Config.safeParse({ boostRewards, targetYield: { ...targetYield, intervalSeconds: 60 }, }).success, ]).toStrictEqual([true, false, false]) }) test('accepts an explicitly disabled boost without removing its configuration', () => { expect( Campaigns.schema.Config.safeParse({ boostRewards: { enabled: false, endTimestamp: 1_800, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, perUserPrincipalCapAssets: '100', startTimestamp: 300, targetAnnualRateBps: 700, totalPrincipalCapAssets: '100', treasury: '0x0000000000000000000000000000000000000004', }, }).success, ).toBe(true) }) test('requires automatic boost payouts to be slower than boost accounting', () => { const boostRewards = { endTimestamp: 2_100, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, payoutSchedule: { intervalSeconds: 901, startTimestamp: 601 }, perUserPrincipalCapAssets: '100', startTimestamp: 300, targetAnnualRateBps: 700, totalPrincipalCapAssets: '100', treasury: '0x0000000000000000000000000000000000000004', } expect( [ boostRewards, { ...boostRewards, payoutSchedule: { intervalSeconds: 300, startTimestamp: 601 } }, { ...boostRewards, payoutSchedule: { intervalSeconds: 901, startTimestamp: 300 } }, { ...boostRewards, payoutSchedule: { intervalSeconds: 901, startTimestamp: 2_400 } }, ].map((value) => Campaigns.schema.Config.safeParse({ boostRewards: value }).success), ).toStrictEqual([true, false, false, false]) }) test('requires target-yield funding thresholds to increase strictly', () => { const targetYield = { annualRate: { bps: 700 }, endTimestamp: 1_800, funding: [ { upToAnnualRate: { bps: 500 }, wallet: '0x0000000000000000000000000000000000000005', }, { upToAnnualRate: { bps: 300 }, wallet: '0x0000000000000000000000000000000000000006', }, { remainder: true as const, wallet: '0x0000000000000000000000000000000000000007' }, ], intervalSeconds: 300, startTimestamp: 300, } expect( [ targetYield, { ...targetYield, funding: [ targetYield.funding[0], { upToAnnualRate: { bps: 500 }, wallet: '0x0000000000000000000000000000000000000006', }, targetYield.funding[2], ], }, ].map((value) => Campaigns.schema.Config.safeParse({ targetYield: value }).success), ).toStrictEqual([false, false]) }) test('bounds target-yield funding to one gas-safe exact batch', () => { const targetYield = { annualRate: { bps: 1_000 }, endTimestamp: 1_800, funding: [ ...Array.from({ length: 8 }, (_, index) => ({ upToAnnualRate: { bps: (index + 1) * 100 }, wallet: `0x${(index + 1).toString(16).padStart(40, '0')}`, })), { remainder: true, wallet: '0x0000000000000000000000000000000000000009' }, ], intervalSeconds: 300, startTimestamp: 300, } expect(Campaigns.schema.Config.safeParse({ targetYield }).success).toBe(false) }) test('accepts target-only, boost-only, and combined V1 campaigns', () => { const boostRewards = { endTimestamp: 1_800, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, perUserPrincipalCapAssets: '100', startTimestamp: 300, targetAnnualRateBps: 700, totalPrincipalCapAssets: '100', treasury: '0x0000000000000000000000000000000000000004', } const targetYield = { annualRate: { maxBps: 1_000, minBps: 100, morphoVault: '0x0000000000000000000000000000000000000005', staleAfterSeconds: 600, }, endTimestamp: 1_800, funding: [ { upToAnnualRate: { bps: 150 }, wallet: '0x0000000000000000000000000000000000000006', }, { remainder: true as const, wallet: '0x0000000000000000000000000000000000000007' }, ], intervalSeconds: 300, startTimestamp: 300, } expect( [{ targetYield }, { boostRewards }, { boostRewards, targetYield }].map( (config) => Campaigns.schema.Config.safeParse(config).success, ), ).toStrictEqual([true, true, true]) }) }) describe('compactEvidence', () => { test('drops active recipient calculation and payout state', () => { const recipient = '0x0000000000000000000000000000000000000001' const evidence: Campaigns.RunEvidence = { closingAccounts: [ { accrualRemainder: '0', allocatedPrincipalAssets: '7', cumulativeEntitlement: '7', cumulativePaid: '7', deferral: null, eligibilityRegisteredAt: null, lots: [], pendingRewardAssets: '0', publicEarnShares: '7', qualifiedEarnShares: '7', recipient, registrationOrder: '1', }, ], intervals: [ { assetPerEarnShareWad: '1000000000000000000', assetRewards: { [recipient]: '7' }, blockNumber: 12, endsAt: 600, startsAt: 300, }, ], paidRecipients: 1, payoutRecipients: [recipient], pendingRecipients: 0, settlementRecipientAddresses: [recipient], settledRewardAssets: '7', stateChecksum: `0x${'11'.repeat(32)}`, } expect(Campaigns.compactEvidence(evidence)).toStrictEqual({ closingAccounts: [], intervals: [ { assetPerEarnShareWad: '1000000000000000000', assetRewards: {}, blockNumber: 12, endsAt: 600, startsAt: 300, }, ], paidRecipients: 1, payoutRecipients: [], pendingRecipients: 0, settlementRecipientAddresses: [], settledRewardAssets: '7', stateChecksum: `0x${'11'.repeat(32)}`, }) }) }) describe('isRunCheckpointEvidence', () => { test('distinguishes cursorless resumable state from legacy completed evidence', () => { const completed: Campaigns.RunEvidence = { closingAccounts: [], intervals: [], stateChecksum: `0x${'11'.repeat(32)}`, } const checkpoint: Campaigns.RunCheckpointEvidence = { kind: 'checkpoint', progress: { intervals: [] }, projection: { accounts: [] }, stage: 'reconcile', } expect([ Campaigns.isRunCheckpointEvidence(completed), Campaigns.isRunCheckpointEvidence(checkpoint), ]).toStrictEqual([false, true]) }) }) describe('eligibilityVersion', () => { test('changes when a registration refreshes without advancing the high-water mark', () => { const entry = { latestRegisteredAt: '2026-09-07T12:00:00.000Z', registrationOrder: '1', walletAddress: '0x0000000000000000000000000000000000000001', } as const expect([ Campaigns.eligibilityVersion([entry]), Campaigns.eligibilityVersion([{ ...entry, latestRegisteredAt: '2026-09-07T12:01:00.000Z' }]), ]).toMatchInlineSnapshot(` [ "0x835c29ac9cce8d020d9195143224513133a4ceaed84f1dc2af9df860cee06a15", "0xf38fc3aea707242bbeba26833aa039d58b3a400ffdb38e81dce802de643d2286", ] `) }) }) describe('exclusionVersion', () => { test('normalizes order, duplicates, and address casing', () => { const first = '0x00000000000000000000000000000000000000aa' as Address const firstUpper = '0x00000000000000000000000000000000000000AA' as Address const second = '0x00000000000000000000000000000000000000bb' as Address expect({ changed: Campaigns.exclusionVersion([first]), normalized: Campaigns.exclusionVersion([second, firstUpper, second]), original: Campaigns.exclusionVersion([first, second]), }).toMatchInlineSnapshot(` { "changed": "0x99046efbb1433484e93dc7c45d1db9b2e48d2502dbc58d86f71b56ee43cbe85a", "normalized": "0x9f0b33830ee1adf49dd245b5d951263d277a137b8653042c2ca0910ad1feac95", "original": "0x9f0b33830ee1adf49dd245b5d951263d277a137b8653042c2ca0910ad1feac95", } `) }) }) describe('targetFundingAssets', () => { const evidence: Campaigns.RunEvidence = { closingAccounts: [], intervals: [], stateChecksum: `0x${'11'.repeat(32)}`, } test('rejects legacy positive funding before any signing or retry', () => { expect(() => Campaigns.targetFundingAssets({ ...evidence, targetFundingAssets: ['0', '3000000'], }), ).toThrowErrorMatchingInlineSnapshot( '[Error: Stored target-yield funding requires principal accounting review.]', ) }) test('restores exact time-weighted funding after evidence serialization', () => { const saved = JSON.parse( JSON.stringify({ ...evidence, targetFundingAssets: ['15450', '15450'], targetPrincipalMethod: 'time-weighted:v1', }), ) expect(Campaigns.targetFundingAssets(saved)).toMatchInlineSnapshot(` [ 15450n, 15450n, ] `) }) test('allows legacy runs with no target funding to continue', () => { expect(Campaigns.targetFundingAssets(evidence)).toStrictEqual([]) expect( Campaigns.targetFundingAssets({ ...evidence, targetFundingAssets: ['0', '0'], }), ).toStrictEqual([0n, 0n]) }) }) describe('fee-inclusive vault quotes', () => { test('rounds reward shares down and required funding assets up', () => { const quote = { totalAssets: 325_506_758n, totalEarnShares: 162_833_412n } const earnShares = Campaigns.assetsToEarnShares({ assets: 1_423n, ...quote }) expect(earnShares).toBe(711n) expect(Campaigns.earnSharesToAssets({ earnShares, ...quote })).toBe(1_422n) }) test('rejects invalid vault anchors without losing zero-amount semantics', () => { expect(Campaigns.assetsToEarnShares({ assets: 0n, totalAssets: 1n, totalEarnShares: 1n })).toBe( 0n, ) expect( Campaigns.earnSharesToAssets({ earnShares: 0n, totalAssets: 1n, totalEarnShares: 1n }), ).toBe(0n) expect(() => Campaigns.assetsToEarnShares({ assets: 1n, totalAssets: 0n, totalEarnShares: 1n }), ).toThrow('vault quote requires positive assets and fee-inclusive EarnShare supply') }) test('tops up small liabilities beyond the one-basis-point conversion floor', () => { expect([0n, 1n, 10_000n, 10_001n, 20_000n].map(Campaigns.settlementFundingEarnShares)).toEqual([ 0n, 10_001n, 10_001n, 10_001n, 20_000n, ]) }) }) describe('accrue', () => { test('carries sub-unit arithmetic across short intervals', () => { const first = Campaigns.accrue({ annualRateBps: 700, elapsedSeconds: 60, principalAssets: 1n, remainder: 0n, }) const second = Campaigns.accrue({ annualRateBps: 700, elapsedSeconds: 60, principalAssets: 1n, remainder: first.remainder, }) expect({ first, second }).toMatchInlineSnapshot(` { "first": { "amount": 0n, "remainder": 42000n, }, "second": { "amount": 0n, "remainder": 84000n, }, } `) }) test('eventually realizes sub-unit target yield across five-minute intervals', () => { let amount = 0n let remainder = 0n for (let interval = 0; interval < 365 * 24 * 12; interval++) { const accrued = Campaigns.accrue({ annualRateBps: 500, elapsedSeconds: 300, principalAssets: 1_000_000n, remainder, }) amount += accrued.amount remainder = accrued.remainder } expect({ amount, remainder }).toStrictEqual({ amount: 50_000n, remainder: 0n }) }) }) describe('organicGrowth', () => { test('advances an interval that opened before the first deposit', () => { expect( Campaigns.organicGrowth({ closingValuePerEarnShare: 1_000_000n, contributions: [], openingAssets: 0n, openingEarnShareSupply: 0n, openingValuePerEarnShare: 0n, }), ).toBe(0n) }) test('does not mistake a prior target contribution for organic yield', () => { expect( Campaigns.organicGrowth({ closingValuePerEarnShare: 1_000_100n, contributions: [{ assets: 100n, earnShareSupply: 1_000_000n }], openingAssets: 1_000_000n, openingEarnShareSupply: 1_000_000n, openingValuePerEarnShare: 1_000_000n, }), ).toBe(0n) }) test('retains growth above every explicit contribution in the interval', () => { expect( Campaigns.organicGrowth({ closingValuePerEarnShare: 1_000_250n, contributions: [ { assets: 100n, earnShareSupply: 1_000_000n }, { assets: 50n, earnShareSupply: 1_000_000n }, ], openingAssets: 1_000_000n, openingEarnShareSupply: 1_000_000n, openingValuePerEarnShare: 1_000_000n, }), ).toBe(100n) }) test('normalizes a mid-interval contribution to opening supply', () => { expect( Campaigns.organicGrowth({ closingValuePerEarnShare: 1_000_010n, contributions: [{ assets: 10n, earnShareSupply: 2_000_000n }], openingAssets: 1_000_000n, openingEarnShareSupply: 1_000_000n, openingValuePerEarnShare: 1_000_000n, }), ).toBe(5n) }) test('keeps the rate observation nonnegative after a loss', () => { expect( Campaigns.organicGrowth(period({ closingValuePerEarnShare: 999_999_000n })), ).toMatchInlineSnapshot(`0n`) }) }) describe('organicChange', () => { test('retains losses and subtracts contributions from signed growth', () => { expect( Campaigns.organicChange( period({ closingValuePerEarnShare: 999_999_000n, contributions: [{ assets: 5_000n, earnShareSupply: 31_536_000_000n }], }), ), ).toMatchInlineSnapshot(`-36536n`) }) }) describe('targetFunding', () => { const funding = [ { upToAnnualRate: { bps: 500 }, wallet: '0x0000000000000000000000000000000000000003' }, { remainder: true, wallet: '0x0000000000000000000000000000000000000004' }, ] as const test.each([ { closingValuePerEarnShare: 1_000_000_000n, expected: [11_250n, 11_250n], name: 'full recovery', }, { closingValuePerEarnShare: 750_000_000n, expected: [11_250n, 11_250n], name: 'partial recovery', }, { closingValuePerEarnShare: 1_250_000_000n, expected: [0n, 0n], name: 'positive net growth' }, ])( 'offsets a period loss against $name before funding', ({ closingValuePerEarnShare, expected }) => { expect( Campaigns.targetFunding({ annualRateBps: 1_000, funding, periods: [ period({ closingValuePerEarnShare: 500_000_000n, elapsedSeconds: 150 }), period({ closingValuePerEarnShare, elapsedSeconds: 150, openingAssets: 15_768_000_000n, openingValuePerEarnShare: 500_000_000n, }), ], remainders: [], }), ).toStrictEqual({ assets: expected, remainders: [0n, 0n] }) }, ) test('retains a boundary loss without accruing principal before the interval starts', () => { expect( Campaigns.targetFunding({ annualRateBps: 1_000, funding, periods: [ period({ closingValuePerEarnShare: 500_000_000n, elapsedSeconds: 0 }), period({ openingAssets: 15_768_000_000n, openingValuePerEarnShare: 500_000_000n, }), ], remainders: [], }), ).toMatchInlineSnapshot(` { "assets": [ 7500n, 7500n, ], "remainders": [ 0n, 0n, ], } `) }) test('retains a contribution-adjusted loss when a later period recovers', () => { expect( Campaigns.targetFunding({ annualRateBps: 1_000, funding, periods: [ period({ closingValuePerEarnShare: 750_000_000n, contributions: [{ assets: 7_884_000_000n, earnShareSupply: 31_536_000_000n }], elapsedSeconds: 150, }), period({ closingValuePerEarnShare: 1_250_000_000n, elapsedSeconds: 150, openingAssets: 23_652_000_000n, openingValuePerEarnShare: 750_000_000n, }), ], remainders: [], }), ).toStrictEqual({ assets: [13_125n, 13_125n], remainders: [0n, 0n] }) }) test.each([ { name: 'stable balances', periods: [period()], expected: [15_000n, 15_000n] }, { name: 'a boundary deposit redeemed at the interval start', periods: [period({ elapsedSeconds: 0, openingAssets: 315_360_000_000n }), period()], expected: [15_000n, 15_000n], }, { name: 'a boundary deposit redeemed one second into the interval', periods: [ period({ elapsedSeconds: 1, openingAssets: 315_360_000_000n }), period({ elapsedSeconds: 299 }), ], expected: [15_450n, 15_450n], }, { name: 'a mid-interval deposit', periods: [ period({ elapsedSeconds: 150 }), period({ elapsedSeconds: 150, openingAssets: 315_360_000_000n }), ], expected: [82_500n, 82_500n], }, { name: 'a mid-interval redemption', periods: [ period({ elapsedSeconds: 150, openingAssets: 315_360_000_000n }), period({ elapsedSeconds: 150 }), ], expected: [82_500n, 82_500n], }, { name: 'an empty vault receiving its first deposit', periods: [ period({ elapsedSeconds: 150, openingAssets: 0n, openingEarnShareSupply: 0n }), period({ elapsedSeconds: 150 }), ], expected: [7_500n, 7_500n], }, { name: 'an entire balance redeemed mid-interval', periods: [ period({ closingValuePerEarnShare: 0n, elapsedSeconds: 150 }), period({ elapsedSeconds: 150, openingAssets: 0n, openingEarnShareSupply: 0n }), ], expected: [7_500n, 7_500n], }, { name: 'capital present at both boundaries but absent between them', periods: [ period({ elapsedSeconds: 1, openingAssets: 315_360_000_000n }), period({ elapsedSeconds: 298 }), period({ elapsedSeconds: 1, openingAssets: 315_360_000_000n }), ], expected: [15_900n, 15_900n], }, ])('funds only time held for $name', ({ expected, periods }) => { expect( Campaigns.targetFunding({ annualRateBps: 1_000, funding, periods, remainders: [] }), ).toStrictEqual({ assets: expected, remainders: [0n, 0n] }) }) test('subtracts organic growth before debiting the ordered funding wallets', () => { expect( Campaigns.targetFunding({ annualRateBps: 1_000, funding, periods: [ period({ closingValuePerEarnShare: 1_000_000_500n, contributions: [{ assets: 7_500n, earnShareSupply: 31_536_000_000n }], }), ], remainders: [], }), ).toMatchInlineSnapshot(` { "assets": [ 6732n, 15000n, ], "remainders": [ 0n, 0n, ], } `) }) test.each([0, 150])( 'nets organic growth from a %i-second opening period before funding', (elapsedSeconds) => { expect( Campaigns.targetFunding({ annualRateBps: 1_000, funding, periods: [ period({ closingValuePerEarnShare: 1_000_001_000n, elapsedSeconds }), period({ elapsedSeconds: 300 - elapsedSeconds }), ], remainders: [], }), ).toStrictEqual({ assets: [0n, 0n], remainders: [0n, 0n] }) }, ) test('normalizes contributions to each period supply after deposits', () => { expect( Campaigns.targetFunding({ annualRateBps: 1_000, funding, periods: [ period({ closingValuePerEarnShare: 1_000_001_000n, contributions: [{ assets: 63_072n, earnShareSupply: 63_072_000_000n }], elapsedSeconds: 150, }), period({ closingValuePerEarnShare: 1_000_001_000n, elapsedSeconds: 150, openingAssets: 63_072_000_000n, openingEarnShareSupply: 63_072_000_000n, openingValuePerEarnShare: 1_000_001_000n, }), ], remainders: [], }), ).toStrictEqual({ assets: [22_500n, 22_500n], remainders: [0n, 0n] }) }) test('carries fractional accrual through historical catch-up and resumed runs', () => { let remainders: bigint[] = [] const assets = [0n, 0n] for (let index = 0; index < 12; index++) { const result = Campaigns.targetFunding({ annualRateBps: 1_000, funding, periods: [period({ openingAssets: 100_000n })], remainders, }) result.assets.forEach((amount, index) => { assets[index] = assets[index]! + amount }) remainders = result.remainders } expect({ assets, remainders }).toMatchInlineSnapshot(` { "assets": [ 0n, 1n, ], "remainders": [ 180000000000n, 44640000000n, ], } `) }) test('rejects invalid period durations', () => { expect(() => Campaigns.targetFunding({ annualRateBps: 1_000, funding, periods: [period({ elapsedSeconds: -1 })], remainders: [], }), ).toThrowErrorMatchingInlineSnapshot( '[Error: target-yield period duration must be a nonnegative integer]', ) }) }) describe('annualRateBps', () => { test('annualizes exact interval growth using simple ACT/365', () => { expect( Campaigns.annualRateBps({ elapsedSeconds: 300, growthAssets: 220n, principalAssets: 1_051_200_000n, }), ).toBe(220) }) test('returns zero for an empty vault', () => { expect( Campaigns.annualRateBps({ elapsedSeconds: 300, growthAssets: 0n, principalAssets: 0n, }), ).toBe(0) }) }) describe('rewardRates', () => { test('does not count target contributions toward the effective base rate', () => { const growthAssets = Campaigns.organicGrowth({ closingValuePerEarnShare: 1_000_100n, contributions: [{ assets: 100n, earnShareSupply: 1_000_000n }], openingAssets: 1_000_000n, openingEarnShareSupply: 1_000_000n, openingValuePerEarnShare: 1_000_000n, }) const organicRateBps = Campaigns.annualRateBps({ elapsedSeconds: 300, growthAssets, principalAssets: 1_000_000n, }) expect( Campaigns.rewardRates({ boostTargetRateBps: 700, organicRateBps, targetRateBps: 220, }), ).toStrictEqual({ baseRateBps: 220, boostRateBps: 480 }) }) test('tops up from organic growth when only boost is active', () => { expect(Campaigns.rewardRates({ boostTargetRateBps: 700, organicRateBps: 100 })).toStrictEqual({ baseRateBps: 100, boostRateBps: 600, }) }) test('tops up from the target floor when organic growth is lower', () => { expect( Campaigns.rewardRates({ boostTargetRateBps: 700, organicRateBps: 100, targetRateBps: 220, }), ).toStrictEqual({ baseRateBps: 220, boostRateBps: 480 }) }) test('tops up from organic growth when it exceeds the target floor', () => { expect( Campaigns.rewardRates({ boostTargetRateBps: 700, organicRateBps: 400, targetRateBps: 220, }), ).toStrictEqual({ baseRateBps: 400, boostRateBps: 300 }) }) test('stops boosting when organic growth exceeds the total target', () => { expect( Campaigns.rewardRates({ boostTargetRateBps: 700, organicRateBps: 800, targetRateBps: 220, }), ).toStrictEqual({ baseRateBps: 800, boostRateBps: 0 }) }) }) describe('allocate', () => { test('assigns every unit with address-order remainder ties', () => { const result = Campaigns.allocate({ earnShares: 5n, rewards: [ { assets: 1n, recipient: '0x0000000000000000000000000000000000000002' }, { assets: 1n, recipient: '0x0000000000000000000000000000000000000001' }, { assets: 1n, recipient: '0x0000000000000000000000000000000000000003' }, ], }) expect([...result]).toStrictEqual([ ['0x0000000000000000000000000000000000000001', 2n], ['0x0000000000000000000000000000000000000002', 2n], ['0x0000000000000000000000000000000000000003', 1n], ]) }) }) describe('meetsPayoutMinimum', () => { test('uses the current vault value and exact cumulative unpaid balance', () => { const options = { cumulativeEntitlement: 149n, cumulativePaid: 100n, minimumAssets: 100n, totalAssets: 2_000n, totalEarnShares: 1_000n, } expect([ Campaigns.meetsPayoutMinimum(options), Campaigns.meetsPayoutMinimum({ ...options, cumulativeEntitlement: 150n }), Campaigns.meetsPayoutMinimum({ ...options, cumulativePaid: 150n }), Campaigns.meetsPayoutMinimum({ ...options, totalAssets: 0n }), ]).toStrictEqual([false, true, false, false]) expect(() => Campaigns.meetsPayoutMinimum({ ...options, minimumAssets: -1n }), ).toThrowErrorMatchingInlineSnapshot(`[Error: minimum assets must be nonnegative]`) }) }) describe('buildStatement', () => { test('completes the combined calculation, allocation, statement, and payout path for 10,000 recipients', () => { const accounts = Array.from({ length: 10_000 }, (_, index) => { const recipient = numberToHex(index + 1, { size: 20 }) as Address return { accrualRemainder: 0n, allocatedPrincipalAssets: 10n ** 18n, cumulativeEntitlement: 0n, cumulativePaid: 0n, lots: [ { allocatedPrincipalAssets: (10n ** 18n).toString(), depositedAssets: (10n ** 18n).toString(), depositedEarnShares: (10n ** 18n).toString(), event: { blockNumber: index + 1, logIndex: 0, transactionIndex: 0 }, remainingEarnShares: (10n ** 18n).toString(), }, ], pendingRewardAssets: 0n, publicEarnShares: 10n ** 18n, qualifiedEarnShares: 10n ** 18n, recipient, registrationOrder: BigInt(index + 1), } }) const calculated = Projection.calculate({ accounts, annualRateBps: 700, assetPerEarnShareWad: 10n ** 18n, endsAt: 300, events: [], excluded: [], perUserPrincipalCapAssets: 10n ** 18n, startsAt: 0, totalPrincipalCapAssets: 10_000n * 10n ** 18n, }) const rewards = [...calculated.rewards].map(([recipient, assets]) => ({ assets, recipient })) const total = rewards.reduce((sum, entry) => sum + entry.assets, 0n) const allocation = Campaigns.allocate({ earnShares: total, rewards }) const statement = Campaigns.buildStatement({ campaignId, chainId: 4217, distributor, entitlements: calculated.accounts.map((account) => ({ cumulativeAmount: allocation.get(account.recipient)!.toString(), recipient: account.recipient, })), }) const batches = Campaigns.buildPayoutBatches(statement, { maximumSize: 80 }) expect({ batches: batches.length, calculated: calculated.accounts.length, entries: statement.entries.length, recipients: calculated.rewards.size, }).toStrictEqual({ batches: 125, calculated: 10_000, entries: 10_000, recipients: 10_000 }) }) test('builds a large campaign into bounded payout batches', () => { const entries = Array.from({ length: 10_000 }, (_, index) => ({ cumulativeAmount: String(index + 1), recipient: numberToHex(index + 1, { size: 20 }) as Address, })) const statement = Campaigns.buildStatement({ campaignId, chainId: 4217, distributor, entitlements: entries, }) const batches = Campaigns.buildPayoutBatches(statement, { maximumSize: 80 }) expect({ batches: batches.length, entries: statement.entries.length }).toStrictEqual({ batches: 125, entries: 10_000, }) }) test('is deterministic and emits valid individual proofs', () => { const statement = Campaigns.buildStatement({ campaignId, chainId: 42431, distributor, entitlements: [ { cumulativeAmount: '3', recipient: '0x0000000000000000000000000000000000000003' }, { cumulativeAmount: '1', recipient: '0x0000000000000000000000000000000000000001' }, { cumulativeAmount: '2', recipient: '0x0000000000000000000000000000000000000002' }, ], }) expect( statement.entries.every((entry) => { const proof = Campaigns.buildProof(statement, entry.recipient) return proof ? verify(statement.root, entry.leaf, proof) : false }), ).toBe(true) expect({ recipients: statement.entries.map((entry) => entry.recipient), root: statement.root, totalEntitlement: statement.totalEntitlement, }).toMatchInlineSnapshot(` { "recipients": [ "0x0000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000003", ], "root": "0x6a2a1fccee8217d124d9183a0efe1c03765e6be9934f760b710afe20153b7671", "totalEntitlement": "6", } `) }) test('builds valid contiguous multiproofs for every chunk shape', () => { const statement = Campaigns.buildStatement({ campaignId, chainId: 42431, distributor, entitlements: Array.from({ length: 19 }, (_, index) => ({ cumulativeAmount: String(index + 1), recipient: `0x${(index + 1).toString(16).padStart(40, '0')}` as Address, })), }) const batches = Campaigns.buildPayoutBatches(statement, { maximumSize: 4 }) expect(batches).toHaveLength(5) for (const batch of batches) { const leaves = batch.recipients.map((recipient, index) => Campaigns.rewardLeaf({ campaignId, chainId: 42431, cumulativeAmount: BigInt(batch.cumulativeAmounts[index]!), distributor, recipient, }), ) expect(processMultiproof(leaves, batch.multiproof, batch.proofFlags)).toBe(statement.root) } }) }) describe('nextBoundary', () => { test('always returns a future boundary relative to the schedule start', () => { expect([ Campaigns.nextBoundary({ intervalSeconds: 300, origin: 100, timestamp: 401 }), Campaigns.nextBoundary({ intervalSeconds: 300, origin: 100, timestamp: 700 }), ]).toStrictEqual([700, 1_000]) }) }) describe('finalBoundary', () => { test('skips a trailing partial interval', () => { const config = Campaigns.schema.Config.parse({ boostRewards: { endTimestamp: 1_050, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, perUserPrincipalCapAssets: '100', startTimestamp: 100, targetAnnualRateBps: 700, totalPrincipalCapAssets: '100', treasury: '0x0000000000000000000000000000000000000004', }, }) expect(Campaigns.finalBoundary(config)).toBe(1_000) }) test('uses the later boundary across independent schedules', () => { const boostRewards = { endTimestamp: 1_950, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, perUserPrincipalCapAssets: '100', startTimestamp: 300, targetAnnualRateBps: 700, totalPrincipalCapAssets: '100', treasury: '0x0000000000000000000000000000000000000004', } const targetYield = { annualRate: { bps: 500 }, endTimestamp: 2_450, funding: [{ remainder: true as const, wallet: '0x0000000000000000000000000000000000000005' }], intervalSeconds: 300, startTimestamp: 600, } expect( Campaigns.finalBoundary(Campaigns.schema.Config.parse({ boostRewards, targetYield })), ).toBe(2_400) }) }) describe('overlapsCompleteIntervals', () => { test('excludes a trailing partial interval', () => { const schedule = Campaigns.schema.BoostRewards.parse({ endTimestamp: 1_050, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, perUserPrincipalCapAssets: '100', startTimestamp: 100, targetAnnualRateBps: 700, totalPrincipalCapAssets: '100', treasury: '0x0000000000000000000000000000000000000004', }) expect([ Campaigns.overlapsCompleteIntervals(schedule, { endsAt: 1_000, startsAfter: 700 }), Campaigns.overlapsCompleteIntervals(schedule, { endsAt: 1_300, startsAfter: 1_000 }), ]).toStrictEqual([true, false]) }) }) describe('shouldPushPayout', () => { const boostRewards = Campaigns.schema.Config.parse({ boostRewards: { endTimestamp: 2_100, excludedAddresses: [], funding: { wallet: '0x0000000000000000000000000000000000000003' }, intervalSeconds: 300, payoutSchedule: { intervalSeconds: 1_000, startTimestamp: 650 }, perUserPrincipalCapAssets: '100', startTimestamp: 300, targetAnnualRateBps: 700, totalPrincipalCapAssets: '100', treasury: '0x0000000000000000000000000000000000000004', }, }).boostRewards! test('pushes on deadlines and retries after the boost ends', () => { expect( [ { endsAt: 600, startsAfter: 300 }, { endsAt: 900, startsAfter: 600 }, { endsAt: 1_500, startsAfter: 900 }, { endsAt: 1_800, startsAfter: 1_500 }, { endsAt: 2_100, startsAfter: 1_800 }, { endsAt: 2_400, startsAfter: 2_100 }, ].map((options) => Campaigns.shouldPushPayout(boostRewards, options)), ).toStrictEqual([false, true, false, true, true, true]) }) test('collapses multiple elapsed payout deadlines into one settlement payout', () => { expect(Campaigns.shouldPushPayout(boostRewards, { endsAt: 1_800, startsAfter: 300 })).toBe(true) }) test('preserves per-settlement pushes when no payout schedule is configured', () => { const { payoutSchedule: _, ...everySettlement } = boostRewards expect( [ { endsAt: 900, startsAfter: 600 }, { endsAt: 2_400, startsAfter: 2_100 }, ].map((options) => Campaigns.shouldPushPayout(everySettlement, options)), ).toStrictEqual([true, true]) }) }) function period( options: Partial = {}, ): Campaigns.targetFunding.Period { return { closingValuePerEarnShare: 1_000_000_000n, contributions: [], elapsedSeconds: 300, openingAssets: 31_536_000_000n, openingEarnShareSupply: 31_536_000_000n, openingValuePerEarnShare: 1_000_000_000n, ...options, } } function verify(root: Hex, leaf: Hex, proof: readonly Hex[]): boolean { return proof.reduce(nodeHash, leaf) === root } function processMultiproof( leaves: readonly Hex[], proof: readonly Hex[], flags: readonly boolean[], ): Hex { if (leaves.length + proof.length !== flags.length + 1) throw new Error('invalid multiproof') const hashes: Hex[] = [] let leaf = 0 let hash = 0 let proofIndex = 0 for (const flag of flags) { const left = leaf < leaves.length ? leaves[leaf++]! : hashes[hash++]! const right = flag ? leaf < leaves.length ? leaves[leaf++]! : hashes[hash++]! : proof[proofIndex++]! hashes.push(nodeHash(left, right)) } return hashes.at(-1) ?? leaves[0] ?? proof[0]! } function nodeHash(left: Hex, right: Hex): Hex { return keccak256( concatHex(left.toLowerCase() < right.toLowerCase() ? [left, right] : [right, left]), ) }