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]); + } +}