Repository navigation
test(B20Security): add revertOrder tests for redeem and share ratio functions #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
487367e
test(B20Security): add redeemWithMemo, updateMinimumRedeemable, updat…
eric-ships 6de266a
chore: forge fmt
eric-ships 13a2019
test(B20Security): implement updateMinimumRedeemable and updateShareR…
eric-ships d2024f2
test(B20Security): rewrite revertOrder tests to sequential walk-throu…
eric-ships File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
86 changes: 86 additions & 0 deletions
86
test/unit/B20Security/redeem/redeemWithMemo_revertOrder.t.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IB20} from "src/interfaces/IB20.sol"; | ||
| import {IB20Security} from "src/interfaces/IB20Security.sol"; | ||
|
|
||
| import {B20Constants} from "src/lib/B20Constants.sol"; | ||
|
|
||
| import {B20SecurityTest} from "test/lib/B20SecurityTest.sol"; | ||
| import {PolicyRegistryConstants} from "test/lib/mocks/MockPolicyRegistry.sol"; | ||
|
|
||
| /// @title Sequential revert-order test for `redeemWithMemo` (security variant). | ||
| /// | ||
| /// @notice **Canonical order (Solidity reference `_redeemBurn`, shared with `redeem`):** | ||
| /// 1. PAUSE (`_isPaused(REDEEM)`) → `ContractPaused` | ||
| /// 2. POLICY (`isAuthorized(REDEEMSenderPolicyId, msg.sender)`) → `PolicyForbids` | ||
| /// 3. BELOW-MIN (`shares == 0 || shares < minimum`) → `BelowMinimumRedeemable` | ||
| /// 4. BALANCE (`fromBalance < amount` in `_burnRaw`) → `InsufficientBalance` | ||
| /// | ||
| /// A single test activates all four conditions at once and fixes them from | ||
| /// highest to lowest priority, asserting each revert in turn then completing | ||
| /// a successful call once all conditions are resolved. | ||
| contract B20SecurityRedeemWithMemoRevertOrderTest is B20SecurityTest { | ||
| /// @dev Unpauses a single feature via the `unpauser` actor, lazily granting | ||
| /// `UNPAUSE_ROLE` on first call. Mirrors the inherited `_pause` helper. | ||
| function _unpause(IB20.PausableFeature feature) private { | ||
| if (!token.hasRole(B20Constants.UNPAUSE_ROLE, unpauser)) _grantRole(B20Constants.UNPAUSE_ROLE, unpauser); | ||
| vm.prank(unpauser); | ||
| token.unpause(_singleFeature(feature)); | ||
| } | ||
|
|
||
| function test_redeemWithMemo_revertOrder(uint256 amount, uint256 minimum, bytes32 memo) public { | ||
| amount = bound(amount, 1, type(uint64).max); | ||
| minimum = bound(minimum, amount + 1, type(uint256).max); | ||
|
|
||
| // Setup: activate ALL revert conditions simultaneously. | ||
| // REDEEM is paused, the sender policy blocks all callers, | ||
| // minimumRedeemable > amount (so shares == amount < minimum with the | ||
| // default 1:1 WAD ratio), and alice has zero balance. | ||
| _pause(IB20.PausableFeature.REDEEM); | ||
| _setRedeemPolicy(PolicyRegistryConstants.ALWAYS_BLOCK_ID); | ||
| _updateMinimumRedeemable(minimum); | ||
| // alice balance is zero by default. | ||
|
|
||
| // 1. PAUSE fires first. | ||
| vm.prank(alice); | ||
| vm.expectRevert(abi.encodeWithSelector(IB20.ContractPaused.selector, IB20.PausableFeature.REDEEM)); | ||
| security().redeemWithMemo(amount, memo); | ||
|
|
||
| // Fix: unpause REDEEM. | ||
| _unpause(IB20.PausableFeature.REDEEM); | ||
|
|
||
| // 2. POLICY fires next (pause fixed). | ||
| vm.prank(alice); | ||
| vm.expectRevert( | ||
| abi.encodeWithSelector( | ||
| IB20.PolicyForbids.selector, security().REDEEM_SENDER_POLICY(), PolicyRegistryConstants.ALWAYS_BLOCK_ID | ||
| ) | ||
| ); | ||
| security().redeemWithMemo(amount, memo); | ||
|
|
||
| // Fix: allow alice through the redeem-sender policy. | ||
| _setRedeemPolicy(PolicyRegistryConstants.ALWAYS_ALLOW_ID); | ||
|
|
||
| // 3. BELOW-MIN fires next (pause and policy fixed). | ||
| // With the default 1:1 WAD ratio shares == amount, and amount < minimum. | ||
| vm.prank(alice); | ||
| vm.expectRevert(abi.encodeWithSelector(IB20Security.BelowMinimumRedeemable.selector, amount, minimum)); | ||
| security().redeemWithMemo(amount, memo); | ||
|
|
||
| // Fix: clear the minimum floor so shares == amount satisfies the check. | ||
| _updateMinimumRedeemable(0); | ||
|
|
||
| // 4. BALANCE fires next (pause, policy, and minimum fixed). | ||
| vm.prank(alice); | ||
| vm.expectRevert(abi.encodeWithSelector(IB20.InsufficientBalance.selector, alice, 0, amount)); | ||
| security().redeemWithMemo(amount, memo); | ||
|
|
||
| // Fix: give alice exactly enough balance to cover the redemption. | ||
| _mint(alice, amount); | ||
|
|
||
| // Success: all conditions resolved. | ||
| vm.prank(alice); | ||
| security().redeemWithMemo(amount, memo); | ||
| } | ||
| } |
40 changes: 40 additions & 0 deletions
40
test/unit/B20Security/redeem/updateMinimumRedeemable_revertOrder.t.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IB20} from "src/interfaces/IB20.sol"; | ||
|
|
||
| import {B20Constants} from "src/lib/B20Constants.sol"; | ||
|
|
||
| import {B20SecurityTest} from "test/lib/B20SecurityTest.sol"; | ||
|
|
||
| /// @title Sequential revert-order test for `updateMinimumRedeemable`. | ||
| /// | ||
| /// @notice **Canonical order (Solidity reference):** | ||
| /// 1. ROLE (`onlyRole(DEFAULT_ADMIN_ROLE)` modifier) → `AccessControlUnauthorizedAccount` | ||
| /// | ||
| /// A single test verifies the guard fires for an unauthorized caller and then | ||
| /// confirms the call succeeds once the role is granted. | ||
| contract B20SecurityUpdateMinimumRedeemableRevertOrderTest is B20SecurityTest { | ||
| function test_updateMinimumRedeemable_revertOrder(address caller, uint256 newMinimum) public { | ||
| // Exclude precompiles (which can distort msg.sender) and admin (who already | ||
| // holds DEFAULT_ADMIN_ROLE, which would make the initial call succeed). | ||
| _assumeValidCaller(caller); | ||
| vm.assume(caller != admin); | ||
|
|
||
| // 1. ROLE fires. | ||
| vm.prank(caller); | ||
| vm.expectRevert( | ||
| abi.encodeWithSelector( | ||
| IB20.AccessControlUnauthorizedAccount.selector, caller, B20Constants.DEFAULT_ADMIN_ROLE | ||
| ) | ||
| ); | ||
| security().updateMinimumRedeemable(newMinimum); | ||
|
|
||
| // Fix: grant DEFAULT_ADMIN_ROLE to caller. | ||
| _grantRole(B20Constants.DEFAULT_ADMIN_ROLE, caller); | ||
|
|
||
| // Success: all conditions resolved. | ||
| vm.prank(caller); | ||
| security().updateMinimumRedeemable(newMinimum); | ||
| } | ||
| } |
39 changes: 39 additions & 0 deletions
39
test/unit/B20Security/shareRatio/updateShareRatio_revertOrder.t.sol
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing tests?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nvm, I understand now, these are just documenting for clarity |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IB20} from "src/interfaces/IB20.sol"; | ||
|
|
||
| import {B20SecurityTest} from "test/lib/B20SecurityTest.sol"; | ||
|
|
||
| /// @title Sequential revert-order test for `updateShareRatio`. | ||
| /// | ||
| /// @notice **Canonical order (Solidity reference):** | ||
| /// 1. ROLE (`onlyRole(SECURITY_OPERATOR_ROLE)` modifier) → `AccessControlUnauthorizedAccount` | ||
| /// | ||
| /// A single test verifies the guard fires for an unauthorized caller and then | ||
| /// confirms the call succeeds once the role is granted. | ||
| contract B20SecurityUpdateShareRatioRevertOrderTest is B20SecurityTest { | ||
| function test_updateShareRatio_revertOrder(address caller, uint256 newRatio) public { | ||
| // Exclude precompiles (which can distort msg.sender) and admin (needed | ||
| // internally by _grantRole to approve the role grant). | ||
| _assumeValidCaller(caller); | ||
| vm.assume(caller != admin); | ||
|
|
||
| // Resolve the role identifier before setting the prank; an external view | ||
| // call inside abi.encodeWithSelector would otherwise consume vm.prank | ||
| // before the state-changing call, sending it as address(this) instead. | ||
| bytes32 operatorRole = security().SECURITY_OPERATOR_ROLE(); | ||
|
|
||
| // 1. ROLE fires. | ||
| vm.prank(caller); | ||
| vm.expectRevert(abi.encodeWithSelector(IB20.AccessControlUnauthorizedAccount.selector, caller, operatorRole)); | ||
| security().updateShareRatio(newRatio); | ||
|
|
||
| // Fix: grant SECURITY_OPERATOR_ROLE to caller. | ||
| _grantRole(operatorRole, caller); | ||
|
|
||
| // Success: all conditions resolved. | ||
| vm.prank(caller); | ||
| security().updateShareRatio(newRatio); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this has no tests in it? Also fine to skip tbh given we are cutting this functionality from B20 tonight
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nvm, I understand now, these are just documenting for clarity