diff --git a/foundry.toml b/foundry.toml index 1954509..d896399 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,7 @@ src = "src" out = "out" libs = ["lib"] +fs_permissions = [{ access = "read", path = "./out" }] test = "test" build_info = true extra_output = ["storageLayout"] diff --git a/src/token/ValidatorRewarder.sol b/src/token/ValidatorRewarder.sol index 2ce3c05..9f81688 100644 --- a/src/token/ValidatorRewarder.sol +++ b/src/token/ValidatorRewarder.sol @@ -32,28 +32,22 @@ contract ValidatorRewarder is IValidatorRewarder, UUPSUpgradeable, OwnableUpgrad /// @notice The token that this rewarder mints Recall public token; - /// @notice The latest checkpoint height that rewards can be claimed for - /// @dev Using uint64 to match Filecoin's epoch height type and save gas when interacting with the network - uint64 public latestClaimedCheckpoint; - /// @notice The bottomup checkpoint period for the subnet. /// @dev The checkpoint period is set when the subnet is created. uint256 public checkpointPeriod; - /// @notice The supply of RECALL tokens at each checkpoint - mapping(uint64 checkpointHeight => uint256 totalSupply) public checkpointToSupply; - - /// @notice The inflation rate for the subnet - /// @dev The rate is expressed as a decimal*1e18. - /// @dev For example 5% APY is 0.0000928276004952% yield per checkpoint period. - /// @dev This is expressed as 928_276_004_952 or 0.000000928276004952*1e18. - uint256 public constant INFLATION_RATE = 928_276_004_952; + /// @notice The number of blocks required to generate 1 new token (with 18 decimals) + uint256 public constant BLOCKS_PER_TOKEN = 3; // ========== EVENTS & ERRORS ========== event ActiveStateChange(bool active, address account); event SubnetUpdated(SubnetID subnet, uint256 checkpointPeriod); - event CheckpointClaimed(uint64 indexed checkpointHeight, address indexed validator, uint256 amount); + /// @notice Emitted when a validator claims their rewards for a checkpoint + /// @param checkpointHeight The height of the checkpoint for which rewards are claimed + /// @param validator The address of the validator claiming rewards + /// @param amount The amount of tokens claimed as reward + event RewardsClaimed(uint64 indexed checkpointHeight, address indexed validator, uint256 amount); error SubnetMismatch(SubnetID id); error InvalidClaimNotifier(address notifier); @@ -136,83 +130,52 @@ contract ValidatorRewarder is IValidatorRewarder, UUPSUpgradeable, OwnableUpgrad revert InvalidClaimNotifier(msg.sender); } - // When the supply for the checkpoint is 0, it means that this is the first claim - // for this checkpoint. - // In this case we will set the supply for the checkpoint and - // calculate the inflation and mint the rewards to the rewarder and the first claimant. - // Otherwise, we know the supply for the checkpoint. - // We will calculate the rewards and transfer them to the other claimants for this checkpoint. - uint256 supplyAtCheckpoint = checkpointToSupply[claimedCheckpointHeight]; - if (supplyAtCheckpoint == 0) { - // Check that the checkpoint height is valid. - if (!validateCheckpointHeight(claimedCheckpointHeight)) { - revert InvalidCheckpointHeight(claimedCheckpointHeight); - } - - // Get the current supply of RECALL tokens - uint256 currentSupply = token.totalSupply(); - - // Set the supply for the checkpoint and update latest claimed checkpoint - checkpointToSupply[claimedCheckpointHeight] = currentSupply; - latestClaimedCheckpoint = claimedCheckpointHeight; - - // Calculate rewards - uint256 supplyDelta = calculateInflationForCheckpoint(currentSupply); - uint256 validatorShare = calculateValidatorShare(data.blocksCommitted, supplyDelta); - - // Perform external interactions after state updates - token.mint(address(this), supplyDelta - validatorShare); - token.mint(data.validator, validatorShare); - emit CheckpointClaimed(claimedCheckpointHeight, data.validator, validatorShare); - } else { - // Calculate the supply delta for the checkpoint - uint256 supplyDelta = calculateInflationForCheckpoint(supplyAtCheckpoint); - // Calculate the validator's share of the supply delta - uint256 validatorShare = calculateValidatorShare(data.blocksCommitted, supplyDelta); - // Transfer the validator's share of the supply delta to the validator - token.safeTransfer(data.validator, validatorShare); - emit CheckpointClaimed(claimedCheckpointHeight, data.validator, validatorShare); + // Check that the checkpoint height is valid + if (!validateCheckpointHeight(claimedCheckpointHeight)) { + revert InvalidCheckpointHeight(claimedCheckpointHeight); } + + // Calculate rewards for this checkpoint + uint256 newTokens = calculateNewTokensForCheckpoint(); + uint256 validatorShare = calculateValidatorShare(data.blocksCommitted, newTokens); + + // Mint the validator's share + token.mint(data.validator, validatorShare); + emit RewardsClaimed(claimedCheckpointHeight, data.validator, validatorShare); } // ========== INTERNAL FUNCTIONS ========== - /// @notice The internal method to calculate the supply delta for a checkpoint - /// @param supply The token supply at the checkpoint - /// @return The supply delta, i.e. the amount of new tokens minted for the checkpoint - function calculateInflationForCheckpoint(uint256 supply) internal pure returns (uint256) { - UD60x18 supplyFixed = ud(supply); - UD60x18 inflationRateFixed = ud(INFLATION_RATE); - UD60x18 result = supplyFixed.mul(inflationRateFixed); - return result.unwrap(); + /// @notice Calculates the total number of new tokens to be minted for a checkpoint + /// @return The number of new tokens to be minted (in base units with 18 decimals) + function calculateNewTokensForCheckpoint() internal view returns (uint256) { + UD60x18 blocksPerToken = ud(BLOCKS_PER_TOKEN); + UD60x18 period = ud(checkpointPeriod); + UD60x18 oneToken = ud(1 ether); + + // Calculate (checkpointPeriod * 1 ether) / BLOCKS_PER_TOKEN using fixed-point math + return period.mul(oneToken).div(blocksPerToken).unwrap(); } - /// @notice The internal method to calculate the validator's share of the supply delta + /// @notice The internal method to calculate the validator's share of the new tokens /// @param blocksCommitted The number of blocks committed by the validator - /// @param supplyDelta The supply delta, i.e. the amount of new tokens minted for the checkpoint - /// @return The validator's share of the supply delta - function calculateValidatorShare(uint256 blocksCommitted, uint256 supplyDelta) internal view returns (uint256) { - UD60x18 blocksFixed = ud(blocksCommitted); - UD60x18 deltaFixed = ud(supplyDelta); - UD60x18 periodFixed = ud(checkpointPeriod); - UD60x18 share = blocksFixed.div(periodFixed); - UD60x18 result = share.mul(deltaFixed); + /// @param totalNewTokens The total number of new tokens for the checkpoint + /// @return The validator's share of the new tokens + function calculateValidatorShare(uint256 blocksCommitted, uint256 totalNewTokens) internal view returns (uint256) { + UD60x18 blocks = ud(blocksCommitted); + UD60x18 tokens = ud(totalNewTokens); + UD60x18 period = ud(checkpointPeriod); + UD60x18 share = blocks.div(period); + UD60x18 result = share.mul(tokens); return result.unwrap(); } /// @notice Validates that the claimed checkpoint height is valid /// @param claimedCheckpointHeight The height of the checkpoint that the validator is claiming for /// @return True if the checkpoint height is valid, false otherwise - /// @dev When the latest claimable checkpoint is not set (0), it means that _this_ is the first ever claim. - /// @dev In this case, we need to ensure the first claim is at the first checkpoint period. - /// @dev Otherwise, we must ensure that the claimed checkpoint is the next expected checkpoint. + /// @dev Ensures the checkpoint height is a multiple of the checkpoint period function validateCheckpointHeight(uint64 claimedCheckpointHeight) internal view returns (bool) { - if (latestClaimedCheckpoint == 0) { - // First claim must be at the first checkpoint period - return claimedCheckpointHeight == checkpointPeriod; - } - // Subsequent claims must be at the next checkpoint - return claimedCheckpointHeight == latestClaimedCheckpoint + checkpointPeriod; + return claimedCheckpointHeight > 0 && claimedCheckpointHeight % checkpointPeriod == 0; } /// @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract diff --git a/test/ValidatorGater.t.sol b/test/ValidatorGater.t.sol index d2c4469..cf73e67 100644 --- a/test/ValidatorGater.t.sol +++ b/test/ValidatorGater.t.sol @@ -83,9 +83,10 @@ contract ValidatorGaterTest is Test { assertFalse(gater.isAllow(validator1, 101)); // Above range } - function testFailUnauthorizedApprove() public { + function testRevertWhenUnauthorizedApprove() public { // Non-owner should not be able to approve a validator vm.prank(validator2); // validator2 is not the owner + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", validator2)); gater.approve(validator1, 10, 100); } diff --git a/test/ValidatorRewarder.t.sol b/test/ValidatorRewarder.t.sol index 5aacacd..497e056 100644 --- a/test/ValidatorRewarder.t.sol +++ b/test/ValidatorRewarder.t.sol @@ -80,7 +80,7 @@ contract ValidatorRewarderInitialStateTest is ValidatorRewarderTestBase { assertTrue(rewarder.isActive()); assertEq(rewarder.subnet(), createSubnet().root); assertEq(address(rewarder.token()), address(token)); - assertEq(rewarder.INFLATION_RATE(), 928_276_004_952); + assertEq(rewarder.BLOCKS_PER_TOKEN(), 3); } } @@ -200,7 +200,6 @@ contract ValidatorRewarderBasicClaimTest is ValidatorRewarderTestBase { address claimant = address(0x999); Consensus.ValidatorData memory validatorData = createValidatorData(claimant, 100); uint256 initialSupply = token.totalSupply(); - uint256 initialRewarderBalance = token.balanceOf(address(rewarder)); uint256 initialClaimantBalance = token.balanceOf(claimant); // Pause the token @@ -214,9 +213,8 @@ contract ValidatorRewarderBasicClaimTest is ValidatorRewarderTestBase { rewarder.notifyValidClaim(createSubnet(), 600, validatorData); vm.stopPrank(); - // Verify no tokens were minted or transferred + // Verify no tokens were minted assertEq(token.totalSupply(), initialSupply, "Total supply should not change"); - assertEq(token.balanceOf(address(rewarder)), initialRewarderBalance, "Rewarder balance should not change"); assertEq(token.balanceOf(claimant), initialClaimantBalance, "Claimant balance should not change"); } @@ -231,7 +229,6 @@ contract ValidatorRewarderBasicClaimTest is ValidatorRewarderTestBase { // Record balances before pausing uint256 supplyBeforePause = token.totalSupply(); - uint256 rewarderBalanceBeforePause = token.balanceOf(address(rewarder)); uint256 claimantBalanceBeforePause = token.balanceOf(claimant); // Pause the token @@ -247,160 +244,84 @@ contract ValidatorRewarderBasicClaimTest is ValidatorRewarderTestBase { // Verify no tokens were transferred assertEq(token.totalSupply(), supplyBeforePause, "Total supply should not change"); - assertEq(token.balanceOf(address(rewarder)), rewarderBalanceBeforePause, "Rewarder balance should not change"); assertEq(token.balanceOf(claimant), claimantBalanceBeforePause, "Claimant balance should not change"); } } // Complex claim notification tests contract ValidatorRewarderComplexClaimTest is ValidatorRewarderTestBase { - function testNotifyValidClaimFirstClaim() public { + function testNotifyValidClaimSingleValidator() public { address claimant = address(0x999); - Consensus.ValidatorData memory validatorData = createValidatorData(claimant, 50); + Consensus.ValidatorData memory validatorData = createValidatorData(claimant, 300); uint256 initialSupply = token.totalSupply(); - // Check initial state - assertTrue(rewarder.isActive()); - assertEq(rewarder.latestClaimedCheckpoint(), 0); - assertEq(token.balanceOf(address(rewarder)), 0); - assertEq(token.totalSupply(), initialSupply); - - // First claim: should be at checkpoint 600 (checkpoint period) + // Claim at checkpoint 600 vm.startPrank(SUBNET_ROUTE); rewarder.notifyValidClaim(createSubnet(), 600, validatorData); vm.stopPrank(); - // Verify rewards - assertApproxEqAbs(token.balanceOf(claimant), 77356333746022, 1000); - assertApproxEqAbs(token.balanceOf(address(rewarder)), 850919671206244, 1000); - - // Verify total inflation - uint256 totalInflation = token.totalSupply() - initialSupply; - assertApproxEqAbs(totalInflation, 850919671206244 + 77356333746022, 1000); - - // Verify checkpoint - assertEq(rewarder.latestClaimedCheckpoint(), 600); - - // Test invalid next checkpoint - vm.startPrank(SUBNET_ROUTE); - vm.expectRevert(abi.encodeWithSelector(ValidatorRewarder.InvalidCheckpointHeight.selector, 2400)); - rewarder.notifyValidClaim(createSubnet(), 2400, validatorData); - vm.stopPrank(); + // For 600 blocks checkpoint period: + // Total new tokens = 600/3 = 200 + // Validator committed 300/600 blocks = 50% of blocks + // Expected reward = 200 * 0.5 = 100 + uint256 expectedReward = 100 ether; + assertApproxEqAbs(token.balanceOf(claimant), expectedReward, 1000); + assertEq(token.totalSupply() - initialSupply, expectedReward); } - function testNotifyValidClaimSubsequentClaims() public { + function testNotifyValidClaimMultipleValidators() public { address[] memory claimants = new address[](3); claimants[0] = address(0x111); claimants[1] = address(0x222); claimants[2] = address(0x333); uint256[] memory blocks = new uint256[](3); - blocks[0] = 100; - blocks[1] = 200; - blocks[2] = 300; - - // First claim: should be at checkpoint 600 (checkpoint period) - vm.startPrank(SUBNET_ROUTE); - rewarder.notifyValidClaim(createSubnet(), 600, createValidatorData(claimants[0], blocks[0])); - vm.stopPrank(); - assertApproxEqAbs(token.balanceOf(claimants[0]), 154712667492044, 1000); - - // Second claim - vm.startPrank(SUBNET_ROUTE); - rewarder.notifyValidClaim(createSubnet(), 600, createValidatorData(claimants[1], blocks[1])); - vm.stopPrank(); - assertApproxEqAbs(token.balanceOf(claimants[1]), 309425334984088, 1000); - - // Third claim - vm.startPrank(SUBNET_ROUTE); - rewarder.notifyValidClaim(createSubnet(), 600, createValidatorData(claimants[2], blocks[2])); - vm.stopPrank(); - assertApproxEqAbs(token.balanceOf(claimants[2]), 464138002476133, 1000); - - // Verify rewarder is drained - assertApproxEqAbs(token.balanceOf(address(rewarder)), 0, 1000); - } - - function testNotifyValidClaimConcurrentClaims() public { - // Setup claimants - address[] memory claimants = new address[](3); - claimants[0] = address(0x111); - claimants[1] = address(0x222); - claimants[2] = address(0x333); - - // Each validator commits different number of blocks - uint256[] memory blocksCommitted = new uint256[](3); - blocksCommitted[0] = 100; // Validator 1: 100 blocks - blocksCommitted[1] = 200; // Validator 2: 200 blocks - blocksCommitted[2] = 300; // Validator 3: 300 blocks - // Total = 600 blocks (equals CHECKPOINT_PERIOD) + blocks[0] = 100; // 1/6 of blocks + blocks[1] = 200; // 1/3 of blocks + blocks[2] = 300; // 1/2 of blocks uint256 initialSupply = token.totalSupply(); - // Validator 1 claims for both checkpoints - vm.startPrank(SUBNET_ROUTE); - rewarder.notifyValidClaim(createSubnet(), 600, createValidatorData(claimants[0], blocksCommitted[0])); - assertApproxEqAbs(token.balanceOf(claimants[0]), 154712667492044, 1000); - - rewarder.notifyValidClaim(createSubnet(), 1200, createValidatorData(claimants[0], blocksCommitted[0])); - assertApproxEqAbs(token.balanceOf(claimants[0]), 154712667492044 + 154712811108101, 1000); - vm.stopPrank(); - - // Validator 2 claims for both checkpoints - vm.startPrank(SUBNET_ROUTE); - rewarder.notifyValidClaim(createSubnet(), 600, createValidatorData(claimants[1], blocksCommitted[1])); - assertApproxEqAbs(token.balanceOf(claimants[1]), 154712667492044 * 2, 1000); - - rewarder.notifyValidClaim(createSubnet(), 1200, createValidatorData(claimants[1], blocksCommitted[1])); - assertApproxEqAbs(token.balanceOf(claimants[1]), (154712667492044 * 2) + (154712811108101 * 2), 1000); - vm.stopPrank(); - - // Validator 3 claims for both checkpoints + // All validators claim for the same checkpoint vm.startPrank(SUBNET_ROUTE); - rewarder.notifyValidClaim(createSubnet(), 600, createValidatorData(claimants[2], blocksCommitted[2])); - assertApproxEqAbs(token.balanceOf(claimants[2]), 154712667492044 * 3, 1000); - - rewarder.notifyValidClaim(createSubnet(), 1200, createValidatorData(claimants[2], blocksCommitted[2])); - assertApproxEqAbs(token.balanceOf(claimants[2]), (154712667492044 * 3) + (154712811108101 * 3), 1000); - vm.stopPrank(); - - // Verify total rewards distributed - uint256 totalRewards = 0; for (uint256 i = 0; i < claimants.length; i++) { - totalRewards += token.balanceOf(claimants[i]); + rewarder.notifyValidClaim(createSubnet(), 600, createValidatorData(claimants[i], blocks[i])); } - uint256 totalInflation = token.totalSupply() - initialSupply; - assertApproxEqAbs(totalRewards, totalInflation, 1000); + vm.stopPrank(); + + // Total new tokens = 600/3 = 200 + // Validator 1 should get: 200 * (100/600) ≈ 33.33 + // Validator 2 should get: 200 * (200/600) ≈ 66.67 + // Validator 3 should get: 200 * (300/600) = 100 + assertApproxEqAbs(token.balanceOf(claimants[0]), 33333333333333333333, 1000); + assertApproxEqAbs(token.balanceOf(claimants[1]), 66666666666666666666, 1000); + assertApproxEqAbs(token.balanceOf(claimants[2]), 100000000000000000000, 1000); - // Verify rewarder has no remaining balance - assertApproxEqAbs(token.balanceOf(address(rewarder)), 0, 1000); + // Total minted should be 200 + assertApproxEqAbs(token.totalSupply() - initialSupply, 200 ether, 1000); } - function testNotifyValidClaimMustBeSequential() public { + function testNotifyValidClaimMultipleCheckpoints() public { address claimant = address(0x999); - Consensus.ValidatorData memory validatorData = createValidatorData(claimant, 50); - - // Try to claim for checkpoint 1200 before 600 - vm.startPrank(SUBNET_ROUTE); - vm.expectRevert(abi.encodeWithSelector(ValidatorRewarder.InvalidCheckpointHeight.selector, 1200)); - rewarder.notifyValidClaim(createSubnet(), 1200, validatorData); - vm.stopPrank(); + Consensus.ValidatorData memory validatorData = createValidatorData(claimant, 300); - // First claim must be at checkpoint 600 - vm.startPrank(SUBNET_ROUTE); - vm.expectRevert(abi.encodeWithSelector(ValidatorRewarder.InvalidCheckpointHeight.selector, 300)); - rewarder.notifyValidClaim(createSubnet(), 300, validatorData); - vm.stopPrank(); + uint256 initialSupply = token.totalSupply(); - // Correct sequence: first claim at 600 + // Claim for three consecutive checkpoints vm.startPrank(SUBNET_ROUTE); rewarder.notifyValidClaim(createSubnet(), 600, validatorData); - assertEq(rewarder.latestClaimedCheckpoint(), 600); - - // Then claim at 1200 rewarder.notifyValidClaim(createSubnet(), 1200, validatorData); - assertEq(rewarder.latestClaimedCheckpoint(), 1200); + rewarder.notifyValidClaim(createSubnet(), 1800, validatorData); vm.stopPrank(); + + // For each checkpoint: + // Total new tokens = 600/3 = 200 + // Validator committed 300/600 blocks = 50% of blocks + // Expected reward per checkpoint = 200 * 0.5 = 100 + // Total expected for 3 checkpoints = 300 + uint256 expectedTotalReward = 300 ether; + assertApproxEqAbs(token.balanceOf(claimant), expectedTotalReward, 1000); + assertEq(token.totalSupply() - initialSupply, expectedTotalReward); } } diff --git a/test/ValidatorRewarderFFI.t.sol b/test/ValidatorRewarderFFI.t.sol index 1b38c5a..98d8da5 100644 --- a/test/ValidatorRewarderFFI.t.sol +++ b/test/ValidatorRewarderFFI.t.sol @@ -14,6 +14,7 @@ contract ValidatorRewarderFFITest is ValidatorRewarderTestBase { } function testRewardCalculationWithFFI() public { + uint256 initialSupply = token.totalSupply(); uint256 blocks = 52560 * 5; // 5 years uint256 checkpointPeriod = 600; uint256 numCheckpoints = blocks / checkpointPeriod; @@ -24,45 +25,39 @@ contract ValidatorRewarderFFITest is ValidatorRewarderTestBase { claimants[2] = address(0x333); uint256[] memory blocksCommitted = new uint256[](3); - blocksCommitted[0] = 100; - blocksCommitted[1] = 200; - blocksCommitted[2] = 300; - - // Calculate expected total increase for all blocks at once - uint256 initialSupply = token.totalSupply(); - uint256[] memory totalParams = new uint256[](3); - totalParams[0] = initialSupply; - totalParams[1] = blocks; - totalParams[2] = 0; // Not needed for total supply calculation + blocksCommitted[0] = 100; // 1/6 of blocks + blocksCommitted[1] = 200; // 1/3 of blocks + blocksCommitted[2] = 300; // 1/2 of blocks - string memory totalJsonStr = runPythonScript(PYTHON_SCRIPT, totalParams); - uint256 expectedTotalIncrease = vm.parseJsonUint(totalJsonStr, ".supply_delta"); + // Calculate expected total tokens for all blocks (in base units) + uint256 totalBlocks = blocks; + uint256 expectedTotalTokens = (totalBlocks * 1 ether) / rewarder.BLOCKS_PER_TOKEN(); - console2.log("Expected total increase for all blocks:", expectedTotalIncrease); + console2.log("Expected total tokens for all blocks:", expectedTotalTokens); // Process claims checkpoint by checkpoint for (uint256 i = 0; i < numCheckpoints; i++) { - uint64 currentCheckpoint = uint64((i + 1) * 600); - uint256 supplyBeforeClaims = token.totalSupply(); + uint64 currentCheckpoint = uint64((i + 1) * checkpointPeriod); + + // Calculate expected rewards for this checkpoint (in base units) + uint256 checkpointTokens = (checkpointPeriod * 1 ether) / rewarder.BLOCKS_PER_TOKEN(); - // Calculate expected rewards for total supply delta + // Run python script to calculate validator shares uint256[] memory params = new uint256[](3); - params[0] = supplyBeforeClaims; - params[1] = 600; // checkpoint period - params[2] = 0; // blocks committed + params[0] = checkpointTokens; // total tokens for checkpoint (in base units) + params[1] = checkpointPeriod; // checkpoint period + params[2] = 0; // blocks committed (set per validator) - string memory jsonStr = runPythonScript(PYTHON_SCRIPT, params); + string memory jsonStr; for (uint256 j = 0; j < claimants.length; j++) { - // Calculate expected validator share + // Calculate expected validator share using Python script params[2] = blocksCommitted[j]; jsonStr = runPythonScript(PYTHON_SCRIPT, params); uint256 expectedValidatorShare = vm.parseJsonUint(jsonStr, ".validator_share"); - uint256 expectedRewarderShare = vm.parseJsonUint(jsonStr, ".rewarder_share"); - // Store balances before claim + // Store balance before claim uint256 balanceBefore = token.balanceOf(claimants[j]); - uint256 currentRewarderBalance = token.balanceOf(address(rewarder)); // Submit claim vm.prank(SUBNET_ROUTE); @@ -70,63 +65,32 @@ contract ValidatorRewarderFFITest is ValidatorRewarderTestBase { createSubnet(), currentCheckpoint, createValidatorData(claimants[j], blocksCommitted[j]) ); + uint256 actualReward = token.balanceOf(claimants[j]) - balanceBefore; + // Verify validator rewards assertApproxEqAbs( - token.balanceOf(claimants[j]) - balanceBefore, + actualReward, expectedValidatorShare, - 1000, + 1000, // Allow for difference of up to 1000 base units string.concat("Validator reward mismatch at checkpoint ", vm.toString(currentCheckpoint)) ); - - // Verify rewarder balance - if (j == 0) { - // After first claim, rewarder should have supply delta minus first claimant's share - assertApproxEqAbs( - token.balanceOf(address(rewarder)), - expectedRewarderShare, - 1000, - string.concat( - "Rewarder balance mismatch on first claim at checkpoint ", vm.toString(currentCheckpoint) - ) - ); - currentRewarderBalance = token.balanceOf(address(rewarder)); - } else { - // Subsequent claims should decrease rewarder balance by current's validator share - assertApproxEqAbs( - currentRewarderBalance - token.balanceOf(address(rewarder)), - expectedValidatorShare, - 1000, - string.concat( - "Rewarder balance mismatch on subsequent claim at checkpoint ", - vm.toString(currentCheckpoint) - ) - ); - } } - - // Verify checkpoint is set correctly - assertEq( - rewarder.latestClaimedCheckpoint(), - currentCheckpoint, - string.concat("Checkpoint not set correctly at checkpoint ", vm.toString(currentCheckpoint)) - ); } - // After all checkpoints are processed, verify total increase matches - uint256 actualTotalIncrease = token.totalSupply() - initialSupply; - // due to rounding in each checkpoint, accumulated error over 5 years is 45,773,118 - // i.e. we print 0.000_000_000_045_679_254 RECALL more than expected! + // After all checkpoints are processed, verify total minted amount + uint256 actualTotalMinted = token.totalSupply() - initialSupply; + assertLe(actualTotalMinted, expectedTotalTokens, "Total minted tokens should be less than or equal to expected"); assertApproxEqAbs( - actualTotalIncrease, - expectedTotalIncrease, - 45679254, - "Total increase after all checkpoints should match single calculation" + actualTotalMinted, + expectedTotalTokens, // Allow for difference of up to 100000 base units or 0.0000000000001 tokens in 5 years + 100000, + "Total minted tokens should match expected" ); - console2.log("Actual total increase after all checkpoints:", actualTotalIncrease); + console2.log("Actual total tokens minted:", actualTotalMinted); } - // Helper functions moved from the original test + // Helper functions function makeScriptExecutable(string memory scriptPath) internal { string[] memory makeExecutable = new string[](3); makeExecutable[0] = "chmod"; diff --git a/test/scripts/calculate_rewards.py b/test/scripts/calculate_rewards.py index 7bd3abf..304f788 100755 --- a/test/scripts/calculate_rewards.py +++ b/test/scripts/calculate_rewards.py @@ -2,60 +2,44 @@ import sys import json -from decimal import Decimal, getcontext +from decimal import Decimal, ROUND_DOWN -# Constants to match Solidity's UD60x18 -WAD = Decimal('1000000000000000000') # 1e18 -SECONDS_PER_YEAR = Decimal('31536000') -APY = Decimal('50000000000000000') # 0.05 * 1e18 -INFLATION_RATE = Decimal('928276004952') # Pre-calculated checkpoint yield for 600 blocks (1 block = 1s) - -def checkpoint_yield_from_apy(supply, total_period=None): - if total_period is not None: - # For total period calculation, use compound interest - getcontext().prec = 36 - period = Decimal(str(total_period)) - time_fraction = (period * WAD) // SECONDS_PER_YEAR - base = WAD + APY - exponent = time_fraction / WAD - result = (pow(base / WAD, exponent) * WAD).quantize(Decimal('1')) - yield_rate = result - WAD - return (supply * yield_rate) // WAD - else: - # For single checkpoint, use pre-calculated inflation rate - return (supply * INFLATION_RATE) // WAD - -def calculate_shares(supply_delta, blocks_committed, checkpoint_period): - """Calculate validator and rewarder shares directly""" - # Calculate validator share - validator_share = (blocks_committed * supply_delta) // checkpoint_period - # Calculate rewarder share - rewarder_share = supply_delta - validator_share - return validator_share, rewarder_share - -def main(): - supply = Decimal(sys.argv[1]) - period = Decimal(sys.argv[2]) - blocks_committed = Decimal(sys.argv[3]) +def calculate_rewards(total_tokens: int, checkpoint_period: int, blocks_committed: int) -> dict: + """ + Calculate validator rewards based on fixed token generation - # We use this when we don't want to calculate shares for a single checkpoint - if blocks_committed == 0: - # Calculate total period yield - supply_delta = checkpoint_yield_from_apy(supply, total_period=period) - validator_share = 0 - rewarder_share = 0 - else: - # Calculate single checkpoint yield and shares - supply_delta = checkpoint_yield_from_apy(supply) - validator_share, rewarder_share = calculate_shares(supply_delta, blocks_committed, period) + Args: + total_tokens: Total tokens for the checkpoint (in base units with 18 decimals) + checkpoint_period: Number of blocks in checkpoint period + blocks_committed: Number of blocks committed by validator - result = { - "supply_delta": str(supply_delta), - "validator_share": str(validator_share), - "rewarder_share": str(rewarder_share) - } + Returns: + Dictionary with validator share and remaining tokens + """ + # Convert to Decimal for precise calculation + tokens = Decimal(total_tokens) + period = Decimal(checkpoint_period) + blocks = Decimal(blocks_committed) + + # Calculate validator's share based on proportion of blocks committed + validator_share = (blocks / period) * tokens - print(json.dumps(result)) + # Round down to match Solidity behavior + validator_share = validator_share.quantize(Decimal('1.'), rounding=ROUND_DOWN) + + return { + "validator_share": int(validator_share), + "total_tokens": int(tokens) + } if __name__ == "__main__": - main() \ No newline at end of file + if len(sys.argv) != 4: + print("Usage: calculate_rewards.py ") + sys.exit(1) + + total_tokens = int(sys.argv[1]) + checkpoint_period = int(sys.argv[2]) + blocks_committed = int(sys.argv[3]) + + result = calculate_rewards(total_tokens, checkpoint_period, blocks_committed) + print(json.dumps(result)) \ No newline at end of file