Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/utility/Utility.sol
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

return ERC6909(THE_COMPACT).balanceOf(owner, id);
}
}
11 changes: 11 additions & 0 deletions test/helpers/HelperConstants.sol

Large diffs are not rendered by default.

308 changes: 308 additions & 0 deletions test/utility/UtilityLib.t.sol
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;
}
}
Loading