Skip to content

Commit

Permalink
add emissary functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
0age committed Oct 24, 2024
1 parent 8102fed commit f950384
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 18 deletions.
73 changes: 69 additions & 4 deletions src/TheCompact.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import {
ExogenousQualifiedSplitBatchMultichainClaimWithWitness
} from "./types/BatchMultichainClaims.sol";

import { PERMIT2_DEPOSIT_WITNESS_FRAGMENT_HASH } from "./types/EIP712Types.sol";
import { PERMIT2_DEPOSIT_WITNESS_FRAGMENT_HASH, EMISSARY_ASSIGNMENT_TYPEHASH } from "./types/EIP712Types.sol";

import { SplitComponent, TransferComponent, SplitByIdComponent, BatchClaimComponent, SplitBatchClaimComponent } from "./types/Components.sol";

Expand Down Expand Up @@ -207,6 +207,9 @@ contract TheCompact is ITheCompact, ERC6909, Extsload, Tstorish {
// TODO: optimize
mapping(address => mapping(bytes32 => bool)) private _registeredClaimHashes;

// TODO: optimize
mapping(address => mapping(address => bool)) private _emissaries;

uint256 private immutable _INITIAL_CHAIN_ID;
bytes32 private immutable _INITIAL_DOMAIN_SEPARATOR;
MetadataRenderer private immutable _METADATA_RENDERER;
Expand Down Expand Up @@ -895,14 +898,64 @@ contract TheCompact is ITheCompact, ERC6909, Extsload, Tstorish {
return true;
}

function registerFor(address sponsor, bytes32 claimHash) external returns (bool) {
// TODO: optimize
if (!_emissaries[sponsor][msg.sender]) {
revert InvalidEmissary(sponsor, msg.sender);
}
_registeredClaimHashes[sponsor][claimHash] = true;
return true;
}

function registerFor(address sponsor, bytes32[] calldata claimHashes) external returns (bool) {
// TODO: optimize
if (!_emissaries[sponsor][msg.sender]) {
revert InvalidEmissary(sponsor, msg.sender);
}

unchecked {
uint256 totalClaimHashes = claimHashes.length;
for (uint256 i = 0; i < totalClaimHashes; ++i) {
_registeredClaimHashes[msg.sender][claimHashes[i]] = true;
}
}

return true;
}

function assignEmissary(address sponsor, address emissary, uint256 nonce, uint256 expires, bool assigned, bytes calldata sponsorSignature) external returns (bool) {
expires.later();

bytes32 messageHash;
assembly ("memory-safe") {
let m := mload(0x40) // Grab the free memory pointer; memory will be left dirtied.
mstore(m, EMISSARY_ASSIGNMENT_TYPEHASH)
mstore(add(m, 0x20), emissary)
mstore(add(m, 0x40), nonce)
mstore(add(m, 0x60), expires)
mstore(add(m, 0x80), assigned)
messageHash := keccak256(m, 0xa0)
}

messageHash.signedBy(sponsor, sponsorSignature, _INITIAL_DOMAIN_SEPARATOR.toLatest(_INITIAL_CHAIN_ID));

nonce.consumeNonceAsSponsor(sponsor);

return _assignEmissary(sponsor, emissary, assigned);
}

function assignEmissary(address emissary, bool assigned) external returns (bool) {
return _assignEmissary(msg.sender, emissary, assigned);
}

function consume(uint256[] calldata nonces) external returns (bool) {
// NOTE: this may not be necessary, consider removing
msg.sender.usingAllocatorId().mustHaveARegisteredAllocator();

unchecked {
uint256 noncesLength = nonces.length;
for (uint256 i = 0; i < noncesLength; ++i) {
nonces[i].consumeNonce(msg.sender);
nonces[i].consumeNonceAsAllocator(msg.sender);
}
}

Expand Down Expand Up @@ -938,8 +991,12 @@ contract TheCompact is ITheCompact, ERC6909, Extsload, Tstorish {
scope = id.toScope();
}

function check(uint256 nonce, address allocator) external view returns (bool consumed) {
consumed = allocator.hasConsumed(nonce);
function hasConsumedAllocatorNonce(uint256 nonce, address allocator) external view returns (bool consumed) {
consumed = allocator.hasConsumedAllocatorNonce(nonce);
}

function hasConsumedEmissaryAssignmentNonce(uint256 nonce, address sponsor) external view returns (bool consumed) {
consumed = sponsor.hasConsumedEmissaryAssignmentNonce(nonce);
}

function DOMAIN_SEPARATOR() external view returns (bytes32 domainSeparator) {
Expand Down Expand Up @@ -979,6 +1036,14 @@ contract TheCompact is ITheCompact, ERC6909, Extsload, Tstorish {
_emitClaim(msg.sender, messageHash, allocator);
}

function _assignEmissary(address sponsor, address emissary, bool assigned) internal returns (bool) {
_emissaries[sponsor][emissary] = assigned;

emit EmissaryAssignment(sponsor, emissary, assigned);

return true;
}

function _processBasicTransfer(BasicTransfer calldata transfer, function(address, address, uint256, uint256) internal returns (bool) operation) internal returns (bool) {
_notExpiredAndSignedByAllocator(transfer.toMessageHash(), transfer.id.toRegisteredAllocatorWithConsumed(transfer.nonce), transfer);

Expand Down
14 changes: 13 additions & 1 deletion src/interfaces/ITheCompact.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ interface ITheCompact {
event ForcedWithdrawalEnabled(address indexed account, uint256 indexed id, uint256 withdrawableAt);
event ForcedWithdrawalDisabled(address indexed account, uint256 indexed id);
event AllocatorRegistered(uint96 allocatorId, address allocator);
event EmissaryAssignment(address indexed sponsor, address indexed emissary, bool assigned);

error InvalidToken(address token);
error Expired(uint256 expiration);
Expand All @@ -98,6 +99,7 @@ interface ITheCompact {
error InvalidScope(uint256 id);
error InvalidDepositTokenOrdering();
error InvalidDepositBalanceChange();
error InvalidEmissary(address sponsor, address emissary);

function deposit(address allocator) external payable returns (uint256 id);

Expand Down Expand Up @@ -352,6 +354,14 @@ interface ITheCompact {

function register(bytes32[] calldata claimHashes) external returns (bool);

function registerFor(address sponsor, bytes32 claimHash) external returns (bool);

function registerFor(address sponsor, bytes32[] calldata claimHashes) external returns (bool);

function assignEmissary(address sponsor, address emissary, uint256 nonce, uint256 expires, bool assigned, bytes calldata sponsorSignature) external returns (bool);

function assignEmissary(address emissary, bool assigned) external returns (bool);

function consume(uint256[] calldata nonces) external returns (bool);

function __registerAllocator(address allocator, bytes calldata proof) external returns (uint96 allocatorId);
Expand All @@ -360,7 +370,9 @@ interface ITheCompact {

function getLockDetails(uint256 id) external view returns (address token, address allocator, ResetPeriod resetPeriod, Scope scope);

function check(uint256 nonce, address allocator) external view returns (bool consumed);
function hasConsumedAllocatorNonce(uint256 nonce, address allocator) external view returns (bool consumed);

function hasConsumedEmissaryAssignmentNonce(uint256 nonce, address sponsor) external view returns (bool consumed);

function DOMAIN_SEPARATOR() external view returns (bytes32 domainSeparator);

Expand Down
27 changes: 22 additions & 5 deletions src/lib/ConsumerLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,28 @@
pragma solidity ^0.8.27;

library ConsumerLib {
bytes4 private constant _CONSUMER_NONCE_SCOPE = 0x153dc4d7;
bytes4 private constant _ALLOCATOR_NONCE_SCOPE = 0x03f37b1a;
bytes4 private constant _SPONSOR_NONCE_SCOPE = 0x8ccd9613;

error InvalidNonce(address account, uint256 nonce);

function consumeNonce(uint256 nonce, address account) internal {
function consumeNonceAsAllocator(uint256 nonce, address allocator) internal {
_consumeNonce(nonce, allocator, _ALLOCATOR_NONCE_SCOPE);
}

function isConsumedByAllocator(uint256 nonceToCheck, address allocator) internal view returns (bool consumed) {
return _isConsumedBy(nonceToCheck, allocator, _ALLOCATOR_NONCE_SCOPE);
}

function consumeNonceAsSponsor(uint256 nonce, address sponsor) internal {
_consumeNonce(nonce, sponsor, _SPONSOR_NONCE_SCOPE);
}

function isConsumedBySponsor(uint256 nonceToCheck, address sponsor) internal view returns (bool consumed) {
return _isConsumedBy(nonceToCheck, sponsor, _SPONSOR_NONCE_SCOPE);
}

function _consumeNonce(uint256 nonce, address account, bytes4 scope) internal {
// The last byte of the nonce is used to assign a bit in a 256-bit bucket;
// specific nonces are consumed for each account and can only be used once.
// NOTE: this function temporarily overwrites the free memory pointer, but
Expand All @@ -16,7 +33,7 @@ library ConsumerLib {

// slot: keccak256(_CONSUMER_NONCE_SCOPE ++ account ++ nonce[0:31])
mstore(0x20, account)
mstore(0x0c, _CONSUMER_NONCE_SCOPE)
mstore(0x0c, scope)
mstore(0x40, nonce)
let bucketSlot := keccak256(0x28, 0x37)

Expand All @@ -34,13 +51,13 @@ library ConsumerLib {
}
}

function isConsumedBy(uint256 nonceToCheck, address account) internal view returns (bool consumed) {
function _isConsumedBy(uint256 nonceToCheck, address account, bytes4 scope) internal view returns (bool consumed) {
assembly ("memory-safe") {
let freeMemoryPointer := mload(0x40)

// slot: keccak256(_CONSUMER_NONCE_SCOPE ++ account ++ nonce[0:31])
mstore(0x20, account)
mstore(0x0c, _CONSUMER_NONCE_SCOPE)
mstore(0x0c, scope)
mstore(0x40, nonceToCheck)
consumed := and(shl(and(0xff, nonceToCheck), 1), sload(keccak256(0x28, 0x37)))

Expand Down
12 changes: 8 additions & 4 deletions src/lib/ValidityLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,20 @@ library ValidityLib {

function fromRegisteredAllocatorIdWithConsumed(uint96 allocatorId, uint256 nonce) internal returns (address allocator) {
allocator = allocatorId.toRegisteredAllocator();
nonce.consumeNonce(allocator);
nonce.consumeNonceAsAllocator(allocator);
}

function toRegisteredAllocatorWithConsumed(uint256 id, uint256 nonce) internal returns (address allocator) {
allocator = id.toAllocator();
nonce.consumeNonce(allocator);
nonce.consumeNonceAsAllocator(allocator);
}

function hasConsumed(address allocator, uint256 nonce) internal view returns (bool) {
return nonce.isConsumedBy(allocator);
function hasConsumedAllocatorNonce(address allocator, uint256 nonce) internal view returns (bool) {
return nonce.isConsumedByAllocator(allocator);
}

function hasConsumedEmissaryAssignmentNonce(address sponsor, uint256 nonce) internal view returns (bool) {
return nonce.isConsumedBySponsor(sponsor);
}

function excludingNative(address token) internal pure returns (address) {
Expand Down
21 changes: 17 additions & 4 deletions src/types/EIP712Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct Compact {
// Optional witness may follow.
}

// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)")
// keccak256(bytes("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"))
bytes32 constant COMPACT_TYPEHASH = 0xcdca950b17b5efc016b74b912d8527dfba5e404a688cbc3dab16cb943287fec2;

// abi.decode(bytes("Compact(address arbiter,address "), (bytes32))
Expand All @@ -40,7 +40,7 @@ struct BatchCompact {
// Optional witness may follow.
}

// keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts)")
// keccak256(bytes("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts)"))
bytes32 constant BATCH_COMPACT_TYPEHASH = 0x5a7fee8000a237929ef9be08f2933c4b4f320b00b38809f3c7aa104d5421049f;

// abi.decode(bytes("BatchCompact(address arbiter,add"), (bytes32))
Expand All @@ -67,7 +67,7 @@ struct Segment {
// Optional witness may follow.
}

// keccak256("Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts)")
// keccak256(bytes("Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts)"))
bytes32 constant SEGMENT_TYPEHASH = 0x295feb095767cc67d7e74695da0adaddede54d7b7194a8a5426fe8f0351e0337;

// Message signed by the sponsor that specifies the conditions under which a set of
Expand All @@ -81,7 +81,7 @@ struct MultichainCompact {
Segment[] segments; // Arbiter, chainId, ids & amounts, and witness for each chain.
}

// keccak256("MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts)")
// keccak256(bytes("MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Segment[] segments)Segment(address arbiter,uint256 chainId,uint256[2][] idsAndAmounts)"))
bytes32 constant MULTICHAIN_COMPACT_TYPEHASH = 0x5ca9a66b8bbf0d2316e90dfa3df465f0790b277b25393a3ef4d67e1f50865057;

// abi.decode(bytes("MultichainCompact(address sponso"), (bytes32))
Expand All @@ -105,6 +105,19 @@ uint176 constant MULTICHAIN_COMPACT_TYPESTRING_FRAGMENT_FIVE = 0x35365b325d5b5d2
// If additional parameters are not required, the allocator instead signs the same
// payload as the sponsor.

// An Emissary is an account that is authorized by a sponsor to register claims.
// This could be a contract that facilitates the creation of dynamic claims, or
// could relay multichain claims registerd on other domains.
struct EmissaryAssignment {
address emissary; // The account to assign as the emissary.
uint256 nonce; // A parameter to enforce replay protection, scoped to sponsor.
uint256 expires; // The time at which the assignment expires.
bool assigned; // Whether to assign the emissary or to unassign them.
}

// keccak256(bytes("EmissaryAssignment(address emissary,uint256 nonce,uint256 expires)"))
bytes32 constant EMISSARY_ASSIGNMENT_TYPEHASH = 0x5ca9a66b8bbf0d2316e90dfa3df465f0790b277b25393a3ef4d67e1f50865057;

/// @dev `keccak256(bytes("CompactDeposit(address depositor,address allocator,uint8 resetPeriod,uint8 scope,address recipient)"))`.
bytes32 constant PERMIT2_DEPOSIT_WITNESS_FRAGMENT_HASH = 0x0091bfc8f1539e204529602051ae82f3e6c6f0f86d0227c9ea890616cedbe646;

Expand Down

0 comments on commit f950384

Please sign in to comment.