-
Notifications
You must be signed in to change notification settings - Fork 37
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
base: main
Are you sure you want to change the base?
Utility lib #162
Changes from 6 commits
72820e9
5bf4ed1
be7040f
5aca082
269cdcc
e2b0a3a
ce659ea
dff998d
4acf1b9
3f5f156
7982820
2506f43
4118ca9
0cc083d
189d677
65db4de
f6059a0
d9d9e78
8044cd7
aa36d94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| // 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"; | ||
|
|
||
| library UtilityLib { | ||
| address internal constant THE_COMPACT = address(0x00000000000000171ede64904551eeDF3C6C9788); | ||
| address internal constant TSTORE_TEST_CONTRACT = address(0x627c1071d6A691688938Bb856659768398262690); | ||
|
|
||
| uint256 internal constant REENTRANCY_GUARD_SLOT = 0x929eee149b4bd21268; | ||
|
|
||
| error TheCompactNotDeployed(); | ||
| error UntrustedBalance(); | ||
|
|
||
| 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 Only if eip-1153 (transient storage) available. | ||
| function settledBalanceOf(address owner, uint256 id) internal view returns (uint256 amount) { | ||
| // If transient storage available | ||
| bytes32 reentrancySlotContent = Extsload(THE_COMPACT).exttload(bytes32(REENTRANCY_GUARD_SLOT)); | ||
| if (uint256(reentrancySlotContent) > 1) { | ||
| revert UntrustedBalance(); | ||
| } | ||
|
|
||
| return ERC6909(THE_COMPACT).balanceOf(owner, id); | ||
| } | ||
|
|
||
| /// @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 Only if eip-1153 (transient storage) is not available. | ||
| function settledBalanceOf_nonTransient(address owner, uint256 id) internal view returns (uint256 amount) { | ||
|
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. for this one, if we want to go try-hard on it we could derive the balance slot and use the batch extsload function to get both values in one call
Collaborator
Author
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. I like it! Not sure how much more efficient this will be though? it will at least lead to higher revert costs, since both values will be read from storage not knowing, if we even need the balance. |
||
| // If storage available | ||
| bytes32 reentrancySlotContent = Extsload(THE_COMPACT).extsload(bytes32(REENTRANCY_GUARD_SLOT)); | ||
| if (uint256(reentrancySlotContent) > 1) { | ||
| revert UntrustedBalance(); | ||
| } | ||
|
|
||
| return ERC6909(THE_COMPACT).balanceOf(owner, id); | ||
| } | ||
| } | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,308 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import { UtilityLib } from "../../src/utility/UtilityLib.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; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.