From d999ecddf513d380351ce1c5adc642cac0f7d6c2 Mon Sep 17 00:00:00 2001 From: Rajath Alex Date: Sun, 17 Dec 2023 22:08:34 -0500 Subject: [PATCH] fix: resolving additional bugs and some refactors (#78) **Motivation:** Small codebase changes **Modifications:** Bugs: * Vote weight adding similar to how we do `_newCastCount` in LlamaCore which has an upper bound condition. This will handle cases when someone has quantity greater than `type(uint96).max) - currentCount`. They get to vote too. (This is mostly applicable to ERC20s who can mint huge numbers). Added `test_CanCastWhenCountisMax` tests. * Due to the way solidity works, we had no way to get the mappings inside the `CastData` struct from an external contract. So added a `hasTokenHolderCast` to `TokenHolderCaster` that gets the values for votes and vetoes. Also added tests. * Cast Vote and Cast Veto should check if the ActionCaster has the defined `role` at ActionCreation time. Currently the tokenholders will be able to Vote even if the underlying TokenHolderCaster no longer has the role. It'll only fail at Submission time which can be annoying. Refactors: * Removed the `checkIf(Dis)ApprovalEnabled` check in submit functions since they're checked in the underlying `cast` functions in the Llama Framework. Also removed the tests since there is no reasonable way to test this since the `castVote` and `castVeto` functions does have the `checkIf(Dis)ApprovalEnabled` check and it would have failed there. And we know for a fact that the `cast(Dis)approval` being called in the submit functions have the `checkIf(Dis)ApprovalEnabled` check and we have tests for that in the Llama Framework. * `Note:` We also don't need an Action state check on the submit functions since they're checked in the underlying `cast` functions in the Llama Framework. There is no reasonable way to test this since the `castVote` and `castVeto` functions does have the Action state check and it would have failed there. And we know for a fact that the `cast(Dis)approval` being called in the submit functions have the Action state check and we have tests for that in the Llama Framework. * Removed the `approvalSubmitted` and `disapprovalSubmitted` bool and respective `AlreadySubmittedApproval` and `AlreadySubmittedDisapproval()` checks. This is since the underlying LlamaFramework handles it through `LlamaCore.DuplicateCast()` error in the worst case. But more likely is that it triggers the `LlamaCore.InvalidActionState` since the state has already transitioned. And we know for a fact that the `cast(Dis)approval` being called in the submit functions have the above checks and we have tests for that in the Llama Framework. * Combined `AlreadyCastVote()` and `AlreadyCastVeto()` errors into a single `DuplicateCast()` error that matches the error style in Llama Framework and reduces the number of defined errors. * Combined `ActionNotActive()` and `ActionNotQueued()` errors into a single `InvalidActionState(ActionState current)` error that matches the error style in Llama Framework and reduces the number of defined errors. Moved this into the common `_preCastAssertions` function to match Llama style. * Changed `VotingDelayNotOver()` error to `DelayPeriodNotOver()` error to match the other Period errors. * Changed `CannotSubmitYet()` error to `CastingPeriodNotOver()` error to match the other Period errors. **Result:** Better and bug free code. --- src/token-voting/LlamaTokenCaster.sol | 221 ++++++++++-------- test/token-voting/LlamaERC20TokenCaster.t.sol | 176 +++++++++++--- .../token-voting/LlamaERC721TokenCaster.t.sol | 122 +++++++--- 3 files changed, 359 insertions(+), 160 deletions(-) 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)); + } +}