diff --git a/src/token-voting/LlamaTokenCaster.sol b/src/token-voting/LlamaTokenCaster.sol index 1864310..43063c7 100644 --- a/src/token-voting/LlamaTokenCaster.sol +++ b/src/token-voting/LlamaTokenCaster.sol @@ -33,11 +33,9 @@ contract LlamaTokenCaster is Initializable { uint96 votesFor; // Number of votes casted for this action. uint96 votesAbstain; // Number of abstentions casted for this action. uint96 votesAgainst; // Number of votes casted against this action. - bool approvalSubmitted; // True if the approval was submitted to `LlamaCore, false otherwise. uint96 vetoesFor; // Number of vetoes casted for this action. uint96 vetoesAbstain; // Number of abstentions casted for this action. uint96 vetoesAgainst; // Number of disapprovals casted against this action. - bool disapprovalSubmitted; // True if the disapproval has been submitted to `LlamaCore`, false otherwise. mapping(address tokenholder => bool) castVote; // True if tokenholder casted a vote, false otherwise. mapping(address tokenholder => bool) castVeto; // True if tokenholder casted a veto, false otherwise. } @@ -46,37 +44,31 @@ contract LlamaTokenCaster is Initializable { // ======== Errors ======== // ======================== - /// @dev Thrown when a user tries to cast a vote but the action is not active. - error ActionNotActive(); - - /// @dev Thrown when a user tries to cast a veto but the action is not queued. - error ActionNotQueued(); - - /// @dev Thrown when a user tries to cast a veto but has already casted. - error AlreadyCastedVeto(); - - /// @dev Thrown when a user tries to cast a vote but has already casted. - error AlreadyCastedVote(); - - /// @dev Thrown when a user tries to cast approval but the casts have already been submitted to `LlamaCore`. - error AlreadySubmittedApproval(); - - /// @dev Thrown when a user tries to cast disapproval but the casts have already been submitted to `LlamaCore. - error AlreadySubmittedDisapproval(); - - /// @dev Thrown when a user tries to cast (dis)approval but the action cannot be submitted yet. - error CannotSubmitYet(); + /// @dev Thrown when a user tries to submit (dis)approval but the casting period has not ended. + error CastingPeriodNotOver(); /// @dev Thrown when a user tries to cast a vote or veto but the casting period has ended. error CastingPeriodOver(); + /// @dev Thrown when a user tries to cast a vote or veto but the delay period has not ended. + error DelayPeriodNotOver(); + + /// @dev Token holders can only cast once. + error DuplicateCast(); + /// @dev Thrown when a user tries to cast a vote or veto but the against surpasses for. error ForDoesNotSurpassAgainst(uint256 castsFor, uint256 castsAgainst); /// @dev Thrown when a user tries to submit an approval but there are not enough votes. error InsufficientVotes(uint256 votes, uint256 threshold); - /// @dev Thrown when a user tries to query checkpoints at non-existant indices. + /// @dev The action is not in the expected state. + /// @param current The current state of the action. + error InvalidActionState(ActionState current); + + /// @dev The indices would result in `Panic: Index Out of Bounds`. + /// @dev Thrown when the `end` index is greater than array length or when the `start` index is greater than the `end` + /// index. error InvalidIndices(); /// @dev Thrown when an invalid `llamaCore` address is passed to the constructor. @@ -85,6 +77,9 @@ contract LlamaTokenCaster is Initializable { /// @dev Thrown when an invalid `castingPeriodPct` and `submissionPeriodPct` are set. error InvalidPeriodPcts(uint16 delayPeriodPct, uint16 castingPeriodPct, uint16 submissionPeriodPct); + /// @dev This token caster contract does not have the defined role at action creation time. + error InvalidPolicyholder(); + /// @dev The recovered signer does not match the expected tokenholder. error InvalidSignature(); @@ -106,9 +101,6 @@ contract LlamaTokenCaster is Initializable { /// @dev Thrown when a user tries to submit (dis)approval but the submission period has ended. error SubmissionPeriodOver(); - /// @dev Thrown when a user tries to cast a vote or veto but the voting delay has not passed. - error VotingDelayNotOver(); - // ======================== // ======== Events ======== // ======================== @@ -311,75 +303,79 @@ contract LlamaTokenCaster is Initializable { /// @notice Submits a cast approval to the `LlamaCore` contract. /// @param actionInfo Data required to create an action. - /// @dev this function can be called by anyone + /// @dev This function can be called by anyone. function submitApproval(ActionInfo calldata actionInfo) external { Action memory action = llamaCore.getAction(actionInfo.id); + uint256 checkpointTime = action.creationTime - 1; - actionInfo.strategy.checkIfApprovalEnabled(actionInfo, address(this), role); // Reverts if not allowed. - if (casts[actionInfo.id].approvalSubmitted) revert AlreadySubmittedApproval(); // Reverts if clock or CLOCK_MODE() has changed tokenAdapter.checkIfInconsistentClock(); - // Checks to ensure it's the submission period. - (uint16 delayPeriodPct, uint16 castingPeriodPct,) = - periodPctsCheckpoint.getAtProbablyRecentTimestamp(action.creationTime - 1); - uint256 approvalPeriod = actionInfo.strategy.approvalPeriod(); - uint256 delayPeriodEndTime = action.creationTime + ((approvalPeriod * delayPeriodPct) / ONE_HUNDRED_IN_BPS); - uint256 castingPeriodEndTime = delayPeriodEndTime + ((approvalPeriod * castingPeriodPct) / ONE_HUNDRED_IN_BPS); - if (block.timestamp <= castingPeriodEndTime) revert CannotSubmitYet(); - // Doing (action.creationTime + approvalPeriod) vs (castingPeriodEndTime + ((approvalPeriod * submissionPeriodPct) / - // ONE_HUNDRED_IN_BPS)) to prevent any off-by-one errors due to precision loss. - // Llama approval period is inclusive of approval end time. - if (block.timestamp > action.creationTime + approvalPeriod) revert SubmissionPeriodOver(); + uint256 delayPeriodEndTime; + // Scoping to prevent stack too deep errors. + { + // Checks to ensure it's the submission period. + (uint16 delayPeriodPct, uint16 castingPeriodPct,) = + periodPctsCheckpoint.getAtProbablyRecentTimestamp(checkpointTime); + uint256 approvalPeriod = actionInfo.strategy.approvalPeriod(); + delayPeriodEndTime = action.creationTime + ((approvalPeriod * delayPeriodPct) / ONE_HUNDRED_IN_BPS); + uint256 castingPeriodEndTime = delayPeriodEndTime + ((approvalPeriod * castingPeriodPct) / ONE_HUNDRED_IN_BPS); + if (block.timestamp <= castingPeriodEndTime) revert CastingPeriodNotOver(); + // Doing (action.creationTime + approvalPeriod) vs (castingPeriodEndTime + ((approvalPeriod * submissionPeriodPct) + // / ONE_HUNDRED_IN_BPS)) to prevent any off-by-one errors due to precision loss. + // Llama approval period is inclusive of approval end time. + if (block.timestamp > action.creationTime + approvalPeriod) revert SubmissionPeriodOver(); + } uint256 totalSupply = tokenAdapter.getPastTotalSupply(tokenAdapter.timestampToTimepoint(delayPeriodEndTime)); uint96 votesFor = casts[actionInfo.id].votesFor; uint96 votesAgainst = casts[actionInfo.id].votesAgainst; uint96 votesAbstain = casts[actionInfo.id].votesAbstain; - (uint16 voteQuorumPct,) = quorumCheckpoints.getAtProbablyRecentTimestamp(action.creationTime - 1); + (uint16 voteQuorumPct,) = quorumCheckpoints.getAtProbablyRecentTimestamp(checkpointTime); uint256 threshold = FixedPointMathLib.mulDivUp(totalSupply, voteQuorumPct, ONE_HUNDRED_IN_BPS); if (votesFor < threshold) revert InsufficientVotes(votesFor, threshold); if (votesFor <= votesAgainst) revert ForDoesNotSurpassAgainst(votesFor, votesAgainst); - casts[actionInfo.id].approvalSubmitted = true; llamaCore.castApproval(role, actionInfo, ""); emit ApprovalSubmitted(actionInfo.id, msg.sender, votesFor, votesAgainst, votesAbstain); } /// @notice Submits a cast disapproval to the `LlamaCore` contract. /// @param actionInfo Data required to create an action. - /// @dev this function can be called by anyone + /// @dev This function can be called by anyone. function submitDisapproval(ActionInfo calldata actionInfo) external { Action memory action = llamaCore.getAction(actionInfo.id); + uint256 checkpointTime = action.creationTime - 1; - actionInfo.strategy.checkIfDisapprovalEnabled(actionInfo, address(this), role); // Reverts if not allowed. - if (casts[actionInfo.id].disapprovalSubmitted) revert AlreadySubmittedDisapproval(); // Reverts if clock or CLOCK_MODE() has changed tokenAdapter.checkIfInconsistentClock(); - // Checks to ensure it's the submission period. - (uint16 delayPeriodPct, uint16 castingPeriodPct,) = - periodPctsCheckpoint.getAtProbablyRecentTimestamp(action.creationTime - 1); - uint256 queuingPeriod = actionInfo.strategy.queuingPeriod(); - uint256 delayPeriodEndTime = - (action.minExecutionTime - queuingPeriod) + ((queuingPeriod * delayPeriodPct) / ONE_HUNDRED_IN_BPS); - uint256 castingPeriodEndTime = delayPeriodEndTime + ((queuingPeriod * castingPeriodPct) / ONE_HUNDRED_IN_BPS); - // Doing (castingPeriodEndTime) vs (action.minExecutionTime - ((queuingPeriod * submissionPeriodPct) / - // ONE_HUNDRED_IN_BPS)) to prevent any off-by-one errors due to precision loss. - if (block.timestamp <= castingPeriodEndTime) revert CannotSubmitYet(); - // Llama disapproval period is exclusive of min execution time. - if (block.timestamp >= action.minExecutionTime) revert SubmissionPeriodOver(); + uint256 delayPeriodEndTime; + // Scoping to prevent stack too deep errors. + { + // Checks to ensure it's the submission period. + (uint16 delayPeriodPct, uint16 castingPeriodPct,) = + periodPctsCheckpoint.getAtProbablyRecentTimestamp(checkpointTime); + uint256 queuingPeriod = actionInfo.strategy.queuingPeriod(); + delayPeriodEndTime = + (action.minExecutionTime - queuingPeriod) + ((queuingPeriod * delayPeriodPct) / ONE_HUNDRED_IN_BPS); + uint256 castingPeriodEndTime = delayPeriodEndTime + ((queuingPeriod * castingPeriodPct) / ONE_HUNDRED_IN_BPS); + // Doing (castingPeriodEndTime) vs (action.minExecutionTime - ((queuingPeriod * submissionPeriodPct) / + // ONE_HUNDRED_IN_BPS)) to prevent any off-by-one errors due to precision loss. + if (block.timestamp <= castingPeriodEndTime) revert CastingPeriodNotOver(); + // Llama disapproval period is exclusive of min execution time. + if (block.timestamp >= action.minExecutionTime) revert SubmissionPeriodOver(); + } uint256 totalSupply = tokenAdapter.getPastTotalSupply(tokenAdapter.timestampToTimepoint(delayPeriodEndTime)); uint96 vetoesFor = casts[actionInfo.id].vetoesFor; uint96 vetoesAgainst = casts[actionInfo.id].vetoesAgainst; uint96 vetoesAbstain = casts[actionInfo.id].vetoesAbstain; - (, uint16 vetoQuorumPct) = quorumCheckpoints.getAtProbablyRecentTimestamp(action.creationTime - 1); + (, uint16 vetoQuorumPct) = quorumCheckpoints.getAtProbablyRecentTimestamp(checkpointTime); uint256 threshold = FixedPointMathLib.mulDivUp(totalSupply, vetoQuorumPct, ONE_HUNDRED_IN_BPS); if (vetoesFor < threshold) revert InsufficientVotes(vetoesFor, threshold); if (vetoesFor <= vetoesAgainst) revert ForDoesNotSurpassAgainst(vetoesFor, vetoesAgainst); - casts[actionInfo.id].disapprovalSubmitted = true; llamaCore.castDisapproval(role, actionInfo, ""); emit DisapprovalSubmitted(actionInfo.id, msg.sender, vetoesFor, vetoesAgainst, vetoesAbstain); } @@ -414,6 +410,16 @@ contract LlamaTokenCaster is Initializable { } // -------- Getters -------- + + /// @notice Returns if a token holder has cast (vote or veto) yet for a given action. + /// @param actionId ID of the action. + /// @param tokenholder The tokenholder to check. + /// @param isVote `true` if checking for a vote, `false` if checking for a veto. + function hasTokenHolderCast(uint256 actionId, address tokenholder, bool isVote) external view returns (bool) { + if (isVote) return casts[actionId].castVote[tokenholder]; + else return casts[actionId].castVeto[tokenholder]; + } + /// @notice Returns the current voting quorum and vetoing quorum. function getQuorum() external view returns (uint16 voteQuorumPct, uint16 vetoQuorumPct) { return quorumCheckpoints.latest(); @@ -494,27 +500,35 @@ contract LlamaTokenCaster is Initializable { returns (uint96) { Action memory action = llamaCore.getAction(actionInfo.id); + uint256 checkpointTime = action.creationTime - 1; actionInfo.strategy.checkIfApprovalEnabled(actionInfo, address(this), role); // Reverts if not allowed. - if (llamaCore.getActionState(actionInfo) != uint8(ActionState.Active)) revert ActionNotActive(); - if (casts[actionInfo.id].castVote[caster]) revert AlreadyCastedVote(); - _preCastAssertions(support); - - // Checks to ensure it's the casting period. - (uint16 delayPeriodPct, uint16 castingPeriodPct,) = - periodPctsCheckpoint.getAtProbablyRecentTimestamp(action.creationTime - 1); - uint256 approvalPeriod = actionInfo.strategy.approvalPeriod(); - uint256 delayPeriodEndTime = action.creationTime + ((approvalPeriod * delayPeriodPct) / ONE_HUNDRED_IN_BPS); - uint256 castingPeriodEndTime = delayPeriodEndTime + ((approvalPeriod * castingPeriodPct) / ONE_HUNDRED_IN_BPS); - if (block.timestamp <= delayPeriodEndTime) revert VotingDelayNotOver(); - if (block.timestamp > castingPeriodEndTime) revert CastingPeriodOver(); + if (casts[actionInfo.id].castVote[caster]) revert DuplicateCast(); + _preCastAssertions(actionInfo, support, ActionState.Active, checkpointTime); + + uint256 delayPeriodEndTime; + // Scoping to prevent stack too deep errors. + { + // Checks to ensure it's the casting period. + (uint16 delayPeriodPct, uint16 castingPeriodPct,) = + periodPctsCheckpoint.getAtProbablyRecentTimestamp(checkpointTime); + uint256 approvalPeriod = actionInfo.strategy.approvalPeriod(); + delayPeriodEndTime = action.creationTime + ((approvalPeriod * delayPeriodPct) / ONE_HUNDRED_IN_BPS); + uint256 castingPeriodEndTime = delayPeriodEndTime + ((approvalPeriod * castingPeriodPct) / ONE_HUNDRED_IN_BPS); + if (block.timestamp <= delayPeriodEndTime) revert DelayPeriodNotOver(); + if (block.timestamp > castingPeriodEndTime) revert CastingPeriodOver(); + } uint96 weight = LlamaUtils.toUint96(tokenAdapter.getPastVotes(caster, tokenAdapter.timestampToTimepoint(delayPeriodEndTime))); - if (support == uint8(VoteType.Against)) casts[actionInfo.id].votesAgainst += weight; - else if (support == uint8(VoteType.For)) casts[actionInfo.id].votesFor += weight; - else if (support == uint8(VoteType.Abstain)) casts[actionInfo.id].votesAbstain += weight; + if (support == uint8(VoteType.Against)) { + casts[actionInfo.id].votesAgainst = _newCastCount(casts[actionInfo.id].votesAgainst, weight); + } else if (support == uint8(VoteType.For)) { + casts[actionInfo.id].votesFor = _newCastCount(casts[actionInfo.id].votesFor, weight); + } else if (support == uint8(VoteType.Abstain)) { + casts[actionInfo.id].votesAbstain = _newCastCount(casts[actionInfo.id].votesAbstain, weight); + } casts[actionInfo.id].castVote[caster] = true; emit VoteCast(actionInfo.id, caster, support, weight, reason); @@ -527,28 +541,36 @@ contract LlamaTokenCaster is Initializable { returns (uint96) { Action memory action = llamaCore.getAction(actionInfo.id); + uint256 checkpointTime = action.creationTime - 1; actionInfo.strategy.checkIfDisapprovalEnabled(actionInfo, address(this), role); // Reverts if not allowed. - if (llamaCore.getActionState(actionInfo) != uint8(ActionState.Queued)) revert ActionNotQueued(); - if (casts[actionInfo.id].castVeto[caster]) revert AlreadyCastedVeto(); - _preCastAssertions(support); - - // Checks to ensure it's the casting period. - (uint16 delayPeriodPct, uint16 castingPeriodPct,) = - periodPctsCheckpoint.getAtProbablyRecentTimestamp(action.creationTime - 1); - uint256 queuingPeriod = actionInfo.strategy.queuingPeriod(); - uint256 delayPeriodEndTime = - (action.minExecutionTime - queuingPeriod) + ((queuingPeriod * delayPeriodPct) / ONE_HUNDRED_IN_BPS); - uint256 castingPeriodEndTime = delayPeriodEndTime + ((queuingPeriod * castingPeriodPct) / ONE_HUNDRED_IN_BPS); - if (block.timestamp <= delayPeriodEndTime) revert VotingDelayNotOver(); - if (block.timestamp > castingPeriodEndTime) revert CastingPeriodOver(); + if (casts[actionInfo.id].castVeto[caster]) revert DuplicateCast(); + _preCastAssertions(actionInfo, support, ActionState.Queued, checkpointTime); + + uint256 delayPeriodEndTime; + // Scoping to prevent stack too deep errors. + { + // Checks to ensure it's the casting period. + (uint16 delayPeriodPct, uint16 castingPeriodPct,) = + periodPctsCheckpoint.getAtProbablyRecentTimestamp(checkpointTime); + uint256 queuingPeriod = actionInfo.strategy.queuingPeriod(); + delayPeriodEndTime = + (action.minExecutionTime - queuingPeriod) + ((queuingPeriod * delayPeriodPct) / ONE_HUNDRED_IN_BPS); + uint256 castingPeriodEndTime = delayPeriodEndTime + ((queuingPeriod * castingPeriodPct) / ONE_HUNDRED_IN_BPS); + if (block.timestamp <= delayPeriodEndTime) revert DelayPeriodNotOver(); + if (block.timestamp > castingPeriodEndTime) revert CastingPeriodOver(); + } uint96 weight = LlamaUtils.toUint96(tokenAdapter.getPastVotes(caster, tokenAdapter.timestampToTimepoint(delayPeriodEndTime))); - if (support == uint8(VoteType.Against)) casts[actionInfo.id].vetoesAgainst += weight; - else if (support == uint8(VoteType.For)) casts[actionInfo.id].vetoesFor += weight; - else if (support == uint8(VoteType.Abstain)) casts[actionInfo.id].vetoesAbstain += weight; + if (support == uint8(VoteType.Against)) { + casts[actionInfo.id].vetoesAgainst = _newCastCount(casts[actionInfo.id].vetoesAgainst, weight); + } else if (support == uint8(VoteType.For)) { + casts[actionInfo.id].vetoesFor = _newCastCount(casts[actionInfo.id].vetoesFor, weight); + } else if (support == uint8(VoteType.Abstain)) { + casts[actionInfo.id].vetoesAbstain = _newCastCount(casts[actionInfo.id].vetoesAbstain, weight); + } casts[actionInfo.id].castVeto[caster] = true; emit VetoCast(actionInfo.id, caster, support, weight, reason); @@ -556,13 +578,30 @@ contract LlamaTokenCaster is Initializable { } /// @dev The only `support` values allowed to be passed into this method are Against (0), For (1) or Abstain (2). - function _preCastAssertions(uint8 support) internal view { + function _preCastAssertions( + ActionInfo calldata actionInfo, + uint8 support, + ActionState expectedState, + uint256 checkpointTime + ) internal view { if (support > uint8(VoteType.Abstain)) revert InvalidSupport(support); + ActionState currentState = ActionState(llamaCore.getActionState(actionInfo)); + if (currentState != expectedState) revert InvalidActionState(currentState); + + bool hasRole = llamaCore.policy().hasRole(address(this), role, checkpointTime); + if (!hasRole) revert InvalidPolicyholder(); + // Reverts if clock or CLOCK_MODE() has changed tokenAdapter.checkIfInconsistentClock(); } + /// @dev Returns the new total count of votes or vetoes in Against (0), For (1) or Abstain (2). + function _newCastCount(uint96 currentCount, uint96 weight) internal pure returns (uint96) { + if (uint256(currentCount) + weight >= type(uint96).max) return type(uint96).max; + return currentCount + weight; + } + /// @dev Sets the voting quorum and vetoing quorum. function _setQuorumPct(uint16 _voteQuorumPct, uint16 _vetoQuorumPct) internal { if (_voteQuorumPct > ONE_HUNDRED_IN_BPS || _voteQuorumPct <= 0) revert InvalidVoteQuorumPct(_voteQuorumPct); diff --git a/test/token-voting/LlamaERC20TokenCaster.t.sol b/test/token-voting/LlamaERC20TokenCaster.t.sol index da6c304..e2973a4 100644 --- a/test/token-voting/LlamaERC20TokenCaster.t.sol +++ b/test/token-voting/LlamaERC20TokenCaster.t.sol @@ -112,7 +112,7 @@ contract CastVote is LlamaERC20TokenCasterTest { function test_RevertsIf_NotPastVotingDelay() public { vm.warp(block.timestamp - 1); - vm.expectRevert(LlamaTokenCaster.VotingDelayNotOver.selector); + vm.expectRevert(LlamaTokenCaster.DelayPeriodNotOver.selector); llamaERC20TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); } @@ -137,15 +137,36 @@ contract CastVote is LlamaERC20TokenCasterTest { function test_RevertsIf_ActionNotActive() public { vm.warp(actionCreationTime + APPROVAL_PERIOD + 1); - vm.expectRevert(LlamaTokenCaster.ActionNotActive.selector); + vm.expectRevert(abi.encodeWithSelector(LlamaTokenCaster.InvalidActionState.selector, ActionState.Failed)); llamaERC20TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); } + function test_RevertsIf_RoleHasBeenRevokedBeforeActionCreation() public { + // Revoking Caster role from Token Holder Caster and assigning it to a random address so that Role has supply. + vm.startPrank(address(EXECUTOR)); + POLICY.setRoleHolder(tokenVotingCasterRole, address(llamaERC20TokenCaster), 0, 0); + POLICY.setRoleHolder(tokenVotingCasterRole, address(0xdeadbeef), DEFAULT_ROLE_QTY, DEFAULT_ROLE_EXPIRATION); + vm.stopPrank(); + + // Mine block so that the revoke will be effective + mineBlock(); + + ActionInfo memory _actionInfo = _createActionWithTokenVotingStrategy(tokenVotingStrategy); + Action memory action = CORE.getAction(_actionInfo.id); + + // Skip voting delay + vm.warp(action.creationTime + ((APPROVAL_PERIOD * ONE_QUARTER_IN_BPS) / ONE_HUNDRED_IN_BPS) + 1); + + vm.startPrank(tokenHolder1); + vm.expectRevert(LlamaTokenCaster.InvalidPolicyholder.selector); + llamaERC20TokenCaster.castVote(_actionInfo, uint8(VoteType.For), ""); + } + function test_RevertsIf_AlreadyCastedVote() public { vm.startPrank(tokenHolder1); llamaERC20TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); - vm.expectRevert(LlamaTokenCaster.AlreadyCastedVote.selector); + vm.expectRevert(LlamaTokenCaster.DuplicateCast.selector); llamaERC20TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); } @@ -168,6 +189,33 @@ contract CastVote is LlamaERC20TokenCasterTest { llamaERC20TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); } + function test_CanCastWhenCountisMax() public { + // Warping to delayPeriodEndTime. + vm.warp(actionCreationTime + ((APPROVAL_PERIOD * ONE_QUARTER_IN_BPS) / ONE_HUNDRED_IN_BPS)); + // Minting type(uint96).max tokens and delegating + erc20VotesToken.mint(address(0xdeadbeef), type(uint96).max); + vm.prank(address(0xdeadbeef)); + erc20VotesToken.delegate(address(0xdeadbeef)); + + // Warping to delayPeriodEndTime + 1 so that voting can start. + mineBlock(); + // Casting vote with weight type(uint96).max. Count should now be type(uint96).max. + vm.prank(address(0xdeadbeef)); + llamaERC20TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); + + (uint96 votesFor,,,,,) = llamaERC20TokenCaster.casts(actionInfo.id); + assertEq(votesFor, type(uint96).max); + + // Can still cast even if count is max. + vm.expectEmit(); + emit VoteCast(actionInfo.id, tokenHolder1, uint8(VoteType.For), ERC20_CREATION_THRESHOLD / 2, ""); + vm.prank(tokenHolder1); + llamaERC20TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); + + (votesFor,,,,,) = llamaERC20TokenCaster.casts(actionInfo.id); + assertEq(votesFor, type(uint96).max); + } + function test_CastsVoteCorrectly(uint8 support) public { support = uint8(bound(support, uint8(VoteType.Against), uint8(VoteType.Against))); vm.expectEmit(); @@ -313,7 +361,7 @@ contract CastVeto is LlamaERC20TokenCasterTest { function test_RevertsIf_NotPastVotingDelay() public { vm.warp(block.timestamp - 1); - vm.expectRevert(LlamaTokenCaster.VotingDelayNotOver.selector); + vm.expectRevert(LlamaTokenCaster.DelayPeriodNotOver.selector); llamaERC20TokenCaster.castVeto(actionInfo, uint8(VoteType.For), ""); } @@ -343,7 +391,7 @@ contract CastVeto is LlamaERC20TokenCasterTest { ActionInfo memory _actionInfo = ActionInfo(actionId, coreTeam1, CORE_TEAM_ROLE, tokenVotingStrategy, address(mockProtocol), 0, data); // Currently at actionCreationTime which is Active state. - vm.expectRevert(LlamaTokenCaster.ActionNotQueued.selector); + vm.expectRevert(abi.encodeWithSelector(LlamaTokenCaster.InvalidActionState.selector, ActionState.Active)); llamaERC20TokenCaster.castVeto(_actionInfo, uint8(VoteType.For), ""); } @@ -351,7 +399,7 @@ contract CastVeto is LlamaERC20TokenCasterTest { vm.startPrank(tokenHolder1); llamaERC20TokenCaster.castVeto(actionInfo, uint8(VoteType.For), ""); - vm.expectRevert(LlamaTokenCaster.AlreadyCastedVeto.selector); + vm.expectRevert(LlamaTokenCaster.DuplicateCast.selector); llamaERC20TokenCaster.castVeto(actionInfo, uint8(VoteType.For), ""); } @@ -376,6 +424,34 @@ contract CastVeto is LlamaERC20TokenCasterTest { llamaERC20TokenCaster.castVeto(actionInfo, uint8(VoteType.For), ""); } + function test_CanCastWhenCountisMax() public { + // Warping to delayPeriodEndTime. + Action memory action = CORE.getAction(actionInfo.id); + vm.warp((action.minExecutionTime - QUEUING_PERIOD) + ((QUEUING_PERIOD * ONE_QUARTER_IN_BPS) / ONE_HUNDRED_IN_BPS)); + // Minting type(uint96).max tokens and delegating + erc20VotesToken.mint(address(0xdeadbeef), type(uint96).max); + vm.prank(address(0xdeadbeef)); + erc20VotesToken.delegate(address(0xdeadbeef)); + + // Warping to delayPeriodEndTime + 1 so that voting can start. + mineBlock(); + // Casting vote with weight type(uint96).max. Count should now be type(uint96).max. + vm.prank(address(0xdeadbeef)); + llamaERC20TokenCaster.castVeto(actionInfo, uint8(VoteType.For), ""); + + (,,, uint96 vetoesFor,,) = llamaERC20TokenCaster.casts(actionInfo.id); + assertEq(vetoesFor, type(uint96).max); + + // Can still cast even if count is max. + vm.expectEmit(); + emit VetoCast(actionInfo.id, tokenHolder1, uint8(VoteType.For), ERC20_CREATION_THRESHOLD / 2, ""); + vm.prank(tokenHolder1); + llamaERC20TokenCaster.castVeto(actionInfo, uint8(VoteType.For), ""); + + (,,, vetoesFor,,) = llamaERC20TokenCaster.casts(actionInfo.id); + assertEq(vetoesFor, type(uint96).max); + } + function test_CastsVetoCorrectly(uint8 support) public { support = uint8(bound(support, uint8(VoteType.Against), uint8(VoteType.Abstain))); vm.expectEmit(); @@ -571,23 +647,13 @@ contract SubmitApprovals is LlamaERC20TokenCasterTest { llamaERC20TokenCaster.submitApproval(notActionInfo); } - function test_RevertsIf_ApprovalNotEnabled() public { - LlamaTokenCaster casterWithWrongRole = LlamaTokenCaster( - Clones.cloneDeterministic( - address(llamaTokenCasterLogic), keccak256(abi.encodePacked(address(erc20VotesToken), msg.sender)) - ) - ); - ILlamaTokenAdapter tokenAdapter = createTimestampTokenAdapter(address(erc20VotesToken), 0); - casterWithWrongRole.initialize(CORE, tokenAdapter, madeUpRole, defaultCasterConfig); - vm.expectRevert(abi.encodeWithSelector(ILlamaRelativeStrategyBase.InvalidRole.selector, tokenVotingCasterRole)); - casterWithWrongRole.submitApproval(actionInfo); - } - function test_RevertsIf_AlreadySubmittedApproval() public { vm.startPrank(tokenHolder1); llamaERC20TokenCaster.submitApproval(actionInfo); - vm.expectRevert(LlamaTokenCaster.AlreadySubmittedApproval.selector); + // This should revert since the underlying Action has transitioned to Queued state. Otherwise it would have reverted + // due to `LlamaCore.DuplicateCast() error`. + vm.expectRevert(abi.encodeWithSelector(ILlamaCore.InvalidActionState.selector, ActionState.Queued)); llamaERC20TokenCaster.submitApproval(actionInfo); } @@ -609,7 +675,7 @@ contract SubmitApprovals is LlamaERC20TokenCasterTest { function test_RevertsIf_CastingPeriodNotOver() public { vm.warp(block.timestamp - 1); - vm.expectRevert(LlamaTokenCaster.CannotSubmitYet.selector); + vm.expectRevert(LlamaTokenCaster.CastingPeriodNotOver.selector); llamaERC20TokenCaster.submitApproval(actionInfo); } @@ -670,23 +736,13 @@ contract SubmitDisapprovals is LlamaERC20TokenCasterTest { llamaERC20TokenCaster.submitDisapproval(notActionInfo); } - function test_RevertsIf_DisapprovalNotEnabled() public { - LlamaTokenCaster casterWithWrongRole = LlamaTokenCaster( - Clones.cloneDeterministic( - address(llamaTokenCasterLogic), keccak256(abi.encodePacked(address(erc20VotesToken), msg.sender)) - ) - ); - ILlamaTokenAdapter tokenAdapter = createTimestampTokenAdapter(address(erc20VotesToken), 0); - casterWithWrongRole.initialize(CORE, tokenAdapter, madeUpRole, defaultCasterConfig); - vm.expectRevert(abi.encodeWithSelector(ILlamaRelativeStrategyBase.InvalidRole.selector, tokenVotingCasterRole)); - casterWithWrongRole.submitDisapproval(actionInfo); - } - function test_RevertsIf_AlreadySubmittedDisapproval() public { vm.startPrank(tokenHolder1); llamaERC20TokenCaster.submitDisapproval(actionInfo); - vm.expectRevert(LlamaTokenCaster.AlreadySubmittedDisapproval.selector); + // This should revert since the underlying Action has transitioned to Failed state. Otherwise it would have reverted + // due to `LlamaCore.DuplicateCast() error`. + vm.expectRevert(abi.encodeWithSelector(ILlamaCore.InvalidActionState.selector, ActionState.Failed)); llamaERC20TokenCaster.submitDisapproval(actionInfo); } @@ -728,7 +784,7 @@ contract SubmitDisapprovals is LlamaERC20TokenCasterTest { function test_RevertsIf_CastingPeriodNotOver() public { vm.warp(block.timestamp - 1); - vm.expectRevert(LlamaTokenCaster.CannotSubmitYet.selector); + vm.expectRevert(LlamaTokenCaster.CastingPeriodNotOver.selector); llamaERC20TokenCaster.submitDisapproval(actionInfo); } @@ -855,3 +911,55 @@ contract SetPeriodPct is LlamaERC20TokenCasterTest { ); } } + +contract CastData is LlamaERC20TokenCasterTest { + function setUp() public virtual override { + LlamaERC20TokenCasterTest.setUp(); + + _skipVotingDelay(actionInfo); + castVotesFor(); + + uint256 delayPeriodEndTime = actionCreationTime + ((APPROVAL_PERIOD * ONE_QUARTER_IN_BPS) / ONE_HUNDRED_IN_BPS); + uint256 castingPeriodEndTime = delayPeriodEndTime + ((APPROVAL_PERIOD * TWO_QUARTERS_IN_BPS) / ONE_HUNDRED_IN_BPS); + vm.warp(castingPeriodEndTime + 1); + + llamaERC20TokenCaster.submitApproval(actionInfo); + + _skipVetoDelay(actionInfo); + castVetosFor(); + + Action memory action = CORE.getAction(actionInfo.id); + delayPeriodEndTime = + (action.minExecutionTime - QUEUING_PERIOD) + ((QUEUING_PERIOD * ONE_QUARTER_IN_BPS) / ONE_HUNDRED_IN_BPS); + castingPeriodEndTime = delayPeriodEndTime + ((QUEUING_PERIOD * TWO_QUARTERS_IN_BPS) / ONE_HUNDRED_IN_BPS); + vm.warp(castingPeriodEndTime + 1); + + llamaERC20TokenCaster.submitDisapproval(actionInfo); + } + + function test_CanGetCastData() public { + ( + uint96 votesFor, + uint96 votesAbstain, + uint96 votesAgainst, + uint96 vetoesFor, + uint96 vetoesAbstain, + uint96 vetoesAgainst + ) = llamaERC20TokenCaster.casts(actionInfo.id); + assertEq(votesFor, (ERC20_CREATION_THRESHOLD / 2) * 3); + assertEq(votesAbstain, 0); + assertEq(votesAgainst, 0); + assertEq(vetoesFor, (ERC20_CREATION_THRESHOLD / 2) * 3); + assertEq(vetoesAbstain, 0); + assertEq(vetoesAgainst, 0); + + assertTrue(llamaERC20TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder1, true)); + assertTrue(llamaERC20TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder2, true)); + assertTrue(llamaERC20TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder3, true)); + assertFalse(llamaERC20TokenCaster.hasTokenHolderCast(actionInfo.id, notTokenHolder, true)); + assertTrue(llamaERC20TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder1, false)); + assertTrue(llamaERC20TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder2, false)); + assertTrue(llamaERC20TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder3, false)); + assertFalse(llamaERC20TokenCaster.hasTokenHolderCast(actionInfo.id, notTokenHolder, false)); + } +} diff --git a/test/token-voting/LlamaERC721TokenCaster.t.sol b/test/token-voting/LlamaERC721TokenCaster.t.sol index 058940f..38fe8ae 100644 --- a/test/token-voting/LlamaERC721TokenCaster.t.sol +++ b/test/token-voting/LlamaERC721TokenCaster.t.sol @@ -114,7 +114,7 @@ contract CastVote is LlamaERC721TokenCasterTest { function test_RevertsIf_NotPastVotingDelay() public { vm.warp(block.timestamp - 1); - vm.expectRevert(LlamaTokenCaster.VotingDelayNotOver.selector); + vm.expectRevert(LlamaTokenCaster.DelayPeriodNotOver.selector); llamaERC721TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); } @@ -141,15 +141,36 @@ contract CastVote is LlamaERC721TokenCasterTest { function test_RevertsIf_ActionNotActive() public { vm.warp(actionCreationTime + APPROVAL_PERIOD + 1); - vm.expectRevert(LlamaTokenCaster.ActionNotActive.selector); + vm.expectRevert(abi.encodeWithSelector(LlamaTokenCaster.InvalidActionState.selector, ActionState.Failed)); llamaERC721TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); } + function test_RevertsIf_RoleHasBeenRevokedBeforeActionCreation() public { + // Revoking Caster role from Token Holder Caster and assigning it to a random address so that Role has supply. + vm.startPrank(address(EXECUTOR)); + POLICY.setRoleHolder(tokenVotingCasterRole, address(llamaERC721TokenCaster), 0, 0); + POLICY.setRoleHolder(tokenVotingCasterRole, address(0xdeadbeef), DEFAULT_ROLE_QTY, DEFAULT_ROLE_EXPIRATION); + vm.stopPrank(); + + // Mine block so that the revoke will be effective + mineBlock(); + + ActionInfo memory _actionInfo = _createActionWithTokenVotingStrategy(tokenVotingStrategy); + Action memory action = CORE.getAction(_actionInfo.id); + + // Skip voting delay + vm.warp(action.creationTime + ((APPROVAL_PERIOD * ONE_QUARTER_IN_BPS) / ONE_HUNDRED_IN_BPS) + 1); + + vm.startPrank(tokenHolder1); + vm.expectRevert(LlamaTokenCaster.InvalidPolicyholder.selector); + llamaERC721TokenCaster.castVote(_actionInfo, uint8(VoteType.For), ""); + } + function test_RevertsIf_AlreadyCastedVote() public { vm.startPrank(tokenHolder1); llamaERC721TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); - vm.expectRevert(LlamaTokenCaster.AlreadyCastedVote.selector); + vm.expectRevert(LlamaTokenCaster.DuplicateCast.selector); llamaERC721TokenCaster.castVote(actionInfo, uint8(VoteType.For), ""); } @@ -317,7 +338,7 @@ contract CastVeto is LlamaERC721TokenCasterTest { function test_RevertsIf_NotPastVotingDelay() public { vm.warp(block.timestamp - 1); - vm.expectRevert(LlamaTokenCaster.VotingDelayNotOver.selector); + vm.expectRevert(LlamaTokenCaster.DelayPeriodNotOver.selector); llamaERC721TokenCaster.castVeto(actionInfo, uint8(VoteType.For), ""); } @@ -348,7 +369,7 @@ contract CastVeto is LlamaERC721TokenCasterTest { ActionInfo memory _actionInfo = ActionInfo(actionId, coreTeam1, CORE_TEAM_ROLE, tokenVotingStrategy, address(mockProtocol), 0, data); // Currently at actionCreationTime which is Active state. - vm.expectRevert(LlamaTokenCaster.ActionNotQueued.selector); + vm.expectRevert(abi.encodeWithSelector(LlamaTokenCaster.InvalidActionState.selector, ActionState.Active)); llamaERC721TokenCaster.castVeto(_actionInfo, uint8(VoteType.For), ""); } @@ -356,7 +377,7 @@ contract CastVeto is LlamaERC721TokenCasterTest { vm.startPrank(tokenHolder1); llamaERC721TokenCaster.castVeto(actionInfo, uint8(VoteType.For), ""); - vm.expectRevert(LlamaTokenCaster.AlreadyCastedVeto.selector); + vm.expectRevert(LlamaTokenCaster.DuplicateCast.selector); llamaERC721TokenCaster.castVeto(actionInfo, uint8(VoteType.For), ""); } @@ -576,24 +597,13 @@ contract SubmitApprovals is LlamaERC721TokenCasterTest { llamaERC721TokenCaster.submitApproval(notActionInfo); } - function test_RevertsIf_ApprovalNotEnabled() public { - ILlamaTokenAdapter tokenAdapter = createTimestampTokenAdapter(address(erc721VotesToken), 0); - - LlamaTokenCaster casterWithWrongRole = LlamaTokenCaster( - Clones.cloneDeterministic( - address(llamaTokenCasterLogic), keccak256(abi.encodePacked(address(erc721VotesToken), msg.sender)) - ) - ); - casterWithWrongRole.initialize(CORE, tokenAdapter, madeUpRole, defaultCasterConfig); - vm.expectRevert(abi.encodeWithSelector(ILlamaRelativeStrategyBase.InvalidRole.selector, tokenVotingCasterRole)); - casterWithWrongRole.submitApproval(actionInfo); - } - function test_RevertsIf_AlreadySubmittedApproval() public { vm.startPrank(tokenHolder1); llamaERC721TokenCaster.submitApproval(actionInfo); - vm.expectRevert(LlamaTokenCaster.AlreadySubmittedApproval.selector); + // This should revert since the underlying Action has transitioned to Queued state. Otherwise it would have reverted + // due to `LlamaCore.DuplicateCast() error`. + vm.expectRevert(abi.encodeWithSelector(ILlamaCore.InvalidActionState.selector, ActionState.Queued)); llamaERC721TokenCaster.submitApproval(actionInfo); } @@ -615,7 +625,7 @@ contract SubmitApprovals is LlamaERC721TokenCasterTest { function test_RevertsIf_CastingPeriodNotOver() public { vm.warp(block.timestamp - 1); - vm.expectRevert(LlamaTokenCaster.CannotSubmitYet.selector); + vm.expectRevert(LlamaTokenCaster.CastingPeriodNotOver.selector); llamaERC721TokenCaster.submitApproval(actionInfo); } @@ -676,23 +686,13 @@ contract SubmitDisapprovals is LlamaERC721TokenCasterTest { llamaERC721TokenCaster.submitDisapproval(notActionInfo); } - function test_RevertsIf_DisapprovalNotEnabled() public { - LlamaTokenCaster casterWithWrongRole = LlamaTokenCaster( - Clones.cloneDeterministic( - address(llamaTokenCasterLogic), keccak256(abi.encodePacked(address(erc721VotesToken), msg.sender)) - ) - ); - ILlamaTokenAdapter tokenAdapter = createTimestampTokenAdapter(address(erc721VotesToken), 0); - casterWithWrongRole.initialize(CORE, tokenAdapter, madeUpRole, defaultCasterConfig); - vm.expectRevert(abi.encodeWithSelector(ILlamaRelativeStrategyBase.InvalidRole.selector, tokenVotingCasterRole)); - casterWithWrongRole.submitDisapproval(actionInfo); - } - function test_RevertsIf_AlreadySubmittedDisapproval() public { vm.startPrank(tokenHolder1); llamaERC721TokenCaster.submitDisapproval(actionInfo); - vm.expectRevert(LlamaTokenCaster.AlreadySubmittedDisapproval.selector); + // This should revert since the underlying Action has transitioned to Failed state. Otherwise it would have reverted + // due to `LlamaCore.DuplicateCast() error`. + vm.expectRevert(abi.encodeWithSelector(ILlamaCore.InvalidActionState.selector, ActionState.Failed)); llamaERC721TokenCaster.submitDisapproval(actionInfo); } @@ -734,7 +734,7 @@ contract SubmitDisapprovals is LlamaERC721TokenCasterTest { function test_RevertsIf_CastingPeriodNotOver() public { vm.warp(block.timestamp - 1); - vm.expectRevert(LlamaTokenCaster.CannotSubmitYet.selector); + vm.expectRevert(LlamaTokenCaster.CastingPeriodNotOver.selector); llamaERC721TokenCaster.submitDisapproval(actionInfo); } @@ -861,3 +861,55 @@ contract SetPeriodPct is LlamaERC721TokenCasterTest { ); } } + +contract CastData is LlamaERC721TokenCasterTest { + function setUp() public virtual override { + LlamaERC721TokenCasterTest.setUp(); + + _skipVotingDelay(actionInfo); + castVotesFor(); + + uint256 delayPeriodEndTime = actionCreationTime + ((APPROVAL_PERIOD * ONE_QUARTER_IN_BPS) / ONE_HUNDRED_IN_BPS); + uint256 castingPeriodEndTime = delayPeriodEndTime + ((APPROVAL_PERIOD * TWO_QUARTERS_IN_BPS) / ONE_HUNDRED_IN_BPS); + vm.warp(castingPeriodEndTime + 1); + + llamaERC721TokenCaster.submitApproval(actionInfo); + + _skipVetoDelay(actionInfo); + castVetosFor(); + + Action memory action = CORE.getAction(actionInfo.id); + delayPeriodEndTime = + (action.minExecutionTime - QUEUING_PERIOD) + ((QUEUING_PERIOD * ONE_QUARTER_IN_BPS) / ONE_HUNDRED_IN_BPS); + castingPeriodEndTime = delayPeriodEndTime + ((QUEUING_PERIOD * TWO_QUARTERS_IN_BPS) / ONE_HUNDRED_IN_BPS); + vm.warp(castingPeriodEndTime + 1); + + llamaERC721TokenCaster.submitDisapproval(actionInfo); + } + + function test_CanGetCastData() public { + ( + uint96 votesFor, + uint96 votesAbstain, + uint96 votesAgainst, + uint96 vetoesFor, + uint96 vetoesAbstain, + uint96 vetoesAgainst + ) = llamaERC721TokenCaster.casts(actionInfo.id); + assertEq(votesFor, 3); + assertEq(votesAbstain, 0); + assertEq(votesAgainst, 0); + assertEq(vetoesFor, 3); + assertEq(vetoesAbstain, 0); + assertEq(vetoesAgainst, 0); + + assertTrue(llamaERC721TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder1, true)); + assertTrue(llamaERC721TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder2, true)); + assertTrue(llamaERC721TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder3, true)); + assertFalse(llamaERC721TokenCaster.hasTokenHolderCast(actionInfo.id, notTokenHolder, true)); + assertTrue(llamaERC721TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder1, false)); + assertTrue(llamaERC721TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder2, false)); + assertTrue(llamaERC721TokenCaster.hasTokenHolderCast(actionInfo.id, tokenHolder3, false)); + assertFalse(llamaERC721TokenCaster.hasTokenHolderCast(actionInfo.id, notTokenHolder, false)); + } +}