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
1 change: 1 addition & 0 deletions .claudeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
16 changes: 13 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
PRIVATE_KEY=
INFURA_KEY=
ETHERSCAN_API_KEY=
# RPC Provider
ALCHEMY_KEY=

# Block Explorer API Keys (for contract verification)
ETHERSCAN_API_KEY=
BASESCAN_API_KEY=
OPTIMISM_ETHERSCAN_API_KEY=
ARBISCAN_API_KEY=
UNICHAIN_EXPLORER_API_KEY=

# HybridAllocator deployment parameters
OWNER_ADDRESS=
SIGNER_ADDRESS=

Large diffs are not rendered by default.

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/1/dry-run/run-latest.json

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/1/run-1766092424131.json

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/1/run-latest.json

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/10/run-1766094320624.json

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/10/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/11155111/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/130/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/1301/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/42161/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/421614/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions broadcast/DeployHybridAllocator.s.sol/8453/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions broadcast/DeployOnChainAllocator.s.sol/1/run-1766091799489.json

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions broadcast/DeployOnChainAllocator.s.sol/1/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions broadcast/DeployOnChainAllocator.s.sol/10/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions broadcast/DeployOnChainAllocator.s.sol/11155111/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions broadcast/DeployOnChainAllocator.s.sol/130/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions broadcast/DeployOnChainAllocator.s.sol/1301/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

65 changes: 65 additions & 0 deletions broadcast/DeployOnChainAllocator.s.sol/42161/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

65 changes: 65 additions & 0 deletions broadcast/DeployOnChainAllocator.s.sol/421614/run-latest.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions broadcast/DeployOnChainAllocator.s.sol/8453/run-latest.json

Large diffs are not rendered by default.

60 changes: 21 additions & 39 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,47 +56,29 @@ number_underscore = "thousands"

[rpc_endpoints]
anvil = "http://127.0.0.1:8545"
mainnet = "https://mainnet.infura.io/v3/${INFURA_KEY}"
sepolia = "https://sepolia.infura.io/v3/${INFURA_KEY}"
linea = "https://linea-mainnet.infura.io/v3/${INFURA_KEY}"
linea_sepolia = "https://linea-sepolia.infura.io/v3/${INFURA_KEY}"
polygon_pos = "https://polygon-mainnet.infura.io/v3/${INFURA_KEY}"
polygon_amoy = "https://polygon-amoy.infura.io/v3/${INFURA_KEY}"
blast = "https://blast-mainnet.infura.io/v3/${INFURA_KEY}"
blast_sepolia = "https://blast-sepolia.infura.io/v3/${INFURA_KEY}"
optimism = "https://optimism-mainnet.infura.io/v3/${INFURA_KEY}"
optimism_sepolia = "https://optimism-sepolia.infura.io/v3/${INFURA_KEY}"
arbitrum = "https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}"
arbitrum_sepolia = "https://arbitrum-sepolia.infura.io/v3/${INFURA_KEY}"
celo = "https://celo-mainnet.infura.io/v3/${INFURA_KEY}"
celo_alfajores = "https://celo-alfajores.infura.io/v3/${INFURA_KEY}"
zksync = "https://zksync-mainnet.infura.io/v3/${INFURA_KEY}"
zksync_sepolia = "https://zksync-sepolia.infura.io/v3/${INFURA_KEY}"
mantle = "https://mantle-mainnet.infura.io/v3/${INFURA_KEY}"
mantle_sepolia = "https://mantle-sepolia.infura.io/v3/${INFURA_KEY}"
polygon_zkevm = "https://zkevm-rpc.com"
polygon_zkevm_testnet = "https://rpc.public.zkevm-test.net"
# Primary chains (Alchemy)
mainnet = "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}"
sepolia = "https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}"
base = "https://base-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}"
base_sepolia = "https://base-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}"
optimism = "https://opt-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}"
optimism_sepolia = "https://opt-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}"
arbitrum = "https://arb-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}"
arbitrum_sepolia = "https://arb-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}"
# Unichain (public RPC)
unichain = "https://mainnet.unichain.org"
unichain_sepolia = "https://sepolia.unichain.org"

