From 306dff4979e20c57228684d85f5818ef5029631e Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 31 Oct 2024 20:48:42 +0100 Subject: [PATCH 01/10] First contract prototype + openzeppelin --- .gitmodules | 3 + src/examples/allocator/ServerAllocator.sol | 242 +++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 src/examples/allocator/ServerAllocator.sol diff --git a/.gitmodules b/.gitmodules index a341767..7ce8b70 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "lib/tstorish"] path = lib/tstorish url = https://github.com/ProjectOpenSea/tstorish +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts \ No newline at end of file diff --git a/src/examples/allocator/ServerAllocator.sol b/src/examples/allocator/ServerAllocator.sol new file mode 100644 index 0000000..aa44e5c --- /dev/null +++ b/src/examples/allocator/ServerAllocator.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {COMPACT_TYPEHASH, Compact} from "src/types/EIP712Types.sol"; +import {Ownable, Ownable2Step} from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol"; +import {ECDSA} from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import {EIP712} from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; + +contract ServerAllocatorNonce is Ownable2Step, EIP712 { + using ECDSA for bytes32; + + struct NonceConsumption { + address signer; + uint256[] nonces; + } + + // keccak256("NonceConsumption(address signer,uint256[] nonces)") + bytes32 private constant _NONCE_CONSUMPTION_TYPE_HASH = + 0x8131ea92bd36581a24ac72c3abac20376f242758e62cdeb68a74dfa4ff3bfdaa; + address private immutable _COMPACT_CONTRACT; + + mapping(address => uint256) private _signers; + address[] private _activeSigners; + mapping(uint256 => bool) private _nonces; + mapping(uint256 => bytes32) private _registeredHashes; // TODO: register this by hash => expiration instead of nonce => hash + + event SignerAdded(address signer_); + event SignerRemoved(address signer_); + event HashRegistered(uint256 nonce_, bytes32 hash_); + event NonceConsumed(uint256 nonce_); + + error InvalidCaller(address caller_, address expected_); + error InvalidSigner(address signer_); + error InvalidHash(bytes32 hash_); + error InvalidNonce(uint256 nonce_); + + modifier isSigner(address signer_) { + if (!_containsSigner(signer_)) { + revert InvalidSigner(signer_); + } + _; + } + + constructor( + address owner_, + address compactContract_ + ) Ownable(owner_) EIP712("ServerAllocator", "1") { + _COMPACT_CONTRACT = compactContract_; + } + + function addSigner(address signer_) external onlyOwner { + if (_containsSigner(signer_)) { + return; + } + + _activeSigners.push(signer_); + _signers[signer_] = _activeSigners.length; + + emit SignerAdded(signer_); + } + + function removeSigner(address signer_) external onlyOwner { + if (!_containsSigner(signer_)) { + return; + } + + uint256 index = _signers[signer_] - 1; + _activeSigners[index] = _activeSigners[_activeSigners.length - 1]; + _activeSigners.pop(); + + _signers[signer_] = 0; + + emit SignerRemoved(signer_); + } + + function registerHash( + bytes32 hash_, + uint256 nonce_ + ) external isSigner(msg.sender) { + if (_nonceUsed(nonce_) || _registeredHashes[nonce_] != bytes32(0)) { + revert InvalidNonce(nonce_); + } + bytes32 noncedHash = keccak256(abi.encode(hash_, nonce_)); + _registeredHashes[nonce_] = noncedHash; + + emit HashRegistered(nonce_, hash_); + } + + function attest( + address from_, + uint256 id_, + uint256 amount_, + uint256 nonce_ + ) external { + if (msg.sender != _COMPACT_CONTRACT) { + revert InvalidCaller(msg.sender, _COMPACT_CONTRACT); + } + if (_nonceUsed(nonce_)) { + revert InvalidNonce(nonce_); + } + bytes32 cleanHash = keccak256(abi.encode(from_, id_, amount_)); + bytes32 noncedHash = keccak256(abi.encode(cleanHash, nonce_)); + + if (_registeredHashes[nonce_] != noncedHash) { + revert InvalidHash(noncedHash); + } + _consumeNonce(nonce_); + } + + /// @dev Treating the nonces individually instead of sequentially + /// TODO: All signers can override nonces of other signers. This allows to consume nonces while attesting. + function consume(uint256[] calldata nonces_) external isSigner(msg.sender) { + _consumeNonces(nonces_); + } + + function consumeViaSignature( + NonceConsumption calldata data_, + bytes calldata signature_ + ) external { + address signer = _validateNonceConsumption(data_, signature_); + if (signer != data_.signer) { + // check is optional, would fail if signer is not a registered signer anyway + revert InvalidSigner(signer); + } + if (!_containsSigner(signer)) { + revert InvalidSigner(signer); + } + _consumeNonces(data_.nonces); + } + + function isValidSignature( + Compact calldata data_, + bytes calldata signature_, + bool checkHash_ + ) external view returns (bool) { + if (data_.expires < block.timestamp) { + return false; + } + if (_nonceUsed(data_.nonce)) { + return false; + } + if (checkHash_ && _registeredHashes[data_.nonce] == bytes32(0)) { + return false; + } + + address signer = _validateData(data_, signature_); + return _containsSigner(signer); + } + + function checkIfSigner(address signer_) external view returns (bool) { + return _containsSigner(signer_); + } + + function getAllSigners() external view returns (address[] memory) { + return _activeSigners; + } + + function checkNonceConsumed(uint256 nonce_) external view returns (bool) { + return _nonceUsed(nonce_); + } + + function checkNonceFree(uint256 nonce_) external view returns (bool) { + return !_nonceUsed(nonce_) && _registeredHashes[nonce_] == bytes32(0); + } + + function getCompactContract() external view returns (address) { + return _COMPACT_CONTRACT; + } + + function _consumeNonces(uint256[] calldata nonces_) internal { + uint256 nonceLength = nonces_.length; + for (uint256 i = 0; i < nonceLength; ++i) { + _consumeNonce(nonces_[i]); + } + } + + function _consumeNonce(uint256 nonce_) internal { + delete _registeredHashes[nonce_]; + _nonces[nonce_] = true; + + emit NonceConsumed(nonce_); + } + + function _validateData( + Compact calldata data_, + bytes calldata signature_ + ) internal view returns (address) { + bytes32 message = _hashCompact(data_); + return message.recover(signature_); + } + + function _hashCompact( + Compact calldata data_ + ) internal view returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + COMPACT_TYPEHASH, + data_.arbiter, + data_.sponsor, + data_.nonce, + data_.expires, + data_.id, + data_.amount + ) + ) + ); + } + + function _validateNonceConsumption( + NonceConsumption calldata data_, + bytes calldata signature_ + ) internal view returns (address) { + bytes32 message = _hashNonceConsumption(data_); + return message.recover(signature_); + } + + function _hashNonceConsumption( + NonceConsumption calldata data_ + ) internal view returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + _NONCE_CONSUMPTION_TYPE_HASH, + data_.signer, + data_.nonces + ) + ) + ); + } + + function _nonceUsed(uint256 nonce_) internal view returns (bool) { + return _nonces[nonce_]; + } + + function _containsSigner(address signer_) internal view returns (bool) { + return _signers[signer_] != 0; + } +} From acf55b048014028e0852f62351b58fea455745f9 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 31 Oct 2024 21:33:35 +0100 Subject: [PATCH 02/10] contract improvements and first tests --- src/examples/allocator/ServerAllocator.sol | 256 +++++++---- src/interfaces/IAllocator.sol | 15 +- src/test/AlwaysOKAllocator.sol | 19 +- src/test/ERC20Mock.sol | 15 + src/test/TheCompactMock.sol | 110 +++++ test/ServerAllocator.t.sol | 494 +++++++++++++++++++++ 6 files changed, 811 insertions(+), 98 deletions(-) create mode 100644 src/test/ERC20Mock.sol create mode 100644 src/test/TheCompactMock.sol create mode 100644 test/ServerAllocator.t.sol diff --git a/src/examples/allocator/ServerAllocator.sol b/src/examples/allocator/ServerAllocator.sol index aa44e5c..6bfddab 100644 --- a/src/examples/allocator/ServerAllocator.sol +++ b/src/examples/allocator/ServerAllocator.sol @@ -2,38 +2,54 @@ pragma solidity ^0.8.27; -import {COMPACT_TYPEHASH, Compact} from "src/types/EIP712Types.sol"; +import {Compact} from "src/types/EIP712Types.sol"; +import {ITheCompact} from "src/interfaces/ITheCompact.sol"; +import {IAllocator} from "src/interfaces/IAllocator.sol"; import {Ownable, Ownable2Step} from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol"; import {ECDSA} from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import {EIP712} from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; +import {IERC1271} from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; -contract ServerAllocatorNonce is Ownable2Step, EIP712 { +contract ServerAllocator is Ownable2Step, EIP712, IAllocator { using ECDSA for bytes32; struct NonceConsumption { address signer; uint256[] nonces; + bytes32[] attests; } - // keccak256("NonceConsumption(address signer,uint256[] nonces)") + // keccak256("Attest(address,address,address,uint256,uint256)") + bytes4 private constant _ATTEST_SELECTOR = 0x1a808f91; + + // keccak256("Allocator(bytes32 hash)") + bytes32 private constant _ALLOCATOR_TYPE_HASH = + 0xcdf324dc7c3490a07fbbb105911393dcbc0676ac7c6c1c32c786721de6179e70; + + // keccak256("NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)") bytes32 private constant _NONCE_CONSUMPTION_TYPE_HASH = - 0x8131ea92bd36581a24ac72c3abac20376f242758e62cdeb68a74dfa4ff3bfdaa; + 0xb06793f900067653959d9bc53299ebf6b5aa5cf5f6c1a463305891a3db695f3c; + address private immutable _COMPACT_CONTRACT; mapping(address => uint256) private _signers; address[] private _activeSigners; - mapping(uint256 => bool) private _nonces; - mapping(uint256 => bytes32) private _registeredHashes; // TODO: register this by hash => expiration instead of nonce => hash + mapping(bytes32 => uint256) private _attestExpirations; + mapping(bytes32 => uint256) private _attestCounts; event SignerAdded(address signer_); event SignerRemoved(address signer_); - event HashRegistered(uint256 nonce_, bytes32 hash_); - event NonceConsumed(uint256 nonce_); + event AttestRegistered(bytes32 attest_, uint256 expiration_); + event NoncesConsumed(uint256[] nonces_); + event Attested(address from_, uint256 id_, uint256 amount_); + error UnregisteredAttest(bytes32 attest_); + error Expired(uint256 expiration_, uint256 currentTimestamp_); + error ExpiredAttests(bytes32 attest_); error InvalidCaller(address caller_, address expected_); error InvalidSigner(address signer_); - error InvalidHash(bytes32 hash_); - error InvalidNonce(uint256 nonce_); + error InvalidSignature(bytes signature_, address signer_); + error InvalidInput(); modifier isSigner(address signer_) { if (!_containsSigner(signer_)) { @@ -45,7 +61,7 @@ contract ServerAllocatorNonce is Ownable2Step, EIP712 { constructor( address owner_, address compactContract_ - ) Ownable(owner_) EIP712("ServerAllocator", "1") { + ) Ownable(owner_) EIP712("Allocator", "1") { _COMPACT_CONTRACT = compactContract_; } @@ -74,78 +90,120 @@ contract ServerAllocatorNonce is Ownable2Step, EIP712 { emit SignerRemoved(signer_); } - function registerHash( - bytes32 hash_, - uint256 nonce_ + /// @dev There is no way to uniquely identify a transfer, so the contract relies on its own accounting of registered attests. + function registerAttest( + bytes32 attest_, + uint256 expiration_ ) external isSigner(msg.sender) { - if (_nonceUsed(nonce_) || _registeredHashes[nonce_] != bytes32(0)) { - revert InvalidNonce(nonce_); + if (expiration_ < block.timestamp) { + revert Expired(expiration_, block.timestamp); } - bytes32 noncedHash = keccak256(abi.encode(hash_, nonce_)); - _registeredHashes[nonce_] = noncedHash; + uint256 count = ++_attestCounts[attest_]; + bytes32 countedAttest = keccak256(abi.encode(attest_, count)); + + _attestExpirations[countedAttest] = expiration_; - emit HashRegistered(nonce_, hash_); + emit AttestRegistered(attest_, expiration_); } + /// @dev There is no way to uniquely identify a transfer, so the contract relies on its own accounting of registered attests. function attest( + address, // operator_ address from_, + address, // to_ uint256 id_, - uint256 amount_, - uint256 nonce_ - ) external { + uint256 amount_ + ) external returns (bytes4) { if (msg.sender != _COMPACT_CONTRACT) { revert InvalidCaller(msg.sender, _COMPACT_CONTRACT); } - if (_nonceUsed(nonce_)) { - revert InvalidNonce(nonce_); - } - bytes32 cleanHash = keccak256(abi.encode(from_, id_, amount_)); - bytes32 noncedHash = keccak256(abi.encode(cleanHash, nonce_)); + bytes32 registeredAttest = keccak256(abi.encode(from_, id_, amount_)); + uint256 count = _attestCounts[registeredAttest]; - if (_registeredHashes[nonce_] != noncedHash) { - revert InvalidHash(noncedHash); + if (count == 0) { + revert UnregisteredAttest(registeredAttest); + } + for (uint256 i = count; i > 0; --i) { + bytes32 countedAttest = keccak256(abi.encode(registeredAttest, i)); + if (_attestExpirations[countedAttest] >= block.timestamp) { + // Found a valid registered attest + if (i == count) { + // Last attest, delete + delete _attestExpirations[countedAttest]; + } else { + // Shift attest and delete from the end + bytes32 lastAttest = keccak256( + abi.encode(registeredAttest, count) + ); + _attestExpirations[countedAttest] = _attestExpirations[ + lastAttest + ]; + delete _attestExpirations[lastAttest]; + } + _attestCounts[registeredAttest] = --count; + + emit Attested(from_, id_, amount_); + return _ATTEST_SELECTOR; + } } - _consumeNonce(nonce_); + + revert ExpiredAttests(registeredAttest); } - /// @dev Treating the nonces individually instead of sequentially - /// TODO: All signers can override nonces of other signers. This allows to consume nonces while attesting. - function consume(uint256[] calldata nonces_) external isSigner(msg.sender) { - _consumeNonces(nonces_); + /// @dev The hashes array needs to be of the same length as the nonces array. + /// @dev If no hash was yet registered, provide a bytes32(0) for the respective index. + /// @dev All signers can override nonces of other signers. + function consume( + uint256[] calldata nonces_, // TODO: STRUCT OF ONE + bytes32[] calldata attests_ + ) external isSigner(msg.sender) { + if (attests_.length != nonces_.length) { + revert InvalidInput(); + } + _consumeNonces(nonces_, attests_); } function consumeViaSignature( NonceConsumption calldata data_, bytes calldata signature_ ) external { - address signer = _validateNonceConsumption(data_, signature_); - if (signer != data_.signer) { - // check is optional, would fail if signer is not a registered signer anyway - revert InvalidSigner(signer); + if (data_.attests.length != data_.nonces.length) { + revert InvalidInput(); } - if (!_containsSigner(signer)) { + address signer = _validateNonceConsumption(data_, signature_); + if (signer != data_.signer || !_containsSigner(signer)) { + // first check is optional, can be deleted for gas efficiency revert InvalidSigner(signer); } - _consumeNonces(data_.nonces); + _consumeNonces(data_.nonces, data_.attests); } + /// @dev A registered attest will be a fallback if no valid signature was provided. + // TODO: https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/interfaces/IERC1271.sol function isValidSignature( - Compact calldata data_, - bytes calldata signature_, - bool checkHash_ - ) external view returns (bool) { - if (data_.expires < block.timestamp) { - return false; - } - if (_nonceUsed(data_.nonce)) { - return false; - } - if (checkHash_ && _registeredHashes[data_.nonce] == bytes32(0)) { - return false; + bytes32 hash_, + bytes calldata signature_ + ) external view returns (bytes4 magicValue) { + address signer = _validateSignedHash(hash_, signature_); + if (!_containsSigner(signer)) { + // Check registered attests as fallback + /// TODO: This fallback must modify state to not be a source of endless verifications. + // uint256 count = _attestCounts[hash_]; + // if (count != 0) { + // for (uint256 i = count; i > 0; --i) { + // bytes32 countedAttest = keccak256(abi.encode(hash_, i)); + // if (_attestExpirations[countedAttest] >= block.timestamp) { + // // Found a valid registered attest + + // // _attestCounts[hash_] = --count; + // // delete _attestExpirations[countedAttest]; + // return IERC1271.isValidSignature.selector; + // } + // } + // } + revert InvalidSignature(signature_, signer); } - - address signer = _validateData(data_, signature_); - return _containsSigner(signer); + return IERC1271.isValidSignature.selector; } function checkIfSigner(address signer_) external view returns (bool) { @@ -156,56 +214,62 @@ contract ServerAllocatorNonce is Ownable2Step, EIP712 { return _activeSigners; } - function checkNonceConsumed(uint256 nonce_) external view returns (bool) { - return _nonceUsed(nonce_); + function checkAttestExpirations( + bytes32 attest_ + ) external view returns (uint256[] memory) { + return _checkAttestExpirations(attest_); } - function checkNonceFree(uint256 nonce_) external view returns (bool) { - return !_nonceUsed(nonce_) && _registeredHashes[nonce_] == bytes32(0); + function checkAttestExpirations( + address sponsor_, + uint256 id_, + uint256 amount_ + ) external view returns (uint256[] memory) { + return + _checkAttestExpirations( + keccak256(abi.encode(sponsor_, id_, amount_)) + ); } function getCompactContract() external view returns (address) { return _COMPACT_CONTRACT; } - function _consumeNonces(uint256[] calldata nonces_) internal { - uint256 nonceLength = nonces_.length; + /// Todo: This will lead to always the last registered hash being consumed. + function _consumeNonces( + uint256[] calldata nonces_, + bytes32[] calldata attests_ + ) internal { + ITheCompact(_COMPACT_CONTRACT).consume(nonces_); + uint256 nonceLength = attests_.length; for (uint256 i = 0; i < nonceLength; ++i) { - _consumeNonce(nonces_[i]); + bytes32 hashToConsume = attests_[i]; + if (hashToConsume != bytes32(0)) { + uint256 count = _attestCounts[attests_[i]]; + if (count != 0) { + // Consume the latest registered attest + delete _attestExpirations[ + keccak256(abi.encode(attests_[i], count)) + ]; + _attestCounts[attests_[i]] = --count; + } + } } + emit NoncesConsumed(nonces_); } - function _consumeNonce(uint256 nonce_) internal { - delete _registeredHashes[nonce_]; - _nonces[nonce_] = true; - - emit NonceConsumed(nonce_); - } - - function _validateData( - Compact calldata data_, + function _validateSignedHash( + bytes32 hash_, bytes calldata signature_ ) internal view returns (address) { - bytes32 message = _hashCompact(data_); + bytes32 message = _hashMessage(hash_); return message.recover(signature_); } - function _hashCompact( - Compact calldata data_ - ) internal view returns (bytes32) { + function _hashMessage(bytes32 data_) internal view returns (bytes32) { return _hashTypedDataV4( - keccak256( - abi.encode( - COMPACT_TYPEHASH, - data_.arbiter, - data_.sponsor, - data_.nonce, - data_.expires, - data_.id, - data_.amount - ) - ) + keccak256(abi.encode(_ALLOCATOR_TYPE_HASH, data_)) ); } @@ -232,11 +296,23 @@ contract ServerAllocatorNonce is Ownable2Step, EIP712 { ); } - function _nonceUsed(uint256 nonce_) internal view returns (bool) { - return _nonces[nonce_]; - } - function _containsSigner(address signer_) internal view returns (bool) { return _signers[signer_] != 0; } + + function _checkAttestExpirations( + bytes32 attest_ + ) internal view returns (uint256[] memory) { + uint256 count = _attestCounts[attest_]; + if (count == 0) { + revert UnregisteredAttest(attest_); + } + uint256[] memory expirations = new uint256[](count); + for (uint256 i = count; i > 0; --i) { + expirations[i - 1] = _attestExpirations[ + keccak256(abi.encode(attest_, i)) + ]; + } + return expirations; + } } diff --git a/src/interfaces/IAllocator.sol b/src/interfaces/IAllocator.sol index 9a488a1..5c8d0dc 100644 --- a/src/interfaces/IAllocator.sol +++ b/src/interfaces/IAllocator.sol @@ -1,8 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -// NOTE: Allocators with smart contract implementations should also implement EIP1271. -interface IAllocator { +import {IERC1271} from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; + +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); + function attest( + address operator, + address from, + address to, + uint256 id, + uint256 amount + ) external returns (bytes4); + + // isValidSignature of IERC1271 will be called during a claim and must verify the signature of the allocation. } diff --git a/src/test/AlwaysOKAllocator.sol b/src/test/AlwaysOKAllocator.sol index fbd6f7f..758b5c2 100644 --- a/src/test/AlwaysOKAllocator.sol +++ b/src/test/AlwaysOKAllocator.sol @@ -1,15 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import { IAllocator } from "../interfaces/IAllocator.sol"; -import { IERC1271 } from "permit2/src/interfaces/IERC1271.sol"; +import {IAllocator} from "../interfaces/IAllocator.sol"; +import {IERC1271} from "permit2/src/interfaces/IERC1271.sol"; -contract AlwaysOKAllocator is IAllocator, IERC1271 { - function attest(address, address, address, uint256, uint256) external pure returns (bytes4) { +contract AlwaysOKAllocator is IAllocator { + function attest( + address, + address, + address, + uint256, + uint256 + ) external pure returns (bytes4) { return IAllocator.attest.selector; } - function isValidSignature(bytes32, bytes calldata) external pure returns (bytes4) { + function isValidSignature( + bytes32, + bytes calldata + ) external pure returns (bytes4) { return IERC1271.isValidSignature.selector; } } diff --git a/src/test/ERC20Mock.sol b/src/test/ERC20Mock.sol new file mode 100644 index 0000000..5504925 --- /dev/null +++ b/src/test/ERC20Mock.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mock is ERC20 { + constructor( + string memory name_, + string memory symbol_ + ) ERC20(name_, symbol_) {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/src/test/TheCompactMock.sol b/src/test/TheCompactMock.sol new file mode 100644 index 0000000..a91ac71 --- /dev/null +++ b/src/test/TheCompactMock.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IAllocator} from "src/interfaces/IAllocator.sol"; +import {ERC6909} from "solady/tokens/ERC6909.sol"; +import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {IdLib} from "src/lib/IdLib.sol"; + +contract TheCompactMock is ERC6909 { + using IdLib for uint96; + using IdLib for uint256; + using IdLib for address; + + mapping(uint256 nonce => bool consumed) public consumedNonces; + + function deposit( + address token, + uint256 amount, + address allocator + ) external { + ERC20(token).transferFrom(msg.sender, address(this), amount); + uint256 id = _getTokenId(token, allocator); + _mint(msg.sender, id, amount); + } + + function transfer( + address from, + address to, + uint256 amount, + address token, + address allocator + ) external { + uint256 id = _getTokenId(token, allocator); + IAllocator(allocator).attest(msg.sender, from, to, id, amount); + _transfer(msg.sender, from, to, id, amount); + } + + function claim( + address from, + address to, + address token, + uint256 amount, + address allocator, + bytes calldata signature + ) external { + uint256 id = _getTokenId(token, allocator); + IAllocator(allocator).isValidSignature( + keccak256(abi.encode(from, id, amount)), + signature + ); + _transfer(msg.sender, from, to, id, amount); + } + + function withdraw( + address token, + uint256 amount, + address allocator + ) external { + uint256 id = _getTokenId(token, allocator); + IAllocator(allocator).attest( + msg.sender, + msg.sender, + msg.sender, + id, + amount + ); + ERC20(token).transferFrom(address(this), msg.sender, amount); + _burn(msg.sender, id, amount); + } + + function consume(uint256[] calldata nonces) external returns (bool) { + for (uint256 i = 0; i < nonces.length; ++i) { + consumedNonces[nonces[i]] = true; + } + return true; + } + + function getTokenId( + address token, + address allocator + ) external pure returns (uint256) { + return _getTokenId(token, allocator); + } + + function name( + uint256 // id + ) public view virtual override returns (string memory) { + return "TheCompactMock"; + } + + function symbol( + uint256 // id + ) public view virtual override returns (string memory) { + return "TCM"; + } + + function tokenURI( + uint256 // id + ) public view virtual override returns (string memory) { + return ""; + } + + function _getTokenId( + address token, + address allocator + ) internal pure returns (uint256) { + return uint256(keccak256(abi.encode(token, allocator))); + } +} diff --git a/test/ServerAllocator.t.sol b/test/ServerAllocator.t.sol new file mode 100644 index 0000000..b8f0bd1 --- /dev/null +++ b/test/ServerAllocator.t.sol @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import {ServerAllocator} from "src/examples/allocator/ServerAllocator.sol"; +import {TheCompactMock} from "src/test/TheCompactMock.sol"; +import {ERC20Mock} from "src/test/ERC20Mock.sol"; +import {console} from "forge-std/console.sol"; + +abstract contract MocksSetup is Test { + address owner = makeAddr("owner"); + address signer = makeAddr("signer"); + address attacker = makeAddr("attacker"); + ERC20Mock usdc; + TheCompactMock compactContract; + ServerAllocator serverAllocator; + uint256 usdcId; + + function setUp() public virtual { + usdc = new ERC20Mock("USDC", "USDC"); + compactContract = new TheCompactMock(); + serverAllocator = new ServerAllocator(owner, address(compactContract)); + usdcId = compactContract.getTokenId( + address(usdc), + address(serverAllocator) + ); + } +} + +abstract contract AttestSetup { + bytes4 internal constant _ATTEST_SELECTOR = 0x1a808f91; + + function createAttest( + address from_, + uint256 id_, + uint256 amount_ + ) internal pure returns (bytes32) { + return keccak256(abi.encode(from_, id_, amount_)); + } +} + +abstract contract CreateHash { + // stringified types + string EIP712_DOMAIN_TYPE = + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the funcion + string ALLOCATOR_TYPE = "Allocator(bytes32 hash)"; // Hashed inside the funcion + string NONCE_CONSUMPTION_TYPE = + "NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)"; // Hashed inside the funcion + // EIP712 domain type + string name = "Allocator"; + string version = "1"; + + function _hashAllocator( + bytes32 data, + address verifyingContract + ) internal view returns (bytes32) { + // hash typed data + return + keccak256( + abi.encodePacked( + "\x19\x01", // backslash is needed to escape the character + _domainSeperator(verifyingContract), + keccak256( + abi.encode(keccak256(bytes(ALLOCATOR_TYPE)), data) + ) + ) + ); + } + + function _hashNonceConsumption( + ServerAllocator.NonceConsumption memory data, + address verifyingContract + ) internal view returns (bytes32) { + // hash typed data + return + keccak256( + abi.encodePacked( + "\x19\x01", // backslash is needed to escape the character + _domainSeperator(verifyingContract), + keccak256( + abi.encode( + keccak256(bytes(NONCE_CONSUMPTION_TYPE)), + data.signer, + data.nonces, + data.attests + ) + ) + ) + ); + } + + function _domainSeperator( + address verifyingContract + ) internal view returns (bytes32) { + return + keccak256( + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract + ) + ); + } +} + +abstract contract SignerSet is MocksSetup, CreateHash, AttestSetup { + function setUp() public virtual override { + super.setUp(); + vm.prank(owner); + serverAllocator.addSigner(signer); + } +} + +contract ServerAllocator_OwnerSet is MocksSetup, CreateHash { + function test_checkOwner() public view { + assertEq(serverAllocator.owner(), owner); + } +} + +contract ServerAllocator_ManageSigners is MocksSetup, CreateHash { + function test_noSigners() public view { + assertEq(serverAllocator.getAllSigners().length, 0); + } + + function test_fuzz_onlyOwnerCanAddSigner(address attacker_) public { + vm.assume(attacker_ != owner); + vm.expectRevert( + abi.encodeWithSelector( + Ownable.OwnableUnauthorizedAccount.selector, + attacker_ + ) + ); + vm.prank(attacker_); + serverAllocator.addSigner(signer); + } + + function test_addSigner() public { + vm.prank(owner); + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.SignerAdded(signer); + serverAllocator.addSigner(signer); + assertEq(serverAllocator.getAllSigners().length, 1); + assertEq(serverAllocator.getAllSigners()[0], signer); + } + + function test_addAnotherSigner() public { + vm.startPrank(owner); + // add first signer + serverAllocator.addSigner(signer); + + // add second signer + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.SignerAdded(attacker); + serverAllocator.addSigner(attacker); + assertEq(serverAllocator.getAllSigners().length, 2); + assertEq(serverAllocator.getAllSigners()[0], signer); + assertEq(serverAllocator.getAllSigners()[1], attacker); + } + + function test_removeSigner() public { + vm.startPrank(owner); + // add first signer + serverAllocator.addSigner(signer); + assertEq(serverAllocator.getAllSigners().length, 1); + + // remove first signer + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.SignerRemoved(signer); + serverAllocator.removeSigner(signer); + assertEq(serverAllocator.getAllSigners().length, 0); + } + + function test_signerCantAddOrRemoveSigners() public { + vm.prank(owner); + + // add first signer + serverAllocator.addSigner(signer); + + vm.startPrank(signer); + // try to add another signer + vm.expectRevert( + abi.encodeWithSelector( + Ownable.OwnableUnauthorizedAccount.selector, + signer + ) + ); + serverAllocator.addSigner(attacker); + + // try to remove a signer + vm.expectRevert( + abi.encodeWithSelector( + Ownable.OwnableUnauthorizedAccount.selector, + signer + ) + ); + serverAllocator.removeSigner(signer); + } + + function test_addingSignerTwice() public { + vm.startPrank(owner); + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.SignerAdded(signer); + serverAllocator.addSigner(signer); + assertEq(serverAllocator.getAllSigners().length, 1); + + // adding signer again will just return without adding the signer again + serverAllocator.addSigner(signer); + assertEq(serverAllocator.getAllSigners().length, 1); + } +} + +contract ServerAllocator_Attest is SignerSet { + function test_fuzz_onlySignerCanRegisterAttest(address attacker_) public { + vm.assume(attacker_ != signer); + + vm.prank(attacker_); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.InvalidSigner.selector, + attacker_ + ) + ); + serverAllocator.registerAttest( + createAttest(attacker_, usdcId, 100), + vm.getBlockTimestamp() + 1 days + ); + } + + function test_fuzz_attestExpired(uint256 expiration_) public { + vm.assume(expiration_ < vm.getBlockTimestamp()); + + vm.prank(signer); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.Expired.selector, + expiration_, + vm.getBlockTimestamp() + ) + ); + serverAllocator.registerAttest( + createAttest(signer, usdcId, 100), + expiration_ + ); + } + + function test_registerAttest() public { + vm.prank(signer); + bytes32 attest = createAttest(signer, usdcId, 100); + uint256 expiration = vm.getBlockTimestamp() + 1 days; + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.AttestRegistered(attest, expiration); + serverAllocator.registerAttest(attest, expiration); + + assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration); + } + + function test_registerSameAttestTwice() public { + vm.startPrank(signer); + bytes32 attest = createAttest(signer, usdcId, 100); + uint256 expiration1 = vm.getBlockTimestamp() + 1 days; + uint256 expiration2 = vm.getBlockTimestamp() + 2 days; + + // first attest + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.AttestRegistered(attest, expiration1); + serverAllocator.registerAttest(attest, expiration1); + + assertEq( + serverAllocator.checkAttestExpirations(attest)[0], + expiration1 + ); + + // second attest with different expiration + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.AttestRegistered(attest, expiration2); + serverAllocator.registerAttest(attest, expiration2); + + assertEq( + serverAllocator.checkAttestExpirations(attest)[0], + expiration1 + ); + assertEq( + serverAllocator.checkAttestExpirations(attest)[1], + expiration2 + ); + } + + function test_fuzz_attest_callerMustBeCompact(address caller_) public { + vm.assume(caller_ != address(compactContract)); + + vm.prank(caller_); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.InvalidCaller.selector, + caller_, + address(compactContract) + ) + ); + serverAllocator.attest(caller_, signer, attacker, usdcId, 100); + } + + function test_fuzz_attest_notRegistered( + address operator_, + address from_, + address to_, + uint256 id_, + uint256 amount_ + ) public { + vm.prank(address(compactContract)); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.UnregisteredAttest.selector, + keccak256(abi.encode(from_, id_, amount_)) + ) + ); + serverAllocator.attest(operator_, from_, to_, id_, amount_); + } + + function test_attest_expired() public { + uint256 amount_ = 100; + bytes32 attest = createAttest(attacker, usdcId, amount_); + uint256 expiration = vm.getBlockTimestamp(); + + // register attest + vm.prank(signer); + serverAllocator.registerAttest(attest, expiration); + + // move time forward + vm.warp(vm.getBlockTimestamp() + 1); + + // check attest + vm.prank(address(compactContract)); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.ExpiredAttests.selector, + attest + ) + ); + serverAllocator.attest( + signer, + attacker, + makeAddr("to"), + usdcId, + amount_ + ); + } + + function test_fuzz_attest_successful( + address operator_, + address from_, + address to_, + uint256 id_, + uint256 amount_ + ) public { + bytes32 attest = createAttest(from_, id_, amount_); + uint256 expiration = vm.getBlockTimestamp(); + + // register attest + vm.prank(signer); + serverAllocator.registerAttest(attest, expiration); + + // check for attest + assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration); + + // check attest + vm.prank(address(compactContract)); + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.Attested(from_, id_, amount_); + bytes4 attestSelector = serverAllocator.attest( + operator_, + from_, + to_, + id_, + amount_ + ); + assertEq(attestSelector, _ATTEST_SELECTOR); + + // check attest was consumed + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.UnregisteredAttest.selector, + attest + ) + ); + serverAllocator.checkAttestExpirations(attest); + } +} + +contract ServerAllocator_Consume is SignerSet { + function test_consume_onlySignerCanConsume() public { + vm.prank(attacker); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.InvalidSigner.selector, + attacker + ) + ); + serverAllocator.consume(new uint256[](0), new bytes32[](0)); + } + + function test_consume_requiresNoncesAndAttestsToBeOfSameLength() public { + vm.prank(signer); + vm.expectRevert( + abi.encodeWithSelector(ServerAllocator.InvalidInput.selector) + ); + serverAllocator.consume(new uint256[](0), new bytes32[](1)); + } + + function test_consume_successfulWithoutAttests() public { + vm.prank(signer); + + uint256[] memory nonces = new uint256[](3); + nonces[0] = 1; + nonces[1] = 2; + nonces[2] = 3; + + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.NoncesConsumed(nonces); + serverAllocator.consume(nonces, new bytes32[](3)); + + assertEq(compactContract.consumedNonces(0), false); + for (uint256 i = 0; i < nonces.length; ++i) { + assertEq(compactContract.consumedNonces(nonces[i]), true); + } + } + + function test_consume_successfulWithAttests() public { + vm.startPrank(signer); + + uint256[] memory nonces = new uint256[](3); + nonces[0] = 1; + nonces[1] = 2; + nonces[2] = 3; + + bytes32[] memory attests = new bytes32[](3); + attests[0] = createAttest(signer, usdcId, 100); + attests[1] = createAttest(signer, usdcId, 200); + attests[2] = createAttest(signer, usdcId, 300); + + // register attests + serverAllocator.registerAttest(attests[0], vm.getBlockTimestamp()); + serverAllocator.registerAttest(attests[1], vm.getBlockTimestamp()); + serverAllocator.registerAttest(attests[2], vm.getBlockTimestamp()); + + assertEq( + serverAllocator.checkAttestExpirations(attests[0])[0], + vm.getBlockTimestamp() + ); + assertEq( + serverAllocator.checkAttestExpirations(attests[1])[0], + vm.getBlockTimestamp() + ); + assertEq( + serverAllocator.checkAttestExpirations(attests[2])[0], + vm.getBlockTimestamp() + ); + + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.NoncesConsumed(nonces); + serverAllocator.consume(nonces, attests); + + assertEq(compactContract.consumedNonces(0), false); + for (uint256 i = 0; i < nonces.length; ++i) { + assertEq(compactContract.consumedNonces(nonces[i]), true); + } + + // check attests were consumed + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.UnregisteredAttest.selector, + attests[0] + ) + ); + serverAllocator.checkAttestExpirations(attests[0]); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.UnregisteredAttest.selector, + attests[1] + ) + ); + serverAllocator.checkAttestExpirations(attests[1]); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.UnregisteredAttest.selector, + attests[2] + ) + ); + serverAllocator.checkAttestExpirations(attests[2]); + } +} From ca180f2f0dd1574ffc4f789995782017765b8778 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Mon, 4 Nov 2024 15:54:24 +0100 Subject: [PATCH 03/10] register attest via signature + tests --- src/examples/allocator/ServerAllocator.sol | 108 +++++-- test/ServerAllocator.t.sol | 312 ++++++++++++++++++++- 2 files changed, 389 insertions(+), 31 deletions(-) diff --git a/src/examples/allocator/ServerAllocator.sol b/src/examples/allocator/ServerAllocator.sol index 6bfddab..f775bfc 100644 --- a/src/examples/allocator/ServerAllocator.sol +++ b/src/examples/allocator/ServerAllocator.sol @@ -13,6 +13,13 @@ import {IERC1271} from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271 contract ServerAllocator is Ownable2Step, EIP712, IAllocator { using ECDSA for bytes32; + struct RegisterAttest { + address signer; + bytes32 attestHash; + uint256 expiration; + uint256 nonce; + } + struct NonceConsumption { address signer; uint256[] nonces; @@ -22,6 +29,10 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { // keccak256("Attest(address,address,address,uint256,uint256)") bytes4 private constant _ATTEST_SELECTOR = 0x1a808f91; + // keccak256("RegisterAttest(address signer,bytes32 attestHash,uint256 expiration,uint256 nonce)") + bytes32 private constant _ATTEST_TYPE_HASH = + 0xaf2dfd3fe08723f490d203be627da2725f4ad38681e455221da2fc1a633bbb18; + // keccak256("Allocator(bytes32 hash)") bytes32 private constant _ALLOCATOR_TYPE_HASH = 0xcdf324dc7c3490a07fbbb105911393dcbc0676ac7c6c1c32c786721de6179e70; @@ -34,8 +45,10 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { mapping(address => uint256) private _signers; address[] private _activeSigners; + mapping(bytes32 => uint256) private _attestExpirations; mapping(bytes32 => uint256) private _attestCounts; + mapping(bytes32 => bool) private _attestSignatures; event SignerAdded(address signer_); event SignerRemoved(address signer_); @@ -49,6 +62,7 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { error InvalidCaller(address caller_, address expected_); error InvalidSigner(address signer_); error InvalidSignature(bytes signature_, address signer_); + error AlreadyUsedSig(bytes32 attest_, uint256 nonce); error InvalidInput(); modifier isSigner(address signer_) { @@ -95,15 +109,34 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { bytes32 attest_, uint256 expiration_ ) external isSigner(msg.sender) { - if (expiration_ < block.timestamp) { - revert Expired(expiration_, block.timestamp); - } - uint256 count = ++_attestCounts[attest_]; - bytes32 countedAttest = keccak256(abi.encode(attest_, count)); + _registerAttest(attest_, expiration_); + } - _attestExpirations[countedAttest] = expiration_; + /// @dev Nonce management in the RegisterAttest is only required for multiple registers of the same attest with the same expiration. + function registerAttestViaSignature( + RegisterAttest calldata attest_, + bytes calldata signature_ + ) external { + bytes32 _attestWithNonce = keccak256( + abi.encode(attest_.attestHash, attest_.expiration, attest_.nonce) + ); + if (_attestSignatures[_attestWithNonce]) { + revert AlreadyUsedSig(attest_.attestHash, attest_.nonce); + } + address signer = _validateSignedAttest( + attest_.signer, + attest_.attestHash, + attest_.expiration, + attest_.nonce, + signature_ + ); + if (signer != attest_.signer || !_containsSigner(signer)) { + revert InvalidSignature(signature_, signer); + } - emit AttestRegistered(attest_, expiration_); + // Invalidate signature + _attestSignatures[_attestWithNonce] = true; + _registerAttest(attest_.attestHash, attest_.expiration); } /// @dev There is no way to uniquely identify a transfer, so the contract relies on its own accounting of registered attests. @@ -186,21 +219,6 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { ) external view returns (bytes4 magicValue) { address signer = _validateSignedHash(hash_, signature_); if (!_containsSigner(signer)) { - // Check registered attests as fallback - /// TODO: This fallback must modify state to not be a source of endless verifications. - // uint256 count = _attestCounts[hash_]; - // if (count != 0) { - // for (uint256 i = count; i > 0; --i) { - // bytes32 countedAttest = keccak256(abi.encode(hash_, i)); - // if (_attestExpirations[countedAttest] >= block.timestamp) { - // // Found a valid registered attest - - // // _attestCounts[hash_] = --count; - // // delete _attestExpirations[countedAttest]; - // return IERC1271.isValidSignature.selector; - // } - // } - // } revert InvalidSignature(signature_, signer); } return IERC1271.isValidSignature.selector; @@ -235,6 +253,18 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { return _COMPACT_CONTRACT; } + function _registerAttest(bytes32 attest_, uint256 expiration_) internal { + if (expiration_ < block.timestamp) { + revert Expired(expiration_, block.timestamp); + } + uint256 count = ++_attestCounts[attest_]; + bytes32 countedAttest = keccak256(abi.encode(attest_, count)); + + _attestExpirations[countedAttest] = expiration_; + + emit AttestRegistered(attest_, expiration_); + } + /// Todo: This will lead to always the last registered hash being consumed. function _consumeNonces( uint256[] calldata nonces_, @@ -258,6 +288,37 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { emit NoncesConsumed(nonces_); } + function _validateSignedAttest( + address signer_, + bytes32 hash_, + uint256 expiration_, + uint256 nonce, + bytes calldata signature_ + ) internal view returns (address) { + bytes32 message = _hashAttest(signer_, hash_, expiration_, nonce); + return message.recover(signature_); + } + + function _hashAttest( + address signer_, + bytes32 hash_, + uint256 expiration_, + uint256 nonce_ + ) internal view returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + _ATTEST_TYPE_HASH, + signer_, + hash_, + expiration_, + nonce_ + ) + ) + ); + } + function _validateSignedHash( bytes32 hash_, bytes calldata signature_ @@ -290,7 +351,8 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { abi.encode( _NONCE_CONSUMPTION_TYPE_HASH, data_.signer, - data_.nonces + data_.nonces, + data_.attests ) ) ); diff --git a/test/ServerAllocator.t.sol b/test/ServerAllocator.t.sol index b8f0bd1..761d9fb 100644 --- a/test/ServerAllocator.t.sol +++ b/test/ServerAllocator.t.sol @@ -8,11 +8,14 @@ import {ServerAllocator} from "src/examples/allocator/ServerAllocator.sol"; import {TheCompactMock} from "src/test/TheCompactMock.sol"; import {ERC20Mock} from "src/test/ERC20Mock.sol"; import {console} from "forge-std/console.sol"; +import {IERC1271} from "lib/permit2/src/interfaces/IERC1271.sol"; abstract contract MocksSetup is Test { address owner = makeAddr("owner"); - address signer = makeAddr("signer"); - address attacker = makeAddr("attacker"); + address signer; + uint256 signerPK; + address attacker; + uint256 attackerPK; ERC20Mock usdc; TheCompactMock compactContract; ServerAllocator serverAllocator; @@ -26,6 +29,8 @@ abstract contract MocksSetup is Test { address(usdc), address(serverAllocator) ); + (signer, signerPK) = makeAddrAndKey("signer"); + (attacker, attackerPK) = makeAddrAndKey("attacker"); } } @@ -41,11 +46,17 @@ abstract contract AttestSetup { } } -abstract contract CreateHash { +abstract contract CreateHash is Test { + struct Allocator { + bytes32 hash; + } + // stringified types string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the funcion string ALLOCATOR_TYPE = "Allocator(bytes32 hash)"; // Hashed inside the funcion + string REGISTER_ATTEST_TYPE = + "RegisterAttest(address signer,bytes32 attestHash,uint256 expiration,uint256 nonce)"; // Hashed inside the funcion string NONCE_CONSUMPTION_TYPE = "NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)"; // Hashed inside the funcion // EIP712 domain type @@ -53,7 +64,7 @@ abstract contract CreateHash { string version = "1"; function _hashAllocator( - bytes32 data, + Allocator memory data, address verifyingContract ) internal view returns (bytes32) { // hash typed data @@ -63,7 +74,29 @@ abstract contract CreateHash { "\x19\x01", // backslash is needed to escape the character _domainSeperator(verifyingContract), keccak256( - abi.encode(keccak256(bytes(ALLOCATOR_TYPE)), data) + abi.encode(keccak256(bytes(ALLOCATOR_TYPE)), data.hash) + ) + ) + ); + } + + function _hashRegisterAttest( + ServerAllocator.RegisterAttest memory data, + address verifyingContract + ) internal view returns (bytes32) { + return + keccak256( + abi.encodePacked( + "\x19\x01", // backslash is needed to escape the character + _domainSeperator(verifyingContract), + keccak256( + abi.encode( + keccak256(bytes(REGISTER_ATTEST_TYPE)), + data.signer, + data.attestHash, + data.expiration, + data.nonce + ) ) ) ); @@ -105,6 +138,14 @@ abstract contract CreateHash { ) ); } + + function _signMessage( + bytes32 hash_, + uint256 signerPK_ + ) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); + return abi.encodePacked(r, s, v); + } } abstract contract SignerSet is MocksSetup, CreateHash, AttestSetup { @@ -214,7 +255,7 @@ contract ServerAllocator_ManageSigners is MocksSetup, CreateHash { } contract ServerAllocator_Attest is SignerSet { - function test_fuzz_onlySignerCanRegisterAttest(address attacker_) public { + function test_fuzz_RegisterAttest_onlySigner(address attacker_) public { vm.assume(attacker_ != signer); vm.prank(attacker_); @@ -230,7 +271,9 @@ contract ServerAllocator_Attest is SignerSet { ); } - function test_fuzz_attestExpired(uint256 expiration_) public { + function test_fuzz_registerAttest_attestExpired( + uint256 expiration_ + ) public { vm.assume(expiration_ < vm.getBlockTimestamp()); vm.prank(signer); @@ -247,7 +290,7 @@ contract ServerAllocator_Attest is SignerSet { ); } - function test_registerAttest() public { + function test_registerAttest_successful() public { vm.prank(signer); bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; @@ -258,6 +301,75 @@ contract ServerAllocator_Attest is SignerSet { assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration); } + function test_registerAttestViaSignature_InvalidSignature() public { + bytes32 attest = createAttest(signer, usdcId, 100); + uint256 expiration = vm.getBlockTimestamp() + 1 days; + + ServerAllocator.RegisterAttest memory attestData = ServerAllocator + .RegisterAttest(signer, attest, expiration, 0); + bytes32 message = _hashRegisterAttest( + attestData, + address(serverAllocator) + ); + bytes memory signature = _signMessage(message, attackerPK); + + vm.prank(attacker); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.InvalidSignature.selector, + signature, + attacker + ) + ); + serverAllocator.registerAttestViaSignature(attestData, signature); + } + + function test_registerAttestViaSignature_successful() public { + bytes32 attest = createAttest(signer, usdcId, 100); + uint256 expiration = vm.getBlockTimestamp() + 1 days; + + ServerAllocator.RegisterAttest memory attestData = ServerAllocator + .RegisterAttest(signer, attest, expiration, 0); + bytes32 message = _hashRegisterAttest( + attestData, + address(serverAllocator) + ); + bytes memory signature = _signMessage(message, signerPK); + + vm.prank(attacker); + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.AttestRegistered(attest, expiration); + serverAllocator.registerAttestViaSignature(attestData, signature); + } + + function test_registerAttestViaSignature_AlreadyUsedSig() public { + bytes32 attest = createAttest(signer, usdcId, 100); + uint256 expiration = vm.getBlockTimestamp() + 1 days; + + ServerAllocator.RegisterAttest memory attestData = ServerAllocator + .RegisterAttest(signer, attest, expiration, 0); + bytes32 message = _hashRegisterAttest( + attestData, + address(serverAllocator) + ); + bytes memory signature = _signMessage(message, signerPK); + + vm.prank(attacker); + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.AttestRegistered(attest, expiration); + serverAllocator.registerAttestViaSignature(attestData, signature); + + vm.prank(attacker); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.AlreadyUsedSig.selector, + attest, + 0 + ) + ); + serverAllocator.registerAttestViaSignature(attestData, signature); + } + function test_registerSameAttestTwice() public { vm.startPrank(signer); bytes32 attest = createAttest(signer, usdcId, 100); @@ -491,4 +603,188 @@ contract ServerAllocator_Consume is SignerSet { ); serverAllocator.checkAttestExpirations(attests[2]); } + + function test_consumeViaSignature_requiresNoncesAndAttestsToBeOfSameLength() + public + { + bytes32 message = _hashNonceConsumption( + ServerAllocator.NonceConsumption( + signer, + new uint256[](0), + new bytes32[](1) + ), + address(serverAllocator) + ); + bytes memory signature = _signMessage(message, signerPK); + + vm.prank(signer); + vm.expectRevert( + abi.encodeWithSelector(ServerAllocator.InvalidInput.selector) + ); + serverAllocator.consumeViaSignature( + ServerAllocator.NonceConsumption( + signer, + new uint256[](0), + new bytes32[](1) + ), + signature + ); + } + + function test_consumeViaSignature_requireValidSignature() public { + bytes32 message = _hashNonceConsumption( + ServerAllocator.NonceConsumption( + signer, + new uint256[](1), + new bytes32[](1) + ), + address(serverAllocator) + ); + bytes memory signature = _signMessage(message, attackerPK); + + vm.prank(signer); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.InvalidSigner.selector, + attacker + ) + ); + serverAllocator.consumeViaSignature( + ServerAllocator.NonceConsumption( + signer, + new uint256[](1), + new bytes32[](1) + ), + signature + ); + } + + function test_consumeViaSignature_successfulWithoutAttests() public { + uint256[] memory nonces = new uint256[](3); + nonces[0] = 1; + nonces[1] = 2; + nonces[2] = 3; + + bytes32 message = _hashNonceConsumption( + ServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), + address(serverAllocator) + ); + bytes memory signature = _signMessage(message, signerPK); + + vm.prank(attacker); + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.NoncesConsumed(nonces); + serverAllocator.consumeViaSignature( + ServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), + signature + ); + } + + function test_consumeViaSignature_successfulWithAttests() public { + uint256[] memory nonces = new uint256[](3); + nonces[0] = 1; + nonces[1] = 2; + nonces[2] = 3; + + bytes32[] memory attests = new bytes32[](3); + attests[0] = createAttest(signer, usdcId, 100); + attests[1] = createAttest(signer, usdcId, 200); + attests[2] = createAttest(signer, usdcId, 300); + + vm.startPrank(signer); + // register attests + serverAllocator.registerAttest(attests[0], vm.getBlockTimestamp()); + serverAllocator.registerAttest(attests[1], vm.getBlockTimestamp()); + serverAllocator.registerAttest(attests[2], vm.getBlockTimestamp()); + vm.stopPrank(); + + assertEq( + serverAllocator.checkAttestExpirations(attests[0])[0], + vm.getBlockTimestamp() + ); + assertEq( + serverAllocator.checkAttestExpirations(attests[1])[0], + vm.getBlockTimestamp() + ); + assertEq( + serverAllocator.checkAttestExpirations(attests[2])[0], + vm.getBlockTimestamp() + ); + + bytes32 message = _hashNonceConsumption( + ServerAllocator.NonceConsumption(signer, nonces, attests), + address(serverAllocator) + ); + bytes memory signature = _signMessage(message, signerPK); + + vm.prank(attacker); + vm.expectEmit(address(serverAllocator)); + emit ServerAllocator.NoncesConsumed(nonces); + serverAllocator.consumeViaSignature( + ServerAllocator.NonceConsumption(signer, nonces, attests), + signature + ); + + assertEq(compactContract.consumedNonces(0), false); + for (uint256 i = 0; i < nonces.length; ++i) { + assertEq(compactContract.consumedNonces(nonces[i]), true); + } + + // check attests were consumed + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.UnregisteredAttest.selector, + attests[0] + ) + ); + serverAllocator.checkAttestExpirations(attests[0]); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.UnregisteredAttest.selector, + attests[1] + ) + ); + serverAllocator.checkAttestExpirations(attests[1]); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.UnregisteredAttest.selector, + attests[2] + ) + ); + serverAllocator.checkAttestExpirations(attests[2]); + } +} + +contract ServerAllocator_isValidSignature is SignerSet { + function test_isValidSignature_revertInvalidSig() public { + bytes32 attest = createAttest(signer, usdcId, 100); + bytes32 message = _hashAllocator( + Allocator(attest), + address(serverAllocator) + ); + bytes memory signature = _signMessage(message, attackerPK); + + vm.prank(attacker); + vm.expectRevert( + abi.encodeWithSelector( + ServerAllocator.InvalidSignature.selector, + signature, + attacker + ) + ); + serverAllocator.isValidSignature(attest, signature); + } + + function test_isValidSignature_successful() public { + bytes32 attest = createAttest(signer, usdcId, 100); + bytes32 message = _hashAllocator( + Allocator(attest), + address(serverAllocator) + ); + bytes memory signature = _signMessage(message, signerPK); + + vm.prank(attacker); + bytes4 magicValue = serverAllocator.isValidSignature(attest, signature); + assertEq(magicValue, IERC1271.isValidSignature.selector); + } } From 07014db161b542e969f7e702c8a80547bfbc58b8 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Mon, 4 Nov 2024 15:59:41 +0100 Subject: [PATCH 04/10] forge fmt --- src/examples/allocator/ServerAllocator.sol | 155 ++----- src/interfaces/IAllocator.sol | 10 +- src/test/AlwaysOKAllocator.sol | 17 +- src/test/ERC20Mock.sol | 5 +- src/test/TheCompactMock.sol | 60 +-- test/ServerAllocator.t.sol | 485 +++++---------------- 6 files changed, 154 insertions(+), 578 deletions(-) diff --git a/src/examples/allocator/ServerAllocator.sol b/src/examples/allocator/ServerAllocator.sol index f775bfc..444f990 100644 --- a/src/examples/allocator/ServerAllocator.sol +++ b/src/examples/allocator/ServerAllocator.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.27; -import {Compact} from "src/types/EIP712Types.sol"; -import {ITheCompact} from "src/interfaces/ITheCompact.sol"; -import {IAllocator} from "src/interfaces/IAllocator.sol"; -import {Ownable, Ownable2Step} from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol"; -import {ECDSA} from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import {EIP712} from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; -import {IERC1271} from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; +import { Compact } from "src/types/EIP712Types.sol"; +import { ITheCompact } from "src/interfaces/ITheCompact.sol"; +import { IAllocator } from "src/interfaces/IAllocator.sol"; +import { Ownable, Ownable2Step } from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol"; +import { ECDSA } from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import { EIP712 } from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; +import { IERC1271 } from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; contract ServerAllocator is Ownable2Step, EIP712, IAllocator { using ECDSA for bytes32; @@ -30,16 +30,13 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { bytes4 private constant _ATTEST_SELECTOR = 0x1a808f91; // keccak256("RegisterAttest(address signer,bytes32 attestHash,uint256 expiration,uint256 nonce)") - bytes32 private constant _ATTEST_TYPE_HASH = - 0xaf2dfd3fe08723f490d203be627da2725f4ad38681e455221da2fc1a633bbb18; + bytes32 private constant _ATTEST_TYPE_HASH = 0xaf2dfd3fe08723f490d203be627da2725f4ad38681e455221da2fc1a633bbb18; // keccak256("Allocator(bytes32 hash)") - bytes32 private constant _ALLOCATOR_TYPE_HASH = - 0xcdf324dc7c3490a07fbbb105911393dcbc0676ac7c6c1c32c786721de6179e70; + bytes32 private constant _ALLOCATOR_TYPE_HASH = 0xcdf324dc7c3490a07fbbb105911393dcbc0676ac7c6c1c32c786721de6179e70; // keccak256("NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)") - bytes32 private constant _NONCE_CONSUMPTION_TYPE_HASH = - 0xb06793f900067653959d9bc53299ebf6b5aa5cf5f6c1a463305891a3db695f3c; + bytes32 private constant _NONCE_CONSUMPTION_TYPE_HASH = 0xb06793f900067653959d9bc53299ebf6b5aa5cf5f6c1a463305891a3db695f3c; address private immutable _COMPACT_CONTRACT; @@ -72,10 +69,7 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { _; } - constructor( - address owner_, - address compactContract_ - ) Ownable(owner_) EIP712("Allocator", "1") { + constructor(address owner_, address compactContract_) Ownable(owner_) EIP712("Allocator", "1") { _COMPACT_CONTRACT = compactContract_; } @@ -105,31 +99,17 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { } /// @dev There is no way to uniquely identify a transfer, so the contract relies on its own accounting of registered attests. - function registerAttest( - bytes32 attest_, - uint256 expiration_ - ) external isSigner(msg.sender) { + function registerAttest(bytes32 attest_, uint256 expiration_) external isSigner(msg.sender) { _registerAttest(attest_, expiration_); } /// @dev Nonce management in the RegisterAttest is only required for multiple registers of the same attest with the same expiration. - function registerAttestViaSignature( - RegisterAttest calldata attest_, - bytes calldata signature_ - ) external { - bytes32 _attestWithNonce = keccak256( - abi.encode(attest_.attestHash, attest_.expiration, attest_.nonce) - ); + function registerAttestViaSignature(RegisterAttest calldata attest_, bytes calldata signature_) external { + bytes32 _attestWithNonce = keccak256(abi.encode(attest_.attestHash, attest_.expiration, attest_.nonce)); if (_attestSignatures[_attestWithNonce]) { revert AlreadyUsedSig(attest_.attestHash, attest_.nonce); } - address signer = _validateSignedAttest( - attest_.signer, - attest_.attestHash, - attest_.expiration, - attest_.nonce, - signature_ - ); + address signer = _validateSignedAttest(attest_.signer, attest_.attestHash, attest_.expiration, attest_.nonce, signature_); if (signer != attest_.signer || !_containsSigner(signer)) { revert InvalidSignature(signature_, signer); } @@ -165,12 +145,8 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { delete _attestExpirations[countedAttest]; } else { // Shift attest and delete from the end - bytes32 lastAttest = keccak256( - abi.encode(registeredAttest, count) - ); - _attestExpirations[countedAttest] = _attestExpirations[ - lastAttest - ]; + bytes32 lastAttest = keccak256(abi.encode(registeredAttest, count)); + _attestExpirations[countedAttest] = _attestExpirations[lastAttest]; delete _attestExpirations[lastAttest]; } _attestCounts[registeredAttest] = --count; @@ -196,10 +172,7 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { _consumeNonces(nonces_, attests_); } - function consumeViaSignature( - NonceConsumption calldata data_, - bytes calldata signature_ - ) external { + function consumeViaSignature(NonceConsumption calldata data_, bytes calldata signature_) external { if (data_.attests.length != data_.nonces.length) { revert InvalidInput(); } @@ -213,10 +186,7 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { /// @dev A registered attest will be a fallback if no valid signature was provided. // TODO: https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/interfaces/IERC1271.sol - function isValidSignature( - bytes32 hash_, - bytes calldata signature_ - ) external view returns (bytes4 magicValue) { + function isValidSignature(bytes32 hash_, bytes calldata signature_) external view returns (bytes4 magicValue) { address signer = _validateSignedHash(hash_, signature_); if (!_containsSigner(signer)) { revert InvalidSignature(signature_, signer); @@ -232,21 +202,12 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { return _activeSigners; } - function checkAttestExpirations( - bytes32 attest_ - ) external view returns (uint256[] memory) { + function checkAttestExpirations(bytes32 attest_) external view returns (uint256[] memory) { return _checkAttestExpirations(attest_); } - function checkAttestExpirations( - address sponsor_, - uint256 id_, - uint256 amount_ - ) external view returns (uint256[] memory) { - return - _checkAttestExpirations( - keccak256(abi.encode(sponsor_, id_, amount_)) - ); + function checkAttestExpirations(address sponsor_, uint256 id_, uint256 amount_) external view returns (uint256[] memory) { + return _checkAttestExpirations(keccak256(abi.encode(sponsor_, id_, amount_))); } function getCompactContract() external view returns (address) { @@ -266,10 +227,7 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { } /// Todo: This will lead to always the last registered hash being consumed. - function _consumeNonces( - uint256[] calldata nonces_, - bytes32[] calldata attests_ - ) internal { + function _consumeNonces(uint256[] calldata nonces_, bytes32[] calldata attests_) internal { ITheCompact(_COMPACT_CONTRACT).consume(nonces_); uint256 nonceLength = attests_.length; for (uint256 i = 0; i < nonceLength; ++i) { @@ -288,92 +246,45 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { emit NoncesConsumed(nonces_); } - function _validateSignedAttest( - address signer_, - bytes32 hash_, - uint256 expiration_, - uint256 nonce, - bytes calldata signature_ - ) internal view returns (address) { + function _validateSignedAttest(address signer_, bytes32 hash_, uint256 expiration_, uint256 nonce, bytes calldata signature_) internal view returns (address) { bytes32 message = _hashAttest(signer_, hash_, expiration_, nonce); return message.recover(signature_); } - function _hashAttest( - address signer_, - bytes32 hash_, - uint256 expiration_, - uint256 nonce_ - ) internal view returns (bytes32) { - return - _hashTypedDataV4( - keccak256( - abi.encode( - _ATTEST_TYPE_HASH, - signer_, - hash_, - expiration_, - nonce_ - ) - ) - ); + function _hashAttest(address signer_, bytes32 hash_, uint256 expiration_, uint256 nonce_) internal view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(_ATTEST_TYPE_HASH, signer_, hash_, expiration_, nonce_))); } - function _validateSignedHash( - bytes32 hash_, - bytes calldata signature_ - ) internal view returns (address) { + function _validateSignedHash(bytes32 hash_, bytes calldata signature_) internal view returns (address) { bytes32 message = _hashMessage(hash_); return message.recover(signature_); } function _hashMessage(bytes32 data_) internal view returns (bytes32) { - return - _hashTypedDataV4( - keccak256(abi.encode(_ALLOCATOR_TYPE_HASH, data_)) - ); + return _hashTypedDataV4(keccak256(abi.encode(_ALLOCATOR_TYPE_HASH, data_))); } - function _validateNonceConsumption( - NonceConsumption calldata data_, - bytes calldata signature_ - ) internal view returns (address) { + function _validateNonceConsumption(NonceConsumption calldata data_, bytes calldata signature_) internal view returns (address) { bytes32 message = _hashNonceConsumption(data_); return message.recover(signature_); } - function _hashNonceConsumption( - NonceConsumption calldata data_ - ) internal view returns (bytes32) { - return - _hashTypedDataV4( - keccak256( - abi.encode( - _NONCE_CONSUMPTION_TYPE_HASH, - data_.signer, - data_.nonces, - data_.attests - ) - ) - ); + function _hashNonceConsumption(NonceConsumption calldata data_) internal view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(_NONCE_CONSUMPTION_TYPE_HASH, data_.signer, data_.nonces, data_.attests))); } function _containsSigner(address signer_) internal view returns (bool) { return _signers[signer_] != 0; } - function _checkAttestExpirations( - bytes32 attest_ - ) internal view returns (uint256[] memory) { + function _checkAttestExpirations(bytes32 attest_) internal view returns (uint256[] memory) { uint256 count = _attestCounts[attest_]; if (count == 0) { revert UnregisteredAttest(attest_); } uint256[] memory expirations = new uint256[](count); for (uint256 i = count; i > 0; --i) { - expirations[i - 1] = _attestExpirations[ - keccak256(abi.encode(attest_, i)) - ]; + expirations[i - 1] = _attestExpirations[keccak256(abi.encode(attest_, i))]; } return expirations; } diff --git a/src/interfaces/IAllocator.sol b/src/interfaces/IAllocator.sol index 5c8d0dc..bf48872 100644 --- a/src/interfaces/IAllocator.sol +++ b/src/interfaces/IAllocator.sol @@ -1,17 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import {IERC1271} from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; +import { IERC1271 } from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; 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); + function attest(address operator, address from, address to, uint256 id, uint256 amount) external returns (bytes4); // isValidSignature of IERC1271 will be called during a claim and must verify the signature of the allocation. } diff --git a/src/test/AlwaysOKAllocator.sol b/src/test/AlwaysOKAllocator.sol index 758b5c2..d40c058 100644 --- a/src/test/AlwaysOKAllocator.sol +++ b/src/test/AlwaysOKAllocator.sol @@ -1,24 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import {IAllocator} from "../interfaces/IAllocator.sol"; -import {IERC1271} from "permit2/src/interfaces/IERC1271.sol"; +import { IAllocator } from "../interfaces/IAllocator.sol"; +import { IERC1271 } from "permit2/src/interfaces/IERC1271.sol"; contract AlwaysOKAllocator is IAllocator { - function attest( - address, - address, - address, - uint256, - uint256 - ) external pure returns (bytes4) { + function attest(address, address, address, uint256, uint256) external pure returns (bytes4) { return IAllocator.attest.selector; } - function isValidSignature( - bytes32, - bytes calldata - ) external pure returns (bytes4) { + function isValidSignature(bytes32, bytes calldata) external pure returns (bytes4) { return IERC1271.isValidSignature.selector; } } diff --git a/src/test/ERC20Mock.sol b/src/test/ERC20Mock.sol index 5504925..7cbeb5d 100644 --- a/src/test/ERC20Mock.sol +++ b/src/test/ERC20Mock.sol @@ -4,10 +4,7 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract ERC20Mock is ERC20 { - constructor( - string memory name_, - string memory symbol_ - ) ERC20(name_, symbol_) {} + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) { } function mint(address to, uint256 amount) external { _mint(to, amount); diff --git a/src/test/TheCompactMock.sol b/src/test/TheCompactMock.sol index a91ac71..474eae1 100644 --- a/src/test/TheCompactMock.sol +++ b/src/test/TheCompactMock.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.27; -import {IAllocator} from "src/interfaces/IAllocator.sol"; -import {ERC6909} from "solady/tokens/ERC6909.sol"; -import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import {IdLib} from "src/lib/IdLib.sol"; +import { IAllocator } from "src/interfaces/IAllocator.sol"; +import { ERC6909 } from "solady/tokens/ERC6909.sol"; +import { ERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import { IdLib } from "src/lib/IdLib.sol"; contract TheCompactMock is ERC6909 { using IdLib for uint96; @@ -14,57 +14,27 @@ contract TheCompactMock is ERC6909 { mapping(uint256 nonce => bool consumed) public consumedNonces; - function deposit( - address token, - uint256 amount, - address allocator - ) external { + function deposit(address token, uint256 amount, address allocator) external { ERC20(token).transferFrom(msg.sender, address(this), amount); uint256 id = _getTokenId(token, allocator); _mint(msg.sender, id, amount); } - function transfer( - address from, - address to, - uint256 amount, - address token, - address allocator - ) external { + function transfer(address from, address to, uint256 amount, address token, address allocator) external { uint256 id = _getTokenId(token, allocator); IAllocator(allocator).attest(msg.sender, from, to, id, amount); _transfer(msg.sender, from, to, id, amount); } - function claim( - address from, - address to, - address token, - uint256 amount, - address allocator, - bytes calldata signature - ) external { + function claim(address from, address to, address token, uint256 amount, address allocator, bytes calldata signature) external { uint256 id = _getTokenId(token, allocator); - IAllocator(allocator).isValidSignature( - keccak256(abi.encode(from, id, amount)), - signature - ); + IAllocator(allocator).isValidSignature(keccak256(abi.encode(from, id, amount)), signature); _transfer(msg.sender, from, to, id, amount); } - function withdraw( - address token, - uint256 amount, - address allocator - ) external { + function withdraw(address token, uint256 amount, address allocator) external { uint256 id = _getTokenId(token, allocator); - IAllocator(allocator).attest( - msg.sender, - msg.sender, - msg.sender, - id, - amount - ); + IAllocator(allocator).attest(msg.sender, msg.sender, msg.sender, id, amount); ERC20(token).transferFrom(address(this), msg.sender, amount); _burn(msg.sender, id, amount); } @@ -76,10 +46,7 @@ contract TheCompactMock is ERC6909 { return true; } - function getTokenId( - address token, - address allocator - ) external pure returns (uint256) { + function getTokenId(address token, address allocator) external pure returns (uint256) { return _getTokenId(token, allocator); } @@ -101,10 +68,7 @@ contract TheCompactMock is ERC6909 { return ""; } - function _getTokenId( - address token, - address allocator - ) internal pure returns (uint256) { + function _getTokenId(address token, address allocator) internal pure returns (uint256) { return uint256(keccak256(abi.encode(token, allocator))); } } diff --git a/test/ServerAllocator.t.sol b/test/ServerAllocator.t.sol index 761d9fb..6107967 100644 --- a/test/ServerAllocator.t.sol +++ b/test/ServerAllocator.t.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.27; -import {Test} from "forge-std/Test.sol"; -import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; -import {ServerAllocator} from "src/examples/allocator/ServerAllocator.sol"; -import {TheCompactMock} from "src/test/TheCompactMock.sol"; -import {ERC20Mock} from "src/test/ERC20Mock.sol"; -import {console} from "forge-std/console.sol"; -import {IERC1271} from "lib/permit2/src/interfaces/IERC1271.sol"; +import { Test } from "forge-std/Test.sol"; +import { Ownable } from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import { ServerAllocator } from "src/examples/allocator/ServerAllocator.sol"; +import { TheCompactMock } from "src/test/TheCompactMock.sol"; +import { ERC20Mock } from "src/test/ERC20Mock.sol"; +import { console } from "forge-std/console.sol"; +import { IERC1271 } from "lib/permit2/src/interfaces/IERC1271.sol"; abstract contract MocksSetup is Test { address owner = makeAddr("owner"); @@ -25,10 +25,7 @@ abstract contract MocksSetup is Test { usdc = new ERC20Mock("USDC", "USDC"); compactContract = new TheCompactMock(); serverAllocator = new ServerAllocator(owner, address(compactContract)); - usdcId = compactContract.getTokenId( - address(usdc), - address(serverAllocator) - ); + usdcId = compactContract.getTokenId(address(usdc), address(serverAllocator)); (signer, signerPK) = makeAddrAndKey("signer"); (attacker, attackerPK) = makeAddrAndKey("attacker"); } @@ -37,11 +34,7 @@ abstract contract MocksSetup is Test { abstract contract AttestSetup { bytes4 internal constant _ATTEST_SELECTOR = 0x1a808f91; - function createAttest( - address from_, - uint256 id_, - uint256 amount_ - ) internal pure returns (bytes32) { + function createAttest(address from_, uint256 id_, uint256 amount_) internal pure returns (bytes32) { return keccak256(abi.encode(from_, id_, amount_)); } } @@ -52,97 +45,51 @@ abstract contract CreateHash is Test { } // stringified types - string EIP712_DOMAIN_TYPE = - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the funcion + string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the funcion string ALLOCATOR_TYPE = "Allocator(bytes32 hash)"; // Hashed inside the funcion - string REGISTER_ATTEST_TYPE = - "RegisterAttest(address signer,bytes32 attestHash,uint256 expiration,uint256 nonce)"; // Hashed inside the funcion - string NONCE_CONSUMPTION_TYPE = - "NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)"; // Hashed inside the funcion + string REGISTER_ATTEST_TYPE = "RegisterAttest(address signer,bytes32 attestHash,uint256 expiration,uint256 nonce)"; // Hashed inside the funcion + string NONCE_CONSUMPTION_TYPE = "NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)"; // Hashed inside the funcion // EIP712 domain type string name = "Allocator"; string version = "1"; - function _hashAllocator( - Allocator memory data, - address verifyingContract - ) internal view returns (bytes32) { + function _hashAllocator(Allocator memory data, address verifyingContract) internal view returns (bytes32) { // hash typed data - return - keccak256( - abi.encodePacked( - "\x19\x01", // backslash is needed to escape the character - _domainSeperator(verifyingContract), - keccak256( - abi.encode(keccak256(bytes(ALLOCATOR_TYPE)), data.hash) - ) - ) - ); - } - - function _hashRegisterAttest( - ServerAllocator.RegisterAttest memory data, - address verifyingContract - ) internal view returns (bytes32) { - return - keccak256( - abi.encodePacked( - "\x19\x01", // backslash is needed to escape the character - _domainSeperator(verifyingContract), - keccak256( - abi.encode( - keccak256(bytes(REGISTER_ATTEST_TYPE)), - data.signer, - data.attestHash, - data.expiration, - data.nonce - ) - ) - ) - ); - } - - function _hashNonceConsumption( - ServerAllocator.NonceConsumption memory data, - address verifyingContract - ) internal view returns (bytes32) { + return keccak256( + abi.encodePacked( + "\x19\x01", // backslash is needed to escape the character + _domainSeperator(verifyingContract), + keccak256(abi.encode(keccak256(bytes(ALLOCATOR_TYPE)), data.hash)) + ) + ); + } + + function _hashRegisterAttest(ServerAllocator.RegisterAttest memory data, address verifyingContract) internal view returns (bytes32) { + return keccak256( + abi.encodePacked( + "\x19\x01", // backslash is needed to escape the character + _domainSeperator(verifyingContract), + keccak256(abi.encode(keccak256(bytes(REGISTER_ATTEST_TYPE)), data.signer, data.attestHash, data.expiration, data.nonce)) + ) + ); + } + + function _hashNonceConsumption(ServerAllocator.NonceConsumption memory data, address verifyingContract) internal view returns (bytes32) { // hash typed data - return - keccak256( - abi.encodePacked( - "\x19\x01", // backslash is needed to escape the character - _domainSeperator(verifyingContract), - keccak256( - abi.encode( - keccak256(bytes(NONCE_CONSUMPTION_TYPE)), - data.signer, - data.nonces, - data.attests - ) - ) - ) - ); - } - - function _domainSeperator( - address verifyingContract - ) internal view returns (bytes32) { - return - keccak256( - abi.encode( - keccak256(bytes(EIP712_DOMAIN_TYPE)), - keccak256(bytes(name)), - keccak256(bytes(version)), - block.chainid, - verifyingContract - ) - ); - } - - function _signMessage( - bytes32 hash_, - uint256 signerPK_ - ) internal pure returns (bytes memory) { + return keccak256( + abi.encodePacked( + "\x19\x01", // backslash is needed to escape the character + _domainSeperator(verifyingContract), + keccak256(abi.encode(keccak256(bytes(NONCE_CONSUMPTION_TYPE)), data.signer, data.nonces, data.attests)) + ) + ); + } + + function _domainSeperator(address verifyingContract) internal view returns (bytes32) { + return keccak256(abi.encode(keccak256(bytes(EIP712_DOMAIN_TYPE)), keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, verifyingContract)); + } + + function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); return abi.encodePacked(r, s, v); } @@ -169,12 +116,7 @@ contract ServerAllocator_ManageSigners is MocksSetup, CreateHash { function test_fuzz_onlyOwnerCanAddSigner(address attacker_) public { vm.assume(attacker_ != owner); - vm.expectRevert( - abi.encodeWithSelector( - Ownable.OwnableUnauthorizedAccount.selector, - attacker_ - ) - ); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, attacker_)); vm.prank(attacker_); serverAllocator.addSigner(signer); } @@ -223,21 +165,11 @@ contract ServerAllocator_ManageSigners is MocksSetup, CreateHash { vm.startPrank(signer); // try to add another signer - vm.expectRevert( - abi.encodeWithSelector( - Ownable.OwnableUnauthorizedAccount.selector, - signer - ) - ); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, signer)); serverAllocator.addSigner(attacker); // try to remove a signer - vm.expectRevert( - abi.encodeWithSelector( - Ownable.OwnableUnauthorizedAccount.selector, - signer - ) - ); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, signer)); serverAllocator.removeSigner(signer); } @@ -259,35 +191,16 @@ contract ServerAllocator_Attest is SignerSet { vm.assume(attacker_ != signer); vm.prank(attacker_); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.InvalidSigner.selector, - attacker_ - ) - ); - serverAllocator.registerAttest( - createAttest(attacker_, usdcId, 100), - vm.getBlockTimestamp() + 1 days - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSigner.selector, attacker_)); + serverAllocator.registerAttest(createAttest(attacker_, usdcId, 100), vm.getBlockTimestamp() + 1 days); } - function test_fuzz_registerAttest_attestExpired( - uint256 expiration_ - ) public { + function test_fuzz_registerAttest_attestExpired(uint256 expiration_) public { vm.assume(expiration_ < vm.getBlockTimestamp()); vm.prank(signer); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.Expired.selector, - expiration_, - vm.getBlockTimestamp() - ) - ); - serverAllocator.registerAttest( - createAttest(signer, usdcId, 100), - expiration_ - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.Expired.selector, expiration_, vm.getBlockTimestamp())); + serverAllocator.registerAttest(createAttest(signer, usdcId, 100), expiration_); } function test_registerAttest_successful() public { @@ -305,22 +218,12 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - ServerAllocator.RegisterAttest memory attestData = ServerAllocator - .RegisterAttest(signer, attest, expiration, 0); - bytes32 message = _hashRegisterAttest( - attestData, - address(serverAllocator) - ); + ServerAllocator.RegisterAttest memory attestData = ServerAllocator.RegisterAttest(signer, attest, expiration, 0); + bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, attackerPK); vm.prank(attacker); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.InvalidSignature.selector, - signature, - attacker - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSignature.selector, signature, attacker)); serverAllocator.registerAttestViaSignature(attestData, signature); } @@ -328,12 +231,8 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - ServerAllocator.RegisterAttest memory attestData = ServerAllocator - .RegisterAttest(signer, attest, expiration, 0); - bytes32 message = _hashRegisterAttest( - attestData, - address(serverAllocator) - ); + ServerAllocator.RegisterAttest memory attestData = ServerAllocator.RegisterAttest(signer, attest, expiration, 0); + bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); @@ -346,12 +245,8 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - ServerAllocator.RegisterAttest memory attestData = ServerAllocator - .RegisterAttest(signer, attest, expiration, 0); - bytes32 message = _hashRegisterAttest( - attestData, - address(serverAllocator) - ); + ServerAllocator.RegisterAttest memory attestData = ServerAllocator.RegisterAttest(signer, attest, expiration, 0); + bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); @@ -360,13 +255,7 @@ contract ServerAllocator_Attest is SignerSet { serverAllocator.registerAttestViaSignature(attestData, signature); vm.prank(attacker); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.AlreadyUsedSig.selector, - attest, - 0 - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.AlreadyUsedSig.selector, attest, 0)); serverAllocator.registerAttestViaSignature(attestData, signature); } @@ -381,54 +270,28 @@ contract ServerAllocator_Attest is SignerSet { emit ServerAllocator.AttestRegistered(attest, expiration1); serverAllocator.registerAttest(attest, expiration1); - assertEq( - serverAllocator.checkAttestExpirations(attest)[0], - expiration1 - ); + assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration1); // second attest with different expiration vm.expectEmit(address(serverAllocator)); emit ServerAllocator.AttestRegistered(attest, expiration2); serverAllocator.registerAttest(attest, expiration2); - assertEq( - serverAllocator.checkAttestExpirations(attest)[0], - expiration1 - ); - assertEq( - serverAllocator.checkAttestExpirations(attest)[1], - expiration2 - ); + assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration1); + assertEq(serverAllocator.checkAttestExpirations(attest)[1], expiration2); } function test_fuzz_attest_callerMustBeCompact(address caller_) public { vm.assume(caller_ != address(compactContract)); vm.prank(caller_); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.InvalidCaller.selector, - caller_, - address(compactContract) - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidCaller.selector, caller_, address(compactContract))); serverAllocator.attest(caller_, signer, attacker, usdcId, 100); } - function test_fuzz_attest_notRegistered( - address operator_, - address from_, - address to_, - uint256 id_, - uint256 amount_ - ) public { + function test_fuzz_attest_notRegistered(address operator_, address from_, address to_, uint256 id_, uint256 amount_) public { vm.prank(address(compactContract)); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.UnregisteredAttest.selector, - keccak256(abi.encode(from_, id_, amount_)) - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, keccak256(abi.encode(from_, id_, amount_)))); serverAllocator.attest(operator_, from_, to_, id_, amount_); } @@ -446,28 +309,11 @@ contract ServerAllocator_Attest is SignerSet { // check attest vm.prank(address(compactContract)); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.ExpiredAttests.selector, - attest - ) - ); - serverAllocator.attest( - signer, - attacker, - makeAddr("to"), - usdcId, - amount_ - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.ExpiredAttests.selector, attest)); + serverAllocator.attest(signer, attacker, makeAddr("to"), usdcId, amount_); } - function test_fuzz_attest_successful( - address operator_, - address from_, - address to_, - uint256 id_, - uint256 amount_ - ) public { + function test_fuzz_attest_successful(address operator_, address from_, address to_, uint256 id_, uint256 amount_) public { bytes32 attest = createAttest(from_, id_, amount_); uint256 expiration = vm.getBlockTimestamp(); @@ -482,22 +328,11 @@ contract ServerAllocator_Attest is SignerSet { vm.prank(address(compactContract)); vm.expectEmit(address(serverAllocator)); emit ServerAllocator.Attested(from_, id_, amount_); - bytes4 attestSelector = serverAllocator.attest( - operator_, - from_, - to_, - id_, - amount_ - ); + bytes4 attestSelector = serverAllocator.attest(operator_, from_, to_, id_, amount_); assertEq(attestSelector, _ATTEST_SELECTOR); // check attest was consumed - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.UnregisteredAttest.selector, - attest - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attest)); serverAllocator.checkAttestExpirations(attest); } } @@ -505,20 +340,13 @@ contract ServerAllocator_Attest is SignerSet { contract ServerAllocator_Consume is SignerSet { function test_consume_onlySignerCanConsume() public { vm.prank(attacker); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.InvalidSigner.selector, - attacker - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSigner.selector, attacker)); serverAllocator.consume(new uint256[](0), new bytes32[](0)); } function test_consume_requiresNoncesAndAttestsToBeOfSameLength() public { vm.prank(signer); - vm.expectRevert( - abi.encodeWithSelector(ServerAllocator.InvalidInput.selector) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidInput.selector)); serverAllocator.consume(new uint256[](0), new bytes32[](1)); } @@ -558,18 +386,9 @@ contract ServerAllocator_Consume is SignerSet { serverAllocator.registerAttest(attests[1], vm.getBlockTimestamp()); serverAllocator.registerAttest(attests[2], vm.getBlockTimestamp()); - assertEq( - serverAllocator.checkAttestExpirations(attests[0])[0], - vm.getBlockTimestamp() - ); - assertEq( - serverAllocator.checkAttestExpirations(attests[1])[0], - vm.getBlockTimestamp() - ); - assertEq( - serverAllocator.checkAttestExpirations(attests[2])[0], - vm.getBlockTimestamp() - ); + assertEq(serverAllocator.checkAttestExpirations(attests[0])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestExpirations(attests[1])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestExpirations(attests[2])[0], vm.getBlockTimestamp()); vm.expectEmit(address(serverAllocator)); emit ServerAllocator.NoncesConsumed(nonces); @@ -581,82 +400,30 @@ contract ServerAllocator_Consume is SignerSet { } // check attests were consumed - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.UnregisteredAttest.selector, - attests[0] - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[0])); serverAllocator.checkAttestExpirations(attests[0]); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.UnregisteredAttest.selector, - attests[1] - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[1])); serverAllocator.checkAttestExpirations(attests[1]); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.UnregisteredAttest.selector, - attests[2] - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[2])); serverAllocator.checkAttestExpirations(attests[2]); } - function test_consumeViaSignature_requiresNoncesAndAttestsToBeOfSameLength() - public - { - bytes32 message = _hashNonceConsumption( - ServerAllocator.NonceConsumption( - signer, - new uint256[](0), - new bytes32[](1) - ), - address(serverAllocator) - ); + function test_consumeViaSignature_requiresNoncesAndAttestsToBeOfSameLength() public { + bytes32 message = _hashNonceConsumption(ServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(signer); - vm.expectRevert( - abi.encodeWithSelector(ServerAllocator.InvalidInput.selector) - ); - serverAllocator.consumeViaSignature( - ServerAllocator.NonceConsumption( - signer, - new uint256[](0), - new bytes32[](1) - ), - signature - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidInput.selector)); + serverAllocator.consumeViaSignature(ServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), signature); } function test_consumeViaSignature_requireValidSignature() public { - bytes32 message = _hashNonceConsumption( - ServerAllocator.NonceConsumption( - signer, - new uint256[](1), - new bytes32[](1) - ), - address(serverAllocator) - ); + bytes32 message = _hashNonceConsumption(ServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), address(serverAllocator)); bytes memory signature = _signMessage(message, attackerPK); vm.prank(signer); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.InvalidSigner.selector, - attacker - ) - ); - serverAllocator.consumeViaSignature( - ServerAllocator.NonceConsumption( - signer, - new uint256[](1), - new bytes32[](1) - ), - signature - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSigner.selector, attacker)); + serverAllocator.consumeViaSignature(ServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), signature); } function test_consumeViaSignature_successfulWithoutAttests() public { @@ -665,19 +432,13 @@ contract ServerAllocator_Consume is SignerSet { nonces[1] = 2; nonces[2] = 3; - bytes32 message = _hashNonceConsumption( - ServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), - address(serverAllocator) - ); + bytes32 message = _hashNonceConsumption(ServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); vm.expectEmit(address(serverAllocator)); emit ServerAllocator.NoncesConsumed(nonces); - serverAllocator.consumeViaSignature( - ServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), - signature - ); + serverAllocator.consumeViaSignature(ServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), signature); } function test_consumeViaSignature_successfulWithAttests() public { @@ -698,32 +459,17 @@ contract ServerAllocator_Consume is SignerSet { serverAllocator.registerAttest(attests[2], vm.getBlockTimestamp()); vm.stopPrank(); - assertEq( - serverAllocator.checkAttestExpirations(attests[0])[0], - vm.getBlockTimestamp() - ); - assertEq( - serverAllocator.checkAttestExpirations(attests[1])[0], - vm.getBlockTimestamp() - ); - assertEq( - serverAllocator.checkAttestExpirations(attests[2])[0], - vm.getBlockTimestamp() - ); + assertEq(serverAllocator.checkAttestExpirations(attests[0])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestExpirations(attests[1])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestExpirations(attests[2])[0], vm.getBlockTimestamp()); - bytes32 message = _hashNonceConsumption( - ServerAllocator.NonceConsumption(signer, nonces, attests), - address(serverAllocator) - ); + bytes32 message = _hashNonceConsumption(ServerAllocator.NonceConsumption(signer, nonces, attests), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); vm.expectEmit(address(serverAllocator)); emit ServerAllocator.NoncesConsumed(nonces); - serverAllocator.consumeViaSignature( - ServerAllocator.NonceConsumption(signer, nonces, attests), - signature - ); + serverAllocator.consumeViaSignature(ServerAllocator.NonceConsumption(signer, nonces, attests), signature); assertEq(compactContract.consumedNonces(0), false); for (uint256 i = 0; i < nonces.length; ++i) { @@ -731,26 +477,11 @@ contract ServerAllocator_Consume is SignerSet { } // check attests were consumed - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.UnregisteredAttest.selector, - attests[0] - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[0])); serverAllocator.checkAttestExpirations(attests[0]); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.UnregisteredAttest.selector, - attests[1] - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[1])); serverAllocator.checkAttestExpirations(attests[1]); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.UnregisteredAttest.selector, - attests[2] - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[2])); serverAllocator.checkAttestExpirations(attests[2]); } } @@ -758,29 +489,17 @@ contract ServerAllocator_Consume is SignerSet { contract ServerAllocator_isValidSignature is SignerSet { function test_isValidSignature_revertInvalidSig() public { bytes32 attest = createAttest(signer, usdcId, 100); - bytes32 message = _hashAllocator( - Allocator(attest), - address(serverAllocator) - ); + bytes32 message = _hashAllocator(Allocator(attest), address(serverAllocator)); bytes memory signature = _signMessage(message, attackerPK); vm.prank(attacker); - vm.expectRevert( - abi.encodeWithSelector( - ServerAllocator.InvalidSignature.selector, - signature, - attacker - ) - ); + vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSignature.selector, signature, attacker)); serverAllocator.isValidSignature(attest, signature); } function test_isValidSignature_successful() public { bytes32 attest = createAttest(signer, usdcId, 100); - bytes32 message = _hashAllocator( - Allocator(attest), - address(serverAllocator) - ); + bytes32 message = _hashAllocator(Allocator(attest), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); From f61521319dc262cff1fd5bbb264f4339afc161c5 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Wed, 6 Nov 2024 11:26:12 +0100 Subject: [PATCH 05/10] Interface, NatSpec and tests with the compact contract --- src/examples/allocator/ServerAllocator.sol | 73 ++++------- src/interfaces/IServerAllocator.sol | 137 +++++++++++++++++++++ src/test/TheCompactMock.sol | 4 + test/ServerAllocator.t.sol | 117 ++++++++++-------- test/TheCompact.t.sol | 49 ++++++++ 5 files changed, 275 insertions(+), 105 deletions(-) create mode 100644 src/interfaces/IServerAllocator.sol diff --git a/src/examples/allocator/ServerAllocator.sol b/src/examples/allocator/ServerAllocator.sol index 444f990..be53d47 100644 --- a/src/examples/allocator/ServerAllocator.sol +++ b/src/examples/allocator/ServerAllocator.sol @@ -5,36 +5,22 @@ pragma solidity ^0.8.27; import { Compact } from "src/types/EIP712Types.sol"; import { ITheCompact } from "src/interfaces/ITheCompact.sol"; import { IAllocator } from "src/interfaces/IAllocator.sol"; +import { IServerAllocator } from "src/interfaces/IServerAllocator.sol"; +import { Compact, COMPACT_TYPEHASH } from "src/types/EIP712Types.sol"; import { Ownable, Ownable2Step } from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol"; import { ECDSA } from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import { EIP712 } from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; import { IERC1271 } from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; -contract ServerAllocator is Ownable2Step, EIP712, IAllocator { +contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { using ECDSA for bytes32; - struct RegisterAttest { - address signer; - bytes32 attestHash; - uint256 expiration; - uint256 nonce; - } - - struct NonceConsumption { - address signer; - uint256[] nonces; - bytes32[] attests; - } - // keccak256("Attest(address,address,address,uint256,uint256)") bytes4 private constant _ATTEST_SELECTOR = 0x1a808f91; // keccak256("RegisterAttest(address signer,bytes32 attestHash,uint256 expiration,uint256 nonce)") bytes32 private constant _ATTEST_TYPE_HASH = 0xaf2dfd3fe08723f490d203be627da2725f4ad38681e455221da2fc1a633bbb18; - // keccak256("Allocator(bytes32 hash)") - bytes32 private constant _ALLOCATOR_TYPE_HASH = 0xcdf324dc7c3490a07fbbb105911393dcbc0676ac7c6c1c32c786721de6179e70; - // keccak256("NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)") bytes32 private constant _NONCE_CONSUMPTION_TYPE_HASH = 0xb06793f900067653959d9bc53299ebf6b5aa5cf5f6c1a463305891a3db695f3c; @@ -47,21 +33,6 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { mapping(bytes32 => uint256) private _attestCounts; mapping(bytes32 => bool) private _attestSignatures; - event SignerAdded(address signer_); - event SignerRemoved(address signer_); - event AttestRegistered(bytes32 attest_, uint256 expiration_); - event NoncesConsumed(uint256[] nonces_); - event Attested(address from_, uint256 id_, uint256 amount_); - - error UnregisteredAttest(bytes32 attest_); - error Expired(uint256 expiration_, uint256 currentTimestamp_); - error ExpiredAttests(bytes32 attest_); - error InvalidCaller(address caller_, address expected_); - error InvalidSigner(address signer_); - error InvalidSignature(bytes signature_, address signer_); - error AlreadyUsedSig(bytes32 attest_, uint256 nonce); - error InvalidInput(); - modifier isSigner(address signer_) { if (!_containsSigner(signer_)) { revert InvalidSigner(signer_); @@ -71,8 +42,10 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { constructor(address owner_, address compactContract_) Ownable(owner_) EIP712("Allocator", "1") { _COMPACT_CONTRACT = compactContract_; + ITheCompact(_COMPACT_CONTRACT).__registerAllocator(address(this), ""); } + /// @inheritdoc IServerAllocator function addSigner(address signer_) external onlyOwner { if (_containsSigner(signer_)) { return; @@ -84,6 +57,7 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { emit SignerAdded(signer_); } + /// @inheritdoc IServerAllocator function removeSigner(address signer_) external onlyOwner { if (!_containsSigner(signer_)) { return; @@ -98,12 +72,12 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { emit SignerRemoved(signer_); } - /// @dev There is no way to uniquely identify a transfer, so the contract relies on its own accounting of registered attests. + /// @inheritdoc IServerAllocator function registerAttest(bytes32 attest_, uint256 expiration_) external isSigner(msg.sender) { _registerAttest(attest_, expiration_); } - /// @dev Nonce management in the RegisterAttest is only required for multiple registers of the same attest with the same expiration. + /// @inheritdoc IServerAllocator function registerAttestViaSignature(RegisterAttest calldata attest_, bytes calldata signature_) external { bytes32 _attestWithNonce = keccak256(abi.encode(attest_.attestHash, attest_.expiration, attest_.nonce)); if (_attestSignatures[_attestWithNonce]) { @@ -119,7 +93,7 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { _registerAttest(attest_.attestHash, attest_.expiration); } - /// @dev There is no way to uniquely identify a transfer, so the contract relies on its own accounting of registered attests. + /// @inheritdoc IAllocator function attest( address, // operator_ address from_, @@ -159,19 +133,15 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { revert ExpiredAttests(registeredAttest); } - /// @dev The hashes array needs to be of the same length as the nonces array. - /// @dev If no hash was yet registered, provide a bytes32(0) for the respective index. - /// @dev All signers can override nonces of other signers. - function consume( - uint256[] calldata nonces_, // TODO: STRUCT OF ONE - bytes32[] calldata attests_ - ) external isSigner(msg.sender) { + /// @inheritdoc IServerAllocator + function consume(uint256[] calldata nonces_, bytes32[] calldata attests_) external isSigner(msg.sender) { if (attests_.length != nonces_.length) { revert InvalidInput(); } _consumeNonces(nonces_, attests_); } + /// @inheritdoc IServerAllocator function consumeViaSignature(NonceConsumption calldata data_, bytes calldata signature_) external { if (data_.attests.length != data_.nonces.length) { revert InvalidInput(); @@ -179,13 +149,12 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { address signer = _validateNonceConsumption(data_, signature_); if (signer != data_.signer || !_containsSigner(signer)) { // first check is optional, can be deleted for gas efficiency - revert InvalidSigner(signer); + revert InvalidSignature(signature_, signer); } _consumeNonces(data_.nonces, data_.attests); } - /// @dev A registered attest will be a fallback if no valid signature was provided. - // TODO: https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/interfaces/IERC1271.sol + /// @inheritdoc IERC1271 function isValidSignature(bytes32 hash_, bytes calldata signature_) external view returns (bytes4 magicValue) { address signer = _validateSignedHash(hash_, signature_); if (!_containsSigner(signer)) { @@ -194,22 +163,27 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { return IERC1271.isValidSignature.selector; } + /// @inheritdoc IServerAllocator function checkIfSigner(address signer_) external view returns (bool) { return _containsSigner(signer_); } + /// @inheritdoc IServerAllocator function getAllSigners() external view returns (address[] memory) { return _activeSigners; } + /// @inheritdoc IServerAllocator function checkAttestExpirations(bytes32 attest_) external view returns (uint256[] memory) { return _checkAttestExpirations(attest_); } + /// @inheritdoc IServerAllocator function checkAttestExpirations(address sponsor_, uint256 id_, uint256 amount_) external view returns (uint256[] memory) { return _checkAttestExpirations(keccak256(abi.encode(sponsor_, id_, amount_))); } + /// @inheritdoc IServerAllocator function getCompactContract() external view returns (address) { return _COMPACT_CONTRACT; } @@ -255,13 +229,8 @@ contract ServerAllocator is Ownable2Step, EIP712, IAllocator { return _hashTypedDataV4(keccak256(abi.encode(_ATTEST_TYPE_HASH, signer_, hash_, expiration_, nonce_))); } - function _validateSignedHash(bytes32 hash_, bytes calldata signature_) internal view returns (address) { - bytes32 message = _hashMessage(hash_); - return message.recover(signature_); - } - - function _hashMessage(bytes32 data_) internal view returns (bytes32) { - return _hashTypedDataV4(keccak256(abi.encode(_ALLOCATOR_TYPE_HASH, data_))); + function _validateSignedHash(bytes32 digest_, bytes calldata signature_) internal pure returns (address) { + return digest_.recover(signature_); } function _validateNonceConsumption(NonceConsumption calldata data_, bytes calldata signature_) internal view returns (address) { diff --git a/src/interfaces/IServerAllocator.sol b/src/interfaces/IServerAllocator.sol new file mode 100644 index 0000000..c36f405 --- /dev/null +++ b/src/interfaces/IServerAllocator.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { IAllocator } from "src/interfaces/IAllocator.sol"; + +interface IServerAllocator is IAllocator { + struct RegisterAttest { + // The address of the signer who must sign the attest + address signer; + // The hash of the attest information, consistent of sponsor, id and amount + bytes32 attestHash; + // The expiration date after which the attest is no longer valid + uint256 expiration; + // A nonce for that specific attest hash to prevent replay attacks + uint256 nonce; + } + + struct NonceConsumption { + // The address of the signer who must sign the attests + address signer; + // The array of nonces that should be consumed + uint256[] nonces; + // The array of previously registered attests that should be consumed + bytes32[] attests; + } + + /// @notice Thrown if no attest was registered for the given transfer + error UnregisteredAttest(bytes32 attest_); + + /// @notice Thrown if the expiration date to register an attest is in the past + error Expired(uint256 expiration_, uint256 currentTimestamp_); + + /// @notice Thrown if all of the registered attests have expired + error ExpiredAttests(bytes32 attest_); + + /// @notice Thrown if the caller of attest is not the compact contract + error InvalidCaller(address caller_, address expected_); + + /// @notice Thrown if the address is not a registered signer + error InvalidSigner(address signer_); + + /// @notice Thrown if a signature is invalid + error InvalidSignature(bytes signature_, address signer_); + + /// @notice Thrown if the same signature is used multiple times + error AlreadyUsedSig(bytes32 attest_, uint256 nonce); + + /// @notice Thrown if the input array lengths are not matching + error InvalidInput(); + + /// @notice Emitted when a signer is added + /// @param signer_ The address of the signer + event SignerAdded(address signer_); + + /// @notice Emitted when a signer is removed + /// @param signer_ The address of the signer + event SignerRemoved(address signer_); + + /// @notice Emitted when an attest is registered + /// @param attest_ The hash of the attest, consistent of sponsor, id and amount + /// @param expiration_ The expiration date of the attest + event AttestRegistered(bytes32 attest_, uint256 expiration_); + + /// @notice Emitted when nonces on the compact contract are consumed successfully + /// @param nonces_ The array of nonces that were consumed + event NoncesConsumed(uint256[] nonces_); + + /// @notice Emitted when an attest was consumed for a transfer + /// @param from_ The address of the sponsor + /// @param id_ The id of the token that was transferred + /// @param amount_ The amount of the token that was transferred + event Attested(address from_, uint256 id_, uint256 amount_); + + /// @notice Add a signer to the allocator + /// @dev Only the owner can add a signer + /// @param signer_ The address of the signer to add + function addSigner(address signer_) external; + + /// @notice Remove a signer from the allocator + /// @dev Only the owner can remove a signer + /// @param signer_ The address of the signer to remove + function removeSigner(address signer_) external; + + /// @notice Register an attest for a transfer + /// @dev There is no way to uniquely identify a transfer, so the contract relies on its own accounting of registered attests. + /// @param attest_ The hash of the attest to whitelist, consistent of sponsor, id and amount + /// @param expiration_ The expiration date of the attest + function registerAttest(bytes32 attest_, uint256 expiration_) external; + + /// @notice Register an attest for a transfer via a signature + /// @dev Nonce management in the RegisterAttest is only required for multiple registers of the same attest with the same expiration. + /// @param attest_ The RegisterAttest struct containing the signer, the hash of the attest, the expiration and the nonce + /// @param signature_ The signature of the signer + function registerAttestViaSignature(RegisterAttest calldata attest_, bytes calldata signature_) external; + + /// @notice Consume nonces on the compact contract and attests on the allocator + /// @dev The hashes array needs to be of the same length as the nonces array. + /// @dev If no hash was yet registered for the respective nonce, provide a bytes32(0) for the index. + /// @dev All signers can override nonces of other signers. + /// @param nonces_ The array of all nonces to consume on the compact contract + /// @param attests_ The array of all attests to consume on the allocator + function consume(uint256[] calldata nonces_, bytes32[] calldata attests_) external; + + /// @notice Consume nonces on the compact contract and attests on the allocator via a signature + /// @param data_ The NonceConsumption struct containing the signer, the array of nonces and the array of attests + /// @param signature_ The signature of the signer + function consumeViaSignature(NonceConsumption calldata data_, bytes calldata signature_) external; + + /// @notice Check if an address is a registered signer + /// @param signer_ The address to check + /// @return bool Whether the address is a registered signer + function checkIfSigner(address signer_) external view returns (bool); + + /// @notice Get all registered signers + /// @return The array of all registered signers + function getAllSigners() external view returns (address[] memory); + + /// @notice Check the expiration dates of an attest + /// @dev If no attest was registered for the provided hash, the function will revert + /// @param attest_ The hash of the attest to check + /// @return The array of expiration dates for the registered attests + function checkAttestExpirations(bytes32 attest_) external view returns (uint256[] memory); + + /// @notice Check the expiration dates of an attest by its components + /// @dev If no attest was registered for the provided components, the function will revert + /// @param sponsor_ The address of the sponsor + /// @param id_ The id of the token + /// @param amount_ The amount of the token + /// @return The array of expiration dates for the registered attests + function checkAttestExpirations(address sponsor_, uint256 id_, uint256 amount_) external view returns (uint256[] memory); + + /// @notice Get the address of the compact contract + /// @dev Only the compact contract can call the attest function + /// @return The address of the compact contract + function getCompactContract() external view returns (address); +} diff --git a/src/test/TheCompactMock.sol b/src/test/TheCompactMock.sol index 474eae1..8932fd8 100644 --- a/src/test/TheCompactMock.sol +++ b/src/test/TheCompactMock.sol @@ -14,6 +14,10 @@ contract TheCompactMock is ERC6909 { mapping(uint256 nonce => bool consumed) public consumedNonces; + function __registerAllocator(address allocator, bytes calldata proof) external returns (uint96) { + return 0; + } + function deposit(address token, uint256 amount, address allocator) external { ERC20(token).transferFrom(msg.sender, address(this), amount); uint256 id = _getTokenId(token, allocator); diff --git a/test/ServerAllocator.t.sol b/test/ServerAllocator.t.sol index 6107967..73bd532 100644 --- a/test/ServerAllocator.t.sol +++ b/test/ServerAllocator.t.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.27; import { Test } from "forge-std/Test.sol"; import { Ownable } from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; import { ServerAllocator } from "src/examples/allocator/ServerAllocator.sol"; +import { IServerAllocator } from "src/interfaces/IServerAllocator.sol"; +import { Compact, COMPACT_TYPEHASH } from "src/types/EIP712Types.sol"; import { TheCompactMock } from "src/test/TheCompactMock.sol"; import { ERC20Mock } from "src/test/ERC20Mock.sol"; import { console } from "forge-std/console.sol"; @@ -46,20 +48,31 @@ abstract contract CreateHash is Test { // stringified types string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the funcion - string ALLOCATOR_TYPE = "Allocator(bytes32 hash)"; // Hashed inside the funcion + // string ALLOCATOR_TYPE = "Allocator(bytes32 hash)"; // Hashed inside the funcion string REGISTER_ATTEST_TYPE = "RegisterAttest(address signer,bytes32 attestHash,uint256 expiration,uint256 nonce)"; // Hashed inside the funcion string NONCE_CONSUMPTION_TYPE = "NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)"; // Hashed inside the funcion // EIP712 domain type string name = "Allocator"; string version = "1"; - function _hashAllocator(Allocator memory data, address verifyingContract) internal view returns (bytes32) { + // function _hashAllocator(Allocator memory data, address verifyingContract) internal view returns (bytes32) { + // // hash typed data + // return keccak256( + // abi.encodePacked( + // "\x19\x01", // backslash is needed to escape the character + // _domainSeperator(verifyingContract), + // keccak256(abi.encode(keccak256(bytes(ALLOCATOR_TYPE)), data.hash)) + // ) + // ); + // } + + function _hashCompact(Compact memory data, address verifyingContract) internal view returns (bytes32) { // hash typed data return keccak256( abi.encodePacked( "\x19\x01", // backslash is needed to escape the character _domainSeperator(verifyingContract), - keccak256(abi.encode(keccak256(bytes(ALLOCATOR_TYPE)), data.hash)) + keccak256(abi.encode(COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount)) ) ); } @@ -124,7 +137,7 @@ contract ServerAllocator_ManageSigners is MocksSetup, CreateHash { function test_addSigner() public { vm.prank(owner); vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.SignerAdded(signer); + emit IServerAllocator.SignerAdded(signer); serverAllocator.addSigner(signer); assertEq(serverAllocator.getAllSigners().length, 1); assertEq(serverAllocator.getAllSigners()[0], signer); @@ -137,7 +150,7 @@ contract ServerAllocator_ManageSigners is MocksSetup, CreateHash { // add second signer vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.SignerAdded(attacker); + emit IServerAllocator.SignerAdded(attacker); serverAllocator.addSigner(attacker); assertEq(serverAllocator.getAllSigners().length, 2); assertEq(serverAllocator.getAllSigners()[0], signer); @@ -152,7 +165,7 @@ contract ServerAllocator_ManageSigners is MocksSetup, CreateHash { // remove first signer vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.SignerRemoved(signer); + emit IServerAllocator.SignerRemoved(signer); serverAllocator.removeSigner(signer); assertEq(serverAllocator.getAllSigners().length, 0); } @@ -176,7 +189,7 @@ contract ServerAllocator_ManageSigners is MocksSetup, CreateHash { function test_addingSignerTwice() public { vm.startPrank(owner); vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.SignerAdded(signer); + emit IServerAllocator.SignerAdded(signer); serverAllocator.addSigner(signer); assertEq(serverAllocator.getAllSigners().length, 1); @@ -191,7 +204,7 @@ contract ServerAllocator_Attest is SignerSet { vm.assume(attacker_ != signer); vm.prank(attacker_); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSigner.selector, attacker_)); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidSigner.selector, attacker_)); serverAllocator.registerAttest(createAttest(attacker_, usdcId, 100), vm.getBlockTimestamp() + 1 days); } @@ -199,7 +212,7 @@ contract ServerAllocator_Attest is SignerSet { vm.assume(expiration_ < vm.getBlockTimestamp()); vm.prank(signer); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.Expired.selector, expiration_, vm.getBlockTimestamp())); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.Expired.selector, expiration_, vm.getBlockTimestamp())); serverAllocator.registerAttest(createAttest(signer, usdcId, 100), expiration_); } @@ -208,7 +221,7 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.AttestRegistered(attest, expiration); + emit IServerAllocator.AttestRegistered(attest, expiration); serverAllocator.registerAttest(attest, expiration); assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration); @@ -218,12 +231,12 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - ServerAllocator.RegisterAttest memory attestData = ServerAllocator.RegisterAttest(signer, attest, expiration, 0); + IServerAllocator.RegisterAttest memory attestData = IServerAllocator.RegisterAttest(signer, attest, expiration, 0); bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, attackerPK); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSignature.selector, signature, attacker)); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidSignature.selector, signature, attacker)); serverAllocator.registerAttestViaSignature(attestData, signature); } @@ -231,13 +244,13 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - ServerAllocator.RegisterAttest memory attestData = ServerAllocator.RegisterAttest(signer, attest, expiration, 0); + IServerAllocator.RegisterAttest memory attestData = IServerAllocator.RegisterAttest(signer, attest, expiration, 0); bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.AttestRegistered(attest, expiration); + emit IServerAllocator.AttestRegistered(attest, expiration); serverAllocator.registerAttestViaSignature(attestData, signature); } @@ -245,17 +258,17 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - ServerAllocator.RegisterAttest memory attestData = ServerAllocator.RegisterAttest(signer, attest, expiration, 0); + IServerAllocator.RegisterAttest memory attestData = IServerAllocator.RegisterAttest(signer, attest, expiration, 0); bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.AttestRegistered(attest, expiration); + emit IServerAllocator.AttestRegistered(attest, expiration); serverAllocator.registerAttestViaSignature(attestData, signature); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.AlreadyUsedSig.selector, attest, 0)); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.AlreadyUsedSig.selector, attest, 0)); serverAllocator.registerAttestViaSignature(attestData, signature); } @@ -267,14 +280,14 @@ contract ServerAllocator_Attest is SignerSet { // first attest vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.AttestRegistered(attest, expiration1); + emit IServerAllocator.AttestRegistered(attest, expiration1); serverAllocator.registerAttest(attest, expiration1); assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration1); // second attest with different expiration vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.AttestRegistered(attest, expiration2); + emit IServerAllocator.AttestRegistered(attest, expiration2); serverAllocator.registerAttest(attest, expiration2); assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration1); @@ -285,13 +298,13 @@ contract ServerAllocator_Attest is SignerSet { vm.assume(caller_ != address(compactContract)); vm.prank(caller_); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidCaller.selector, caller_, address(compactContract))); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidCaller.selector, caller_, address(compactContract))); serverAllocator.attest(caller_, signer, attacker, usdcId, 100); } function test_fuzz_attest_notRegistered(address operator_, address from_, address to_, uint256 id_, uint256 amount_) public { vm.prank(address(compactContract)); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, keccak256(abi.encode(from_, id_, amount_)))); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, keccak256(abi.encode(from_, id_, amount_)))); serverAllocator.attest(operator_, from_, to_, id_, amount_); } @@ -309,7 +322,7 @@ contract ServerAllocator_Attest is SignerSet { // check attest vm.prank(address(compactContract)); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.ExpiredAttests.selector, attest)); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.ExpiredAttests.selector, attest)); serverAllocator.attest(signer, attacker, makeAddr("to"), usdcId, amount_); } @@ -327,12 +340,12 @@ contract ServerAllocator_Attest is SignerSet { // check attest vm.prank(address(compactContract)); vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.Attested(from_, id_, amount_); + emit IServerAllocator.Attested(from_, id_, amount_); bytes4 attestSelector = serverAllocator.attest(operator_, from_, to_, id_, amount_); assertEq(attestSelector, _ATTEST_SELECTOR); // check attest was consumed - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attest)); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attest)); serverAllocator.checkAttestExpirations(attest); } } @@ -340,13 +353,13 @@ contract ServerAllocator_Attest is SignerSet { contract ServerAllocator_Consume is SignerSet { function test_consume_onlySignerCanConsume() public { vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSigner.selector, attacker)); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidSigner.selector, attacker)); serverAllocator.consume(new uint256[](0), new bytes32[](0)); } function test_consume_requiresNoncesAndAttestsToBeOfSameLength() public { vm.prank(signer); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidInput.selector)); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidInput.selector)); serverAllocator.consume(new uint256[](0), new bytes32[](1)); } @@ -359,7 +372,7 @@ contract ServerAllocator_Consume is SignerSet { nonces[2] = 3; vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.NoncesConsumed(nonces); + emit IServerAllocator.NoncesConsumed(nonces); serverAllocator.consume(nonces, new bytes32[](3)); assertEq(compactContract.consumedNonces(0), false); @@ -391,7 +404,7 @@ contract ServerAllocator_Consume is SignerSet { assertEq(serverAllocator.checkAttestExpirations(attests[2])[0], vm.getBlockTimestamp()); vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.NoncesConsumed(nonces); + emit IServerAllocator.NoncesConsumed(nonces); serverAllocator.consume(nonces, attests); assertEq(compactContract.consumedNonces(0), false); @@ -400,30 +413,30 @@ contract ServerAllocator_Consume is SignerSet { } // check attests were consumed - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[0])); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[0])); serverAllocator.checkAttestExpirations(attests[0]); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[1])); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[1])); serverAllocator.checkAttestExpirations(attests[1]); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[2])); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[2])); serverAllocator.checkAttestExpirations(attests[2]); } function test_consumeViaSignature_requiresNoncesAndAttestsToBeOfSameLength() public { - bytes32 message = _hashNonceConsumption(ServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), address(serverAllocator)); + bytes32 message = _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(signer); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidInput.selector)); - serverAllocator.consumeViaSignature(ServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), signature); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidInput.selector)); + serverAllocator.consumeViaSignature(IServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), signature); } function test_consumeViaSignature_requireValidSignature() public { - bytes32 message = _hashNonceConsumption(ServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), address(serverAllocator)); + bytes32 message = _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), address(serverAllocator)); bytes memory signature = _signMessage(message, attackerPK); vm.prank(signer); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSigner.selector, attacker)); - serverAllocator.consumeViaSignature(ServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), signature); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidSignature.selector, signature, attacker)); + serverAllocator.consumeViaSignature(IServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), signature); } function test_consumeViaSignature_successfulWithoutAttests() public { @@ -432,13 +445,13 @@ contract ServerAllocator_Consume is SignerSet { nonces[1] = 2; nonces[2] = 3; - bytes32 message = _hashNonceConsumption(ServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), address(serverAllocator)); + bytes32 message = _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.NoncesConsumed(nonces); - serverAllocator.consumeViaSignature(ServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), signature); + emit IServerAllocator.NoncesConsumed(nonces); + serverAllocator.consumeViaSignature(IServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), signature); } function test_consumeViaSignature_successfulWithAttests() public { @@ -463,13 +476,13 @@ contract ServerAllocator_Consume is SignerSet { assertEq(serverAllocator.checkAttestExpirations(attests[1])[0], vm.getBlockTimestamp()); assertEq(serverAllocator.checkAttestExpirations(attests[2])[0], vm.getBlockTimestamp()); - bytes32 message = _hashNonceConsumption(ServerAllocator.NonceConsumption(signer, nonces, attests), address(serverAllocator)); + bytes32 message = _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, nonces, attests), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); vm.expectEmit(address(serverAllocator)); - emit ServerAllocator.NoncesConsumed(nonces); - serverAllocator.consumeViaSignature(ServerAllocator.NonceConsumption(signer, nonces, attests), signature); + emit IServerAllocator.NoncesConsumed(nonces); + serverAllocator.consumeViaSignature(IServerAllocator.NonceConsumption(signer, nonces, attests), signature); assertEq(compactContract.consumedNonces(0), false); for (uint256 i = 0; i < nonces.length; ++i) { @@ -477,33 +490,31 @@ contract ServerAllocator_Consume is SignerSet { } // check attests were consumed - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[0])); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[0])); serverAllocator.checkAttestExpirations(attests[0]); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[1])); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[1])); serverAllocator.checkAttestExpirations(attests[1]); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.UnregisteredAttest.selector, attests[2])); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[2])); serverAllocator.checkAttestExpirations(attests[2]); } } contract ServerAllocator_isValidSignature is SignerSet { function test_isValidSignature_revertInvalidSig() public { - bytes32 attest = createAttest(signer, usdcId, 100); - bytes32 message = _hashAllocator(Allocator(attest), address(serverAllocator)); + bytes32 message = _hashCompact(Compact(signer, signer, 0, vm.getBlockTimestamp(), usdcId, 100), address(serverAllocator)); bytes memory signature = _signMessage(message, attackerPK); vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(ServerAllocator.InvalidSignature.selector, signature, attacker)); - serverAllocator.isValidSignature(attest, signature); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidSignature.selector, signature, attacker)); + serverAllocator.isValidSignature(message, signature); } function test_isValidSignature_successful() public { - bytes32 attest = createAttest(signer, usdcId, 100); - bytes32 message = _hashAllocator(Allocator(attest), address(serverAllocator)); + bytes32 message = _hashCompact(Compact(signer, signer, 0, vm.getBlockTimestamp(), usdcId, 100), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); - bytes4 magicValue = serverAllocator.isValidSignature(attest, signature); + bytes4 magicValue = serverAllocator.isValidSignature(message, signature); assertEq(magicValue, IERC1271.isValidSignature.selector); } } diff --git a/test/TheCompact.t.sol b/test/TheCompact.t.sol index 0ba954b..0d539d3 100644 --- a/test/TheCompact.t.sol +++ b/test/TheCompact.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import { Test, console } from "forge-std/Test.sol"; import { TheCompact } from "../src/TheCompact.sol"; +import { ServerAllocator } from "../src/examples/allocator/ServerAllocator.sol"; import { MockERC20 } from "../lib/solady/test/utils/mocks/MockERC20.sol"; import { Compact, BatchCompact, Segment } from "../src/types/EIP712Types.sol"; import { ResetPeriod } from "../src/types/ResetPeriod.sol"; @@ -1074,6 +1075,54 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(claimant, id), amount); } + function test_claim_viaServerAllocator() public { + ResetPeriod resetPeriod = ResetPeriod.TenMinutes; + Scope scope = Scope.Multichain; + uint256 amount = 1e18; + uint256 nonce = 0; + uint256 expires = block.timestamp + 1000; + address claimant = 0x1111111111111111111111111111111111111111; + address arbiter = 0x2222222222222222222222222222222222222222; + + address owner = makeAddr("Owner"); + + // Contract registers as an allocator in the Compact contract on deployment + ServerAllocator serverAllocator = new ServerAllocator(owner, address(theCompact)); + + (address signer, uint256 signerPK) = makeAddrAndKey("Signer"); + // Register the signer with the allocator + vm.prank(owner); + serverAllocator.addSigner(signer); + + vm.prank(swapper); + uint256 id = theCompact.deposit{ value: amount }(address(serverAllocator), resetPeriod, scope, swapper); + assertEq(theCompact.balanceOf(swapper, id), amount); + + bytes32 claimHash = keccak256(abi.encode(keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"), arbiter, swapper, nonce, expires, id, amount)); + + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash)); + + (bytes32 r_sponsor, bytes32 vs_sponsor) = vm.signCompact(swapperPrivateKey, digest); + bytes memory sponsorSignature = abi.encodePacked(r_sponsor, vs_sponsor); + + // Server Allocator expects a 65 byte (non compact) signature + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK, digest); + + bytes memory allocatorSignature = abi.encodePacked(r, s, v); + + BasicClaim memory claim = BasicClaim(allocatorSignature, sponsorSignature, swapper, nonce, expires, id, amount, claimant, amount); + + vm.prank(arbiter); + bool status = theCompact.claim(claim); + vm.snapshotGasLastCall("claim"); + assert(status); + + assertEq(address(theCompact).balance, amount); + assertEq(claimant.balance, 0); + assertEq(theCompact.balanceOf(swapper, id), 0); + assertEq(theCompact.balanceOf(claimant, id), amount); + } + function test_registerAndClaim() public { ResetPeriod resetPeriod = ResetPeriod.TenMinutes; Scope scope = Scope.Multichain; From cea117f87d30eaef0e9c484e8ffd8a992d70d560 Mon Sep 17 00:00:00 2001 From: 0age <37939117+0age@users.noreply.github.com> Date: Thu, 7 Nov 2024 08:52:09 -0800 Subject: [PATCH 06/10] forge install: openzeppelin-contracts v5.1.0 --- .gitmodules | 2 +- lib/openzeppelin-contracts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 7ce8b70..f58861b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,4 +15,4 @@ url = https://github.com/ProjectOpenSea/tstorish [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts \ No newline at end of file + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..69c8def --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79 From 0850e2566eff27ae1a3a123f8ff90fa9a26d6770 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Mon, 16 Dec 2024 15:56:26 +0100 Subject: [PATCH 07/10] _signers commented --- src/examples/allocator/ServerAllocator.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/examples/allocator/ServerAllocator.sol b/src/examples/allocator/ServerAllocator.sol index be53d47..946177b 100644 --- a/src/examples/allocator/ServerAllocator.sol +++ b/src/examples/allocator/ServerAllocator.sol @@ -26,7 +26,8 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { address private immutable _COMPACT_CONTRACT; - mapping(address => uint256) private _signers; + /// @dev mapping of a signer to their index (incremented to skip 0) in _activeSigners + mapping(address signer => uint256 index) private _signers; address[] private _activeSigners; mapping(bytes32 => uint256) private _attestExpirations; From 582799ea0589441a2f9a325609af8d37fe8afaf1 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Mon, 16 Dec 2024 16:35:09 +0100 Subject: [PATCH 08/10] Attest to Attestation where appropriate --- src/examples/allocator/ServerAllocator.sol | 128 ++++++++++----------- src/interfaces/IServerAllocator.sol | 88 +++++++------- 2 files changed, 108 insertions(+), 108 deletions(-) diff --git a/src/examples/allocator/ServerAllocator.sol b/src/examples/allocator/ServerAllocator.sol index 946177b..da54e3b 100644 --- a/src/examples/allocator/ServerAllocator.sol +++ b/src/examples/allocator/ServerAllocator.sol @@ -15,14 +15,14 @@ import { IERC1271 } from "lib/openzeppelin-contracts/contracts/interfaces/IERC12 contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { using ECDSA for bytes32; - // keccak256("Attest(address,address,address,uint256,uint256)") + // bytes4(keccak256("attest(address,address,address,uint256,uint256)")). bytes4 private constant _ATTEST_SELECTOR = 0x1a808f91; - // keccak256("RegisterAttest(address signer,bytes32 attestHash,uint256 expiration,uint256 nonce)") - bytes32 private constant _ATTEST_TYPE_HASH = 0xaf2dfd3fe08723f490d203be627da2725f4ad38681e455221da2fc1a633bbb18; + // keccak256("RegisterAttestation(address signer,bytes32 attestationHash,uint256 expiration,uint256 nonce)") + bytes32 private constant _ATTESTATION_TYPE_HASH = 0x6017ed71e505719876ff40d1e87ed2a0a078883c87bd2902ea9988c117f7ca7f; - // keccak256("NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)") - bytes32 private constant _NONCE_CONSUMPTION_TYPE_HASH = 0xb06793f900067653959d9bc53299ebf6b5aa5cf5f6c1a463305891a3db695f3c; + // keccak256("NonceConsumption(address signer,uint256[] nonces,bytes32[] attestations)") + bytes32 private constant _NONCE_CONSUMPTION_TYPE_HASH = 0x626e2c6c331510cafaa5cc323e6ac1e87f32c48cba2a61d81c86b50534f7cc91; address private immutable _COMPACT_CONTRACT; @@ -30,9 +30,9 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { mapping(address signer => uint256 index) private _signers; address[] private _activeSigners; - mapping(bytes32 => uint256) private _attestExpirations; - mapping(bytes32 => uint256) private _attestCounts; - mapping(bytes32 => bool) private _attestSignatures; + mapping(bytes32 => uint256) private _attestationExpirations; + mapping(bytes32 => uint256) private _attestationCounts; + mapping(bytes32 => bool) private _attestationSignatures; modifier isSigner(address signer_) { if (!_containsSigner(signer_)) { @@ -74,24 +74,24 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { } /// @inheritdoc IServerAllocator - function registerAttest(bytes32 attest_, uint256 expiration_) external isSigner(msg.sender) { - _registerAttest(attest_, expiration_); + function registerAttestation(bytes32 attestation_, uint256 expiration_) external isSigner(msg.sender) { + _registerAttestation(attestation_, expiration_); } /// @inheritdoc IServerAllocator - function registerAttestViaSignature(RegisterAttest calldata attest_, bytes calldata signature_) external { - bytes32 _attestWithNonce = keccak256(abi.encode(attest_.attestHash, attest_.expiration, attest_.nonce)); - if (_attestSignatures[_attestWithNonce]) { - revert AlreadyUsedSig(attest_.attestHash, attest_.nonce); + function registerAttestationViaSignature(RegisterAttestation calldata attestation_, bytes calldata signature_) external { + bytes32 _attestationWithNonce = keccak256(abi.encode(attestation_.attestationHash, attestation_.expiration, attestation_.nonce)); + if (_attestationSignatures[_attestationWithNonce]) { + revert AlreadyUsedSig(attestation_.attestationHash, attestation_.nonce); } - address signer = _validateSignedAttest(attest_.signer, attest_.attestHash, attest_.expiration, attest_.nonce, signature_); - if (signer != attest_.signer || !_containsSigner(signer)) { + address signer = _validateSignedAttestation(attestation_.signer, attestation_.attestationHash, attestation_.expiration, attestation_.nonce, signature_); + if (signer != attestation_.signer || !_containsSigner(signer)) { revert InvalidSignature(signature_, signer); } // Invalidate signature - _attestSignatures[_attestWithNonce] = true; - _registerAttest(attest_.attestHash, attest_.expiration); + _attestationSignatures[_attestationWithNonce] = true; + _registerAttestation(attestation_.attestationHash, attestation_.expiration); } /// @inheritdoc IAllocator @@ -105,46 +105,46 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { if (msg.sender != _COMPACT_CONTRACT) { revert InvalidCaller(msg.sender, _COMPACT_CONTRACT); } - bytes32 registeredAttest = keccak256(abi.encode(from_, id_, amount_)); - uint256 count = _attestCounts[registeredAttest]; + bytes32 registeredAttestation = keccak256(abi.encode(from_, id_, amount_)); + uint256 count = _attestationCounts[registeredAttestation]; if (count == 0) { - revert UnregisteredAttest(registeredAttest); + revert UnregisteredAttestation(registeredAttestation); } for (uint256 i = count; i > 0; --i) { - bytes32 countedAttest = keccak256(abi.encode(registeredAttest, i)); - if (_attestExpirations[countedAttest] >= block.timestamp) { - // Found a valid registered attest + bytes32 countedAttestation = keccak256(abi.encode(registeredAttestation, i)); + if (_attestationExpirations[countedAttestation] >= block.timestamp) { + // Found a valid registered attestation if (i == count) { - // Last attest, delete - delete _attestExpirations[countedAttest]; + // Last attestation, delete + delete _attestationExpirations[countedAttestation]; } else { - // Shift attest and delete from the end - bytes32 lastAttest = keccak256(abi.encode(registeredAttest, count)); - _attestExpirations[countedAttest] = _attestExpirations[lastAttest]; - delete _attestExpirations[lastAttest]; + // Shift attestation and delete from the end + bytes32 lastAttestation = keccak256(abi.encode(registeredAttestation, count)); + _attestationExpirations[countedAttestation] = _attestationExpirations[lastAttestation]; + delete _attestationExpirations[lastAttestation]; } - _attestCounts[registeredAttest] = --count; + _attestationCounts[registeredAttestation] = --count; - emit Attested(from_, id_, amount_); + emit AttestationConsumed(from_, id_, amount_); return _ATTEST_SELECTOR; } } - revert ExpiredAttests(registeredAttest); + revert ExpiredAttestations(registeredAttestation); } /// @inheritdoc IServerAllocator - function consume(uint256[] calldata nonces_, bytes32[] calldata attests_) external isSigner(msg.sender) { - if (attests_.length != nonces_.length) { + function consume(uint256[] calldata nonces_, bytes32[] calldata attestations_) external isSigner(msg.sender) { + if (attestations_.length != nonces_.length) { revert InvalidInput(); } - _consumeNonces(nonces_, attests_); + _consumeNonces(nonces_, attestations_); } /// @inheritdoc IServerAllocator function consumeViaSignature(NonceConsumption calldata data_, bytes calldata signature_) external { - if (data_.attests.length != data_.nonces.length) { + if (data_.attestations.length != data_.nonces.length) { revert InvalidInput(); } address signer = _validateNonceConsumption(data_, signature_); @@ -152,7 +152,7 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { // first check is optional, can be deleted for gas efficiency revert InvalidSignature(signature_, signer); } - _consumeNonces(data_.nonces, data_.attests); + _consumeNonces(data_.nonces, data_.attestations); } /// @inheritdoc IERC1271 @@ -175,13 +175,13 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { } /// @inheritdoc IServerAllocator - function checkAttestExpirations(bytes32 attest_) external view returns (uint256[] memory) { - return _checkAttestExpirations(attest_); + function checkAttestationExpirations(bytes32 attestation_) external view returns (uint256[] memory) { + return _checkAttestationExpirations(attestation_); } /// @inheritdoc IServerAllocator - function checkAttestExpirations(address sponsor_, uint256 id_, uint256 amount_) external view returns (uint256[] memory) { - return _checkAttestExpirations(keccak256(abi.encode(sponsor_, id_, amount_))); + function checkAttestationExpirations(address sponsor_, uint256 id_, uint256 amount_) external view returns (uint256[] memory) { + return _checkAttestationExpirations(keccak256(abi.encode(sponsor_, id_, amount_))); } /// @inheritdoc IServerAllocator @@ -189,45 +189,45 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { return _COMPACT_CONTRACT; } - function _registerAttest(bytes32 attest_, uint256 expiration_) internal { + function _registerAttestation(bytes32 attestation_, uint256 expiration_) internal { if (expiration_ < block.timestamp) { revert Expired(expiration_, block.timestamp); } - uint256 count = ++_attestCounts[attest_]; - bytes32 countedAttest = keccak256(abi.encode(attest_, count)); + uint256 count = ++_attestationCounts[attestation_]; + bytes32 countedAttestation = keccak256(abi.encode(attestation_, count)); - _attestExpirations[countedAttest] = expiration_; + _attestationExpirations[countedAttestation] = expiration_; - emit AttestRegistered(attest_, expiration_); + emit AttestationRegistered(attestation_, expiration_); } /// Todo: This will lead to always the last registered hash being consumed. - function _consumeNonces(uint256[] calldata nonces_, bytes32[] calldata attests_) internal { + function _consumeNonces(uint256[] calldata nonces_, bytes32[] calldata attestations_) internal { ITheCompact(_COMPACT_CONTRACT).consume(nonces_); - uint256 nonceLength = attests_.length; + uint256 nonceLength = attestations_.length; for (uint256 i = 0; i < nonceLength; ++i) { - bytes32 hashToConsume = attests_[i]; + bytes32 hashToConsume = attestations_[i]; if (hashToConsume != bytes32(0)) { - uint256 count = _attestCounts[attests_[i]]; + uint256 count = _attestationCounts[attestations_[i]]; if (count != 0) { - // Consume the latest registered attest - delete _attestExpirations[ - keccak256(abi.encode(attests_[i], count)) + // Consume the latest registered attestation + delete _attestationExpirations[ + keccak256(abi.encode(attestations_[i], count)) ]; - _attestCounts[attests_[i]] = --count; + _attestationCounts[attestations_[i]] = --count; } } } emit NoncesConsumed(nonces_); } - function _validateSignedAttest(address signer_, bytes32 hash_, uint256 expiration_, uint256 nonce, bytes calldata signature_) internal view returns (address) { - bytes32 message = _hashAttest(signer_, hash_, expiration_, nonce); + function _validateSignedAttestation(address signer_, bytes32 hash_, uint256 expiration_, uint256 nonce, bytes calldata signature_) internal view returns (address) { + bytes32 message = _hashAttestation(signer_, hash_, expiration_, nonce); return message.recover(signature_); } - function _hashAttest(address signer_, bytes32 hash_, uint256 expiration_, uint256 nonce_) internal view returns (bytes32) { - return _hashTypedDataV4(keccak256(abi.encode(_ATTEST_TYPE_HASH, signer_, hash_, expiration_, nonce_))); + function _hashAttestation(address signer_, bytes32 hash_, uint256 expiration_, uint256 nonce_) internal view returns (bytes32) { + return _hashTypedDataV4(keccak256(abi.encode(_ATTESTATION_TYPE_HASH, signer_, hash_, expiration_, nonce_))); } function _validateSignedHash(bytes32 digest_, bytes calldata signature_) internal pure returns (address) { @@ -240,21 +240,21 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { } function _hashNonceConsumption(NonceConsumption calldata data_) internal view returns (bytes32) { - return _hashTypedDataV4(keccak256(abi.encode(_NONCE_CONSUMPTION_TYPE_HASH, data_.signer, data_.nonces, data_.attests))); + return _hashTypedDataV4(keccak256(abi.encode(_NONCE_CONSUMPTION_TYPE_HASH, data_.signer, data_.nonces, data_.attestations))); } function _containsSigner(address signer_) internal view returns (bool) { return _signers[signer_] != 0; } - function _checkAttestExpirations(bytes32 attest_) internal view returns (uint256[] memory) { - uint256 count = _attestCounts[attest_]; + function _checkAttestationExpirations(bytes32 attestation_) internal view returns (uint256[] memory) { + uint256 count = _attestationCounts[attestation_]; if (count == 0) { - revert UnregisteredAttest(attest_); + revert UnregisteredAttestation(attestation_); } uint256[] memory expirations = new uint256[](count); for (uint256 i = count; i > 0; --i) { - expirations[i - 1] = _attestExpirations[keccak256(abi.encode(attest_, i))]; + expirations[i - 1] = _attestationExpirations[keccak256(abi.encode(attestation_, i))]; } return expirations; } diff --git a/src/interfaces/IServerAllocator.sol b/src/interfaces/IServerAllocator.sol index c36f405..75802de 100644 --- a/src/interfaces/IServerAllocator.sol +++ b/src/interfaces/IServerAllocator.sol @@ -5,34 +5,34 @@ pragma solidity ^0.8.27; import { IAllocator } from "src/interfaces/IAllocator.sol"; interface IServerAllocator is IAllocator { - struct RegisterAttest { - // The address of the signer who must sign the attest + struct RegisterAttestation { + // The address of the signer who must sign the attestation address signer; - // The hash of the attest information, consistent of sponsor, id and amount - bytes32 attestHash; - // The expiration date after which the attest is no longer valid + // The hash of the attestation information, consistent of sponsor, id and amount + bytes32 attestationHash; + // The expiration date after which the attestation is no longer valid uint256 expiration; - // A nonce for that specific attest hash to prevent replay attacks + // A nonce for that specific attestation hash to prevent replay attacks uint256 nonce; } struct NonceConsumption { - // The address of the signer who must sign the attests + // The address of the signer who must sign the attestations address signer; // The array of nonces that should be consumed uint256[] nonces; - // The array of previously registered attests that should be consumed - bytes32[] attests; + // The array of previously registered attestations that should be consumed + bytes32[] attestations; } - /// @notice Thrown if no attest was registered for the given transfer - error UnregisteredAttest(bytes32 attest_); + /// @notice Thrown if no attestation was registered for the given transfer + error UnregisteredAttestation(bytes32 attestation_); - /// @notice Thrown if the expiration date to register an attest is in the past + /// @notice Thrown if the expiration date to register an attestation is in the past error Expired(uint256 expiration_, uint256 currentTimestamp_); - /// @notice Thrown if all of the registered attests have expired - error ExpiredAttests(bytes32 attest_); + /// @notice Thrown if all of the registered attestations have expired + error ExpiredAttestations(bytes32 attestation_); /// @notice Thrown if the caller of attest is not the compact contract error InvalidCaller(address caller_, address expected_); @@ -44,7 +44,7 @@ interface IServerAllocator is IAllocator { error InvalidSignature(bytes signature_, address signer_); /// @notice Thrown if the same signature is used multiple times - error AlreadyUsedSig(bytes32 attest_, uint256 nonce); + error AlreadyUsedSig(bytes32 attestation_, uint256 nonce); /// @notice Thrown if the input array lengths are not matching error InvalidInput(); @@ -57,20 +57,20 @@ interface IServerAllocator is IAllocator { /// @param signer_ The address of the signer event SignerRemoved(address signer_); - /// @notice Emitted when an attest is registered - /// @param attest_ The hash of the attest, consistent of sponsor, id and amount - /// @param expiration_ The expiration date of the attest - event AttestRegistered(bytes32 attest_, uint256 expiration_); + /// @notice Emitted when an attestation is registered + /// @param attestation_ The hash of the attestation, consistent of sponsor, id and amount + /// @param expiration_ The expiration date of the attestation + event AttestationRegistered(bytes32 attestation_, uint256 expiration_); /// @notice Emitted when nonces on the compact contract are consumed successfully /// @param nonces_ The array of nonces that were consumed event NoncesConsumed(uint256[] nonces_); - /// @notice Emitted when an attest was consumed for a transfer + /// @notice Emitted when an attestation was consumed for a transfer /// @param from_ The address of the sponsor /// @param id_ The id of the token that was transferred /// @param amount_ The amount of the token that was transferred - event Attested(address from_, uint256 id_, uint256 amount_); + event AttestationConsumed(address from_, uint256 id_, uint256 amount_); /// @notice Add a signer to the allocator /// @dev Only the owner can add a signer @@ -82,28 +82,28 @@ interface IServerAllocator is IAllocator { /// @param signer_ The address of the signer to remove function removeSigner(address signer_) external; - /// @notice Register an attest for a transfer - /// @dev There is no way to uniquely identify a transfer, so the contract relies on its own accounting of registered attests. - /// @param attest_ The hash of the attest to whitelist, consistent of sponsor, id and amount - /// @param expiration_ The expiration date of the attest - function registerAttest(bytes32 attest_, uint256 expiration_) external; + /// @notice Register an attestation for a transfer + /// @dev There is no way to uniquely identify a transfer, so the contract relies on its own accounting of registered attestations. + /// @param attestation_ The hash of the attestation to whitelist, consistent of sponsor, id and amount + /// @param expiration_ The expiration date of the attestation + function registerAttestation(bytes32 attestation_, uint256 expiration_) external; - /// @notice Register an attest for a transfer via a signature - /// @dev Nonce management in the RegisterAttest is only required for multiple registers of the same attest with the same expiration. - /// @param attest_ The RegisterAttest struct containing the signer, the hash of the attest, the expiration and the nonce + /// @notice Register an attestation for a transfer via a signature + /// @dev Nonce management in the RegisterAttestation is only required for multiple registers of the same attestation with the same expiration. + /// @param attestation_ The RegisterAttestation struct containing the signer, the hash of the attestation, the expiration and the nonce /// @param signature_ The signature of the signer - function registerAttestViaSignature(RegisterAttest calldata attest_, bytes calldata signature_) external; + function registerAttestationViaSignature(RegisterAttestation calldata attestation_, bytes calldata signature_) external; - /// @notice Consume nonces on the compact contract and attests on the allocator + /// @notice Consume nonces on the compact contract and attestations on the allocator /// @dev The hashes array needs to be of the same length as the nonces array. /// @dev If no hash was yet registered for the respective nonce, provide a bytes32(0) for the index. /// @dev All signers can override nonces of other signers. /// @param nonces_ The array of all nonces to consume on the compact contract - /// @param attests_ The array of all attests to consume on the allocator - function consume(uint256[] calldata nonces_, bytes32[] calldata attests_) external; + /// @param attestations_ The array of all attestations to consume on the allocator + function consume(uint256[] calldata nonces_, bytes32[] calldata attestations_) external; - /// @notice Consume nonces on the compact contract and attests on the allocator via a signature - /// @param data_ The NonceConsumption struct containing the signer, the array of nonces and the array of attests + /// @notice Consume nonces on the compact contract and attestations on the allocator via a signature + /// @param data_ The NonceConsumption struct containing the signer, the array of nonces and the array of attestations /// @param signature_ The signature of the signer function consumeViaSignature(NonceConsumption calldata data_, bytes calldata signature_) external; @@ -116,19 +116,19 @@ interface IServerAllocator is IAllocator { /// @return The array of all registered signers function getAllSigners() external view returns (address[] memory); - /// @notice Check the expiration dates of an attest - /// @dev If no attest was registered for the provided hash, the function will revert - /// @param attest_ The hash of the attest to check - /// @return The array of expiration dates for the registered attests - function checkAttestExpirations(bytes32 attest_) external view returns (uint256[] memory); + /// @notice Check the expiration dates of an attestation + /// @dev If no attestation was registered for the provided hash, the function will revert + /// @param attestation_ The hash of the attestation to check + /// @return The array of expiration dates for the registered attestations + function checkAttestationExpirations(bytes32 attestation_) external view returns (uint256[] memory); - /// @notice Check the expiration dates of an attest by its components - /// @dev If no attest was registered for the provided components, the function will revert + /// @notice Check the expiration dates of an attestation by its components + /// @dev If no attestation was registered for the provided components, the function will revert /// @param sponsor_ The address of the sponsor /// @param id_ The id of the token /// @param amount_ The amount of the token - /// @return The array of expiration dates for the registered attests - function checkAttestExpirations(address sponsor_, uint256 id_, uint256 amount_) external view returns (uint256[] memory); + /// @return The array of expiration dates for the registered attestations + function checkAttestationExpirations(address sponsor_, uint256 id_, uint256 amount_) external view returns (uint256[] memory); /// @notice Get the address of the compact contract /// @dev Only the compact contract can call the attest function From 50ab84c5f789a34b8b03d9e11edd889bd227d7fd Mon Sep 17 00:00:00 2001 From: vimageDE Date: Mon, 16 Dec 2024 16:49:24 +0100 Subject: [PATCH 09/10] Fixed tests --- test/ServerAllocator.t.sol | 116 ++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/test/ServerAllocator.t.sol b/test/ServerAllocator.t.sol index 73bd532..82843a5 100644 --- a/test/ServerAllocator.t.sol +++ b/test/ServerAllocator.t.sol @@ -49,8 +49,8 @@ abstract contract CreateHash is Test { // stringified types string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the funcion // string ALLOCATOR_TYPE = "Allocator(bytes32 hash)"; // Hashed inside the funcion - string REGISTER_ATTEST_TYPE = "RegisterAttest(address signer,bytes32 attestHash,uint256 expiration,uint256 nonce)"; // Hashed inside the funcion - string NONCE_CONSUMPTION_TYPE = "NonceConsumption(address signer,uint256[] nonces,bytes32[] attests)"; // Hashed inside the funcion + string REGISTER_ATTESTATION_TYPE = "RegisterAttestation(address signer,bytes32 attestationHash,uint256 expiration,uint256 nonce)"; // Hashed inside the funcion + string NONCE_CONSUMPTION_TYPE = "NonceConsumption(address signer,uint256[] nonces,bytes32[] attestations)"; // Hashed inside the funcion // EIP712 domain type string name = "Allocator"; string version = "1"; @@ -77,12 +77,12 @@ abstract contract CreateHash is Test { ); } - function _hashRegisterAttest(ServerAllocator.RegisterAttest memory data, address verifyingContract) internal view returns (bytes32) { + function _hashRegisterAttest(ServerAllocator.RegisterAttestation memory data, address verifyingContract) internal view returns (bytes32) { return keccak256( abi.encodePacked( "\x19\x01", // backslash is needed to escape the character _domainSeperator(verifyingContract), - keccak256(abi.encode(keccak256(bytes(REGISTER_ATTEST_TYPE)), data.signer, data.attestHash, data.expiration, data.nonce)) + keccak256(abi.encode(keccak256(bytes(REGISTER_ATTESTATION_TYPE)), data.signer, data.attestationHash, data.expiration, data.nonce)) ) ); } @@ -93,7 +93,7 @@ abstract contract CreateHash is Test { abi.encodePacked( "\x19\x01", // backslash is needed to escape the character _domainSeperator(verifyingContract), - keccak256(abi.encode(keccak256(bytes(NONCE_CONSUMPTION_TYPE)), data.signer, data.nonces, data.attests)) + keccak256(abi.encode(keccak256(bytes(NONCE_CONSUMPTION_TYPE)), data.signer, data.nonces, data.attestations)) ) ); } @@ -205,7 +205,7 @@ contract ServerAllocator_Attest is SignerSet { vm.prank(attacker_); vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidSigner.selector, attacker_)); - serverAllocator.registerAttest(createAttest(attacker_, usdcId, 100), vm.getBlockTimestamp() + 1 days); + serverAllocator.registerAttestation(createAttest(attacker_, usdcId, 100), vm.getBlockTimestamp() + 1 days); } function test_fuzz_registerAttest_attestExpired(uint256 expiration_) public { @@ -213,7 +213,7 @@ contract ServerAllocator_Attest is SignerSet { vm.prank(signer); vm.expectRevert(abi.encodeWithSelector(IServerAllocator.Expired.selector, expiration_, vm.getBlockTimestamp())); - serverAllocator.registerAttest(createAttest(signer, usdcId, 100), expiration_); + serverAllocator.registerAttestation(createAttest(signer, usdcId, 100), expiration_); } function test_registerAttest_successful() public { @@ -221,55 +221,55 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; vm.expectEmit(address(serverAllocator)); - emit IServerAllocator.AttestRegistered(attest, expiration); - serverAllocator.registerAttest(attest, expiration); + emit IServerAllocator.AttestationRegistered(attest, expiration); + serverAllocator.registerAttestation(attest, expiration); - assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration); + assertEq(serverAllocator.checkAttestationExpirations(attest)[0], expiration); } function test_registerAttestViaSignature_InvalidSignature() public { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - IServerAllocator.RegisterAttest memory attestData = IServerAllocator.RegisterAttest(signer, attest, expiration, 0); + IServerAllocator.RegisterAttestation memory attestData = IServerAllocator.RegisterAttestation(signer, attest, expiration, 0); bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, attackerPK); vm.prank(attacker); vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidSignature.selector, signature, attacker)); - serverAllocator.registerAttestViaSignature(attestData, signature); + serverAllocator.registerAttestationViaSignature(attestData, signature); } function test_registerAttestViaSignature_successful() public { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - IServerAllocator.RegisterAttest memory attestData = IServerAllocator.RegisterAttest(signer, attest, expiration, 0); + IServerAllocator.RegisterAttestation memory attestData = IServerAllocator.RegisterAttestation(signer, attest, expiration, 0); bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); vm.expectEmit(address(serverAllocator)); - emit IServerAllocator.AttestRegistered(attest, expiration); - serverAllocator.registerAttestViaSignature(attestData, signature); + emit IServerAllocator.AttestationRegistered(attest, expiration); + serverAllocator.registerAttestationViaSignature(attestData, signature); } function test_registerAttestViaSignature_AlreadyUsedSig() public { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - IServerAllocator.RegisterAttest memory attestData = IServerAllocator.RegisterAttest(signer, attest, expiration, 0); + IServerAllocator.RegisterAttestation memory attestData = IServerAllocator.RegisterAttestation(signer, attest, expiration, 0); bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); vm.expectEmit(address(serverAllocator)); - emit IServerAllocator.AttestRegistered(attest, expiration); - serverAllocator.registerAttestViaSignature(attestData, signature); + emit IServerAllocator.AttestationRegistered(attest, expiration); + serverAllocator.registerAttestationViaSignature(attestData, signature); vm.prank(attacker); vm.expectRevert(abi.encodeWithSelector(IServerAllocator.AlreadyUsedSig.selector, attest, 0)); - serverAllocator.registerAttestViaSignature(attestData, signature); + serverAllocator.registerAttestationViaSignature(attestData, signature); } function test_registerSameAttestTwice() public { @@ -280,18 +280,18 @@ contract ServerAllocator_Attest is SignerSet { // first attest vm.expectEmit(address(serverAllocator)); - emit IServerAllocator.AttestRegistered(attest, expiration1); - serverAllocator.registerAttest(attest, expiration1); + emit IServerAllocator.AttestationRegistered(attest, expiration1); + serverAllocator.registerAttestation(attest, expiration1); - assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration1); + assertEq(serverAllocator.checkAttestationExpirations(attest)[0], expiration1); // second attest with different expiration vm.expectEmit(address(serverAllocator)); - emit IServerAllocator.AttestRegistered(attest, expiration2); - serverAllocator.registerAttest(attest, expiration2); + emit IServerAllocator.AttestationRegistered(attest, expiration2); + serverAllocator.registerAttestation(attest, expiration2); - assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration1); - assertEq(serverAllocator.checkAttestExpirations(attest)[1], expiration2); + assertEq(serverAllocator.checkAttestationExpirations(attest)[0], expiration1); + assertEq(serverAllocator.checkAttestationExpirations(attest)[1], expiration2); } function test_fuzz_attest_callerMustBeCompact(address caller_) public { @@ -304,7 +304,7 @@ contract ServerAllocator_Attest is SignerSet { function test_fuzz_attest_notRegistered(address operator_, address from_, address to_, uint256 id_, uint256 amount_) public { vm.prank(address(compactContract)); - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, keccak256(abi.encode(from_, id_, amount_)))); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttestation.selector, keccak256(abi.encode(from_, id_, amount_)))); serverAllocator.attest(operator_, from_, to_, id_, amount_); } @@ -315,14 +315,14 @@ contract ServerAllocator_Attest is SignerSet { // register attest vm.prank(signer); - serverAllocator.registerAttest(attest, expiration); + serverAllocator.registerAttestation(attest, expiration); // move time forward vm.warp(vm.getBlockTimestamp() + 1); // check attest vm.prank(address(compactContract)); - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.ExpiredAttests.selector, attest)); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.ExpiredAttestations.selector, attest)); serverAllocator.attest(signer, attacker, makeAddr("to"), usdcId, amount_); } @@ -332,21 +332,21 @@ contract ServerAllocator_Attest is SignerSet { // register attest vm.prank(signer); - serverAllocator.registerAttest(attest, expiration); + serverAllocator.registerAttestation(attest, expiration); // check for attest - assertEq(serverAllocator.checkAttestExpirations(attest)[0], expiration); + assertEq(serverAllocator.checkAttestationExpirations(attest)[0], expiration); // check attest vm.prank(address(compactContract)); vm.expectEmit(address(serverAllocator)); - emit IServerAllocator.Attested(from_, id_, amount_); + emit IServerAllocator.AttestationConsumed(from_, id_, amount_); bytes4 attestSelector = serverAllocator.attest(operator_, from_, to_, id_, amount_); assertEq(attestSelector, _ATTEST_SELECTOR); // check attest was consumed - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attest)); - serverAllocator.checkAttestExpirations(attest); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttestation.selector, attest)); + serverAllocator.checkAttestationExpirations(attest); } } @@ -395,13 +395,13 @@ contract ServerAllocator_Consume is SignerSet { attests[2] = createAttest(signer, usdcId, 300); // register attests - serverAllocator.registerAttest(attests[0], vm.getBlockTimestamp()); - serverAllocator.registerAttest(attests[1], vm.getBlockTimestamp()); - serverAllocator.registerAttest(attests[2], vm.getBlockTimestamp()); + serverAllocator.registerAttestation(attests[0], vm.getBlockTimestamp()); + serverAllocator.registerAttestation(attests[1], vm.getBlockTimestamp()); + serverAllocator.registerAttestation(attests[2], vm.getBlockTimestamp()); - assertEq(serverAllocator.checkAttestExpirations(attests[0])[0], vm.getBlockTimestamp()); - assertEq(serverAllocator.checkAttestExpirations(attests[1])[0], vm.getBlockTimestamp()); - assertEq(serverAllocator.checkAttestExpirations(attests[2])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestationExpirations(attests[0])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestationExpirations(attests[1])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestationExpirations(attests[2])[0], vm.getBlockTimestamp()); vm.expectEmit(address(serverAllocator)); emit IServerAllocator.NoncesConsumed(nonces); @@ -413,12 +413,12 @@ contract ServerAllocator_Consume is SignerSet { } // check attests were consumed - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[0])); - serverAllocator.checkAttestExpirations(attests[0]); - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[1])); - serverAllocator.checkAttestExpirations(attests[1]); - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[2])); - serverAllocator.checkAttestExpirations(attests[2]); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttestation.selector, attests[0])); + serverAllocator.checkAttestationExpirations(attests[0]); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttestation.selector, attests[1])); + serverAllocator.checkAttestationExpirations(attests[1]); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttestation.selector, attests[2])); + serverAllocator.checkAttestationExpirations(attests[2]); } function test_consumeViaSignature_requiresNoncesAndAttestsToBeOfSameLength() public { @@ -467,14 +467,14 @@ contract ServerAllocator_Consume is SignerSet { vm.startPrank(signer); // register attests - serverAllocator.registerAttest(attests[0], vm.getBlockTimestamp()); - serverAllocator.registerAttest(attests[1], vm.getBlockTimestamp()); - serverAllocator.registerAttest(attests[2], vm.getBlockTimestamp()); + serverAllocator.registerAttestation(attests[0], vm.getBlockTimestamp()); + serverAllocator.registerAttestation(attests[1], vm.getBlockTimestamp()); + serverAllocator.registerAttestation(attests[2], vm.getBlockTimestamp()); vm.stopPrank(); - assertEq(serverAllocator.checkAttestExpirations(attests[0])[0], vm.getBlockTimestamp()); - assertEq(serverAllocator.checkAttestExpirations(attests[1])[0], vm.getBlockTimestamp()); - assertEq(serverAllocator.checkAttestExpirations(attests[2])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestationExpirations(attests[0])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestationExpirations(attests[1])[0], vm.getBlockTimestamp()); + assertEq(serverAllocator.checkAttestationExpirations(attests[2])[0], vm.getBlockTimestamp()); bytes32 message = _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, nonces, attests), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); @@ -490,12 +490,12 @@ contract ServerAllocator_Consume is SignerSet { } // check attests were consumed - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[0])); - serverAllocator.checkAttestExpirations(attests[0]); - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[1])); - serverAllocator.checkAttestExpirations(attests[1]); - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttest.selector, attests[2])); - serverAllocator.checkAttestExpirations(attests[2]); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttestation.selector, attests[0])); + serverAllocator.checkAttestationExpirations(attests[0]); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttestation.selector, attests[1])); + serverAllocator.checkAttestationExpirations(attests[1]); + vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttestation.selector, attests[2])); + serverAllocator.checkAttestationExpirations(attests[2]); } } From bcb19c923da88dfa92f3f7fdccc871ebfc03c24b Mon Sep 17 00:00:00 2001 From: vimageDE Date: Mon, 16 Dec 2024 16:55:28 +0100 Subject: [PATCH 10/10] fixed TheCompactMock --- src/test/TheCompactMock.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/TheCompactMock.sol b/src/test/TheCompactMock.sol index 8932fd8..8905324 100644 --- a/src/test/TheCompactMock.sol +++ b/src/test/TheCompactMock.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.27; import { IAllocator } from "src/interfaces/IAllocator.sol"; -import { ERC6909 } from "solady/tokens/ERC6909.sol"; +import { ERC6909 } from "lib/solady/src/tokens/ERC6909.sol"; import { ERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import { IdLib } from "src/lib/IdLib.sol"; @@ -13,8 +13,10 @@ contract TheCompactMock is ERC6909 { using IdLib for address; mapping(uint256 nonce => bool consumed) public consumedNonces; + mapping(address allocator => bool registered) public registeredAllocators; - function __registerAllocator(address allocator, bytes calldata proof) external returns (uint96) { + function __registerAllocator(address allocator, bytes calldata) external returns (uint96) { + registeredAllocators[allocator] = true; return 0; }