Skip to content

Commit 41feaa9

Browse files
committed
test: add comprehensive early finalization test suite
1 parent 05f5578 commit 41feaa9

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed

packages/evm/test/timebased/TimeBasedIncentiveManager.t.sol

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3392,6 +3392,209 @@ contract TimeBasedIncentiveManagerTest is Test {
33923392
vm.prank(CREATOR);
33933393
manager.withdraw(campaignId);
33943394
}
3395+
3396+
////////////////////////////////
3397+
// Budget-exhausted early finalization (totalCommitted >= totalRewards)
3398+
////////////////////////////////
3399+
3400+
function test_EarlyFinalization_WhenBudgetFullyCommitted() public {
3401+
(uint256 campaignId, TimeBasedIncentiveCampaign campaign) = _createCampaignWithRoot();
3402+
uint256 netRewards = campaign.totalRewards(); // 9 ether after 10% fee
3403+
3404+
// Set totalCommitted = totalRewards (budget fully committed) — still before endTime
3405+
manager.updateRoot(campaignId, keccak256("root"), netRewards, false);
3406+
3407+
// Finalize should succeed even though endTime hasn't passed
3408+
manager.updateRoot(campaignId, keccak256("final"), netRewards, true);
3409+
assertTrue(campaign.finalized(), "Should be finalized when totalCommitted >= totalRewards");
3410+
}
3411+
3412+
function test_EarlyFinalization_StillRevertsWhenBudgetNotFullyCommitted() public {
3413+
(uint256 campaignId, TimeBasedIncentiveCampaign campaign) = _createCampaignWithRoot();
3414+
uint256 netRewards = campaign.totalRewards();
3415+
3416+
// Set totalCommitted < totalRewards — finalize should still revert before endTime
3417+
manager.updateRoot(campaignId, keccak256("root"), netRewards - 1, false);
3418+
3419+
vm.expectRevert(TimeBasedIncentiveCampaign.CampaignNotEnded.selector);
3420+
manager.updateRoot(campaignId, keccak256("final"), netRewards - 1, true);
3421+
3422+
assertFalse(campaign.finalized(), "Should not be finalized");
3423+
}
3424+
3425+
function test_EarlyFinalization_WithdrawBeforeEndTime() public {
3426+
(uint256 campaignId, TimeBasedIncentiveCampaign campaign) = _createCampaignWithRoot();
3427+
uint256 netRewards = campaign.totalRewards(); // 9 ether
3428+
3429+
// Mint extra tokens to create excess beyond what's owed
3430+
rewardToken.mint(address(campaign), 3 ether);
3431+
// balance = 12 ether, totalRewards = 9 ether
3432+
3433+
// Commit full budget and finalize before endTime
3434+
manager.updateRoot(campaignId, keccak256("final"), netRewards, true);
3435+
assertTrue(campaign.finalized());
3436+
3437+
// stillOwed = 9e - 0 = 9e, balance = 12e, withdrawable = 3e
3438+
uint256 budgetBalanceBefore = rewardToken.balanceOf(address(budget));
3439+
3440+
vm.prank(CREATOR);
3441+
manager.withdraw(campaignId);
3442+
3443+
assertEq(
3444+
rewardToken.balanceOf(address(budget)),
3445+
budgetBalanceBefore + 3 ether,
3446+
"Budget should receive excess funds before endTime"
3447+
);
3448+
}
3449+
3450+
function test_EarlyFinalization_ClawbackBeforeEndTime() public {
3451+
(uint256 campaignId, TimeBasedIncentiveCampaign campaign) = _createCampaignWithRoot();
3452+
uint256 netRewards = campaign.totalRewards(); // 9 ether
3453+
3454+
// Mint extra tokens to create excess beyond what's owed
3455+
rewardToken.mint(address(campaign), 3 ether);
3456+
// balance = 12 ether, totalRewards = 9 ether
3457+
3458+
// Commit full budget and finalize before endTime
3459+
manager.updateRoot(campaignId, keccak256("final"), netRewards, true);
3460+
assertTrue(campaign.finalized(), "Should be finalized");
3461+
3462+
// stillOwed = 9e, balance = 12e, available = 3e
3463+
uint256 budgetBalanceBefore = rewardToken.balanceOf(address(budget));
3464+
3465+
AIncentive.ClawbackPayload memory payload =
3466+
AIncentive.ClawbackPayload({target: address(budget), data: abi.encode(3 ether)});
3467+
bytes memory data = abi.encode(payload);
3468+
3469+
vm.prank(address(budget));
3470+
campaign.clawback(data, 0, 0);
3471+
3472+
assertEq(
3473+
rewardToken.balanceOf(address(budget)),
3474+
budgetBalanceBefore + 3 ether,
3475+
"Budget should receive clawed back funds before endTime"
3476+
);
3477+
}
3478+
3479+
function test_EarlyFinalization_GetWithdrawableBeforeEndTime() public {
3480+
(uint256 campaignId, TimeBasedIncentiveCampaign campaign) = _createCampaignWithRoot();
3481+
uint256 netRewards = campaign.totalRewards(); // 9 ether
3482+
3483+
// Before finalization, getWithdrawable = 0
3484+
assertEq(manager.getWithdrawable(campaignId), 0, "Should be 0 before finalization");
3485+
3486+
// Mint extra tokens to create excess
3487+
rewardToken.mint(address(campaign), 3 ether);
3488+
// balance = 12 ether
3489+
3490+
// Finalize with full budget committed
3491+
manager.updateRoot(campaignId, keccak256("final"), netRewards, true);
3492+
3493+
// getWithdrawable should work before endTime when totalCommitted >= totalRewards
3494+
// balance = 12e, owed = 9e - 0 = 9e, withdrawable = 3e
3495+
assertEq(manager.getWithdrawable(campaignId), 3 ether, "Should return correct withdrawable before endTime");
3496+
}
3497+
3498+
function test_EarlyFinalization_GetWithdrawableStillZeroWhenNotExhausted() public {
3499+
(uint256 campaignId, TimeBasedIncentiveCampaign campaign) = _createCampaignWithRoot();
3500+
uint256 netRewards = campaign.totalRewards();
3501+
3502+
// Finalize after endTime (normal flow) with partial commitment
3503+
manager.updateRoot(campaignId, keccak256("root"), netRewards / 2, false);
3504+
3505+
// Before endTime, totalCommitted < totalRewards → getWithdrawable should be 0
3506+
assertEq(manager.getWithdrawable(campaignId), 0, "Should be 0 when not exhausted and before endTime");
3507+
}
3508+
3509+
function test_EarlyFinalization_ExactlyEqualCommitted() public {
3510+
(uint256 campaignId, TimeBasedIncentiveCampaign campaign) = _createCampaignWithRoot();
3511+
uint256 netRewards = campaign.totalRewards();
3512+
3513+
// totalCommitted == totalRewards exactly
3514+
manager.updateRoot(campaignId, keccak256("final"), netRewards, true);
3515+
3516+
assertTrue(campaign.finalized(), "Should finalize when totalCommitted == totalRewards exactly");
3517+
}
3518+
3519+
function test_EarlyFinalization_ExceedsCommitted() public {
3520+
(uint256 campaignId, TimeBasedIncentiveCampaign campaign) = _createCampaignWithRoot();
3521+
uint256 netRewards = campaign.totalRewards();
3522+
3523+
// totalCommitted > totalRewards (edge case — dust rounding up)
3524+
manager.updateRoot(campaignId, keccak256("final"), netRewards + 1, true);
3525+
3526+
assertTrue(campaign.finalized(), "Should finalize when totalCommitted > totalRewards");
3527+
}
3528+
3529+
function test_EarlyFinalization_UsersCanStillClaim() public {
3530+
(uint256 campaignId, TimeBasedIncentiveCampaign campaign) = _createCampaignWithRoot();
3531+
uint256 netRewards = campaign.totalRewards();
3532+
3533+
// Set up merkle tree with two users
3534+
bytes32 leaf1 = _makeLeaf(CLAIMER, address(rewardToken), 3 ether);
3535+
bytes32 leaf2 = _makeLeaf(CLAIMER2, address(rewardToken), 2 ether);
3536+
bytes32 root = leaf1 < leaf2
3537+
? keccak256(abi.encodePacked(leaf1, leaf2))
3538+
: keccak256(abi.encodePacked(leaf2, leaf1));
3539+
bytes32[] memory proof1 = new bytes32[](1);
3540+
proof1[0] = leaf2;
3541+
bytes32[] memory proof2 = new bytes32[](1);
3542+
proof2[0] = leaf1;
3543+
3544+
// Commit full budget and finalize before endTime
3545+
manager.updateRoot(campaignId, root, netRewards, true);
3546+
assertTrue(campaign.finalized());
3547+
3548+
// Users can claim after early finalization
3549+
manager.claim(campaignId, CLAIMER, 3 ether, proof1);
3550+
assertEq(rewardToken.balanceOf(CLAIMER), 3 ether, "CLAIMER should receive 3 ether");
3551+
3552+
manager.claim(campaignId, CLAIMER2, 2 ether, proof2);
3553+
assertEq(rewardToken.balanceOf(CLAIMER2), 2 ether, "CLAIMER2 should receive 2 ether");
3554+
3555+
// Unclaimed funds (9e - 5e = 4e) are still protected
3556+
assertEq(campaign.totalClaimed(), 5 ether);
3557+
// stillOwed = totalCommitted(9e) - totalClaimed(5e) = 4e
3558+
// balance = 9e - 5e = 4e, withdrawable = 0
3559+
assertEq(campaign.getWithdrawable(), 0, "Unclaimed funds should still be protected");
3560+
}
3561+
3562+
function test_EarlyFinalization_DirectFunded() public {
3563+
uint256 totalAmount = 10 ether;
3564+
uint64 startTime = uint64(block.timestamp + 1 hours);
3565+
uint64 endTime = uint64(block.timestamp + 30 days);
3566+
3567+
rewardToken.mint(CREATOR, totalAmount);
3568+
vm.prank(CREATOR);
3569+
rewardToken.approve(address(manager), totalAmount);
3570+
3571+
vm.prank(CREATOR);
3572+
uint256 campaignId = manager.createCampaignDirect(
3573+
keccak256("test-direct"), address(rewardToken), totalAmount, startTime, endTime
3574+
);
3575+
3576+
TimeBasedIncentiveCampaign campaign = TimeBasedIncentiveCampaign(manager.getCampaign(campaignId));
3577+
uint256 netRewards = campaign.totalRewards();
3578+
3579+
// Mint extra tokens to create withdrawable excess
3580+
rewardToken.mint(address(campaign), 2 ether);
3581+
3582+
// Commit full budget and finalize before endTime
3583+
manager.updateRoot(campaignId, keccak256("final"), netRewards, true);
3584+
assertTrue(campaign.finalized(), "Direct-funded campaign should finalize early");
3585+
3586+
// Creator withdraws excess before endTime
3587+
uint256 creatorBalanceBefore = rewardToken.balanceOf(CREATOR);
3588+
3589+
vm.prank(CREATOR);
3590+
manager.withdraw(campaignId);
3591+
3592+
assertEq(
3593+
rewardToken.balanceOf(CREATOR),
3594+
creatorBalanceBefore + 2 ether,
3595+
"Creator should receive excess funds"
3596+
);
3597+
}
33953598
}
33963599

33973600
/// @notice Mock ERC20 that deducts a fee only on transfer() not transferFrom() (outbound-only fee)

0 commit comments

Comments
 (0)