[etherscan]
sepolia = { key = "${ETHERSCAN_API_KEY}" }
mainnet = { key = "${ETHERSCAN_API_KEY}" }
linea = { key = "${ETHERSCAN_API_KEY}" }
linea_sepolia = { key = "${ETHERSCAN_API_KEY}" }
polygon_pos = { key = "${ETHERSCAN_API_KEY}" }
polygon_amoy = { key = "${ETHERSCAN_API_KEY}" }
blast = { key = "${ETHERSCAN_API_KEY}" }
blast_sepolia = { key = "${ETHERSCAN_API_KEY}" }
optimism = { key = "${ETHERSCAN_API_KEY}" }
optimism_sepolia = { key = "${ETHERSCAN_API_KEY}" }
arbitrum = { key = "${ETHERSCAN_API_KEY}" }
arbitrum_sepolia = { key = "${ETHERSCAN_API_KEY}" }
celo = { key = "${ETHERSCAN_API_KEY}" }
celo_alfajores = { key = "${ETHERSCAN_API_KEY}" }
zksync = { key = "${ETHERSCAN_API_KEY}" }
zksync_sepolia = { key = "${ETHERSCAN_API_KEY}" }
mantle = { key = "${ETHERSCAN_API_KEY}" }
mantle_sepolia = { key = "${ETHERSCAN_API_KEY}" }
polygon_zkevm = { key = "${ETHERSCAN_API_KEY}" }
polygon_zkevm_testnet = { key = "${ETHERSCAN_API_KEY}" }
sepolia = { key = "${ETHERSCAN_API_KEY}" }
base = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api" }
base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" }
optimism = { key = "${OPTIMISM_ETHERSCAN_API_KEY}", url = "https://api-optimistic.etherscan.io/api" }
optimism_sepolia = { key = "${OPTIMISM_ETHERSCAN_API_KEY}", url = "https://api-sepolia-optimistic.etherscan.io/api" }
arbitrum = { key = "${ARBISCAN_API_KEY}", url = "https://api.arbiscan.io/api" }
arbitrum_sepolia = { key = "${ARBISCAN_API_KEY}", url = "https://api-sepolia.arbiscan.io/api" }
unichain = { key = "${UNICHAIN_EXPLORER_API_KEY}", url = "https://api.uniscan.xyz/api" }
unichain_sepolia = { key = "${UNICHAIN_EXPLORER_API_KEY}", url = "https://api-sepolia.uniscan.xyz/api" }

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
38 changes: 38 additions & 0 deletions script/ComputeInitCodeHash.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {HybridAllocator} from '../src/allocators/HybridAllocator.sol';
import {OnChainAllocator} from '../src/allocators/OnChainAllocator.sol';
import {Script, console} from 'forge-std/Script.sol';

contract ComputeInitCodeHash is Script {
function run() public view {
// OnChainAllocator - no constructor args
bytes memory onChainInitCode = type(OnChainAllocator).creationCode;
bytes32 onChainInitCodeHash = keccak256(onChainInitCode);

console.log('=== OnChainAllocator ===');
console.log('Init code hash:');
console.logBytes32(onChainInitCodeHash);
console.log('');

// HybridAllocator - requires constructor args
address owner = vm.envOr('OWNER_ADDRESS', address(0));
address signer = vm.envOr('SIGNER_ADDRESS', address(0));

if (owner != address(0)) {
bytes memory hybridInitCode =
abi.encodePacked(type(HybridAllocator).creationCode, abi.encode(owner, signer));
bytes32 hybridInitCodeHash = keccak256(hybridInitCode);

console.log('=== HybridAllocator ===');
console.log('Owner:', owner);
console.log('Signer:', signer);
console.log('Init code hash:');
console.logBytes32(hybridInitCodeHash);
} else {
console.log('=== HybridAllocator ===');
console.log('Set OWNER_ADDRESS env var to compute init code hash');
}
}
}
29 changes: 29 additions & 0 deletions script/DeployHybridAllocator.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {HybridAllocator} from '../src/allocators/HybridAllocator.sol';
import {Script, console} from 'forge-std/Script.sol';

contract DeployHybridAllocator is Script {
// Use: cast create2 --starts-with <prefix> --ends-with <suffix> --init-code-hash <hash>
bytes32 constant SALT = 0xa73a1d77c4eeee782776c6349b59663c8e38157fbfc7f7ed35b46bb0479d0cc3;

function run() public {
// Read deployment parameters from environment
address owner = vm.envAddress('OWNER_ADDRESS');
address signer = vm.envOr('SIGNER_ADDRESS', address(0));

console.log('=== HybridAllocator CREATE2 Deployment ===');
console.log('Owner:', owner);
console.log('Signer:', signer);
console.log('Salt:', vm.toString(SALT));

vm.startBroadcast();

HybridAllocator allocator = new HybridAllocator{salt: SALT}(owner, signer);

vm.stopBroadcast();

console.log('Deployed at:', address(allocator));
}
}
27 changes: 27 additions & 0 deletions script/DeployOnChainAllocator.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {OnChainAllocator} from '../src/allocators/OnChainAllocator.sol';
import {Script, console} from 'forge-std/Script.sol';

