diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce6a168..f71b6bd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,8 @@ jobs: --report debug --optimize --optimizer-runs 200 - --no-auto-detect + --no-auto-detect + --skip script/DeployUpgradableLuminoProtocol.s.sol id: coverage # - name: Upload coverage to Codecov diff --git a/test/ACL.t.sol b/test/ACL.t.sol new file mode 100644 index 0000000..5a10ffb --- /dev/null +++ b/test/ACL.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/Core/ACL.sol"; + +contract ACLTest is Test { + ACL public acl; + address public admin; + address public user1; + address public user2; + + // Example role for testing + bytes32 public constant TEST_ROLE = keccak256("TEST_ROLE"); + + function setUp() public { + admin = address(this); + user1 = address(0x1); + user2 = address(0x2); + + acl = new ACL(); + } + + function testInitialize() public { + acl.initialize(admin); + + // Verify admin has DEFAULT_ADMIN_ROLE + assertTrue(acl.hasRole(acl.DEFAULT_ADMIN_ROLE(), admin)); + + // Verify initialization can't be called twice + bytes memory expectedRevertMessage = abi.encodeWithSignature( + "InvalidInitialization()" + ); + vm.expectRevert(expectedRevertMessage); + acl.initialize(user1); + } + + function testRoleManagement() public { + acl.initialize(admin); + + // Grant role + acl.grantRole(TEST_ROLE, user1); + assertTrue(acl.hasRole(TEST_ROLE, user1)); + + // Revoke role + acl.revokeRole(TEST_ROLE, user1); + assertFalse(acl.hasRole(TEST_ROLE, user1)); + + // Test role admin + assertTrue(acl.getRoleAdmin(TEST_ROLE) == acl.DEFAULT_ADMIN_ROLE()); + } + + function testUnauthorizedAccess() public { + acl.initialize(admin); + + // Try to grant role from non-admin account + vm.startPrank(user1); + + bytes memory expectedRevertMessage = abi.encodeWithSignature( + "AccessControlUnauthorizedAccount(address,bytes32)", + user1, + acl.DEFAULT_ADMIN_ROLE() + ); + vm.expectRevert(expectedRevertMessage); + acl.grantRole(TEST_ROLE, user2); + + // Try to revoke role from non-admin account + vm.expectRevert(expectedRevertMessage); + acl.revokeRole(TEST_ROLE, user2); + + vm.stopPrank(); + } + + function testRenounceRole() public { + acl.initialize(admin); + + // Grant role to user1 + acl.grantRole(TEST_ROLE, user1); + assertTrue(acl.hasRole(TEST_ROLE, user1)); + + // User1 renounces their role + vm.prank(user1); + acl.renounceRole(TEST_ROLE, user1); + assertFalse(acl.hasRole(TEST_ROLE, user1)); + + // Try to renounce role for another account (should fail) + vm.prank(user1); + bytes memory expectedRevertMessage = abi.encodeWithSignature( + "AccessControlBadConfirmation()" + ); + vm.expectRevert(expectedRevertMessage); + acl.renounceRole(TEST_ROLE, user2); + } +} \ No newline at end of file diff --git a/test/JobsManager.t.sol b/test/JobsManager.t.sol index f7da7bf..497ec26 100644 --- a/test/JobsManager.t.sol +++ b/test/JobsManager.t.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/Core/JobsManager.sol"; import "../src/Core/StakeManager.sol"; +import "../src/Core/StateManager.sol"; import "../src/Core/storage/Constants.sol"; +import "forge-std/console.sol"; -contract JobsManagerTest is Test, Constants { +contract JobsManagerTest is Test, Constants, StateManager { JobsManager public jobsManager; StakeManager public stakeManager; address public admin; @@ -14,24 +16,27 @@ contract JobsManagerTest is Test, Constants { address public user2; uint8 public constant BUFFER = 5; + event JobCreated(uint256 indexed jobId, address indexed creator, uint32 epoch); + event JobStatusUpdated(uint256 indexed jobId, Status newStatus); + event JobAssigned(uint256 indexed jobId, address indexed assigneeAddress); + function setUp() public { admin = address(this); user1 = address(0x1); user2 = address(0x2); - // Deploy & initialize contracts in the correct order - // 1. Deploy StakeManager + // Deploy contracts stakeManager = new StakeManager(); - - // 2. Deploy JobsManager jobsManager = new JobsManager(); - // 3. Initialize both with correct references to each other + // Initialize contracts stakeManager.initialize(address(jobsManager)); jobsManager.initialize(5, address(stakeManager)); - vm.prank(admin); + // Setup roles jobsManager.grantRole(jobsManager.DEFAULT_ADMIN_ROLE(), admin); + vm.deal(user1, 100 ether); + vm.deal(user2, 100 ether); } function testInitialization() public view { @@ -41,8 +46,14 @@ contract JobsManagerTest is Test, Constants { } function testCreateJob() public { - vm.prank(user1); - uint256 jobId = jobsManager.createJob("Test Job Details"); + uint256 jobFee = 1 ether; + vm.startPrank(user1); + + vm.expectEmit(true, true, true, true); + emit JobCreated(1, user1, 0); + + uint256 jobId = jobsManager.createJob{value: jobFee}("Test Job Details"); + vm.stopPrank(); assertEq(jobId, 1); assertEq(jobsManager.jobIdCounter(), 2); @@ -50,6 +61,7 @@ contract JobsManagerTest is Test, Constants { Structs.Job memory job = jobsManager.getJobDetails(jobId); assertEq(job.creator, user1); assertEq(job.jobDetailsInJSON, "Test Job Details"); + assertEq(job.jobFee, jobFee); assertEq(uint(jobsManager.getJobStatus(jobId)), uint(Status.NEW)); uint256[] memory activeJobs = jobsManager.getActiveJobs(); @@ -57,112 +69,199 @@ contract JobsManagerTest is Test, Constants { assertEq(activeJobs[0], jobId); } - function testUpdateJobStatus() public { - // First, register user1 as a staker - vm.deal(user1, 10 ether); // Give user1 some ETH to stake - vm.prank(user1); + function testAssignJob() public { + // Setup staker + vm.startPrank(user1); stakeManager.stake{value: 10 ether}(0, 10 ether, "test-spec"); + vm.stopPrank(); - // Create a job - vm.prank(user1); - uint256 jobId = jobsManager.createJob("Test Job Details"); + // Create job + vm.prank(user2); + uint256 jobId = jobsManager.createJob{value: 1 ether}("Test Job"); - // Assign job (in Assign state) - uint256 assignTime = (EPOCH_LENGTH / NUM_STATES) / 2; // Middle of Assign state + // Move to Assign state + uint256 assignTime = (EPOCH_LENGTH / NUM_STATES) / 2; vm.warp(assignTime); + + // Assign job + vm.startPrank(admin); + vm.expectEmit(true, true, true, true); + emit JobAssigned(jobId, user1); + jobsManager.assignJob(jobId, user1, BUFFER); + vm.stopPrank(); + + // Verify assignment + Structs.Job memory job = jobsManager.getJobDetails(jobId); + assertEq(job.assignee, user1); + assertEq(uint(jobsManager.getJobStatus(jobId)), uint(Status.QUEUED)); + assertEq(jobsManager.getJobForStaker(user1), jobId); + } + + function testJobStatusTransitions() public { + // Setup staker + vm.startPrank(user1); + stakeManager.stake{value: 10 ether}(0, 10 ether, "test-spec"); + vm.stopPrank(); + + // Create and assign job + vm.prank(user2); + uint256 jobId = jobsManager.createJob{value: 1 ether}("Test Job"); + + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; + + // Assign state + vm.warp(stateLength / 2); vm.prank(admin); jobsManager.assignJob(jobId, user1, BUFFER); assertEq(uint(jobsManager.getJobStatus(jobId)), uint(Status.QUEUED)); - // Update to RUNNING (in Update state) - uint256 updateTime = (EPOCH_LENGTH / NUM_STATES) + (EPOCH_LENGTH / NUM_STATES / 2); - vm.warp(updateTime); + // Update state - Set to RUNNING + vm.warp(stateLength + (stateLength / 2)); vm.prank(user1); + vm.expectEmit(true, true, true, true); + emit JobStatusUpdated(jobId, Status.RUNNING); jobsManager.updateJobStatus(jobId, Status.RUNNING, BUFFER); assertEq(uint(jobsManager.getJobStatus(jobId)), uint(Status.RUNNING)); - // Complete job (in Confirm state) - uint256 confirmTime = 2 * (EPOCH_LENGTH / NUM_STATES) + (EPOCH_LENGTH / NUM_STATES / 2); - vm.warp(confirmTime); + // Confirm state - Complete job + vm.warp(2 * stateLength + (stateLength / 2)); vm.prank(user1); jobsManager.updateJobStatus(jobId, Status.COMPLETED, BUFFER); assertEq(uint(jobsManager.getJobStatus(jobId)), uint(Status.COMPLETED)); } - function testUpdateJobStatusInvalidTransition() public { - // First, register user1 as a staker - vm.deal(user1, 10 ether); - vm.prank(user1); + function testFailedJobStatus() public { + // Setup staker + vm.startPrank(user1); stakeManager.stake{value: 10 ether}(0, 10 ether, "test-spec"); + vm.stopPrank(); - vm.prank(user1); - uint256 jobId = jobsManager.createJob("Test Job Details"); + // Create and assign job + vm.prank(user2); + uint256 jobId = jobsManager.createJob{value: 1 ether}("Test Job"); - // Try to update status without being in the correct state - uint256 wrongTime = (EPOCH_LENGTH / NUM_STATES) / 2; // In Assign state - vm.warp(wrongTime); - vm.prank(user1); - vm.expectRevert("Only assignee can update the jobStatus"); - jobsManager.updateJobStatus(jobId, Status.RUNNING, BUFFER); - } + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; - function testUpdateNonExistentJob() public { + // Assign job in Assign state (first state) + uint256 assignTime = (stateLength / 2); // Middle of Assign state + vm.warp(assignTime); vm.prank(admin); - vm.expectRevert("Job does not exist"); - jobsManager.updateJobStatus(999, Status.QUEUED, BUFFER); - } + jobsManager.assignJob(jobId, user1, BUFFER); + assertEq(uint(jobsManager.getJobStatus(jobId)), uint(Status.QUEUED), "Job should be QUEUED after assignment"); + + // Update to RUNNING in Update state (second state) + uint256 updateTime = stateLength + (stateLength / 2); // Middle of Update state + vm.warp(updateTime); + vm.prank(user1); + jobsManager.updateJobStatus(jobId, Status.RUNNING, BUFFER); + assertEq(uint(jobsManager.getJobStatus(jobId)), uint(Status.RUNNING), "Job should be RUNNING after update"); - function testUpdateJobStatusUnauthorized() public { + // Set to FAILED in Confirm state (third state) + uint256 confirmTime = 2 * stateLength + (stateLength / 2); // Middle of Confirm state + vm.warp(confirmTime); + vm.prank(user1); - uint256 jobId = jobsManager.createJob("Test Job Details"); + jobsManager.updateJobStatus(jobId, Status.FAILED, BUFFER); + + // Verify job is properly failed + Structs.Job memory job = jobsManager.getJobDetails(jobId); + assertEq(uint(jobsManager.getJobStatus(jobId)), uint(Status.FAILED), "Job status should be FAILED"); + assertTrue(job.conclusionEpoch > 0, "Conclusion epoch should be set"); + assertEq(job.assignee, user1, "Assignee should remain unchanged"); + } + + function testJobReward() public { + // Setup staker + vm.startPrank(user1); + stakeManager.stake{value: 10 ether}(0, 10 ether, "test-spec"); + vm.stopPrank(); + // Create job with fee + uint256 jobFee = 1 ether; vm.prank(user2); - vm.expectRevert("Only assignee can update the jobStatus"); - jobsManager.updateJobStatus(jobId, Status.QUEUED, BUFFER); - } + uint256 jobId = jobsManager.createJob{value: jobFee}("Test Job"); - function testCreateMultipleJobs() public { - uint256 numJobs = 5; - for (uint256 i = 0; i < numJobs; i++) { - vm.prank(user1); - jobsManager.createJob(string(abi.encodePacked("Job ", vm.toString(i)))); - } + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; + + // Assign job + vm.warp(stateLength / 2); + vm.prank(admin); + jobsManager.assignJob(jobId, user1, BUFFER); - assertEq(jobsManager.jobIdCounter(), numJobs + 1); - assertEq(jobsManager.getActiveJobs().length, numJobs); + // Complete job successfully + vm.warp(stateLength + (stateLength / 2)); + vm.prank(user1); + jobsManager.updateJobStatus(jobId, Status.RUNNING, BUFFER); + + vm.warp(2 * stateLength + (stateLength / 2)); + uint256 balanceBefore = user1.balance; + vm.prank(user1); + jobsManager.updateJobStatus(jobId, Status.COMPLETED, BUFFER); + + // Verify reward transfer + assertEq(user1.balance - balanceBefore, jobFee); } - function testGetActiveJobs() public { + function testInvalidStateTransitions() public { + // Setup staker vm.startPrank(user1); - jobsManager.createJob("Job 1"); - jobsManager.createJob("Job 2"); - jobsManager.createJob("Job 3"); + stakeManager.stake{value: 10 ether}(0, 10 ether, "test-spec"); vm.stopPrank(); - uint256[] memory activeJobs = jobsManager.getActiveJobs(); - assertEq(activeJobs.length, 3); - assertEq(activeJobs[0], 1); - assertEq(activeJobs[1], 2); - assertEq(activeJobs[2], 3); + // Create job + vm.prank(user2); + uint256 jobId = jobsManager.createJob{value: 1 ether}("Test Job"); + + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; + + // Try to complete job without assignment + vm.warp(2 * stateLength + (stateLength / 2)); + vm.prank(user1); + vm.expectRevert("Only assignee can update the jobStatus"); + jobsManager.updateJobStatus(jobId, Status.COMPLETED, BUFFER); + + // Try to assign in wrong state + vm.warp(stateLength + (stateLength / 2)); + vm.prank(admin); + vm.expectRevert("Can only assign job in Assign State"); + jobsManager.assignJob(jobId, user1, BUFFER); } - function testJobDetailsRetrieval() public { + function testGetJobDetails() public { vm.prank(user1); - uint256 jobId = jobsManager.createJob("Detailed Job Info"); + uint256 jobId = jobsManager.createJob{value: 1 ether}("Test Job"); Structs.Job memory job = jobsManager.getJobDetails(jobId); assertEq(job.jobId, jobId); assertEq(job.creator, user1); assertEq(job.assignee, address(0)); - assertEq(job.creationEpoch, jobsManager.getEpoch()); - assertEq(job.executionEpoch, 0); - assertEq(job.proofGenerationEpoch, 0); - assertEq(job.conclusionEpoch, 0); - assertEq(job.jobDetailsInJSON, "Detailed Job Info"); + assertEq(job.jobFee, 1 ether); + assertEq(job.jobDetailsInJSON, "Test Job"); + } + + function testMultipleJobs() public { + uint256 numJobs = 5; + for (uint256 i = 0; i < numJobs; i++) { + vm.prank(user1); + jobsManager.createJob{value: 1 ether}(string(abi.encodePacked("Job ", vm.toString(i)))); + } + + assertEq(jobsManager.jobIdCounter(), numJobs + 1); + uint256[] memory activeJobs = jobsManager.getActiveJobs(); + assertEq(activeJobs.length, numJobs); } - function testNonExistentJobDetails() public { + function testInvalidJobOperations() public { + // Test non-existent job vm.expectRevert("Job does not exist"); jobsManager.getJobDetails(999); + + // Test unauthorized job assignment + vm.prank(user2); + uint256 jobId = jobsManager.createJob{value: 1 ether}("Test Job"); + + vm.prank(user1); + vm.expectRevert("Job assigner Role required to assignJob"); + jobsManager.assignJob(jobId, user1, BUFFER); } } \ No newline at end of file diff --git a/test/StakeManager.t.sol b/test/StakeManager.t.sol index c995a93..33a03aa 100644 --- a/test/StakeManager.t.sol +++ b/test/StakeManager.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; +import "forge-std/console.sol"; import "../src/Core/StakeManager.sol"; import "../src/Core/JobsManager.sol"; import "../src/Core/storage/Constants.sol"; @@ -12,6 +13,10 @@ contract StakeManagerTest is Test, Constants { address public admin; address public staker1; address public staker2; + uint256 public constant MIN_STAKE = 10 ether; + + event NewStaker(uint32 indexed stakerId, address indexed stakerAddress); + event StakeUpdated(uint32 indexed stakerId, uint256 newStake); function setUp() public { admin = address(this); @@ -25,7 +30,7 @@ contract StakeManagerTest is Test, Constants { stakeManager = new StakeManager(); stakeManager.initialize(address(jobsManager)); - // Fund stakers with some ETH for testing + // Fund test accounts vm.deal(staker1, 100 ether); vm.deal(staker2, 100 ether); } @@ -33,84 +38,174 @@ contract StakeManagerTest is Test, Constants { function testInitialization() public view { assertEq(stakeManager.numStakers(), 0); assertTrue(stakeManager.hasRole(stakeManager.DEFAULT_ADMIN_ROLE(), admin)); + assertEq(stakeManager.numStakers(), 0); } - function testStake() public { - uint32 currentEpoch = uint32(block.timestamp / EPOCH_LENGTH); - uint256 stakeAmount = 10 ether; - - vm.prank(staker1); - stakeManager.stake{value: stakeAmount}(currentEpoch, stakeAmount, "test-spec"); + function testFirstTimeStake() public { + vm.startPrank(staker1); + + // Just make the stake call and verify the results + stakeManager.stake{value: MIN_STAKE}(0, MIN_STAKE, "test-spec"); + vm.stopPrank(); assertEq(stakeManager.numStakers(), 1); assertEq(stakeManager.getStakerId(staker1), 1); Structs.Staker memory stakerInfo = stakeManager.getStaker(1); - assertEq(stakerInfo.stake, stakeAmount); assertEq(stakerInfo._address, staker1); - assertEq(stakerInfo.epochFirstStaked, currentEpoch); + assertEq(stakerInfo.stake, MIN_STAKE); + assertEq(stakerInfo.epochFirstStaked, 0); + assertFalse(stakerInfo.isSlashed); } - function testStakeInsufficientAmount() public { - uint32 currentEpoch = uint32(block.timestamp / EPOCH_LENGTH); - uint256 stakeAmount = 0.5 ether; // Less than minSafeLumToken + function testAdditionalStake() public { + // Initial stake + vm.prank(staker1); + stakeManager.stake{value: MIN_STAKE}(0, MIN_STAKE, "test-spec"); + + uint32 stakerId = stakeManager.getStakerId(staker1); + uint256 additionalStake = 5 ether; + + // Add more stake + vm.prank(staker1); + stakeManager.stake{value: additionalStake}(0, additionalStake, "test-spec"); + + Structs.Staker memory stakerInfo = stakeManager.getStaker(stakerId); + assertEq(stakerInfo.stake, MIN_STAKE + additionalStake); + } + function testStakeWithInsufficientFunds() public { vm.prank(staker1); vm.expectRevert("Less than minimum safe LUMINO token amount"); - stakeManager.stake{value: stakeAmount}(currentEpoch, stakeAmount, "test-spec"); + stakeManager.stake{value: 0.5 ether}(0, 0.5 ether, "test-spec"); } - function testUnstake() public { - uint32 currentEpoch = uint32(block.timestamp / EPOCH_LENGTH); - uint256 stakeAmount = 10 ether; - - vm.startPrank(staker1); - stakeManager.stake{value: stakeAmount}(currentEpoch, stakeAmount, "test-spec"); + function testUnstakeFlow() public { + // Initial stake + vm.prank(staker1); + stakeManager.stake{value: MIN_STAKE}(0, MIN_STAKE, "test-spec"); uint32 stakerId = stakeManager.getStakerId(staker1); - stakeManager.unstake(stakerId, 5 ether); - vm.stopPrank(); + uint256 unstakeAmount = 5 ether; + + // Unstake partial amount + vm.prank(staker1); + stakeManager.unstake(stakerId, unstakeAmount); + // Check lock Structs.Lock memory lock = stakeManager.getLocks(staker1); - assertEq(lock.amount, 5 ether); - assertEq(lock.unlockAfter, currentEpoch + stakeManager.unstakeLockPeriod()); - } + assertEq(lock.amount, unstakeAmount); + assertEq(lock.unlockAfter, uint32(block.timestamp / EPOCH_LENGTH) + unstakeLockPeriod); - function testWithdraw() public { - uint32 currentEpoch = uint32(block.timestamp / EPOCH_LENGTH); - uint256 stakeAmount = 10 ether; + // Try to unstake again before withdrawal + vm.prank(staker1); + vm.expectRevert("Existing unstake lock"); + stakeManager.unstake(stakerId, 1 ether); + } - vm.startPrank(staker1); - stakeManager.stake{value: stakeAmount}(currentEpoch, stakeAmount, "test-spec"); + function testWithdrawFlow() public { + // Initial stake + vm.prank(staker1); + stakeManager.stake{value: MIN_STAKE}(0, MIN_STAKE, "test-spec"); uint32 stakerId = stakeManager.getStakerId(staker1); - stakeManager.unstake(stakerId, 5 ether); + uint256 unstakeAmount = 5 ether; - // Advance time to after the unlock period - vm.warp(block.timestamp + (stakeManager.unstakeLockPeriod() + 1) * EPOCH_LENGTH); + // Unstake + vm.prank(staker1); + stakeManager.unstake(stakerId, unstakeAmount); - uint256 balanceBefore = address(staker1).balance; + // Try to withdraw before lock period + vm.prank(staker1); + vm.expectRevert("Unlock period not reached"); stakeManager.withdraw(stakerId); - uint256 balanceAfter = address(staker1).balance; - assertEq(balanceAfter - balanceBefore, 5 ether); - vm.stopPrank(); + // Move forward past lock period + vm.warp(block.timestamp + (unstakeLockPeriod + 1) * EPOCH_LENGTH); + + // Withdraw successfully + uint256 balanceBefore = staker1.balance; + vm.prank(staker1); + stakeManager.withdraw(stakerId); + uint256 balanceAfter = staker1.balance; + + assertEq(balanceAfter - balanceBefore, unstakeAmount); + + // Verify remaining stake + Structs.Staker memory stakerInfo = stakeManager.getStaker(stakerId); + assertEq(stakerInfo.stake, MIN_STAKE - unstakeAmount); } - function testWithdrawBeforeUnlock() public { - uint32 currentEpoch = uint32(block.timestamp / EPOCH_LENGTH); - uint256 stakeAmount = 10 ether; + function testUnauthorizedUnstake() public { + // Initial stake + vm.prank(staker1); + stakeManager.stake{value: MIN_STAKE}(0, MIN_STAKE, "test-spec"); + + uint32 stakerId = stakeManager.getStakerId(staker1); - vm.startPrank(staker1); - stakeManager.stake{value: stakeAmount}(currentEpoch, stakeAmount, "test-spec"); + // Try to unstake from different address + vm.prank(staker2); + vm.expectRevert("Can only unstake your own funds"); + stakeManager.unstake(stakerId, 1 ether); + } + + function testExcessiveUnstake() public { + // Initial stake + vm.prank(staker1); + stakeManager.stake{value: MIN_STAKE}(0, MIN_STAKE, "test-spec"); uint32 stakerId = stakeManager.getStakerId(staker1); - stakeManager.unstake(stakerId, 5 ether); - vm.expectRevert("Unlock period not reached"); + // Try to unstake more than staked + vm.prank(staker1); + vm.expectRevert("Unstake amount exceeds current stake"); + stakeManager.unstake(stakerId, MIN_STAKE + 1 ether); + } + + function testInvalidStakerId() public { + vm.prank(staker1); + vm.expectRevert("Invalid staker ID"); + stakeManager.unstake(0, 1 ether); + + vm.prank(staker1); + vm.expectRevert("No unstake request found"); + stakeManager.withdraw(1); + } + + function testLocksAfterWithdraw() public { + // Initial stake + vm.prank(staker1); + stakeManager.stake{value: MIN_STAKE}(0, MIN_STAKE, "test-spec"); + + uint32 stakerId = stakeManager.getStakerId(staker1); + + // Unstake and withdraw + vm.startPrank(staker1); + stakeManager.unstake(stakerId, 1 ether); + + vm.warp(block.timestamp + (unstakeLockPeriod + 1) * EPOCH_LENGTH); stakeManager.withdraw(stakerId); vm.stopPrank(); + + // Verify locks are reset + Structs.Lock memory lock = stakeManager.getLocks(staker1); + assertEq(lock.amount, 0); + assertEq(lock.unlockAfter, 0); } - // Add more tests here for edge cases and other functions + // TODO: testSlashedStakerCannotStake, once we have the slashing function + + function testGetterFunctions() public { + vm.prank(staker1); + stakeManager.stake{value: MIN_STAKE}(0, MIN_STAKE, "test-spec"); + + uint32 stakerId = stakeManager.getStakerId(staker1); + + assertEq(stakeManager.getNumStakers(), 1); + assertEq(stakeManager.getStake(stakerId), MIN_STAKE); + + Structs.Staker memory stakerInfo = stakeManager.getStaker(stakerId); + assertEq(stakerInfo._address, staker1); + assertEq(stakerInfo.stake, MIN_STAKE); + } } \ No newline at end of file diff --git a/test/StateManager.t.sol b/test/StateManager.t.sol index cf49c12..630c8f7 100644 --- a/test/StateManager.t.sol +++ b/test/StateManager.t.sol @@ -6,60 +6,163 @@ import "forge-std/console.sol"; import "../src/Core/StateManager.sol"; import "../src/Core/storage/Constants.sol"; -contract StateManagerTest is Constants, Test { - StateManager stateManager; +contract StateManagerTest is Test, Constants, StateManager { + StateManager public stateManager; + uint8 public constant BUFFER = 5; function setUp() public { stateManager = new StateManager(); } - function testCommitState() public { - uint8 buffer = 5; - // Set block.timestamp to be in the Assign state window - uint256 assignTime = (EPOCH_LENGTH / NUM_STATES) / 2; // Middle of Assign state - vm.warp(assignTime); + function testGetEpoch() public { + // Test at epoch 0 + assertEq(stateManager.getEpoch(), 0); + + // Test at epoch 1 + vm.warp(EPOCH_LENGTH); + assertEq(stateManager.getEpoch(), 1); + + // Test at epoch 5 + vm.warp(5 * EPOCH_LENGTH); + assertEq(stateManager.getEpoch(), 5); + + // Test at a specific timestamp + uint256 timestamp = 1703347200; // Some specific timestamp + vm.warp(timestamp); + assertEq(stateManager.getEpoch(), timestamp / EPOCH_LENGTH); + } + + function testAssignState() public { + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; - State state = stateManager.getState(buffer); - assertEq(uint8(state), uint8(State.Assign)); + // Test middle of Assign state + uint256 assignTime = stateLength / 2; + vm.warp(assignTime); + assertEq(uint8(stateManager.getState(BUFFER)), uint8(State.Assign)); } - function testRevealState() public { - uint8 buffer = 5; - // Set block.timestamp to be in the Update state window - uint256 updateTime = (EPOCH_LENGTH / NUM_STATES) + (EPOCH_LENGTH / NUM_STATES / 2); // Middle of Update state - vm.warp(updateTime); + function testUpdateState() public { + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; - State state = stateManager.getState(buffer); - assertEq(uint8(state), uint8(State.Update)); + // Test middle of Update state + uint256 updateTime = stateLength + (stateLength / 2); + vm.warp(updateTime); + assertEq(uint8(stateManager.getState(BUFFER)), uint8(State.Update)); } - function testProposeState() public { - uint8 buffer = 5; - // Set block.timestamp to be in the Confirm state window - uint256 confirmTime = 2 * (EPOCH_LENGTH / NUM_STATES) + (EPOCH_LENGTH / NUM_STATES / 2); // Middle of Confirm state - vm.warp(confirmTime); + function testConfirmState() public { + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; - State state = stateManager.getState(buffer); - assertEq(uint8(state), uint8(State.Confirm)); + // Test middle of Confirm state + uint256 confirmTime = 2 * stateLength + (stateLength / 2); + vm.warp(confirmTime); + assertEq(uint8(stateManager.getState(BUFFER)), uint8(State.Confirm)); } function testBufferState() public { - uint8 buffer = 5; - // Set block.timestamp to be in the buffer period - uint256 bufferTime = (EPOCH_LENGTH / NUM_STATES) - 2; // Just before state transition - vm.warp(bufferTime); + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; + + // Test at the start buffer + vm.warp(2); // Just after epoch start + assertEq(uint8(stateManager.getState(BUFFER)), uint8(State.Buffer)); + + // Test at the end buffer of first state + vm.warp(stateLength - 2); + assertEq(uint8(stateManager.getState(BUFFER)), uint8(State.Buffer)); + + // Test at the buffer between states + vm.warp(stateLength + 2); + assertEq(uint8(stateManager.getState(BUFFER)), uint8(State.Buffer)); + } + + function testStateModifier() public { + function(State, uint8) external returns (bool) fn = this.dummyStateFunction; + + // Test happy path + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; + uint256 assignTime = stateLength / 2; + vm.warp(assignTime); + assertTrue(fn(State.Assign, BUFFER)); + + // Test incorrect state + vm.expectRevert("Incorrect state"); + fn(State.Update, BUFFER); + } + + function testEpochModifier() public { + function(uint32) external returns (bool) fn = this.dummyEpochFunction; - State state = stateManager.getState(buffer); - assertEq(uint8(state), uint8(State.Buffer)); + // Test happy path + assertTrue(fn(0)); + + // Test incorrect epoch + vm.warp(EPOCH_LENGTH); // Move to epoch 1 + vm.expectRevert("Incorrect epoch"); + fn(0); } - function testNonBufferState() public { - uint8 buffer = 5; - // Set block.timestamp to be clearly in a non-buffer period - uint256 nonBufferTime = (EPOCH_LENGTH / NUM_STATES) / 2; // Middle of first state - vm.warp(nonBufferTime); + function testEpochAndStateModifier() public { + function(State, uint32, uint8) external returns (bool) fn = this.dummyEpochAndStateFunction; - State state = stateManager.getState(buffer); - assertTrue(state != State.Buffer); + // Test happy path + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; + uint256 assignTime = stateLength / 2; + vm.warp(assignTime); + assertTrue(fn(State.Assign, 0, BUFFER)); + + // Test incorrect epoch + vm.warp(EPOCH_LENGTH); // Move to epoch 1 + vm.expectRevert("Incorrect epoch"); + fn(State.Assign, 0, BUFFER); + + // Test incorrect state + vm.warp(assignTime); + vm.expectRevert("Incorrect state"); + fn(State.Update, 0, BUFFER); + } + + // Dummy functions to test modifiers + function dummyStateFunction(State state, uint8 buffer) external view checkState(state, buffer) returns (bool) { + return true; + } + + function dummyEpochFunction(uint32 epoch) external view checkEpoch(epoch) returns (bool) { + return true; + } + + function dummyEpochAndStateFunction(State state, uint32 epoch, uint8 buffer) + external + view + checkEpochAndState(state, epoch, buffer) + returns (bool) + { + return true; + } + + function testStateTransitions() public { + uint256 stateLength = EPOCH_LENGTH / NUM_STATES; + + // Test each state in sequence + for (uint8 i = 0; i < NUM_STATES; i++) { + // Start of state (plus buffer to avoid being in buffer state) + uint256 stateStart = i * stateLength; + vm.warp(stateStart + BUFFER + 1); + + State currentState = stateManager.getState(BUFFER); + console.log("Testing state transition. Expected:", i, "Got:", uint(currentState)); + assertEq(uint(currentState), uint(i), string(abi.encodePacked("State should be ", vm.toString(i)))); + + // Middle of state + vm.warp(stateStart + (stateLength / 2)); + assertEq(uint(stateManager.getState(BUFFER)), uint(i), "Middle of state should match"); + + // Just before buffer (should still be in current state) + vm.warp(stateStart + stateLength - BUFFER - 1); + assertEq(uint(stateManager.getState(BUFFER)), uint(i), "Before buffer should match"); + + // In buffer period + vm.warp(stateStart + stateLength - (BUFFER / 2)); + assertEq(uint(stateManager.getState(BUFFER)), uint(State.Buffer), "Should be in buffer state"); + } } } \ No newline at end of file