diff --git a/.gas-snapshot b/.gas-snapshot index 6e8e4e3..9462dc6 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,77 +1,85 @@ AuthorshipTokenTest:test_curtaMint() (gas: 85648) -AuthorshipTokenTest:test_curtaMint_SenderIsNotCurta_RevertsUnauthorized(address) (runs: 256, μ: 10204, ~: 10204) -AuthorshipTokenTest:test_ownerMint_FuzzMintTimestamps_IssuesTokensCorrectly(uint256) (runs: 256, μ: 6430685, ~: 5928022) -AuthorshipTokenTest:test_ownerMint_SenderIsNotOwner_RevertUnauthorized(address) (runs: 256, μ: 12935, ~: 12935) +AuthorshipTokenTest:test_curtaMint_SenderIsNotCurta_RevertsUnauthorized(address) (runs: 256, μ: 10248, ~: 10248) +AuthorshipTokenTest:test_ownerMint_FuzzMintTimestamps_IssuesTokensCorrectly(uint256) (runs: 256, μ: 6412423, ~: 5915354) +AuthorshipTokenTest:test_ownerMint_SenderIsNotOwner_RevertUnauthorized(address) (runs: 256, μ: 12979, ~: 12979) AuthorshipTokenTest:test_ownerMint_SenderIsOwner_AllowsMint() (gas: 108202) -AuthorshipTokenTest:test_tokenURI_MintedToken_Succeeds() (gas: 232) -AuthorshipTokenTest:test_tokenURI_UnmintedToken_Fails() (gas: 12629) -CurtaTest:test_Initialization_DeployAddressesMatch() (gas: 11297) -CurtaTest:test_addPuzzle() (gas: 305332) -CurtaTest:test_addPuzzle_UseAuthorshipToken_UpdatesStorage() (gas: 298535) -CurtaTest:test_addPuzzle_UseSameAuthorshipTokenTwice_Fails() (gas: 299355) -CurtaTest:test_addPuzzle_UseUnownedAuthorshipToken_RevertsUnauthorized() (gas: 199252) -CurtaTest:test_approve() (gas: 434876) -CurtaTest:test_approve_SenderIsNotOwner_RevertsUnauthorized() (gas: 410380) -CurtaTest:test_approve_WithApprovalForAllTrue_AllowsTransfer() (gas: 457096) -CurtaTest:test_balanceOf_ZeroAddress_Fails() (gas: 8674) -CurtaTest:test_setApprovalForAll_False_UpdatesStorage() (gas: 15966) -CurtaTest:test_setApprovalForAll_True_UpdatesStorage() (gas: 35841) +AuthorshipTokenTest:test_tokenURI_MintedToken_Succeeds() (gas: 254) +AuthorshipTokenTest:test_tokenURI_UnmintedToken_Fails() (gas: 12652) +CurtaTest:test_Initialization_DeployAddressesMatch() (gas: 11275) +CurtaTest:test_addPuzzle() (gas: 305309) +CurtaTest:test_addPuzzle_UseAuthorshipToken_UpdatesStorage() (gas: 298512) +CurtaTest:test_addPuzzle_UseSameAuthorshipTokenTwice_Fails() (gas: 299421) +CurtaTest:test_addPuzzle_UseUnownedAuthorshipToken_RevertsUnauthorized() (gas: 199296) +CurtaTest:test_approve() (gas: 434878) +CurtaTest:test_approve_SenderIsNotOwner_RevertsUnauthorized() (gas: 410379) +CurtaTest:test_approve_WithApprovalForAllTrue_AllowsTransfer() (gas: 457095) +CurtaTest:test_balanceOf_ZeroAddress_Fails() (gas: 8718) +CurtaTest:test_setApprovalForAll_False_UpdatesStorage() (gas: 15965) +CurtaTest:test_setApprovalForAll_True_UpdatesStorage() (gas: 35840) CurtaTest:test_setFermat_AsRandomAccount_Succeeds(address) (runs: 256, μ: 455137, ~: 455137) -CurtaTest:test_setFermat_InitialSet_UpdatesStorage() (gas: 511807) -CurtaTest:test_setFermat_SetAfterTransfer_Succeeds(address) (runs: 256, μ: 876595, ~: 876595) -CurtaTest:test_setFermat_SetDifferentPuzzlesTwiceInIncreasingOrder_Succeeds() (gas: 870154) -CurtaTest:test_setFermat_SetNonFermatPuzzle_Fails() (gas: 780207) -CurtaTest:test_setFermat_SetSamePuzzleTwice_Fails() (gas: 456913) -CurtaTest:test_setFermat_SetUnsolvedPuzzle_Fails() (gas: 300144) -CurtaTest:test_setPuzzleColors() (gas: 306229) -CurtaTest:test_setPuzzleColors_SetUnauthoredPuzzle_RevertsUnauthorized() (gas: 299507) -CurtaTest:test_solve() (gas: 657540) -CurtaTest:test_solve_DuringAllPhases_FirstSolveTimestampOnlySetOnFirstBlood(uint40) (runs: 256, μ: 520949, ~: 520275) -CurtaTest:test_solve_DuringPhase1WithPayment_PaysAuthor(uint256) (runs: 256, μ: 495665, ~: 496974) -CurtaTest:test_solve_DuringPhase2WithPayment_PaysAuthor(uint256) (runs: 256, μ: 533897, ~: 533897) -CurtaTest:test_solve_DuringPhase2_RequiresETH(uint256) (runs: 256, μ: 489577, ~: 487644) -CurtaTest:test_solve_DuringPhase3_Fails(uint40) (runs: 256, μ: 434098, ~: 434098) +CurtaTest:test_setFermat_InitialSet_UpdatesStorage() (gas: 511851) +CurtaTest:test_setFermat_SetAfterTransfer_Succeeds(address) (runs: 256, μ: 876639, ~: 876639) +CurtaTest:test_setFermat_SetDifferentPuzzlesTwiceInIncreasingOrder_Succeeds() (gas: 870131) +CurtaTest:test_setFermat_SetNonFermatPuzzle_Fails() (gas: 780187) +CurtaTest:test_setFermat_SetSamePuzzleTwice_Fails() (gas: 456892) +CurtaTest:test_setFermat_SetUnsolvedPuzzle_Fails() (gas: 300099) +CurtaTest:test_setPuzzleColors() (gas: 306206) +CurtaTest:test_setPuzzleColors_SetUnauthoredPuzzle_RevertsUnauthorized() (gas: 299486) +CurtaTest:test_solve() (gas: 657518) +CurtaTest:test_solve_DuringAllPhases_FirstSolveTimestampOnlySetOnFirstBlood(uint40) (runs: 256, μ: 521015, ~: 520341) +CurtaTest:test_solve_DuringPhase1WithPayment_PaysAuthor(uint256) (runs: 256, μ: 495643, ~: 496952) +CurtaTest:test_solve_DuringPhase2WithPayment_PaysAuthor(uint256) (runs: 256, μ: 533875, ~: 533875) +CurtaTest:test_solve_DuringPhase2_RequiresETH(uint256) (runs: 256, μ: 489257, ~: 487666) +CurtaTest:test_solve_DuringPhase3_Fails(uint40) (runs: 256, μ: 434097, ~: 434097) CurtaTest:test_solve_FirstBlood_AuthorshipTokenMintPotentialRevertBranch() (gas: 311249) -CurtaTest:test_solve_FirstBlood_MintsAuthorshipToken() (gas: 413358) -CurtaTest:test_solve_FirstBlood_UpdatesFirstSolveTimestamp(uint40) (runs: 256, μ: 407340, ~: 407340) -CurtaTest:test_solve_IncorrectSolution_Fails(uint256) (runs: 256, μ: 309250, ~: 309250) +CurtaTest:test_solve_FirstBlood_MintsAuthorshipToken() (gas: 413335) +CurtaTest:test_solve_FirstBlood_UpdatesFirstSolveTimestamp(uint40) (runs: 256, μ: 407384, ~: 407384) +CurtaTest:test_solve_IncorrectSolution_Fails(uint256) (runs: 256, μ: 309228, ~: 309228) CurtaTest:test_solve_NonExistantPuzzle_Fails() (gas: 13650) -CurtaTest:test_solve_SamePuzzleTwice_Fails() (gas: 407397) -CurtaTest:test_solve_Success_MintsFlag() (gas: 409078) -CurtaTest:test_solve_Success_UpdatesSolveCounters() (gas: 613748) -CurtaTest:test_solve_Success_UpdatesStorage() (gas: 408000) -CurtaTest:test_supportsInterface() (gas: 8058) -CurtaTest:test_tokenURI_MintedToken_Succeeds() (gas: 234) -CurtaTest:test_tokenURI_UnmintedToken_Fails() (gas: 12952) -CurtaTest:test_transferFrom() (gas: 450609) -CurtaTest:test_transferFrom_SenderIsOwner_AllowsTransfer() (gas: 434582) -CurtaTest:test_transferFrom_ToZeroAddress_Fails() (gas: 407936) -CurtaTest:test_transferFrom_Unauthorized_RevertsUnauthorized() (gas: 412838) -CurtaTest:test_transferFrom_WithApprovalForAllTrue_AllowsTransfer() (gas: 460343) -CurtaTest:test_transferFrom_WithTokenApproval_AllowsTransfer() (gas: 440767) -CurtaTest:test_transferFrom_WrongFrom_Fails() (gas: 407858) -DeployBaseGoerliTest:test_AddressInitializationCorrectness() (gas: 23494) -DeployBaseGoerliTest:test_authorshipTokenAuthorsEquality() (gas: 13651) -DeployBaseGoerliTest:test_authorshipTokenIssueLengthEquality() (gas: 11460) -DeployBaseGoerliTest:test_authorshipTokenMinting() (gas: 108701) +CurtaTest:test_solve_SamePuzzleTwice_Fails() (gas: 407374) +CurtaTest:test_solve_Success_MintsFlag() (gas: 409057) +CurtaTest:test_solve_Success_UpdatesSolveCounters() (gas: 613814) +CurtaTest:test_solve_Success_UpdatesStorage() (gas: 408044) +CurtaTest:test_supportsInterface() (gas: 8102) +CurtaTest:test_tokenURI_MintedToken_Succeeds() (gas: 278) +CurtaTest:test_tokenURI_UnmintedToken_Fails() (gas: 12908) +CurtaTest:test_transferFrom() (gas: 450588) +CurtaTest:test_transferFrom_SenderIsOwner_AllowsTransfer() (gas: 434581) +CurtaTest:test_transferFrom_ToZeroAddress_Fails() (gas: 407958) +CurtaTest:test_transferFrom_Unauthorized_RevertsUnauthorized() (gas: 412860) +CurtaTest:test_transferFrom_WithApprovalForAllTrue_AllowsTransfer() (gas: 460342) +CurtaTest:test_transferFrom_WithTokenApproval_AllowsTransfer() (gas: 440811) +CurtaTest:test_transferFrom_WrongFrom_Fails() (gas: 407902) +DeployBaseGoerliTest:test_AddressInitializationCorrectness() (gas: 23561) +DeployBaseGoerliTest:test_authorshipTokenAuthorsEquality() (gas: 13695) +DeployBaseGoerliTest:test_authorshipTokenIssueLengthEquality() (gas: 11482) +DeployBaseGoerliTest:test_authorshipTokenMinting() (gas: 108745) DeployBaseGoerliTest:test_authorshipTokenOwnerEquality() (gas: 13799) -DeployBaseGoerliTest:test_curtaOwnerEquality() (gas: 13832) -DeployBaseMainnetTest:test_AddressInitializationCorrectness() (gas: 23494) -DeployBaseMainnetTest:test_authorshipTokenAuthorsEquality() (gas: 27900) -DeployBaseMainnetTest:test_authorshipTokenIssueLengthEquality() (gas: 11460) -DeployBaseMainnetTest:test_authorshipTokenMinting() (gas: 91601) +DeployBaseGoerliTest:test_curtaOwnerEquality() (gas: 13854) +DeployBaseMainnetTest:test_AddressInitializationCorrectness() (gas: 23561) +DeployBaseMainnetTest:test_authorshipTokenAuthorsEquality() (gas: 27944) +DeployBaseMainnetTest:test_authorshipTokenIssueLengthEquality() (gas: 11482) +DeployBaseMainnetTest:test_authorshipTokenMinting() (gas: 91645) DeployBaseMainnetTest:test_authorshipTokenOwnerEquality() (gas: 13799) -DeployBaseMainnetTest:test_curtaOwnerEquality() (gas: 13832) -DeployGoerliTest:test_AddressInitializationCorrectness() (gas: 23494) -DeployGoerliTest:test_authorshipTokenAuthorsEquality() (gas: 13651) -DeployGoerliTest:test_authorshipTokenIssueLengthEquality() (gas: 11460) -DeployGoerliTest:test_authorshipTokenMinting() (gas: 108701) +DeployBaseMainnetTest:test_curtaOwnerEquality() (gas: 13854) +DeployGoerliTest:test_AddressInitializationCorrectness() (gas: 23561) +DeployGoerliTest:test_authorshipTokenAuthorsEquality() (gas: 13695) +DeployGoerliTest:test_authorshipTokenIssueLengthEquality() (gas: 11482) +DeployGoerliTest:test_authorshipTokenMinting() (gas: 108745) DeployGoerliTest:test_authorshipTokenOwnerEquality() (gas: 13799) -DeployGoerliTest:test_curtaOwnerEquality() (gas: 13832) -DeployMainnetTest:test_AddressInitializationCorrectness() (gas: 23494) -DeployMainnetTest:test_authorshipTokenAuthorsEquality() (gas: 227362) -DeployMainnetTest:test_authorshipTokenIssueLengthEquality() (gas: 11460) -DeployMainnetTest:test_authorshipTokenMinting() (gas: 91601) +DeployGoerliTest:test_curtaOwnerEquality() (gas: 13854) +DeployMainnetTest:test_AddressInitializationCorrectness() (gas: 23561) +DeployMainnetTest:test_authorshipTokenAuthorsEquality() (gas: 227406) +DeployMainnetTest:test_authorshipTokenIssueLengthEquality() (gas: 11482) +DeployMainnetTest:test_authorshipTokenMinting() (gas: 91645) DeployMainnetTest:test_authorshipTokenOwnerEquality() (gas: 13799) -DeployMainnetTest:test_curtaOwnerEquality() (gas: 13832) -OptimizationsTest:testFuzzComputePhaseFromTimestampBranchlessOptimization(uint40,uint40) (runs: 256, μ: 3545, ~: 3531) \ No newline at end of file +DeployMainnetTest:test_curtaOwnerEquality() (gas: 13854) +OptimizationsTest:testFuzzComputePhaseFromTimestampBranchlessOptimization(uint40,uint40) (runs: 256, μ: 3602, ~: 3575) +TeamRegistyTest:test_acceptInvite() (gas: 264448) +TeamRegistyTest:test_createTeam(address[]) (runs: 256, μ: 3459354, ~: 3449502) +TeamRegistyTest:test_inviteMember() (gas: 287392) +TeamRegistyTest:test_inviteMembers() (gas: 318320) +TeamRegistyTest:test_kickMember() (gas: 229136) +TeamRegistyTest:test_kickMembers() (gas: 241032) +TeamRegistyTest:test_leaveTeam() (gas: 223522) +TeamRegistyTest:test_transferLeadership() (gas: 301341) \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std index 058d200..bdea49f 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 058d2004ac10cc8f194625fb107fb7a87c4e702d +Subproject commit bdea49f9bb3c58c8c35850c3bdc17eaeea756e9a diff --git a/src/TeamRegistry.sol b/src/TeamRegistry.sol index 7eed63f..96444ba 100644 --- a/src/TeamRegistry.sol +++ b/src/TeamRegistry.sol @@ -65,7 +65,7 @@ contract TeamRegistry { mapping(address => bool) public isTeamMember; /// @notice Team ID counter. Starts at 1. - uint16 teamId; + uint16 private teamId; // ------------------------------------------------------------------------- // Functions @@ -131,7 +131,7 @@ contract TeamRegistry { function acceptInvite(uint32 _teamId) external { if (isTeamMember[msg.sender] == true) revert AlreadyInTeam(msg.sender); if (teamMemberStatus[_teamId][msg.sender] != 1) revert NoPendingInvite(_teamId, msg.sender); - teamMemberStatus[_teamId][msg.sender] == 2; + teamMemberStatus[_teamId][msg.sender] = 2; isTeamMember[msg.sender] = true; emit TeamInviteAccepted(_teamId, msg.sender); @@ -144,7 +144,7 @@ contract TeamRegistry { /// or if `_member` is not in the team. function kickMember(uint32 _teamId, address _member) external { if (teamMemberStatus[_teamId][msg.sender] < 3) revert NotTeamLeader(_teamId, msg.sender); - if (teamMemberStatus[_teamId][_member] < 2) revert NotInTeam(_teamId, msg.sender); + if (teamMemberStatus[_teamId][_member] < 2) revert NotInTeam(_teamId, _member); delete teamMemberStatus[_teamId][_member]; isTeamMember[_member] = false; } @@ -157,9 +157,12 @@ contract TeamRegistry { function kickMembers(uint32 _teamId, address[] calldata _members) external { if (teamMemberStatus[_teamId][msg.sender] < 3) revert NotTeamLeader(_teamId, msg.sender); for (uint8 i; i < _members.length;) { - if (teamMemberStatus[_teamId][_members[i]] < 2) revert NotInTeam(_teamId, msg.sender); + if (teamMemberStatus[_teamId][_members[i]] < 2) revert NotInTeam(_teamId, _members[i]); delete teamMemberStatus[_teamId][_members[i]]; isTeamMember[_members[i]] = false; + unchecked { + i++; + } } } @@ -178,7 +181,7 @@ contract TeamRegistry { /// @param _newLeader The address of the new leader. /// @dev Reverts if `msg.sender` is not the current leader /// or `_newLeader` is not a team member. - function transferTeamOwnership(uint32 _teamId, address _newLeader) external { + function transferLeadership(uint32 _teamId, address _newLeader) external { if (teamMemberStatus[_teamId][msg.sender] < 3) revert NotTeamLeader(_teamId, msg.sender); if (teamMemberStatus[_teamId][_newLeader] != 2) revert NotInTeam(_teamId, _newLeader); teamMemberStatus[_teamId][msg.sender] = 2; diff --git a/test/TeamRegistry.t.sol b/test/TeamRegistry.t.sol new file mode 100644 index 0000000..81627ba --- /dev/null +++ b/test/TeamRegistry.t.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { Test, console } from "forge-std/Test.sol"; + +import { TeamRegistry } from "@/contracts/TeamRegistry.sol"; + +contract TeamRegistyTest is Test { + // ------------------------------------------------------------------------- + // Events + // ------------------------------------------------------------------------- + + /// @notice Emitted when a new team is created. + /// @param teamId The ID of the team. + /// @param leader The leader of the team. + event TeamCreated(uint32 teamId, address leader); + + /// @notice Emitted when a team invite has been accepted. + /// @param teamId The ID of the team. + /// @param member The member of the team that accepted the invite. + event TeamInviteAccepted(uint32 teamId, address member); + + /// @notice Emitted when team leadership is transferred. + /// @param oldLeader The old leader of the team. + /// @param newLeader The new leader of the team. + event TeamLeadershipTransferred(uint32 teamId, address oldLeader, address newLeader); + + TeamRegistry public tr; + + function setUp() public { + tr = new TeamRegistry(); + } + + function test_createTeam(address[] calldata members) public { + vm.assume(members.length > 0); + + vm.expectEmit(false, false, false, true); + emit TeamCreated(1, address(this)); + tr.createTeam(members); + + // Test that `msg.sender` is team leader and part of a team + assertEq(tr.teamMemberStatus(1, address(this)), 3); + assertTrue(tr.isTeamMember(address(this))); + + // Test that all `members` have been invited + for (uint8 i; i < members.length;) { + assertEq(tr.teamMemberStatus(1, members[i]), 1); + unchecked { + i++; + } + } + + // Test that team leader cannot create multiple teams + vm.expectRevert(abi.encodeWithSelector(TeamRegistry.AlreadyInTeam.selector, address(this))); + tr.createTeam(members); + + // Test that members of another team cannot be invited by + // pranking one of the invitees and trying to add this address + // which is already part of team 1. + address[] memory tms = new address[](1); + tms[0] = address(this); + + vm.expectRevert(abi.encodeWithSelector(TeamRegistry.AlreadyInTeam.selector, address(this))); + vm.prank(members[0]); + tr.createTeam(tms); + } + + function test_inviteMember() public { + _createTeam(); + + // Test that team leader can invite a member + vm.prank(makeAddr("chainlight")); + tr.inviteMember(1, makeAddr("plotchy")); + + // Test that the team member has been invited + assertEq(tr.teamMemberStatus(1, makeAddr("plotchy")), 1); + + // Test that non team leaders cannont invite members + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.NotTeamLeader.selector, 1, makeAddr("sudolabel")) + ); + vm.prank(makeAddr("sudolabel")); + tr.inviteMember(1, makeAddr("popular")); + + // Test that team members cannot be invited + vm.prank(makeAddr("sudolabel")); + tr.acceptInvite(1); + + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.AlreadyInTeam.selector, makeAddr("sudolabel")) + ); + vm.prank(makeAddr("chainlight")); + tr.inviteMember(1, makeAddr("sudolabel")); + } + + function test_inviteMembers() public { + _createTeam(); + + address[] memory tms = new address[](2); + tms[0] = makeAddr("plotchy"); + tms[1] = makeAddr("popular"); + + // Test that team leader can invite members + vm.prank(makeAddr("chainlight")); + tr.inviteMembers(1, tms); + + // Test that team members have been invited + assertEq(tr.teamMemberStatus(1, makeAddr("plotchy")), 1); + assertEq(tr.teamMemberStatus(1, makeAddr("popular")), 1); + + // Test that non team leaders cannot invite members + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.NotTeamLeader.selector, 1, makeAddr("sudolabel")) + ); + vm.prank(makeAddr("sudolabel")); + tr.inviteMembers(1, tms); + + // Test that team members cannot be invited + vm.prank(makeAddr("sudolabel")); + tr.acceptInvite(1); + + tms[0] = makeAddr("sudolabel"); + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.AlreadyInTeam.selector, makeAddr("sudolabel")) + ); + vm.prank(makeAddr("chainlight")); + tr.inviteMembers(1, tms); + } + + function test_acceptInvite() public { + _createTeam(); + + // Test that members can accept invites + vm.expectEmit(false, false, false, true); + emit TeamInviteAccepted(1, makeAddr("sudolabel")); + vm.prank(makeAddr("sudolabel")); + tr.acceptInvite(1); + + // Test that members cannot accept invites if they are already in a team + vm.expectRevert(abi.encodeWithSelector(TeamRegistry.AlreadyInTeam.selector, makeAddr("chainlight"))); + vm.prank(makeAddr("chainlight")); + tr.acceptInvite(1); + + // Test that members cannot accept invites if they don't have one pending + vm.expectRevert(abi.encodeWithSelector(TeamRegistry.NoPendingInvite.selector, 1, makeAddr("plotchy"))); + vm.prank(makeAddr("plotchy")); + tr.acceptInvite(1); + + // Test that members are updated after accepting invites + assertEq(tr.teamMemberStatus(1, makeAddr("sudolabel")), 2); + assertTrue(tr.isTeamMember(makeAddr("sudolabel"))); + } + + function test_kickMember() public { + _createTeam(); + + vm.prank(makeAddr("sudolabel")); + tr.acceptInvite(1); + + // Test that team leaders can kick a member + vm.prank(makeAddr("chainlight")); + tr.kickMember(1, makeAddr("sudolabel")); + + // Test that member statuses have been updated + assertEq(tr.teamMemberStatus(1, makeAddr("sudolabel")), 0); + assertFalse(tr.isTeamMember(makeAddr("sudolabel"))); + + // Test that non team leaders cannot kick a member + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.NotTeamLeader.selector, 1, makeAddr("sudolabel")) + ); + vm.prank(makeAddr("sudolabel")); + tr.kickMember(1, makeAddr("sudolabel")); + + // Test that team leaders cannot kick a non team member + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.NotInTeam.selector, 1, makeAddr("plotchy")) + ); + vm.prank(makeAddr("chainlight")); + tr.kickMember(1, makeAddr("plotchy")); + } + + function test_kickMembers() public { + _createTeam(); + + vm.prank(makeAddr("sudolabel")); + tr.acceptInvite(1); + vm.prank(makeAddr("igorline")); + tr.acceptInvite(1); + + address[] memory tms = new address[](2); + tms[0] = makeAddr("sudolabel"); + tms[1] = makeAddr("igorline"); + + // Test that team leaders can kick members + vm.prank(makeAddr("chainlight")); + tr.kickMembers(1, tms); + + // Test that team member statuses have been updated + assertEq(tr.teamMemberStatus(1, makeAddr("sudolabel")), 0); + assertEq(tr.teamMemberStatus(1, makeAddr("igorline")), 0); + assertFalse(tr.isTeamMember(makeAddr("sudolabel"))); + assertFalse(tr.isTeamMember(makeAddr("igorline"))); + + tms[0] = makeAddr("chainlight"); + + // Test that non team leaders cannot kick members + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.NotTeamLeader.selector, 1, makeAddr("sudolabel")) + ); + vm.prank(makeAddr("sudolabel")); + tr.kickMembers(1, tms); + + tms[0] = makeAddr("plotchy"); + + // Test that team leaders cannot kick non team members + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.NotInTeam.selector, 1, makeAddr("plotchy")) + ); + vm.prank(makeAddr("chainlight")); + tr.kickMembers(1, tms); + } + + function test_leaveTeam() public { + _createTeam(); + + vm.prank(makeAddr("sudolabel")); + tr.acceptInvite(1); + + // Test that members can leave teams + vm.prank(makeAddr("sudolabel")); + tr.leaveTeam(1); + + // Test that member status has been updated + assertEq(tr.teamMemberStatus(1, makeAddr("sudolabel")), 0); + assertFalse(tr.isTeamMember(makeAddr("sudolabel"))); + + // Test that team leaders cannot leave team + vm.expectRevert(abi.encodeWithSelector(TeamRegistry.IsTeamLeader.selector, 1, makeAddr("chainlight"))); + vm.prank(makeAddr("chainlight")); + tr.leaveTeam(1); + + // Test that non team members cannot leave team + vm.expectRevert(abi.encodeWithSelector(TeamRegistry.NotInTeam.selector, 1, makeAddr("plotchy"))); + vm.prank(makeAddr("plotchy")); + tr.leaveTeam(1); + } + + function test_transferLeadership() public { + _createTeam(); + + vm.prank(makeAddr("sudolabel")); + tr.acceptInvite(1); + vm.prank(makeAddr("igorline")); + tr.acceptInvite(1); + + // Test that team leadership can be transferred + vm.expectEmit(false, false, false, false); + emit TeamLeadershipTransferred(1, makeAddr("chainlight"), makeAddr("sudolabel")); + vm.prank(makeAddr("chainlight")); + tr.transferLeadership(1, makeAddr("sudolabel")); + + // Test that member statuses have been updated + assertEq(tr.teamMemberStatus(1, makeAddr("chainlight")), 2); + assertEq(tr.teamMemberStatus(1, makeAddr("sudolabel")), 3); + + // Test that non leaders cannot transfer leadership + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.NotTeamLeader.selector, 1, makeAddr("igorline")) + ); + vm.prank(makeAddr("igorline")); + tr.transferLeadership(1, makeAddr("chainlight")); + + // Test that leadership cannot be transferred to non team members + vm.expectRevert( + abi.encodeWithSelector(TeamRegistry.NotInTeam.selector, 1, makeAddr("plotchy")) + ); + vm.prank(makeAddr("sudolabel")); + tr.transferLeadership(1, makeAddr("plotchy")); + } + + function _createTeam() internal { + // create an initial team + address[] memory tms = new address[](5); + tms[0] = makeAddr("sudolabel"); + tms[1] = makeAddr("igorline"); + tms[2] = makeAddr("jinu"); + tms[3] = makeAddr("minimooger"); + tms[4] = makeAddr("kalzak"); + + vm.prank(makeAddr("chainlight")); + tr.createTeam(tms); + } +}