contract DeployOnChainAllocator is Script {
// Use: cast create2 --starts-with <prefix> --ends-with <suffix> --init-code-hash <hash>
bytes32 constant SALT = 0x6bd70ededcc48f126a839895c822f173c097ede0d2d896dcb5b2a80c3b5e8205;

function run() public {
bytes32 initCodeHash = keccak256(type(OnChainAllocator).creationCode);

console.log('=== OnChainAllocator CREATE2 Deployment ===');
console.log('Salt:', vm.toString(SALT));
console.log('Init code hash:', vm.toString(initCodeHash));

vm.startBroadcast();

OnChainAllocator allocator = new OnChainAllocator{salt: SALT}();

vm.stopBroadcast();

console.log('Deployed at:', address(allocator));
console.log('Allocator ID:', allocator.ALLOCATOR_ID());
}
}
2 changes: 1 addition & 1 deletion snapshots/ERC7683Allocator_open.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"open_simpleOrder": "168885"
"open_simpleOrder": "168866"
}
2 changes: 1 addition & 1 deletion snapshots/ERC7683Allocator_openFor.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"openFor_simpleOrder_userHimself": "172344"
"openFor_simpleOrder_userHimself": "172325"
}
16 changes: 8 additions & 8 deletions snapshots/HybridAllocatorTest.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"allocateAndRegister_erc20Token": "170500",
"allocateAndRegister_erc20Token_emptyAmountInput": "171409",
"allocateAndRegister_multipleTokens": "206429",
"allocateAndRegister_nativeToken": "122063",
"allocateAndRegister_nativeToken_emptyAmountInput": "121899",
"allocateAndRegister_second_erc20Token": "114805",
"allocateAndRegister_second_nativeToken": "104799",
"hybrid_execute_single": "159843",
"allocateAndRegister_erc20Token": "170575",
"allocateAndRegister_erc20Token_emptyAmountInput": "171484",
"allocateAndRegister_multipleTokens": "206498",
"allocateAndRegister_nativeToken": "122120",
"allocateAndRegister_nativeToken_emptyAmountInput": "121956",
"allocateAndRegister_second_erc20Token": "114880",
"allocateAndRegister_second_nativeToken": "104856",
"hybrid_execute_single": "159875",
"hybrid_permit2Allocation_multipleERC20": "255228",
"hybrid_permit2Allocation_singleERC20": "188567",
"hybrid_permit2Allocation_singleERC20_withWitness": "189597"
Expand Down
121 changes: 117 additions & 4 deletions src/allocators/HybridAllocator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pragma solidity ^0.8.27;

