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
2 changes: 1 addition & 1 deletion lib/the-compact
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": "168865"
"open_simpleOrder": "168885"
}
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": "172309"
"openFor_simpleOrder_userHimself": "172344"
}
22 changes: 11 additions & 11 deletions snapshots/HybridAllocatorTest.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"allocateAndRegister_erc20Token": "170490",
"allocateAndRegister_erc20Token_emptyAmountInput": "171400",
"allocateAndRegister_multipleTokens": "206420",
"allocateAndRegister_nativeToken": "122053",
"allocateAndRegister_nativeToken_emptyAmountInput": "121889",
"allocateAndRegister_second_erc20Token": "114796",
"allocateAndRegister_second_nativeToken": "104789",
"hybrid_execute_single": "157779",
"hybrid_permit2Allocation_multipleERC20": "253503",
"hybrid_permit2Allocation_singleERC20": "187123",
"hybrid_permit2Allocation_singleERC20_withWitness": "188140"
"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",
"hybrid_permit2Allocation_multipleERC20": "255228",
"hybrid_permit2Allocation_singleERC20": "188567",
"hybrid_permit2Allocation_singleERC20_withWitness": "189597"
}
18 changes: 9 additions & 9 deletions snapshots/OnChainAllocatorTest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"allocateFor_success_withRegistration": "134199",
"allocate_and_delete_expired_allocation": "66401",
"allocate_erc20": "129672",
"allocate_native": "129432",
"allocate_second_erc20": "97684",
"onchain_execute_double": "346560",
"onchain_execute_single": "220180",
"onchain_permit2Allocation_multipleERC20": "365724",
"onchain_permit2Allocation_singleERC20": "232132"
"allocateFor_success_withRegistration": "134247",
"allocate_and_delete_expired_allocation": "66426",
"allocate_erc20": "129697",
"allocate_native": "129457",
"allocate_second_erc20": "97709",
"onchain_execute_double": "355504",
"onchain_execute_single": "225281",
"onchain_permit2Allocation_multipleERC20": "374116",
"onchain_permit2Allocation_singleERC20": "236970"
}
188 changes: 172 additions & 16 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} from '@uniswap/the-compact/types/EIP712Types.sol';
import {LOCK_TYPEHASH, Lock} from '@uniswap/the-compact/types/EIP712Types.sol';

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

Expand All @@ -29,6 +29,11 @@ contract HybridAllocator is IHybridAllocator {
event OwnerReplaced(address oldOwner, address newOwner);
event AllocatorInitialized(address compact, address owner, uint96 allocatorId);

/// @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;

/// @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 @@ -187,58 +192,126 @@ contract HybridAllocator is IHybridAllocator {
return (claimHash, registeredAmounts, nonce);
}

