import * as TestApp from '../../../test/App.js' import * as Campaigns from '../../internal/rewards/Campaigns.js' import * as Signer from '../../internal/rewards/Signer.js' import * as Runner from '../../internal/rewards/Runner.js' import * as EarnVaults from './earnVaults.js' import * as RewardCampaignDistributors from './rewardCampaignDistributors.js' import * as RewardCampaigns from './rewardCampaigns.js' import * as RewardRuns from './rewardRuns.js' import * as RewardTransactionAttempts from './rewardTransactionAttempts.js' const vaultAddress = '0x1000000000000000000000000000000000000001' const shareAddress = '0x2000000000000000000000000000000000000002' const assetAddress = '0x3000000000000000000000000000000000000003' const funder = '0x4000000000000000000000000000000000000004' const treasury = '0x5000000000000000000000000000000000000005' const signer = '0x6000000000000000000000000000000000000006' const config = { boostRewards: { endTimestamp: 2_000_000_600, excludedAddresses: [], funding: { wallet: funder }, intervalSeconds: 300, perUserPrincipalCapAssets: '25000000000', startTimestamp: 2_000_000_000, targetAnnualRateBps: 700, totalPrincipalCapAssets: '10000000000000', treasury, }, } satisfies Campaigns.Config const targetYieldConfig = { annualRate: { bps: 500 }, endTimestamp: 2_000_000_600, funding: [{ remainder: true, wallet: funder }], intervalSeconds: 300, startTimestamp: 2_000_000_000, } satisfies NonNullable describe('reward campaign persistence', () => { test('schedules deterministic periphery provisioning before campaign start', async () => { const db = TestApp.database() await seedCampaign(db) await expect(Runner.due(db, 1_999_999_000_000)).resolves.toStrictEqual([ { chainId: 4217, type: 'earn:reward:run', vaultAddress }, ]) }) test('schedules periphery required only by a pending configuration', async () => { const db = TestApp.database() await seedVault(db) const targetOnly = { targetYield: targetYieldConfig } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: targetOnly, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) await RewardCampaigns.setBindings(db, { chainId: 4217, config: targetOnly, controllerAddress: '0x7000000000000000000000000000000000000007', vaultAddress, }) await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_000_900, startTimestamp: 2_000_000_300, }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }) await expect(RewardCampaigns.listUnprovisioned(db, { limit: 100 })).resolves.toMatchObject([ { controllerAddress: '0x7000000000000000000000000000000000000007', vaultAddress }, ]) }) test('does not reschedule a fully delivered provisioned campaign', async () => { const db = TestApp.database() await seedCampaign(db) await RewardCampaigns.setBindings(db, { chainId: 4217, distributorAddress: '0x7000000000000000000000000000000000000007', vaultAddress, }) await RewardCampaigns.advance(db, { chainId: 4217, deliveredThrough: config.boostRewards.endTimestamp, expectedDeliveredThrough: config.boostRewards.startTimestamp, vaultAddress, }) await expect(Runner.due(db, 2_000_001_000_000)).resolves.toStrictEqual([]) }) test('does not list a campaign delivered through its final complete boundary', async () => { const db = TestApp.database() await seedVault(db) const partial = { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_000_650 }, } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: partial, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) await RewardCampaigns.advance(db, { chainId: 4217, deliveredThrough: 2_000_000_600, expectedDeliveredThrough: 2_000_000_000, vaultAddress, }) await expect( RewardCampaigns.listDue(db, { limit: 100, through: 2_000_001_000 }), ).resolves.toStrictEqual([]) }) test('keeps a campaign due through the later independent schedule', async () => { const db = TestApp.database() await seedVault(db) const independent = { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_001_200 }, targetYield: { ...targetYieldConfig, endTimestamp: 2_000_000_600 }, } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: independent, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) await RewardCampaigns.advance(db, { chainId: 4217, deliveredThrough: 2_000_000_600, expectedDeliveredThrough: 2_000_000_000, vaultAddress, }) await expect( RewardCampaigns.listDue(db, { limit: 100, through: 2_000_001_200 }), ).resolves.toMatchObject([{ vaultAddress }]) }) test('preserves the original accounting grid after an earlier schedule is removed', async () => { const db = TestApp.database() await seedVault(db) const independent = { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_001_800, startTimestamp: 2_000_001_200, }, targetYield: targetYieldConfig, } const campaign = await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: independent, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) const run = await RewardRuns.ensure(db, { chainId: 4217, config: campaign.config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) await RewardRuns.transition(db, { fence: '0', id: run.id, patch: {}, phase: 'delivered' }) await RewardCampaigns.advance(db, { chainId: 4217, deliveredThrough: 2_000_000_300, expectedDeliveredThrough: 2_000_000_000, vaultAddress, }) await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: independent.boostRewards }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_310, vaultAddress, }) await RewardCampaigns.advance(db, { chainId: 4217, deliveredThrough: 2_000_000_600, expectedDeliveredThrough: 2_000_000_300, vaultAddress, }) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: { ...independent.boostRewards, intervalSeconds: 60, }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_700, vaultAddress, }), ).rejects.toThrow('intervalSeconds cannot change after campaign start') }) test('replaces pre-start configuration and schedules live updates prospectively', async () => { const db = TestApp.database() await seedVault(db) const created = await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) const replay = await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_100, vaultAddress, }) const preStart = await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { ...config, boostRewards: { ...config.boostRewards, endTimestamp: 2_000_000_900, startTimestamp: 2_000_000_300, targetAnnualRateBps: 725, }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_100, vaultAddress, }) const updated = await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { ...preStart.config, boostRewards: { ...preStart.config.boostRewards!, targetAnnualRateBps: 750 }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_310, vaultAddress, }) expect({ deliveredThrough: created.deliveredThrough, preStartDeliveredThrough: preStart.deliveredThrough, preStartPending: preStart.pendingConfig, replayPending: replay.pendingConfig, pendingEffectiveAt: updated.pendingEffectiveAt, pendingRate: updated.pendingConfig?.boostRewards?.targetAnnualRateBps, }).toStrictEqual({ deliveredThrough: '2000000000', pendingEffectiveAt: '2000000600', pendingRate: 750, preStartDeliveredThrough: '2000000300', preStartPending: null, replayPending: null, }) }) test('keeps provisioned boost bindings immutable before campaign start', async () => { const db = TestApp.database() await seedCampaign(db) await RewardCampaigns.setBindings(db, { chainId: 4217, distributorAddress: '0x7000000000000000000000000000000000000007', vaultAddress, }) const replace = (config_next: Campaigns.Config) => RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: config_next, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_100, vaultAddress, }) await expect( replace({ boostRewards: { ...config.boostRewards, treasury: '0x8000000000000000000000000000000000000008', }, }), ).rejects.toThrow('boostRewards treasury is immutable after distributor provisioning') await expect(replace({ targetYield: targetYieldConfig })).rejects.toThrow( 'boostRewards cannot be removed after distributor provisioning', ) }) test('rejects stale provisioning bindings after configuration changes', async () => { const db = TestApp.database() await seedCampaign(db) await RewardCampaigns.setBindings(db, { chainId: 4217, config, distributorAddress: '0x6000000000000000000000000000000000000006', vaultAddress, }) const updated = await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: { ...config.boostRewards, targetAnnualRateBps: 750 }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_100, vaultAddress, }) const stale = await RewardCampaigns.setBindings(db, { chainId: 4217, config, distributorAddress: '0x7000000000000000000000000000000000000007', vaultAddress, }) const current = await RewardCampaigns.setBindings(db, { chainId: 4217, config: updated.config, distributorAddress: '0x8000000000000000000000000000000000000008', vaultAddress, }) expect([stale, current?.distributorAddress]).toStrictEqual([ undefined, '0x8000000000000000000000000000000000000008', ]) await expect( RewardCampaignDistributors.list(db, { chainId: 4217, vaultAddress }), ).resolves.toMatchObject([ { distributorAddress: '0x6000000000000000000000000000000000000006' }, { distributorAddress: '0x8000000000000000000000000000000000000008' }, ]) }) test('exposes provisioning failures until verified bindings replace them', async () => { const db = TestApp.database() await seedCampaign(db) const failed = await RewardCampaigns.setProvisioningError(db, { chainId: 4217, config, error: 'reviewed factory is unavailable', vaultAddress, }) const ready = await RewardCampaigns.setBindings(db, { chainId: 4217, config, distributorAddress: '0x7000000000000000000000000000000000000007', error: null, vaultAddress, }) expect([failed?.provisioningError, ready?.provisioningError]).toStrictEqual([ 'reviewed factory is unavailable', null, ]) await expect( RewardCampaigns.setProvisioningError(db, { chainId: 4217, config, error: 'late duplicate failure', vaultAddress, }), ).resolves.toBeUndefined() await expect(RewardCampaigns.get(db, { chainId: 4217, vaultAddress })).resolves.toMatchObject({ provisioningError: null, }) }) test('clears a provisioning failure when its missing capability is removed', async () => { const db = TestApp.database() await seedCampaign(db) await RewardCampaigns.setBindings(db, { chainId: 4217, config, distributorAddress: '0x7000000000000000000000000000000000000007', vaultAddress, }) const combined = { ...config, targetYield: targetYieldConfig, } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: combined, earnShareAddress: shareAddress, earnShareDecimals: 6, now: 1_999_999_100, vaultAddress, }) await RewardCampaigns.setProvisioningError(db, { chainId: 4217, config: combined, error: 'controller deployment failed', vaultAddress, }) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config, earnShareAddress: shareAddress, earnShareDecimals: 6, now: 1_999_999_200, vaultAddress, }), ).resolves.toMatchObject({ provisioningError: null }) }) test('rejects cap changes after boost accrual begins', async () => { const db = TestApp.database() await seedVault(db) await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { ...config, boostRewards: { ...config.boostRewards, totalPrincipalCapAssets: '10000000000001' }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }), ).rejects.toThrow('boost principal caps are immutable') }) test('rejects exclusion changes after boost accrual begins', async () => { const db = TestApp.database() await seedCampaign(db) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: { ...config.boostRewards, excludedAddresses: ['0x7000000000000000000000000000000000000007'], }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }), ).rejects.toThrow('boost excludedAddresses are immutable') }) test('schedules updates after the active run snapshot', async () => { const db = TestApp.database() await seedCampaign(db) await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_600, startsAfter: 2_000_000_000, vaultAddress, }) const updated = await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_000_900, targetAnnualRateBps: 750, }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }) expect(updated.pendingEffectiveAt).toBe('2000000600') }) test('rejects extending a campaign after its durable schedule ended', async () => { const db = TestApp.database() await seedCampaign(db) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_000_900 }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: config.boostRewards.endTimestamp, vaultAddress, }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[RewardCampaigns.ConfigurationError: an ended reward campaign cannot be extended]`, ) }) test('rejects updates after the final complete boundary of a partial schedule', async () => { const db = TestApp.database() await seedVault(db) const partial = { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_000_650 }, } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: partial, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: { ...partial.boostRewards, endTimestamp: 2_000_000_900 }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_610, vaultAddress, }), ).rejects.toThrow('an ended reward campaign cannot be extended') }) test('updates an active schedule after the other schedule has ended', async () => { const db = TestApp.database() await seedVault(db) const independent = { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_001_200 }, targetYield: { ...targetYieldConfig, endTimestamp: 2_000_000_300 }, } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: independent, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { ...independent, boostRewards: { ...independent.boostRewards, enabled: false }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_310, vaultAddress, }), ).resolves.toMatchObject({ pendingConfig: { boostRewards: { enabled: false }, targetYield: independent.targetYield }, pendingEffectiveAt: '2000000600', }) }) test('updates a future boost after target yield has started', async () => { const db = TestApp.database() await seedVault(db) const independent = { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_001_800, startTimestamp: 2_000_000_600, }, targetYield: { ...targetYieldConfig, endTimestamp: 2_000_001_200 }, } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: independent, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) const boostRewards = { ...independent.boostRewards, endTimestamp: 2_000_002_100, perUserPrincipalCapAssets: '26000000000', startTimestamp: 2_000_000_900, totalPrincipalCapAssets: '11000000000000', } await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards, targetYield: independent.targetYield }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }), ).resolves.toMatchObject({ pendingConfig: { boostRewards }, pendingEffectiveAt: '2000000300', }) }) test('rejects a retroactive target replacement start', async () => { const db = TestApp.database() await seedVault(db) const independent = { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_001_800 }, targetYield: { ...targetYieldConfig, endTimestamp: 2_000_001_800, startTimestamp: 2_000_000_600, }, } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: independent, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { ...independent, targetYield: { ...independent.targetYield, startTimestamp: 2_000_000_000 }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }), ).rejects.toThrow( 'replacement targetYield schedules must start at or after the update boundary', ) }) test('treats omitted and explicit enabled boost flags as equal', async () => { const db = TestApp.database() const campaign = await seedCampaign(db) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: { ...config.boostRewards, enabled: true } }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }), ).resolves.toStrictEqual(campaign) }) test('keeps replacement schedules on the active boundary grid', async () => { const db = TestApp.database() await seedVault(db) const targetOnly = { targetYield: targetYieldConfig } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: targetOnly, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { targetYield: { ...targetYieldConfig, endTimestamp: 2_000_000_901, startTimestamp: 2_000_000_001, }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }), ).rejects.toThrow('replacement schedules must remain on the active boundary grid') }) test('requires a complete interval after the replacement boundary', async () => { const db = TestApp.database() await seedVault(db) const targetOnly = { targetYield: targetYieldConfig } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: targetOnly, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { targetYield: { ...targetYieldConfig, endTimestamp: 2_000_000_650 }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_310, vaultAddress, }), ).rejects.toThrow('targetYield must have a complete interval after the update boundary') }) test('reposting the active document cancels its pending replacement', async () => { const db = TestApp.database() await seedCampaign(db) const pending = await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: { boostRewards: { ...config.boostRewards, targetAnnualRateBps: 750 }, }, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }) const cancelled = await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_020, vaultAddress, }) expect({ cancelled: cancelled.pendingConfig, pending: pending.pendingConfig }).toMatchObject({ cancelled: null, pending: { boostRewards: { targetAnnualRateBps: 750 } }, }) }) test('does not discard boost bindings provisioned for a pending configuration', async () => { const db = TestApp.database() await seedVault(db) const targetOnly = { targetYield: targetYieldConfig } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: targetOnly, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) const pending = { boostRewards: { ...config.boostRewards, endTimestamp: 2_000_000_900, startTimestamp: 2_000_000_300, }, } await RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: pending, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_010, vaultAddress, }) await RewardCampaigns.setBindings(db, { chainId: 4217, config: pending, distributorAddress: '0x7000000000000000000000000000000000000007', vaultAddress, }) await expect( RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: targetOnly, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 2_000_000_020, vaultAddress, }), ).rejects.toThrowErrorMatchingInlineSnapshot( `[RewardCampaigns.ConfigurationError: boostRewards cannot be removed after distributor provisioning]`, ) }) test('permanently binds the first reward signer', async () => { const db = TestApp.database() await seedCampaign(db) const bound = await RewardCampaigns.bindSigner(db, { chainId: 4217, signerAddress: signer, vaultAddress, }) const replay = await RewardCampaigns.bindSigner(db, { chainId: 4217, signerAddress: signer, vaultAddress, }) const rejected = await RewardCampaigns.bindSigner(db, { chainId: 4217, signerAddress: '0x7000000000000000000000000000000000000007', vaultAddress, }) expect([bound?.signerAddress, replay?.signerAddress, rejected]).toStrictEqual([ signer.toLowerCase(), signer.toLowerCase(), undefined, ]) }) test('preserves the TIDX cursor when an interval contains no new events', async () => { const db = TestApp.database() const campaign = await seedCampaign(db) const cursor = { blockNumber: 12, logIndex: 4, transactionIndex: 3 } const first = await RewardCampaigns.advance(db, { chainId: 4217, deliveredThrough: Number(campaign.deliveredThrough) + 300, eventCursor: cursor, expectedDeliveredThrough: Number(campaign.deliveredThrough), vaultAddress, }) const second = await RewardCampaigns.advance(db, { chainId: 4217, deliveredThrough: Number(campaign.deliveredThrough) + 600, expectedDeliveredThrough: Number(campaign.deliveredThrough) + 300, vaultAddress, }) expect([first?.eventCursor, second?.eventCursor]).toStrictEqual([cursor, cursor]) }) }) describe('reward execution fencing and transaction journal', () => { test('exposes a root-confirmed statement before the phase transition completes', async () => { const db = TestApp.database() await seedCampaign(db) const run = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) const statement = Campaigns.buildStatement({ campaignId: `0x${'00'.repeat(12)}${vaultAddress.slice(2)}`, chainId: 4217, distributor: '0x7000000000000000000000000000000000000007', entitlements: [{ cumulativeAmount: '1', recipient: vaultAddress }], }) await RewardRuns.transition(db, { fence: run.fence, id: run.id, patch: { root: statement.root, rootVersion: '0', statement, statementHash: statement.statementHash, }, phase: 'statement', }) const attempt = await RewardTransactionAttempts.create(db, { chainId: 4217, intent: { id: `0x${'11'.repeat(32)}`, operation: 'publish', payload: {}, }, runId: run.id, signer, vaultAddress, }) await RewardTransactionAttempts.confirm(db, { id: attempt.id, receipt: { blockHash: `0x${'22'.repeat(32)}`, blockNumber: '1', gasUsed: '1', status: 'success', transactionHash: `0x${'33'.repeat(32)}`, }, }) await expect( RewardRuns.latestStatement(db, { chainId: 4217, vaultAddress }), ).resolves.toMatchObject({ id: run.id, rootVersion: '1', statementHash: statement.statementHash, }) }) test('leases one run at a time and fences a later owner', async () => { vi.useFakeTimers() try { vi.setSystemTime('2026-01-01T00:00:00.000Z') const db = TestApp.database() await seedCampaign(db) const run = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) const first = await RewardRuns.acquire(db, { id: run.id, leaseMilliseconds: 60_000 }) expect( await RewardRuns.acquire(db, { id: run.id, leaseMilliseconds: 60_000 }), ).toBeUndefined() vi.advanceTimersByTime(60_001) const second = await RewardRuns.acquire(db, { id: run.id, leaseMilliseconds: 60_000 }) expect([first?.fence, second?.fence]).toStrictEqual(['1', '2']) expect( await RewardRuns.transition(db, { fence: first!.fence, id: run.id, patch: {}, phase: 'calculating', }), ).toBeUndefined() } finally { vi.useRealTimers() } }) test('renews only the current fenced run lease', async () => { vi.useFakeTimers() try { vi.setSystemTime('2026-01-01T00:00:00.000Z') const db = TestApp.database() await seedCampaign(db) const run = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) const leased = await RewardRuns.acquire(db, { id: run.id, leaseMilliseconds: 60_000 }) const renewed = await RewardRuns.renew(db, { fence: leased!.fence, id: run.id, leaseExpiresAt: '2026-01-01T00:02:00.000Z', }) const stale = await RewardRuns.renew(db, { fence: '0', id: run.id, leaseExpiresAt: '2026-01-01T00:03:00.000Z', }) vi.advanceTimersByTime(120_001) const expired = await RewardRuns.renew(db, { fence: leased!.fence, id: run.id, leaseExpiresAt: '2026-01-01T00:04:00.000Z', }) expect({ expired, renewed, stale }).toStrictEqual({ expired: false, renewed: true, stale: false, }) } finally { vi.useRealTimers() } }) test('resumes an existing interval snapshot after later boundaries become available', async () => { const db = TestApp.database() await seedCampaign(db) const first = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) const resumed = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_600, startsAfter: 2_000_000_000, vaultAddress, }) expect({ endsAt: resumed.endsAt, sameRun: resumed.id === first.id }).toStrictEqual({ endsAt: '2000000300', sameRun: true, }) }) test('persists bytes before broadcast and prevents nonce reuse', async () => { const db = TestApp.database() await seedCampaign(db) const signerIntent = { boostRewards: true, domain: { chainId: 4217, earnShare: shareAddress, earnVault: vaultAddress, factory: '0x7000000000000000000000000000000000000007', treasury, }, operation: 'deploy' as const, targetYield: false, treasury, } satisfies Signer.Request['intent'] const intentId = Signer.id(signerIntent) const first = await RewardTransactionAttempts.create(db, { chainId: 4217, intent: { id: intentId, operation: 'deploy', payload: signerIntent }, signer, vaultAddress, }) const signed = await RewardTransactionAttempts.recordSignature(db, { expiresAt: null, id: first.id, nonce: '9', signedBytes: '0x01', transactionHash: `0x${'11'.repeat(32)}`, }) expect(signed?.state).toBe('signed') const second = await RewardTransactionAttempts.create(db, { chainId: 4217, intent: { id: `0x${'22'.repeat(32)}`, operation: 'deploy', payload: signerIntent }, signer, vaultAddress, }) await expect( RewardTransactionAttempts.recordSignature(db, { expiresAt: null, id: second.id, nonce: '9', signedBytes: '0x02', transactionHash: `0x${'33'.repeat(32)}`, }), ).rejects.toMatchObject({ code: '23505' }) }) test('keeps confirmed target funding terminal for reconciliation', async () => { const db = TestApp.database() await seedCampaign(db) const run = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) const signerIntent = { controller: '0x7000000000000000000000000000000000000007', domain: { chainId: 4217, earnShare: shareAddress, earnVault: vaultAddress, factory: '0x8000000000000000000000000000000000000008', treasury: '0x0000000000000000000000000000000000000000', }, funders: [funder], maxEarnShareSupply: '100', operation: 'targetYield' as const, requestedAssets: ['5'], } satisfies Signer.Request['intent'] const intentId = Signer.id(signerIntent) const attempt = await RewardTransactionAttempts.create(db, { chainId: 4217, intent: { id: intentId, operation: 'targetYield', payload: signerIntent }, runId: run.id, signer, vaultAddress, }) await RewardTransactionAttempts.recordSignature(db, { expiresAt: null, id: attempt.id, nonce: '10', signedBytes: '0x01', transactionHash: `0x${'44'.repeat(32)}`, }) await expect( RewardTransactionAttempts.latestRecoverable(db, { operation: 'targetYield', runId: run.id, }), ).resolves.toMatchObject({ id: attempt.id, state: 'signed' }) await RewardTransactionAttempts.confirm(db, { id: attempt.id, receipt: { blockHash: `0x${'55'.repeat(32)}`, blockNumber: '12', gasUsed: '100', status: 'success', transactionHash: `0x${'44'.repeat(32)}`, }, }) await expect( RewardTransactionAttempts.latestRecoverable(db, { operation: 'targetYield', runId: run.id, }), ).resolves.toMatchObject({ id: attempt.id, state: 'confirmed' }) await expect( RewardTransactionAttempts.latestRecoverable(db, { operation: 'targetYield', runId: run.id, }), ).resolves.toMatchObject({ id: attempt.id, state: 'confirmed' }) }) test('retries confirmed target funding that made no contribution', async () => { const db = TestApp.database() await seedCampaign(db) const run = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) const signerIntent = { controller: '0x7000000000000000000000000000000000000007', domain: { chainId: 4217, earnShare: shareAddress, earnVault: vaultAddress, factory: '0x8000000000000000000000000000000000000008', treasury: '0x0000000000000000000000000000000000000000', }, funders: [funder], maxEarnShareSupply: '100', operation: 'targetYield' as const, requestedAssets: ['5'], } satisfies Signer.Request['intent'] const intentId = Signer.id(signerIntent) const attempt = await RewardTransactionAttempts.create(db, { chainId: 4217, intent: { id: intentId, operation: 'targetYield', payload: signerIntent }, runId: run.id, signer, vaultAddress, }) await RewardTransactionAttempts.recordSignature(db, { expiresAt: null, id: attempt.id, nonce: '10', signedBytes: '0x01', transactionHash: `0x${'44'.repeat(32)}`, }) await RewardTransactionAttempts.confirm(db, { id: attempt.id, receipt: { blockHash: `0x${'55'.repeat(32)}`, blockNumber: '12', gasUsed: '100', status: 'success', transactionHash: `0x${'44'.repeat(32)}`, }, }) await expect(RewardTransactionAttempts.markIneffective(db, attempt.id)).resolves.toMatchObject({ id: attempt.id, receipt: { status: 'success' }, state: 'ineffective', }) await expect( RewardTransactionAttempts.latestRecoverable(db, { operation: 'targetYield', runId: run.id, }), ).resolves.toBeUndefined() await expect( RewardTransactionAttempts.create(db, { chainId: 4217, intent: { id: intentId, operation: 'targetYield', payload: signerIntent }, replacementOf: attempt.id, runId: run.id, signer, vaultAddress, }), ).resolves.toMatchObject({ replacementOf: attempt.id, state: 'created' }) }) test('does not reuse a confirmed intent from another reward run', async () => { const db = TestApp.database() await seedCampaign(db) const firstRun = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) await RewardRuns.transition(db, { fence: '0', id: firstRun.id, patch: {}, phase: 'delivered', }) await RewardCampaigns.advance(db, { chainId: 4217, deliveredThrough: 2_000_000_300, expectedDeliveredThrough: 2_000_000_000, vaultAddress, }) const secondRun = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_600, startsAfter: 2_000_000_300, vaultAddress, }) const signerIntent = { boostRewards: true, domain: { chainId: 4217, earnShare: shareAddress, earnVault: vaultAddress, factory: '0x7000000000000000000000000000000000000007', treasury, }, operation: 'deploy' as const, targetYield: false, treasury, } satisfies Signer.Request['intent'] const intentId = Signer.id(signerIntent) const first = await RewardTransactionAttempts.create(db, { chainId: 4217, intent: { id: intentId, operation: 'deploy', payload: signerIntent }, runId: firstRun.id, signer, vaultAddress, }) await RewardTransactionAttempts.confirm(db, { id: first.id, receipt: { blockHash: `0x${'11'.repeat(32)}`, blockNumber: '1', gasUsed: '1', status: 'success', transactionHash: `0x${'22'.repeat(32)}`, }, }) const second = await RewardTransactionAttempts.create(db, { chainId: 4217, intent: { id: intentId, operation: 'deploy', payload: signerIntent }, runId: secondRun.id, signer, vaultAddress, }) await expect( Promise.all([ RewardTransactionAttempts.latestForIntent(db, { intentId, runId: firstRun.id, }), RewardTransactionAttempts.latestForIntent(db, { intentId, runId: secondRun.id, }), ]), ).resolves.toMatchObject([{ id: first.id }, { id: second.id }]) }) }) async function seedCampaign(db: ReturnType) { await seedVault(db) return RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress, }) } async function seedVault(db: ReturnType) { return EarnVaults.upsert(db, { chainId: 4217, description: null, label: 'Reward test vault', privateInputTokens: [], privateOutputTokens: [], slug: 'reward-test-vault', vaultAddress, zones: [], }) }