Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
57 changes: 49 additions & 8 deletions src/interfaces/IOnChainAllocation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,99 @@ pragma solidity ^0.8.0;

import { IAllocator } from "./IAllocator.sol";
import { Lock } from "../types/EIP712Types.sol";
import { ISignatureTransfer } from "permit2/src/interfaces/ISignatureTransfer.sol";
import { DepositDetails } from "../types/DepositDetails.sol";

interface IOnChainAllocation is IAllocator {
error InvalidPreparation();
error InvalidRegistration(address sponsor, bytes32 claimHash);

/// @notice Emitted when a tokens are successfully allocated
/// @param sponsor The address of the sponsor
/// @param commitments The commitments of the allocations
/// @param nonce The nonce of the allocation
/// @param expires The expiration of the allocation
/// @param claimHash The hash of the allocation
/**
* @notice Emitted when a tokens are successfully allocated
* @param sponsor The address of the sponsor
* @param commitments The commitments of the allocations
* @param nonce The nonce of the allocation
* @param expires The expiration of the allocation
* @param claimHash The hash of the allocation
*/
event Allocated(address indexed sponsor, Lock[] commitments, uint256 nonce, uint256 expires, bytes32 claimHash);

/**
* @notice Deposits, registers and allocates a claim via Permit2 signature transfer
* @dev Deposits the tokens subject to the order and registers the claim directly with the compact, then allocates the claim
* @param arbiter The arbiter of the allocation
* @param depositor The address depositing tokens and the sponsor of the claim (must sign the Permit2 message)
* @param permitted The token permissions for the Permit2 transfer. Must match the commitments in the claim
* @param additionalCommitmentAmounts Additional commitment amounts to allocate. Allocator must verify those tokens are unallocated.
* @param details The deposit details including nonce, deadline, and lock tag
* Nonce must match the nonce structure expected by the allocator
* Deadline will be used as the expiration of the claim
* @param claimHash The hash of the claim to register. Must match the claim hash recreated by the allocator
* @param witness The witness typestring for the Permit2 signature (empty string if no witness)
* @param witnessHash The hash of the witness data (bytes32(0) if no witness)
* @param signature The Permit2 signature from the depositor, will be verified by the compact
* @param context Additional context for the allocation
* @return commitments The lock commitments created by the allocation
*/
function permit2Allocation(
address arbiter,
address depositor,
uint256 expires,
ISignatureTransfer.TokenPermissions[] calldata permitted,
uint256[] calldata additionalCommitmentAmounts,
DepositDetails calldata details,
bytes32 claimHash,
string calldata witness,
bytes32 witnessHash,
bytes calldata signature,
bytes calldata context
) external returns (Lock[] memory commitments);

/**
* @notice Allows to create an allocation on behalf of a recipient without the contract being in control over the funds.
* @notice Will typically be used in combination with `batchDepositAndRegisterFor` on the compact.
* @dev Must be called before `executeAllocation` to ensure a valid balance change has occurred for the recipient.
* @param recipient The account to receive the tokens.
* @param idsAndAmounts The ids and amounts to allocate.
* @param additionalCommitmentAmounts Additional commitment amounts to allocate. Allocator must verify those tokens are unallocated.
* @param arbiter The account tasked with verifying and submitting the claim.
* @param expires The time at which the claim expires.
* @param typehash The typehash of the claim.
* @param witness The witness of the claim.
* @param context Additional context for the allocation
* @return nonce The next valid nonce. It is only guaranteed that the nonce is valid within the same transaction..
*/
function prepareAllocation(
address recipient,
uint256[2][] calldata idsAndAmounts,
uint256[] calldata additionalCommitmentAmounts,
address arbiter,
uint256 expires,
bytes32 typehash,
bytes32 witness,
bytes calldata orderData
bytes calldata context
) external returns (uint256 nonce);

/**
* @notice Executes an allocation on behalf of a recipient.
* @dev Must be called after `prepareAllocation` to ensure a valid balance change has occurred for the recipient.
* @param recipient The account to receive the tokens.
* @param idsAndAmounts The ids and amounts to allocate.
* @param additionalCommitmentAmounts Additional commitment amounts to allocate. Allocator must verify those tokens are unallocated.
* @param arbiter The account tasked with verifying and submitting the claim.
* @param expires The time at which the claim expires.
* @param typehash The typehash of the claim.
* @param witness The witness of the claim.
* @param context Additional context for the allocation
*/
function executeAllocation(
address recipient,
uint256[2][] calldata idsAndAmounts,
uint256[] calldata additionalCommitmentAmounts,
address arbiter,
uint256 expires,
bytes32 typehash,
bytes32 witness,
bytes calldata orderData
bytes calldata context
) external;
}
114 changes: 114 additions & 0 deletions src/utility/Utility.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// 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 = 0x00000000000000171ede64904551eeDF3C6C9788;
address internal constant TSTORE_TEST_CONTRACT = 0x627c1071d6A691688938Bb856659768398262690;