/// @inheritdoc IHybridAllocator
/// @inheritdoc IOnChainAllocation
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
) external returns (Lock[] memory commitments) {
commitments = AL.permit2Allocation(
arbiter, depositor, expires, permitted, details, claimHash, witness, witnessHash, signature
bytes calldata signature,
bytes calldata context // allocator signature
) external returns (Lock[] memory) {
(Lock[] memory commitments,, bool containsAdditionalCommitments) = AL.permit2Allocation(
arbiter,
depositor,
expires,
permitted,
additionalCommitmentAmounts,
details,
claimHash,
witness,
witnessHash,
signature
);

if (containsAdditionalCommitments) {
// Validate the allocator's signature for the additional commitments
_validateContext(commitments, additionalCommitmentAmounts, claimHash, context);
}

// Allocate the claim
claims[claimHash] = true;

emit Allocated(depositor, commitments, details.nonce, expires, claimHash);

return commitments;
}

/// @inheritdoc IOnChainAllocation
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) {
uint88 nonce88 = nonces + 1;

nonce =
AL.prepareAllocation(nonce88, recipient, idsAndAmounts, arbiter, expires, typehash, witness, ALLOCATOR_ID);
if (context.length > 0) {
// Potential off chain nonce provided
HybridAllocationContext calldata allocationContext = _decodeContext(context);
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure whether or not this is going to work; do we have any tests of this functionality yet? the decode-bytes-to-calldata logic is always a little gnarly/counterintuitive

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for looking so detailed at this!
Had all the tests and some tiny fixes committed, but forgot to push this.
I believe this is working as intended, I came up with a similar design for the ERC7683 order data parsing.

This is the setup and the test for this:

function _encodeHybridAllocationContext(uint256 nonce, bytes memory signature)

function test_prepareAndExecuteAllocation_withAdditionalCommitments() public {

I don't have any tests that check the revert scenarios on this _decodeContext function yet though.


// Verify the nonce is scoped to an off chain allocation and to the recipient
AL.verifyNonce(allocationContext.nonce, AL.OFF_CHAIN_NONCE, recipient);
nonce = allocationContext.nonce;
} else {
// No off chain nonce provided, use an on chain nonce
uint88 nonce88 = nonces + 1;
nonce = AL.getNonceWithCommand(AL.ON_CHAIN_NONCE, nonce88);
}
AL.prepareAllocation(
nonce,
recipient,
idsAndAmounts,
additionalCommitmentAmounts,
arbiter,
expires,
typehash,
witness,
ALLOCATOR_ID
);
}

/// @inheritdoc IOnChainAllocation
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 {
uint88 nonce88 = ++nonces;
uint256 nonce;
bytes32 claimHash;
Lock[] memory commitments;
bool containsAdditionalCommitments;

if (context.length > 0) {
// Off chain nonce and additional commitments amounts provided
HybridAllocationContext calldata allocationContext = _decodeContext(context);

// Verify the nonce is scoped to an off chain allocation and to the recipient
AL.verifyNonce(allocationContext.nonce, AL.OFF_CHAIN_NONCE, recipient);

nonce = allocationContext.nonce;
(claimHash, commitments,, containsAdditionalCommitments) = AL.executeAllocation(
nonce, recipient, idsAndAmounts, additionalCommitmentAmounts, arbiter, expires, typehash, witness
);
// Validate the signers signature for the hybrid allocation context
_validateContext(commitments, additionalCommitmentAmounts, claimHash, allocationContext.signature);
} else {
// No off chain nonce provided, use an on chain nonce
uint88 nonce88 = ++nonces;
nonce = AL.getNonceWithCommand(AL.ON_CHAIN_NONCE, nonce88);
(claimHash, commitments,, containsAdditionalCommitments) = AL.executeAllocation(
nonce, recipient, idsAndAmounts, additionalCommitmentAmounts, arbiter, expires, typehash, witness
);
if (containsAdditionalCommitments) {
revert InvalidSignature();
}
}

(bytes32 claimHash, Lock[] memory commitments, uint256 nonce) =
AL.executeAllocation(nonce88, recipient, idsAndAmounts, arbiter, expires, typehash, witness);
// If the claim was already allocated, skip the allocation and the event emission
if (claims[claimHash]) {
return;
}

// Allocate the claim
claims[claimHash] = true;
Expand Down Expand Up @@ -372,4 +445,87 @@ contract HybridAllocator is IHybridAllocator {
digest := keccak256(add(m, 0x1e), 0x42)
}
}

function _decodeContext(bytes calldata context)
internal
pure
returns (HybridAllocationContext calldata allocationContext)
{
assembly ("memory-safe") {
// context structure
// 0x00: HybridAllocationContext.offset (0x20)
// 0x20: HybridAllocationContext.nonce
// 0x40: HybridAllocationContext.signature.offset (0x40 relative to struct start at 0x20)
// 0x60: HybridAllocationContext.signature.length
// 0x80: HybridAllocationContext.signature.content

// required length must be 0x80 + signature length of 64 or 96 bytes (65 bytes will be padded to 96 bytes)

let minimumLength := 0xc0

let errorBuffer := or(lt(context.length, minimumLength), gt(context.length, add(minimumLength, 0x20))) // check length of context is valid
errorBuffer := or(errorBuffer, xor(calldataload(add(context.offset, 0x40)), 0x40)) // check signature offset is valid (0x40 relative to struct start)

// Check the signature is valid
let calldataSignatureLength := calldataload(add(context.offset, 0x60))
errorBuffer := or(errorBuffer, or(lt(calldataSignatureLength, 0x40), gt(calldataSignatureLength, 0x41))) // check signature length is valid (must be 64 or 65 bytes)
if errorBuffer { revert(0x00, 0x00) }

allocationContext := add(context.offset, 0x20)
}
}

function _validateContext(
Lock[] memory commitments,
uint256[] calldata additionalCommitmentAmounts,
bytes32 claimHash,
bytes calldata allocatorSignature
) internal view {
bytes32 hybridAllocationHash;
bytes32[] memory commitmentsHashes = new bytes32[](commitments.length);

// Create the hybrid allocation context hash
assembly ("memory-safe") {
// hybrid allocation context hash:
// 0x00: typehash
// 0x20: claimHash
// 0x40: additionalCommitments hash

let m := mload(0x40)
mstore(m, HYBRID_ALLOCATION_CONTEXT_TYPEHASH) // typehash
mstore(add(m, 0x20), claimHash) // claimHash

// Create the commitments hash
// Use the commitments lockTag and token, but the amount from additionalCommitmentAmounts
let freeMemoryPointer := add(m, 0x60)
let commitmentsLength := mload(commitments)
// Populate all thecommitmentHashes
mstore(freeMemoryPointer, LOCK_TYPEHASH)
for { let i := 0 } lt(i, commitmentsLength) { i := add(i, 1) } {
let commitmentOffset := mload(add(add(commitments, 0x20), mul(i, 0x20)))
mstore(add(freeMemoryPointer, 0x20), mload(commitmentOffset)) // lockTag from commitments
mstore(add(freeMemoryPointer, 0x40), mload(add(commitmentOffset, 0x20))) // token from commitments
mstore(
add(freeMemoryPointer, 0x60), calldataload(add(additionalCommitmentAmounts.offset, mul(i, 0x20)))
) // amount from additionalCommitmentAmounts
let commitmentsHashPointer := add(add(commitmentsHashes, 0x20 /* skip length */ ), mul(i, 0x20))
mstore(commitmentsHashPointer, keccak256(freeMemoryPointer, 0x80))
}

// Create the commitments hash: keccak256(abi.encodePacked(commitmentsHashes))
mstore(
add(m, 0x40), keccak256(add(commitmentsHashes, 0x20 /* skip length */ ), mul(commitmentsLength, 0x20))
)

hybridAllocationHash := keccak256(m, 0x60)
}
bytes32 digest = _deriveDigest(hybridAllocationHash, _COMPACT_DOMAIN_SEPARATOR);
if (block.chainid != _INITIAL_CHAIN_ID) {
// If the chain was forked, we can not use the cached domain separator
digest = _deriveDigest(claimHash, ITheCompact(AL.THE_COMPACT).DOMAIN_SEPARATOR());
}
if (!_checkSignature(digest, allocatorSignature)) {
revert InvalidSignature();
}
}
}
Loading