From 4ef4f60af0596edcf071d7ce5d3b56d8bc417c9e Mon Sep 17 00:00:00 2001 From: vimageDE Date: Tue, 21 Jan 2025 16:18:56 +0100 Subject: [PATCH] wip draft --- .gitignore | 2 + src/core/TheCompactCore.sol | 116 +++++++++ src/core/lib/Deposit.sol | 401 +++++++++++++++++++++++++++++ src/core/lib/Errors.sol | 16 ++ src/interfaces/IAllocator.sol | 10 +- src/interfaces/ITheCompactCore.sol | 157 +++++++++++ 6 files changed, 700 insertions(+), 2 deletions(-) create mode 100644 src/core/TheCompactCore.sol create mode 100644 src/core/lib/Deposit.sol create mode 100644 src/core/lib/Errors.sol create mode 100644 src/interfaces/ITheCompactCore.sol diff --git a/.gitignore b/.gitignore index 85198aa..16868cc 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ docs/ # Dotenv file .env + +.DS_STORE \ No newline at end of file diff --git a/src/core/TheCompactCore.sol b/src/core/TheCompactCore.sol new file mode 100644 index 0000000..c0aab45 --- /dev/null +++ b/src/core/TheCompactCore.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { ERC6909 } from "solady/tokens/ERC6909.sol"; +import { Scope } from "../types/Scope.sol"; +import { ResetPeriod } from "../types/ResetPeriod.sol"; +import { Deposit } from "./lib/Deposit.sol"; +import { ITheCompactCore } from "../interfaces/ITheCompactCore.sol"; + +contract TheCompactCore is ERC6909, Deposit { + + error InvalidToken(); + + function deposit(address allocator, Scope scope, ResetPeriod resetPeriod, address recipient) external payable returns (uint256 id) { + return _deposit(address(0), msg.value, allocator, scope, resetPeriod, recipient); + } + + function deposit(address token, uint256 amount, address allocator, ResetPeriod resetPeriod, Scope scope, address recipient) external returns (uint256) { + // Collects the tokens from the sender, reverts if the token is zero address. Returns the actual received amount + amount = _collect(token, amount, msg.sender); + return _deposit(token, amount, allocator, scope, resetPeriod, recipient); + } + + function register(ITheCompactCore.Compact calldata compact) external { + bytes32 digest = _compactDigest(compact); + _register(msg.sender, compact.sponsor, digest, compact.expires); + } + + function registerWithWitness(ITheCompactCore.Compact calldata compact, bytes32 witness, string calldata typeString) external { + bytes32 digest = _compactDigestWitness(compact, witness, typeString); + _register(msg.sender, compact.sponsor, digest, compact.expires); + } + + function transfer(address to, uint256 id, uint256 amount) public payable override returns (bool) { + _ensureAttested(msg.sender, to, id, amount); + return super.transfer(to, id, amount); + } + + function transferFrom(address from, address to, uint256 id, uint256 amount) public payable override returns (bool) { + _ensureAttested(from, to, id, amount); + return super.transferFrom(from, to, id, amount); + } + + function allocatedTransfer(ITheCompactCore.Transfer calldata transfer_, bytes calldata allocatorSignature) external returns (bool) { + uint256 length = _ensureBatchAttested(msg.sender, transfer_, allocatorSignature); + for(uint256 i = 0; i < length; ++i) { + _transfer(address(0), msg.sender, _castToAddress(transfer_.recipients[i].recipient), transfer_.recipients[i].id, transfer_.recipients[i].amount); + } + return true; + } + + function allocatedTransferFrom(ITheCompactCore.DelegatedTransfer calldata delegatedTransfer, bytes calldata allocatorSignature) external returns (bool) { + uint256 length = _ensureBatchAttested(msg.sender, delegatedTransfer.transfer, allocatorSignature); + for(uint256 i = 0; i < length; ++i) { + _transfer(msg.sender, delegatedTransfer.from, _castToAddress(delegatedTransfer.transfer.recipients[i].recipient), delegatedTransfer.transfer.recipients[i].id, delegatedTransfer.transfer.recipients[i].amount); + } + return true; + } + + // @notice Flexible withdrawal of tokens + // @dev Works for server based allocators and on chain allocators + // @dev On chain allocators can supply an empty bytes for the allocatorSignature + function withdrawal(ITheCompactCore.Transfer calldata withdrawal_, bytes calldata allocatorSignature) external returns (bool) { + uint256 length = _ensureBatchAttested(msg.sender, withdrawal_, allocatorSignature); + for(uint256 i = 0; i < length; ++i) { + _burn(msg.sender, withdrawal_.recipients[i].id, withdrawal_.recipients[i].amount); // reverts if insufficient balance + _distribute(withdrawal_.recipients[i].id, withdrawal_.recipients[i].amount, _castToAddress(withdrawal_.recipients[i].recipient)); + } + return true; + } + + // @notice Flexible withdrawal of tokens delegated by a sponsor + // @dev Works for server based allocators and on chain allocators + // @dev Requires an approval from the sender + function withdrawalFrom(ITheCompactCore.DelegatedTransfer calldata delegatedWithdrawal, bytes calldata sponsorSignature) external returns (bool) { + uint256 length = _ensureBatchAttested(msg.sender, delegatedWithdrawal.transfer, sponsorSignature); + for(uint256 i = 0; i < length; ++i) { + _checkApproval(msg.sender, delegatedWithdrawal.from, delegatedWithdrawal.transfer.recipients[i].id, delegatedWithdrawal.transfer.recipients[i].amount); + _burn(delegatedWithdrawal.from, delegatedWithdrawal.transfer.recipients[i].id, delegatedWithdrawal.transfer.recipients[i].amount); + _distribute(delegatedWithdrawal.transfer.recipients[i].id, delegatedWithdrawal.transfer.recipients[i].amount, _castToAddress(delegatedWithdrawal.transfer.recipients[i].recipient)); + } + return true; + } + + function claim(ITheCompactCore.Claim calldata claim_, bool withdraw) external returns (bool) { + (address allocator, ITheCompactCore.Compact memory compact) = _verifyClaim(claim_); + _verifySignatures(_compactDigest(compact), claim_.compact.sponsor, claim_.sponsorSignature, allocator, claim_.allocatorSignature); + uint256 length = compact.inputs.length; + for(uint256 i = 0; i < length; ++i) { + if(withdraw) { + _burn(claim_.compact.sponsor, claim_.compact.inputs[i].id, claim_.compact.inputs[i].amount); + _distribute(claim_.compact.inputs[i].id, claim_.compact.inputs[i].amount, _castToAddress(claim_.compact.inputs[i].recipient)); + } else { + _rebalance(claim_.compact.sponsor, _castToAddress(claim_.compact.inputs[i].recipient), claim_.compact.inputs[i].id, claim_.compact.inputs[i].amount, false); + // TODO: add event + } + } + return true; + } + + + /// @dev Returns the symbol for token `id`. + function name(uint256) public view virtual override returns (string memory) { + return ""; + } + + /// @dev Returns the symbol for token `id`. + function symbol(uint256) public view virtual override returns (string memory) { + return ""; + } + + /// @dev Returns the Uniform Resource Identifier (URI) for token `id`. + function tokenURI(uint256) public view virtual override returns (string memory) { + return ""; + } +} \ No newline at end of file diff --git a/src/core/lib/Deposit.sol b/src/core/lib/Deposit.sol new file mode 100644 index 0000000..58bdc6b --- /dev/null +++ b/src/core/lib/Deposit.sol @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; +import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol"; +import { IAllocator } from "../../interfaces/IAllocator.sol"; +import { ITheCompactCore } from "../../interfaces/ITheCompactCore.sol"; +import { IdLib } from "../../lib/IdLib.sol"; +import { Scope } from "../../types/Scope.sol"; +import { ResetPeriod } from "../../types/ResetPeriod.sol"; +import { Errors } from "./Errors.sol"; + +contract Deposit { + using SafeTransferLib for address; + + function _deposit(address token, uint256 amount, address allocator, Scope scope, ResetPeriod resetPeriod, address recipient) internal returns (uint256 id) { + id = IdLib.toIdIfRegistered(token, scope, resetPeriod, allocator); + _addBalance(recipient, id, amount, true); + } + + function _collect(address token, uint256 amount, address from) internal returns (uint256 amountCollected) { + // TODO: Implement reentrancy guard + + if(token == address(0)) { + revert Errors.InvalidToken(); + } + uint256 initialBalance = token.balanceOf(address(this)); + // transfer tokens to this contract + token.safeTransferFrom(from, address(this), amount); + uint256 finalBalance = token.balanceOf(address(this)); + if (initialBalance >= finalBalance) { + revert Errors.InvalidBalanceChange(initialBalance, finalBalance); + } + return finalBalance - initialBalance; + } + + function _distribute(uint256 id, uint256 amount, address to) internal { + // TODO: Implement reentrancy guard + + address token = IdLib.toToken(id); + token.safeTransfer(to, amount); + } + + /// TODO: Move everything below to a separate transfer contract + + // Storage scope for active registrations: + // slot: keccak256(_ACTIVE_REGISTRATIONS_SCOPE ++ sponsor ++ claimHash ++ typehash) => expires. + uint256 private constant _ACTIVE_REGISTRATIONS_SCOPE = 0x68a30dd0; + uint32 private constant _MAX_EXPIRATION = 30 days; + + + function _register(address caller, address sponsor, bytes32 digest, uint256 expires) internal { + if(caller != sponsor) { + revert Errors.NotSponsor(caller, sponsor); + } + bytes32 slot = keccak256(abi.encode(_ACTIVE_REGISTRATIONS_SCOPE, sponsor, digest)); + uint256 currentExpiration; + assembly ("memory-safe") { + currentExpiration := sload(slot) + } + if(currentExpiration > expires || expires > block.timestamp + _MAX_EXPIRATION) { + revert Errors.InvalidRegistrationDuration(expires); + } + assembly ("memory-safe") { + sstore(slot, expires) + } + } + + function _verifyClaim(ITheCompactCore.Claim calldata claim_) internal view returns (address allocator, ITheCompactCore.Compact memory compact) { + if(msg.sender != claim_.compact.arbiter) { + revert Errors.NotArbiter(msg.sender, claim_.compact.arbiter); + } + compact = claim_.compact; + allocator = IdLib.toAllocator(claim_.compact.inputs[0].id); + uint256 length = compact.inputs.length; + for(uint256 i = 0; i < length; ++i) { + // If the last bit is set, the recipient was unknown and the arbiter is responsible for setting the recipient + if(_lastBitIsSet(compact.inputs[i].recipient)) { + // Remove the recipient from the compact, because it was not signed for by the sponsor and allocator + compact.inputs[i].recipient = ""; + } + // Ensure all inputs are from the same allocator + if(allocator != IdLib.toAllocator(compact.inputs[i].id)) { + revert Errors.AllocatorMismatch(allocator, IdLib.toAllocator(compact.inputs[i].id)); + } + } + } + + // abi.decode(bytes("Compact(address arbiter,address "), (bytes32)) + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_ONE = 0x436f6d70616374286164647265737320617262697465722c6164647265737320; + // abi.decode(bytes("sponsor,uint256 nonce,uint256 ex"), (bytes32)) + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_TWO = 0x73706f6e736f722c75696e74323536206e6f6e63652c75696e74323536206578; + // abi.decode(bytes("pires,uint256 id,uint256 amount)"), (bytes32)) + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_THREE = 0x70697265732c75696e743235362069642c75696e7432353620616d6f756e7429; + + // bytes32 compactEIP712DomainHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + // bytes32 domainSeparator = keccak256(abi.encode(compactEIP712DomainHash, keccak256(bytes("The Compact")), keccak256(bytes("0")), block.chainid, address(this))); + bytes32 constant DOMAIN_SEPARATOR = 0x423efda6f5a4d5cd578a57b46b5306d04ae04f054e798cb0cd6074f08bf583ee; + + // keccak256("Compact(uint256 chainId,address arbiter,address sponsor,uint256 nonce,uint256 expires,Allocation[] inputs)"); + bytes32 constant COMPACT_TYPEHASH = 0x0fee4917c24cc0706c3f2cb7a7b89603d1fef1a7efb46bd67061fe47d0f8df1b; + + function _compactDigest(ITheCompactCore.Compact memory compact) internal pure returns (bytes32) { + return keccak256( + abi.encodePacked( + bytes2(0x1901), + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + COMPACT_TYPEHASH, + compact.chainId, + compact.arbiter, + compact.sponsor, + compact.nonce, + compact.expires, + compact.inputs + ) + ) + ) + ); + } + + function _compactDigestWitness(ITheCompactCore.Compact calldata compact, bytes32 witness, string calldata typeString) internal pure returns (bytes32) { + return keccak256( + abi.encodePacked( + bytes2(0x1901), + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + keccak256(bytes(typeString)), + compact.chainId, + compact.arbiter, + compact.sponsor, + compact.nonce, + compact.expires, + compact.inputs, + witness + ) + ) + ) + ); + } + + bytes4 constant SIGNATURE_MAGIC_VALUE = 0x1626ba7e; + + function _verifySignatures(bytes32 digest, address sponsor, bytes calldata sponsorSignature, address allocator, bytes calldata allocatorSignature) internal view { + // Check if the digest was registered + bytes32 slot = keccak256(abi.encode(_ACTIVE_REGISTRATIONS_SCOPE, sponsor, digest)); + uint256 currentExpiration; + assembly ("memory-safe") { + currentExpiration := sload(slot) + } + if(currentExpiration < block.timestamp) { + if(!SignatureCheckerLib.isValidSignatureNowCalldata(sponsor, digest, sponsorSignature)) { + revert Errors.InvalidSignature(sponsor, sponsorSignature); + } + } + if(!SignatureCheckerLib.isValidSignatureNowCalldata(allocator, digest, allocatorSignature)) { + if(IAllocator(allocator).isValidSignature(digest, allocatorSignature) != SIGNATURE_MAGIC_VALUE) { + revert Errors.InvalidSignature(allocator, allocatorSignature); + } + } + } + + + // // TODO: First concept was to sort the type string structs, but likely unnecessary. Easier if User provides the full type string. + // string constant COMPACT_TYPESTRING = "Compact(uint256 chainId,address arbiter,address sponsor,uint256 nonce,uint256 expires,Allocation[] inputs)"; + // string constant COMPACT_WITNESS_TYPESTRING = "Compact(uint256 chainId,address arbiter,address sponsor,uint256 nonce,uint256 expires,Allocation[] inputs,Witness witness)"; + // string constant ALLOCATION_TYPESTRING = "Allocation(uint256 id,uint256 amount,address recipient)"; + // string constant WITNESS_TYPESTRING_FRAGMENT_ONE = "Witness("; + // string constant WITNESS_TYPESTRING_FRAGMENT_TWO = ")"; + // // Value of the first two bytes of the Compact type string + // uint16 constant COMPACT_VALUE = 17263; + // // Value of the first two bytes of the Allocation type string + // uint16 constant ALLOCATION_VALUE = 16748; + // // Value of the first two bytes of the Witness type string + // uint16 constant WITNESS_VALUE = 22377; + // function _typeHashWitness(string calldata typeString, string[] calldata structTypestrings) internal returns (bytes32) { + + // uint16 currentValue; + // uint16 nextValue = ALLOCATION_VALUE; + // uint256 structTypestringsLength = structTypestrings.length; + + // bytes memory currentTypestring; + + // for(uint256 i = 0; i < structTypestringsLength; ++i) { + // uint16 value = _getFirstTwoBytes(structTypestrings[i]); + // if(value < currentValue) { + // revert Errors.InvalidStructTypestringOrder(structTypestrings[i]); + // } + // if(value < nextValue) { + // currentTypestring = abi.encodePacked(bytes(currentTypestring), structTypestrings[i]); + // } else if(value == nextValue) { + // revert Errors.InvalidStructName(structTypestrings[i]); + // } else if(value > nextValue) { + // if(nextValue == ALLOCATION_VALUE) { + // currentTypestring = abi.encodePacked(bytes(currentTypestring), ALLOCATION_TYPESTRING); + // nextValue = COMPACT_VALUE; + // } else if (nextValue == COMPACT_VALUE) { + // currentTypestring = abi.encodePacked(bytes(currentTypestring), COMPACT_TYPESTRING); + // nextValue = WITNESS_VALUE; + // } else { + // currentTypestring = abi.encodePacked(bytes(currentTypestring), WITNESS_TYPESTRING_FRAGMENT_ONE); + + // } + // } + // currentValue = value; + // } + + // return keccak256(abi.encode(typeString)); + // } + + // function _getFirstTwoBytes(string calldata typeString) internal pure returns (uint16) { + // uint16 result; + // assembly ("memory-safe") { + // // Load first two bytes from calldata + // result := shr(240, calldataload(typeString.offset)) + // } + // return result; + // } + + + // bytes4(keccak256("attest(address,address,address,uint256,uint256)")) + bytes4 private constant _ATTEST_SELECTOR = 0x1a808f91; + // bytes4(keccak256("attest(address,address,address[],uint256[],uint256[],uint256,uint256,bytes)")) + bytes4 private constant _ATTEST_BATCH_SELECTOR = 0x9da23c98; + // Storage slot seed for ERC6909 state, used in computing balance slots. + uint256 private constant _ERC6909_MASTER_SLOT_SEED = 0xedcaa89a82293940; + + // keccak256(bytes("Transfer(address,address,address,uint256,uint256)")). + uint256 private constant _TRANSFER_EVENT_SIGNATURE = 0x1b3d7edb2e9c0b0e7c525b20aaaef0f5940d2ed71663c7d39266ecafac728859; + + + function _ensureAttested(address from, address to, uint256 id, uint256 amount) internal { + // Derive the allocator address from the supplied id. + address allocator = IdLib.toAllocator(id); + // Ensure the allocator attests the transfer. + if( IAllocator(allocator).attest(msg.sender, from, to, id, amount) != _ATTEST_SELECTOR) { + revert Errors.AllocatorDenied(allocator); + } + } + + function _ensureBatchAttested(address caller, ITheCompactCore.Transfer calldata transfer, bytes calldata allocatorSignature) internal returns (uint256 length) { + address expectedAllocator = IdLib.toAllocator(transfer.recipients[0].id); + // Ensure the allocator attests the transfers. + length = transfer.recipients.length; + address[] memory to = new address[](length); + uint256[] memory id = new uint256[](length); + uint256[] memory amount = new uint256[](length); + for(uint256 i = 0; i < length; ++i) { + address allocator = IdLib.toAllocator(id[i]); + if(expectedAllocator != allocator) { + revert Errors.AllocatorMismatch(expectedAllocator, allocator); + } + + to[i] = _castToAddress(transfer.recipients[i].recipient); + id[i] = transfer.recipients[i].id; + amount[i] = transfer.recipients[i].amount; + } + + if( IAllocator(expectedAllocator).attest(caller, caller, to, id, amount, transfer.nonce, transfer.expires, allocatorSignature) != _ATTEST_BATCH_SELECTOR) { + revert Errors.AllocatorDenied(expectedAllocator); + } + } + + function _castToAddress(bytes32 address_) internal pure returns (address output_) { + assembly ("memory-safe") { + output_ := shr(96, shl(96, address_)) + } + } + + function _lastBitIsSet(bytes32 value) internal pure returns (bool) { + // Shift right 255 bits and check if 1 + return uint256(value) >> 255 == 1; + } + + // @notice Reverts if the caller does not have approval. Reduces the allowance by the amount. + // @dev Copied from ERC6909.sol _transfer + function _checkApproval(address by, address from, uint256 id, uint256 amount) internal { + /// @solidity memory-safe-assembly + assembly { + let bitmaskAddress := 0xffffffffffffffffffffffffffffffffffffffff + // Compute the operator slot and load its value. + mstore(0x34, _ERC6909_MASTER_SLOT_SEED) + mstore(0x28, from) + // If `by` is not the zero address. + if and(bitmaskAddress, by) { + mstore(0x14, by) + // Check if the `by` is an operator. + if iszero(sload(keccak256(0x20, 0x34))) { + // Compute the allowance slot and load its value. + mstore(0x00, id) + let allowanceSlot := keccak256(0x00, 0x54) + let allowance_ := sload(allowanceSlot) + // If the allowance is not the maximum uint256 value. + if add(allowance_, 1) { + // Revert if the amount to be transferred exceeds the allowance. + if gt(amount, allowance_) { + mstore(0x00, 0xdeda9030) // `InsufficientPermission()`. + revert(0x1c, 0x04) + } + // Subtract and store the updated allowance. + sstore(allowanceSlot, sub(allowance_, amount)) + } + } + } + } + } + + + // @dev Adapts the ERC6909 balance without requiring an approval + // @dev Skips the _beforeTokenTransfer and _afterTokenTransfer hooks + function _rebalance(address from, address to, uint256 id, uint256 amount, bool triggerEvent) internal { + _removeBalance(from, id, amount, false); + _addBalance(to, id, amount, false); + if(triggerEvent) { + assembly ("memory-safe") { + // Emit the {Transfer} event. + mstore(0x00, caller()) + mstore(0x20, amount) + log4(0x00, 0x40, _TRANSFER_EVENT_SIGNATURE, caller(), shr(96, shl(96, to)), id) + } + } + } + + function _addBalance(address to, uint256 id, uint256 amount, bool triggerEvent) internal { + assembly ("memory-safe") { + // Compute the recipient's balance slot using the master slot seed. + mstore(0x20, _ERC6909_MASTER_SLOT_SEED) // length of 64 bits + mstore(0x14, to) // Length of 160 bits + mstore(0x00, id) // length of 256 bits + // -----------SLOT 1----------- -----------SLOT 2----------- + // master: | - 256 bits - | [0000000000000000000][--64 bits--] + // to: | - 160 bits - [[0000] | [---160 bits---]] + // id: | [---------256 bits---------] | - 256 bits - + + let toBalanceSlot := keccak256(0x00, 0x40) + + // Load current balance and compute new balance. + let toBalanceBefore := sload(toBalanceSlot) + let toBalanceAfter := add(toBalanceBefore, amount) + + // Revert on balance overflow. + if lt(toBalanceAfter, toBalanceBefore) { + mstore(0x00, 0x89560ca1) // `BalanceOverflow()`. + revert(0x1c, 0x04) + } + + // Store the updated balance. + sstore(toBalanceSlot, toBalanceAfter) + + if triggerEvent { + // Emit the Transfer event: + // - topic1: Transfer event signature + // - topic2: address(0) signifying a mint + // - topic3: recipient address (sanitized) + // - topic4: token id + // - data: [caller, amount] + mstore(0x00, caller()) + mstore(0x20, amount) + log4(0, 0x40, _TRANSFER_EVENT_SIGNATURE, 0, shr(0x60, shl(0x60, to)), id) + } + } + } + + function _removeBalance(address from, uint256 id, uint256 amount, bool triggerEvent) internal { + assembly ("memory-safe") { + // Compute the sender's balance slot using the master slot seed. + mstore(0x20, _ERC6909_MASTER_SLOT_SEED) + mstore(0x14, from) + mstore(0x00, id) + let fromBalanceSlot := keccak256(0x00, 0x40) + + // Load from sender's current balance. + let fromBalance := sload(fromBalanceSlot) + + // SAME COMMENT AS ABOVE, LETS UNIFY THIS BALANCE / SLOT RETRIEVAL LOGIC INTO ONE INTERNAL FUNCTION. + + // Revert if insufficient balance. + if gt(amount, fromBalance) { + mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`. + revert(0x1c, 0x04) + } + + // Subtract from current balance and store the updated balance. + sstore(fromBalanceSlot, sub(fromBalance, amount)) + + if triggerEvent { + // Emit the Transfer event: + // - topic1: Transfer event signature + // - topic2: sender address (sanitized) + // - topic3: address(0) signifying a burn + // - topic4: token id + // - data: [caller, amount] + mstore(0x00, caller()) + mstore(0x20, amount) + log4(0x00, 0x40, _TRANSFER_EVENT_SIGNATURE, shr(0x60, shl(0x60, from)), 0, id) + } + } + } +} diff --git a/src/core/lib/Errors.sol b/src/core/lib/Errors.sol new file mode 100644 index 0000000..e457de7 --- /dev/null +++ b/src/core/lib/Errors.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + + +library Errors { + error InvalidToken(); + error InvalidBalanceChange(uint256 initialBalance, uint256 finalBalance); + error AllocatorDenied(address allocator); + error InvalidRegistrationDuration(uint256 duration); + error InvalidStructTypestringOrder(string structTypestring); + error InvalidStructName(string structTypestring); + error NotSponsor(address caller, address sponsor); + error AllocatorMismatch(address expectedAllocator, address allocator); + error InvalidSignature(address signer, bytes signature); + error NotArbiter(address caller, address arbiter); +} diff --git a/src/interfaces/IAllocator.sol b/src/interfaces/IAllocator.sol index 9a488a1..af7048e 100644 --- a/src/interfaces/IAllocator.sol +++ b/src/interfaces/IAllocator.sol @@ -1,8 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { ITheCompactCore } from "./ITheCompactCore.sol"; -// NOTE: Allocators with smart contract implementations should also implement EIP1271. -interface IAllocator { +interface IAllocator is IERC1271 { // Called on standard transfers; must return this function selector (0x1a808f91). function attest(address operator, address from, address to, uint256 id, uint256 amount) external returns (bytes4); + + // Called on standard transfers; must return this function selector (0x9da23c98). + function attest(address operator, address from, address[] calldata to, uint256[] calldata id, uint256[] calldata amount, uint256 nonce, uint256 expires, bytes calldata allocatorSignature) external returns (bytes4); + + // isValidSignature of IERC1271 will be called during a claim and must verify the signature of the allocation. } diff --git a/src/interfaces/ITheCompactCore.sol b/src/interfaces/ITheCompactCore.sol new file mode 100644 index 0000000..3f3387d --- /dev/null +++ b/src/interfaces/ITheCompactCore.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { Scope } from "../types/Scope.sol"; +import { ResetPeriod } from "../types/ResetPeriod.sol"; + +interface ITheCompactCore { + + struct Compact { + uint256 chainId; // The chain Id of the allocated tokens + address arbiter; // The account tasked with verifying and submitting the claim. + address sponsor; // The account to source the tokens from. + uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + uint256 expires; // The time at which the claim expires. + Allocation[] inputs; // The inputs to the claim. + // Optional witness may follow. + } + + struct Claim { + Compact compact; // The compact to claim from. + string typeString; // The full type string of the claim, including potential witness data + bytes32 witness; // Hash of the witness data. + bytes allocatorSignature; // Authorization from the allocator. + bytes sponsorSignature; // Authorization from the sponsor. + } + + struct Allocation { + uint256 id; // The token ID of the ERC6909 token to allocate. + uint256 amount; // The amount of ERC6909 tokens to allocate. + bytes32 recipient; // The address to receive the tokens. + // NOTE: For claims, leave recipient empty if fillers are unknown. Must be a 160 bit address. + // Can be used to ensure known entities receive their share of the claim + } + + struct Transfer { + uint256 nonce; + uint256 expires; + Allocation[] recipients; + } + + struct DelegatedTransfer { + address from; + Transfer transfer; + } + + enum ForcedWithdrawalStatus { + Disabled, // Not pending or enabled for forced withdrawal + Pending, // Not yet available, but initiated + Enabled // Available for forced withdrawal on demand + } + + // @notice Deposit native tokens into the compact + // @dev can be used for a delegated deposit by setting a recipient + function deposit(address allocator, Scope scope, ResetPeriod resetPeriod, address recipient) external payable returns (uint256 id); + + // @notice Deposit ERC20 tokens into the compact + // @dev can be used for a delegated deposit by setting a recipient + function deposit(address token, uint256 amount, address allocator, ResetPeriod resetPeriod, Scope scope, address recipient) external returns (uint256 id); + + // @notice Register a Compact to skip the sponsors signature at claim time + // @dev Does not require a sponsor signature if the msg.sender is the sponsor + /// TODO: Figure out away to have a delegated register without the ability to maliciously set a claim for someone else without a sponsor signature + function register(Compact calldata compact) external; + + // @notice Register a Compact with a witness to skip the sponsors signature at claim time + // @dev Does not require a sponsor signature if the msg.sender is the sponsor + /// TODO: Figure out away to have a delegated register without the ability to maliciously set a claim for someone else without a sponsor signature + function registerWithWitness(Compact calldata compact, bytes32 witness, string calldata typeString) external; + + // @notice Overrides 6909 transfer function + // @dev Expects an on chain allocator + function transfer(address to, uint256 id, uint256 amount) external returns (bool); + + // @notice Overrides 6909 transfer function + // @dev Expects an on chain allocator + // @dev Requires an approval from the sender + function transferFrom(address from, address to, uint256 id, uint256 amount) external returns (bool); + + // @notice Flexible transfer of tokens + // @dev Server based allocators must use this function for transfers + function allocatedTransfer(Transfer calldata transfer) external returns (bool); + + // @notice Flexible transfer of tokens + // @dev Requires an approval from the sender + function allocatedTransferFrom(DelegatedTransfer calldata transfer, bytes calldata sponsorSignature) external returns (bool); + + // @notice Flexible withdrawal of tokens + // @dev Works for server based allocators and on chain allocators + function withdrawal(Transfer calldata transfer) external returns (bool); + + // @notice Flexible withdrawal of tokens delegated by a sponsor + // @dev Works for server based allocators and on chain allocators + // @dev Requires an approval from the sender + function withdrawalFrom(DelegatedTransfer calldata transfer, bytes calldata sponsorSignature) external returns (bool); + + // @notice Overrides 6909 setOperator function + // @notice Sets whether an operator is approved to manage the tokens of the caller + function setOperator(address operator, bool approved) external returns (bool); + + // @notice Overrides 6909 approve function + // @notice Approves a spender to spend tokens on behalf of the caller + function approve(address spender, uint256 id, uint256 amount) external returns (bool); + + // @notice Approves a spender to spend tokens on behalf of the caller + // @dev Approves a spender by a signature of the sponsor + function approveBySignature(address spender, uint256 id, uint256 amount, uint32 expires, bytes calldata signature) external returns (bool); + + // @notice Claims tokens from the compact + // @dev Only the arbiter can call this function + // @dev The first bit of the bytes32 recipient MUST be set to 1 by the arbiter, if the recipient was unknown to the sponsor + // and the arbiter was made responsible for setting the recipient. + // If the first bit is not set, the recipient was known to the sponsor / allocator and included in the signed data. + // @dev If the arbiter wants to split the claim even more, they may claim the tokens themselves and distribute them at will. + function claim(Claim calldata claim, bool withdraw) external returns (bool); + + // @notice Enables a forced withdrawal for a resource lock + // @dev Blocks new deposits for the resource lock + function enableForcedWithdrawal(uint256[] calldata ids) external returns (uint256 withdrawableAt); + + // @notice Disables a forced withdrawal for a resource lock + // @dev Unblocks new deposits for the resource lock + function disableForcedWithdrawal(uint256[] calldata ids) external returns (bool); + + // @notice Executes a forced withdrawal from a resource lock after the reset period has elapsed + // @dev Will withdraw all of the sponsors tokens from the resource lock + function forcedWithdrawal(uint256[] calldata ids, address recipient) external returns (bool); + + // @notice Consumes a set of nonces + // @dev Only callable by a registered allocator + function consume(uint256[] calldata nonces) external returns (bool); + + // @notice Registers an allocator + // @dev Can be called by anyone if one of three conditions is met: the caller is the allocator address being registered, + // the allocator address contains code, or a proof is supplied representing valid create2 deployment parameters that resolve to the supplied allocator address. + function __registerAllocator(address allocator, bytes calldata proof) external returns (uint96 allocatorId); + + // @notice Retrieves the fees for a claim + // @dev Allocators or an Arbiter may require a fee to be paid in order to process a claim + // @dev The fees must be included in the Compacts inputs if the allocator or arbiter require a fee + function getClaimFee(uint256[2][] calldata idAndAmount, bool allocator, bool arbiter) external view returns (uint256 allocatorFee, uint256 arbiterFee); + + // @notice Checks the forced withdrawal status of a resource lock for a given account + // @dev Returns both the current status (disabled, pending, or enabled) and the timestamp at which forced withdrawals will be enabled + // (if status is pending) or became enabled (if status is enabled). + function getForcedWithdrawalStatus(address account, uint256 id) external view returns (ForcedWithdrawalStatus status, uint256 availableAt); + + // @notice Checks whether a specific nonce has been consumed by an allocator + // @dev Once consumed, a nonce cannot be reused for claims mediated by that allocator + function hasConsumedAllocatorNonce(uint256 nonce, address allocator) external view returns (bool consumed); + + // @notice Returns the domain separator of the contract + function DOMAIN_SEPARATOR() external view returns (bytes32 domainSeparator); + + // @notice Returns the name of the contract + function name() external pure returns (string memory); + +} \ No newline at end of file