bytes4 internal constant EXTTLOAD_SELECTOR = 0xf135baaa;
bytes4 internal constant EXTSLOAD_SELECTOR = 0x1e2eaeaf;

uint256 internal constant REENTRANCY_GUARD_SLOT = 0x929eee149b4bd21268;
// ╭------------------------+---------+------+--------+-------+-------------------------------╮
// | Name | Type | Slot | Offset | Bytes | Contract |
// +==========================================================================================+
// | _tstoreSupportActiveAt | uint256 | 0 | 0 | 32 | src/TheCompact.sol:TheCompact |
// ╰------------------------+---------+------+--------+-------+-------------------------------╯
bytes32 internal constant TSTORE_SUPPORT_ACTIVE_AT_SLOT = 0x00;

bool internal immutable TSTORE_INITIAL_SUPPORT;

error TheCompactNotDeployed();
error BalanceNotSettled();

constructor() {
TSTORE_INITIAL_SUPPORT = checkTstoreAvailable();
if (TSTORE_INITIAL_SUPPORT) {
try Tstorish(THE_COMPACT).__activateTstore() {
// Successfully activated TSTORE
/// @dev This leads to tstore only being active after the current block.
/// As a precaution, we deactivate TSTORE_INITIAL_SUPPORT.
TSTORE_INITIAL_SUPPORT = false;
} catch {
// Failed to activate TSTORE
/// @dev Since we know the chain supports tstore, this call can only revert with:
/// TStoreAlreadyActivated(). We have to read _tstoreSupportActiveAt to confirm it is already active.
bytes32 tstoreSupportActiveAt = Extsload(THE_COMPACT).extsload(TSTORE_SUPPORT_ACTIVE_AT_SLOT);
if (uint256(tstoreSupportActiveAt) > block.number) {
TSTORE_INITIAL_SUPPORT = false;
}
}
}
}

/// @notice Checks if the Compact is deployed and if transient storage is available on the chain.
function checkTstoreAvailable() internal view returns (bool ok) {
Comment thread
mgretzke marked this conversation as resolved.
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 tstoreTestContract = TSTORE_TEST_CONTRACT;
assembly ("memory-safe") {
ok := staticcall(div(gas(), 10), tstoreTestContract, 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 {
// tstore not initially available. Check if it is active now by reading the tstore support active at slot.
assembly ("memory-safe") {
// Read the tstore support active at slot on the compact
mstore(0x1c, EXTSLOAD_SELECTOR)
mstore(0x20, TSTORE_SUPPORT_ACTIVE_AT_SLOT)
let ok := staticcall(gas(), THE_COMPACT, 0x1c, 0x24, 0x20, 0x20)
if iszero(ok) {
// Indicating the call has failed. Since we ensure the compact is deployed in the constructor, this can only be due to out of gas.
revert(0, 0)
}

let tstoreSupportActiveAt := mload(0x20)

// If tstoreSupportActiveAt is 0 or is greater than the current block number, then tstore is not supported in this case.
// 0 could only be indicating tstore is valid, if TSTORE_INITIAL_SUPPORT was true, so we can safely assume tstore is not supported in this case.
let tstoreSupported := and(gt(tstoreSupportActiveAt, 0), iszero(gt(tstoreSupportActiveAt, number())))

// If tstore is supported update the selector to read from the transient storage slot
if tstoreSupported {
mstore(0x1c, EXTTLOAD_SELECTOR)
}

// Update the slot pointer to the reentrancy guard slot
mstore(0x20, REENTRANCY_GUARD_SLOT)

// Call the Compact to read the reentrancy guard slot
pop(staticcall(gas(), THE_COMPACT, 0x1c, 0x24, 0, 0x20))
reentrancySlotContent := mload(0)

// We do not need to check for success.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tiny nit on comments: the convention is to avoid "we" and instead just be declarative / talk about what is happening

// If the reentrancy slot read runs out of gas, mload(0) will read the selector, which will trigger a BalanceNotSettled() revert.
}
}

if (uint256(reentrancySlotContent) > 1) {
revert BalanceNotSettled();
}

// If we get here, the balance is settled, so returning the balance
Copy link
Copy Markdown
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.

Loading