import {SafeTransferLib} from '@solady/utils/SafeTransferLib.sol';
import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol';
import {BATCH_COMPACT_TYPEHASH, LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol';

import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';

Expand All @@ -19,6 +19,10 @@ import {ISignatureTransfer} from 'permit2/src/interfaces/ISignatureTransfer.sol'
import {IHybridAllocator} from 'src/interfaces/IHybridAllocator.sol';

/// @title HybridAllocator
/// @notice DISCLAIMER: This contract is a work in progress and is not audited. Use at your own risk.
/// @author mgretzke (mgretzke.eth)
/// @custom:coauthor 0age (0age.eth)
/// @custom:coauthor ccashwell (ccashwell.eth)
/// @notice Hybrid allocator for The Compact supporting both on-chain and off-chain allocation authorization mechanisms
/// @dev Combines direct deposit functionality with signature-based off-chain authorization through multiple authorized signers
/// @custom:security-contact security@uniswap.org
Expand All @@ -28,12 +32,17 @@ contract HybridAllocator is IHybridAllocator {
event OwnerReplacementProposed(address newOwner);
event OwnerReplaced(address oldOwner, address newOwner);
event AllocatorInitialized(address compact, address owner, uint96 allocatorId);
event AttestationAuthorized(uint256 nonce);

/// @dev The typehash for the HybridAllocationContext:
/// keccak256('HybridAllocationContext(bytes32 claimHash,Lock[] additionalCommitments)Lock(bytes12 lockTag,address token,uint256 amount)')
bytes32 constant HYBRID_ALLOCATION_CONTEXT_TYPEHASH =
0x3d88798eb330fca0ab1589827743878b2ccd0cdaa353080dffeb0d3e6fd7a639;

/// @dev The slot for the attestation in transient storage
/// bytes4(keccak256('ATTESTATION_SLOT_SEED'))
bytes4 constant ATTESTATION_SLOT_SEED = 0xd32d8248;

/// @notice The unique identifier for this allocator within The Compact protocol
uint96 public immutable ALLOCATOR_ID;
uint256 private immutable _INITIAL_CHAIN_ID;
Expand Down Expand Up @@ -149,13 +158,116 @@ contract HybridAllocator is IHybridAllocator {
emit OwnerReplaced(previousOwner, msg.sender);
}

/// @inheritdoc IHybridAllocator
function authorizeAttestation(
address sponsor,
uint256 nonce,
uint256 expires,
Lock[] calldata commitments,
bytes calldata allocatorSignature
) external returns (bool authorized) {
// Verify expiration
if (expires <= block.timestamp) {
revert AttestationExpired();
}
// Verify the provided nonce
AL.verifyNonce(nonce, AL.OFF_CHAIN_NONCE, sponsor);

address theCompact = AL.THE_COMPACT;

bytes32 hybridAttestationHash;
// Store the attestation in transient storage and create the hybrid attestation hash
assembly ("memory-safe") {
let m := mload(0x40)
mstore(m, LOCK_TYPEHASH) // prestore the lock typehash for the commitmentsHash creation

mstore(0x00, or(ATTESTATION_SLOT_SEED, sponsor)) // Store a combination of the slot seed and the sponsor address
for { let i := 0 } lt(i, commitments.length) { i := add(i, 1) } {
// Continue creating the transient slot hash
let commitmentOffset := add(commitments.offset, mul(i, 0x60))
let lockTag := calldataload(commitmentOffset)
let token := calldataload(add(commitmentOffset, 0x20))
let amount := calldataload(add(commitmentOffset, 0x40))

mstore(0x20, or(lockTag, token)) // token id
let slot := keccak256(0x00, 0x40) // create the slot out of the attestation slot seed, sponsor and token id

// Load the currently available amount for this token and sponsor
let availableAmount := tload(slot)

// Add the amount to the currently available authorized amount. This allows to use multiple attestations for a single token transaction.
/// @dev This can overflow if the amounts an off chain signer is trying to allocate are more then uint256.max tokens.
/// We skip a check on this, since this contracts trusts the off chain signer. Additionally, the worst case
/// scenario is that a smaller amount then allocated for this purpose will be available.
tstore(slot, add(availableAmount, amount))

// Create the commitment hash
mstore(add(m, 0x20), lockTag)
mstore(add(m, 0x40), token)
mstore(add(m, 0x60), amount)
let commitmentHash := keccak256(m, 0x80)
// Store the commitment hash
mstore(add(m, add(0x80, mul(i, 0x20))), commitmentHash)
}
let commitmentsHash := keccak256(add(m, 0x80), mul(commitments.length, 0x20))

// Create the hybrid attestation hash
mstore(m, BATCH_COMPACT_TYPEHASH)
mstore(add(m, 0x20), theCompact)
mstore(add(m, 0x40), sponsor)
mstore(add(m, 0x60), nonce)
mstore(add(m, 0x80), expires)
mstore(add(m, 0xa0), commitmentsHash)
hybridAttestationHash := keccak256(m, 0xc0)
}

// Verify signature
bytes32 digest = _deriveDigest(hybridAttestationHash, _COMPACT_DOMAIN_SEPARATOR);
if (block.chainid != _INITIAL_CHAIN_ID) {
// If the chain was forked, we can not use the cached domain separator
digest = _deriveDigest(hybridAttestationHash, ITheCompact(AL.THE_COMPACT).DOMAIN_SEPARATOR());
}
if (!_checkSignature(digest, allocatorSignature)) {
revert InvalidSignature();
}

// Consume the nonce. Use the compacts nonce management
uint256[] memory nonceArray = new uint256[](1);
nonceArray[0] = nonce;
ITheCompact(AL.THE_COMPACT).consume(nonceArray); // will revert if the nonce was already consumed

emit AttestationAuthorized(nonce);
authorized = true;
}

/// @inheritdoc IAllocator
function attest(address, /*operator*/ address, /*from*/ address, /*to*/ uint256, /*id*/ uint256 /*amount*/ )
function attest(address, /*operator*/ address sponsor, address, /*to*/ uint256 id, uint256 amount)
external
pure
returns (bytes4)
{
revert Unsupported();
// Verify the caller is the compact
if (msg.sender != AL.THE_COMPACT) {
revert InvalidCaller(msg.sender, AL.THE_COMPACT);
}

assembly ("memory-safe") {
mstore(0x00, or(ATTESTATION_SLOT_SEED, sponsor))
mstore(0x20, id)
let slot := keccak256(0x00, 0x40)
let availableAmount := tload(slot)
if lt(availableAmount, amount) {
mstore(0x00, 0xc74b9fab) // InsufficientAttestationAmount()
mstore(0x20, availableAmount)
mstore(0x40, amount)
revert(0x1c, 0x44)
}

tstore(slot, sub(availableAmount, amount))

// Return the attest() selector to indicate a successful attestation
mstore(0x00, 0x1a808f91)
return(0x1c, 0x04)
}
}

/// @inheritdoc IHybridAllocator
Expand Down Expand Up @@ -268,6 +380,7 @@ contract HybridAllocator is IHybridAllocator {
);
}

/// @inheritdoc IOnChainAllocation
function executeAllocation(
address recipient,
uint256[2][] calldata idsAndAmounts,
Expand Down
Loading