-
Notifications
You must be signed in to change notification settings - Fork 35
Utility lib #162
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
Open
mgretzke
wants to merge
20
commits into
main
Choose a base branch
from
utility-lib
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Utility lib #162
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
72820e9
safe balance of and tstore checks
mgretzke 5bf4ed1
renamed functions
mgretzke be7040f
turned tstore check stateless
mgretzke 5aca082
added tests
mgretzke 269cdcc
fixed tests for coverage report
mgretzke e2b0a3a
comments
mgretzke ce659ea
developer friendly redesign
mgretzke dff998d
changed stateless lib to helper contract
mgretzke 4acf1b9
fixed tests
mgretzke 3f5f156
fixed coverage test
mgretzke 7982820
added permit2Allocation to IOnChainAllocation
mgretzke 2506f43
Updated permit2Allocation return value type
mgretzke 4118ca9
removed permit2 from IOnChainAllocation interface
mgretzke 0cc083d
Updated IOnChainAllocation
mgretzke 189d677
fixed import
mgretzke 65db4de
additionalCommitmentAmounts in permit2Allocation
mgretzke f6059a0
M-05
mgretzke d9d9e78
N-27 + N-28
mgretzke 8044cd7
enable tstore read based on tstoreSupportActiveAt
mgretzke aa36d94
removed unused revert data
mgretzke 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
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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,64 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import { Extsload } from "../lib/Extsload.sol"; | ||
| import { ERC6909 } from "solady/tokens/ERC6909.sol"; | ||
| import { Tstorish } from "../lib/Tstorish.sol"; | ||
|
|
||
| contract Utility { | ||
| address internal constant THE_COMPACT = address(0x00000000000000171ede64904551eeDF3C6C9788); | ||
| address internal constant TSTORE_TEST_CONTRACT = address(0x627c1071d6A691688938Bb856659768398262690); | ||
|
|
||
| uint256 internal constant REENTRANCY_GUARD_SLOT = 0x929eee149b4bd21268; | ||
|
|
||
| bool internal immutable TSTORE_INITIAL_SUPPORT; | ||
|
|
||
| error TheCompactNotDeployed(); | ||
| error BalanceNotSettled(); | ||
|
|
||
| constructor() { | ||
| TSTORE_INITIAL_SUPPORT = checkTstoreAvailable(); | ||
| } | ||
|
|
||
| /// @notice Checks if the Compact is deployed and if transient storage is available on the chain. | ||
| function checkTstoreAvailable() internal view returns (bool ok) { | ||
| if (TSTORE_TEST_CONTRACT.code.length == 0) { | ||
| revert TheCompactNotDeployed(); | ||
| } | ||
|
|
||
| // Call the test contract, which will perform a TLOAD test. If the call | ||
| // does not revert, then TLOAD/TSTORE is supported. Do not forward all | ||
| // available gas, as all forwarded gas will be consumed on revert. | ||
| // Note that this assumes that the contract was successfully deployed. | ||
| address tloadTestContract = TSTORE_TEST_CONTRACT; | ||
| assembly ("memory-safe") { | ||
| ok := staticcall(div(gas(), 10), tloadTestContract, 0, 0, 0, 0) | ||
| } | ||
| } | ||
|
|
||
| /// @notice Returns the users balance only if reentrancy protection is not active on the Compact. This eliminates in flight balances before the ERC6909 tokens were burned. | ||
| /// @dev The function favors chains supporting eip-1153 (transient storage) | ||
| function settledBalanceOf(address owner, uint256 id) internal view returns (uint256 amount) { | ||
| bytes32 reentrancySlotContent; | ||
|
|
||
| if (TSTORE_INITIAL_SUPPORT) { | ||
| // Only check the tstore reentrancy guard slot | ||
| reentrancySlotContent = Extsload(THE_COMPACT).exttload(bytes32(REENTRANCY_GUARD_SLOT)); | ||
| } else { | ||
| // Check both slots to cover the potential transition period | ||
| try Extsload(THE_COMPACT).exttload(bytes32(REENTRANCY_GUARD_SLOT)) returns (bytes32 content) { | ||
| reentrancySlotContent = content; | ||
| } catch { } | ||
|
|
||
| // Independent of the result, check the persistent storage slot | ||
| reentrancySlotContent |= Extsload(THE_COMPACT).extsload(bytes32(REENTRANCY_GUARD_SLOT)); | ||
| } | ||
|
|
||
| if (uint256(reentrancySlotContent) > 1) { | ||
| revert BalanceNotSettled(); | ||
| } | ||
|
|
||
| // If we get here, the balance is settled, so returning the balance | ||
|
Contributor
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. Same here |
||
| return ERC6909(THE_COMPACT).balanceOf(owner, id); | ||
| } | ||
| } | ||
Large diffs are not rendered by default.
Oops, something went wrong.
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,308 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import { Utility } from "../../src/utility/Utility.sol"; | ||
| import { TheCompact } from "../../src/TheCompact.sol"; | ||
| import { ERC20 } from "solady/tokens/ERC20.sol"; | ||
|
|
||
| import { Setup } from "../integration/Setup.sol"; | ||
| import { HelperConstants } from "../helpers/HelperConstants.sol"; | ||
| import { Claim } from "../../src/types/Claims.sol"; | ||
| import { Component } from "../../src/types/Components.sol"; | ||
|
|
||
| contract UtilityLibTest is Setup { | ||
| uint256 private id; | ||
|
|
||
| function setUp() public override { | ||
| super.setUp(); | ||
|
|
||
| if (vm.envOr("COVERAGE", false)) { | ||
| // Deploy the compact on the correct address for coverage | ||
| vm.etch(UtilityLib.THE_COMPACT, address(theCompact).code); | ||
| theCompact = TheCompact(UtilityLib.THE_COMPACT); | ||
| } | ||
| } | ||
|
|
||
| function test_checkTheCompactAddress() public view { | ||
| assertEq(address(theCompact), UtilityLib.THE_COMPACT); | ||
| } | ||
|
|
||
| function test_checkCheckTstoreAvailable_success() public view { | ||
| if (!vm.envOr("COVERAGE", false)) { | ||
| bool available = UtilityLib.checkTstoreAvailable(); | ||
| assertTrue(available); | ||
| } | ||
| } | ||
|
|
||
| function test_checkCheckTstoreAvailable_failure() public { | ||
| // Deploy a contract that immediately reverts to simulate TSTORE not being available | ||
| bytes memory revertCode = hex"5f5ffd"; // PUSH0 PUSH0 REVERT | ||
| vm.etch(UtilityLib.TSTORE_TEST_CONTRACT, revertCode); | ||
| bool available = UtilityLib.checkTstoreAvailable(); | ||
| assertFalse(available); | ||
| } | ||
| } | ||
|
|
||
| contract UtilityLibTest_Transient is Setup { | ||
| bytes12 lockTag; | ||
| uint256 private idEth; | ||
| uint256 private idERC20; | ||
| CheckBalanceDuringTransfer public checkBalanceDuringTransfer; | ||
|
|
||
| function setUp() public override { | ||
| super.setUp(); | ||
|
|
||
| if (vm.envOr("COVERAGE", false)) { | ||
| // Deploy the compact on the correct address for coverage | ||
| vm.etch(UtilityLib.THE_COMPACT, address(theCompact).code); | ||
| theCompact = TheCompact(UtilityLib.THE_COMPACT); | ||
| } | ||
|
|
||
| (, lockTag) = _registerAllocator(alwaysOKAllocator); | ||
| idEth = theCompact.depositNative{ value: 1e18 }(lockTag, address(this)); | ||
|
|
||
| // Deploy malicious ERC20 token | ||
| checkBalanceDuringTransfer = new CheckBalanceDuringTransfer(false); | ||
| // Set approval | ||
| checkBalanceDuringTransfer.approve(address(theCompact), 1e18); | ||
| // Deposit malicious ERC20 token | ||
| idERC20 = theCompact.depositERC20(address(checkBalanceDuringTransfer), lockTag, 1e18, address(this)); | ||
| checkBalanceDuringTransfer.setId(idERC20); | ||
| } | ||
|
|
||
| function test_makeSureTransientStorageIsUsed() public { | ||
| vm.expectRevert(abi.encodeWithSignature("TStoreAlreadyActivated()")); | ||
| TheCompact(UtilityLib.THE_COMPACT).__activateTstore(); | ||
| } | ||
|
|
||
| function test_checkSettledBalanceOf_transient() public view { | ||
| uint256 balance = UtilityLib.settledBalanceOf(address(this), idEth); | ||
| assertEq(balance, 1e18); | ||
| } | ||
|
|
||
| function test_checkSettledBalanceOf_transient_reentrant() public { | ||
| uint256 balance = UtilityLib.settledBalanceOf(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = theCompact.balanceOf(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = checkBalanceDuringTransfer.balanceOf(address(this)); | ||
| assertEq(balance, 0); | ||
|
|
||
| // Check successful withdrawal without reentrancy | ||
| Component memory component = | ||
| Component({ claimant: uint256(bytes32(abi.encodePacked(bytes12(0), address(this)))), amount: 1e18 }); | ||
| Component[] memory claimants = new Component[](1); | ||
| claimants[0] = component; | ||
| Claim memory claim = Claim({ | ||
| allocatorData: bytes(""), | ||
| sponsorSignature: bytes(""), | ||
| sponsor: address(this), | ||
| nonce: 0, | ||
| expires: type(uint32).max, | ||
| witness: bytes32(0), | ||
| witnessTypestring: "", | ||
| id: idERC20, | ||
| allocatedAmount: 1e18, | ||
| claimants: claimants | ||
| }); | ||
| theCompact.claim(claim); | ||
|
|
||
| balance = UtilityLib.settledBalanceOf(address(this), idERC20); | ||
| assertEq(balance, 0); | ||
| balance = theCompact.balanceOf(address(this), idERC20); | ||
| assertEq(balance, 0); | ||
| balance = checkBalanceDuringTransfer.balanceOf(address(this)); | ||
| assertEq(balance, 1e18); | ||
|
|
||
| checkBalanceDuringTransfer.approve(address(theCompact), 1e18); | ||
|
|
||
| // Activate reentrancy balance to check the deposit revert | ||
| checkBalanceDuringTransfer.setAfterTokenTransferActive(true); | ||
| vm.expectRevert(abi.encodeWithSignature("TransferFromFailed()")); | ||
| theCompact.depositERC20(address(checkBalanceDuringTransfer), lockTag, 1e18, address(this)); | ||
| // Deactivate reentrancy balance to deposit correctly | ||
| checkBalanceDuringTransfer.setAfterTokenTransferActive(false); | ||
|
|
||
| // Deposit again | ||
| theCompact.depositERC20(address(checkBalanceDuringTransfer), lockTag, 1e18, address(this)); | ||
| balance = theCompact.balanceOf(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = checkBalanceDuringTransfer.balanceOf(address(this)); | ||
| assertEq(balance, 0); | ||
|
|
||
| // Activate reentrancy balance | ||
| checkBalanceDuringTransfer.setAfterTokenTransferActive(true); | ||
|
|
||
| // Try to claim again - should fail due to an invalid balance check in the _afterTokenTransfer hook | ||
| claim.nonce++; | ||
| // The claim transaction will NOT FAIL, even if the claim transfer actually failed. Instead it will release the tokens, which will trigger a release (no balance change) | ||
| theCompact.claim(claim); | ||
|
|
||
| // While the claim only silently failed the balance should still NOT have been affected | ||
| balance = UtilityLib.settledBalanceOf(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = theCompact.balanceOf(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = checkBalanceDuringTransfer.balanceOf(address(this)); | ||
| assertEq(balance, 0); | ||
| } | ||
| } | ||
|
|
||
| contract UtilityLibTest_NonTransient is Setup { | ||
| bytes12 lockTag; | ||
| uint256 private idEth; | ||
| uint256 private idERC20; | ||
| CheckBalanceDuringTransfer public checkBalanceDuringTransfer; | ||
|
|
||
| function setUp() public override { | ||
| super.setUp(); | ||
|
|
||
| if (vm.envOr("COVERAGE", false)) { | ||
| // Deploy the compact on the correct address for coverage | ||
| vm.etch(UtilityLib.THE_COMPACT, address(theCompact).code); | ||
| theCompact = TheCompact(UtilityLib.THE_COMPACT); | ||
| } | ||
|
|
||
| // manipulate the code of the TSTORE_TEST_CONTRACT to be the code of theCompact_deployedBytecode_noTransientStorage | ||
| bytes memory deployedCode = HelperConstants.theCompact_deployedBytecode_noTransientStorage; | ||
| vm.etch(UtilityLib.THE_COMPACT, deployedCode); | ||
|
|
||
| (, lockTag) = _registerAllocator(alwaysOKAllocator); | ||
| idEth = theCompact.depositNative{ value: 1e18 }(lockTag, address(this)); | ||
|
|
||
| // Deploy malicious ERC20 token | ||
| checkBalanceDuringTransfer = new CheckBalanceDuringTransfer(true); | ||
| // Set approval | ||
| checkBalanceDuringTransfer.approve(address(theCompact), 1e18); | ||
| // Deposit malicious ERC20 token | ||
| idERC20 = theCompact.depositERC20(address(checkBalanceDuringTransfer), lockTag, 1e18, address(this)); | ||
| checkBalanceDuringTransfer.setId(idERC20); | ||
| } | ||
|
|
||
| function test_checkTheCompactAddress() public view { | ||
| assertEq(address(theCompact), UtilityLib.THE_COMPACT); | ||
| } | ||
|
|
||
| function test_makeSureTransientStorageIsNotUsed() public { | ||
| TheCompact(UtilityLib.THE_COMPACT).__activateTstore(); | ||
| } | ||
|
|
||
| function test_checkSettledBalanceOf_nonTransient() public view { | ||
| uint256 balance = UtilityLib.settledBalanceOf_nonTransient(address(this), idEth); | ||
| assertEq(balance, 1e18); | ||
| } | ||
|
|
||
| function test_checkSettledBalanceOf_nonTransient_reentrant() public { | ||
| uint256 balance = UtilityLib.settledBalanceOf_nonTransient(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = theCompact.balanceOf(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = checkBalanceDuringTransfer.balanceOf(address(this)); | ||
| assertEq(balance, 0); | ||
|
|
||
| // Check successful withdrawal without reentrancy | ||
| Component memory component = | ||
| Component({ claimant: uint256(bytes32(abi.encodePacked(bytes12(0), address(this)))), amount: 1e18 }); | ||
| Component[] memory claimants = new Component[](1); | ||
| claimants[0] = component; | ||
| Claim memory claim = Claim({ | ||
| allocatorData: bytes(""), | ||
| sponsorSignature: bytes(""), | ||
| sponsor: address(this), | ||
| nonce: 0, | ||
| expires: type(uint32).max, | ||
| witness: bytes32(0), | ||
| witnessTypestring: "", | ||
| id: idERC20, | ||
| allocatedAmount: 1e18, | ||
| claimants: claimants | ||
| }); | ||
| theCompact.claim(claim); | ||
|
|
||
| balance = UtilityLib.settledBalanceOf_nonTransient(address(this), idERC20); | ||
| assertEq(balance, 0); | ||
| balance = theCompact.balanceOf(address(this), idERC20); | ||
| assertEq(balance, 0); | ||
| balance = checkBalanceDuringTransfer.balanceOf(address(this)); | ||
| assertEq(balance, 1e18); | ||
|
|
||
| checkBalanceDuringTransfer.approve(address(theCompact), 1e18); | ||
|
|
||
| // Activate reentrancy balance to check the deposit revert | ||
| checkBalanceDuringTransfer.setAfterTokenTransferActive(true); | ||
| vm.expectRevert(abi.encodeWithSignature("TransferFromFailed()")); | ||
| theCompact.depositERC20(address(checkBalanceDuringTransfer), lockTag, 1e18, address(this)); | ||
| // Deactivate reentrancy balance to deposit correctly | ||
| checkBalanceDuringTransfer.setAfterTokenTransferActive(false); | ||
|
|
||
| // Deposit again | ||
| theCompact.depositERC20(address(checkBalanceDuringTransfer), lockTag, 1e18, address(this)); | ||
| balance = theCompact.balanceOf(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = checkBalanceDuringTransfer.balanceOf(address(this)); | ||
| assertEq(balance, 0); | ||
|
|
||
| // Activate reentrancy balance | ||
| checkBalanceDuringTransfer.setAfterTokenTransferActive(true); | ||
|
|
||
| // Try to claim again - should fail due to an invalid balance check in the _afterTokenTransfer hook | ||
| claim.nonce++; | ||
| // The claim transaction will NOT FAIL, even if the claim transfer actually failed. Instead it will release the tokens, which will trigger a release (no balance change) | ||
| theCompact.claim(claim); | ||
|
|
||
| // While the claim only silently failed the balance should still NOT have been affected | ||
| balance = UtilityLib.settledBalanceOf_nonTransient(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = theCompact.balanceOf(address(this), idERC20); | ||
| assertEq(balance, 1e18); | ||
| balance = checkBalanceDuringTransfer.balanceOf(address(this)); | ||
| assertEq(balance, 0); | ||
| } | ||
| } | ||
|
|
||
| // --- Mock Contracts --- | ||
|
|
||
| contract CheckBalanceDuringTransfer is ERC20 { | ||
| bool private immutable _NON_TRANSIENT; | ||
|
|
||
| uint256 private id; | ||
| bool public afterTokenTransferActive; | ||
|
|
||
| constructor(bool nonTransient_) { | ||
| _NON_TRANSIENT = nonTransient_; | ||
| _mint(msg.sender, 1e18); | ||
| } | ||
|
|
||
| function _afterTokenTransfer(address from, address, uint256) internal view override { | ||
| if (afterTokenTransferActive) { | ||
| if (_NON_TRANSIENT) { | ||
| UtilityLib.settledBalanceOf_nonTransient(from, id); | ||
| } else { | ||
| UtilityLib.settledBalanceOf(from, id); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// @dev Returns the name of the token. | ||
| function name() public pure override returns (string memory) { | ||
| return "CheckBalanceDuringTransfer"; | ||
| } | ||
|
|
||
| /// @dev Returns the symbol of the token. | ||
| function symbol() public pure override returns (string memory) { | ||
| return "CBDT"; | ||
| } | ||
|
|
||
| /// @dev Returns the decimals places of the token. | ||
| function decimals() public pure override returns (uint8) { | ||
| return 18; | ||
| } | ||
|
|
||
| function setId(uint256 id_) public { | ||
| id = id_; | ||
| } | ||
|
|
||
| function setAfterTokenTransferActive(bool active) public { | ||
| afterTokenTransferActive = active; | ||
| } | ||
| } |
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.
Uh oh!
There was an error while loading. Please reload this page.