diff --git a/src/examples/allocator/SimpleAllocator.sol b/src/examples/allocator/SimpleAllocator.sol index 74fc876..2c692ca 100644 --- a/src/examples/allocator/SimpleAllocator.sol +++ b/src/examples/allocator/SimpleAllocator.sol @@ -9,8 +9,21 @@ import { ITheCompact } from "src/interfaces/ITheCompact.sol"; import { ISimpleAllocator } from "src/interfaces/ISimpleAllocator.sol"; import { Compact } from "src/types/EIP712Types.sol"; import { ResetPeriod } from "src/lib/IdLib.sol"; +import { console } from "forge-std/console.sol"; contract SimpleAllocator is ISimpleAllocator { + // abi.decode(bytes("Compact(address arbiter,address "), (bytes32)) + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_ONE = 0x436f6d70616374286164647265737320617262697465722c6164647265737320; + // abi.decode(bytes("sponsor,uint256 nonce,uint256 ex"), (bytes32)) + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_TWO = 0x73706f6e736f722c75696e74323536206e6f6e63652c75696e74323536206578; + // abi.decode(bytes("pires,uint256 id,uint256 amount)"), (bytes32)) + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_THREE = 0x70697265732c75696e743235362069642c75696e7432353620616d6f756e7429; + // uint200(abi.decode(bytes(",Witness witness)Witness("), (bytes25))) + uint200 constant WITNESS_TYPESTRING = 0x2C5769746E657373207769746E657373295769746E65737328; + + // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)") + bytes32 constant COMPACT_TYPEHASH = 0xcdca950b17b5efc016b74b912d8527dfba5e404a688cbc3dab16cb943287fec2; + address public immutable COMPACT_CONTRACT; address public immutable ARBITER; uint256 public immutable MIN_WITHDRAWAL_DELAY; @@ -36,43 +49,7 @@ contract SimpleAllocator is ISimpleAllocator { /// @inheritdoc ISimpleAllocator function lock(Compact calldata compact_) external { - // Check msg.sender is sponsor - if (msg.sender != compact_.sponsor) { - revert InvalidCaller(msg.sender, compact_.sponsor); - } - bytes32 tokenHash = _getTokenHash(compact_.id, msg.sender); - // Check no lock is already active for this sponsor - if (_claim[tokenHash] > block.timestamp && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))) { - revert ClaimActive(compact_.sponsor); - } - // Check arbiter is valid - if (compact_.arbiter != ARBITER) { - revert InvalidArbiter(compact_.arbiter); - } - // Check expiration is not too soon or too late - if (compact_.expires < block.timestamp + MIN_WITHDRAWAL_DELAY || compact_.expires > block.timestamp + MAX_WITHDRAWAL_DELAY) { - revert InvalidExpiration(compact_.expires); - } - // Check expiration is not longer then the tokens forced withdrawal time - (,, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id); - if (compact_.expires > block.timestamp + _resetPeriodToSeconds(resetPeriod)) { - revert ForceWithdrawalAvailable(compact_.expires, block.timestamp + _resetPeriodToSeconds(resetPeriod)); - } - // Check expiration is not past an active force withdrawal - (, uint256 forcedWithdrawalExpiration) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); - if (forcedWithdrawalExpiration != 0 && forcedWithdrawalExpiration < compact_.expires) { - revert ForceWithdrawalAvailable(compact_.expires, forcedWithdrawalExpiration); - } - // Check nonce is not yet consumed - if (ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(compact_.nonce, address(this))) { - revert NonceAlreadyConsumed(compact_.nonce); - } - - uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, compact_.id); - // Check balance is enough - if (balance < compact_.amount) { - revert InsufficientBalance(msg.sender, compact_.id, balance, compact_.amount); - } + bytes32 tokenHash = _checkAllocation(compact_); bytes32 digest = keccak256( abi.encodePacked( @@ -80,7 +57,7 @@ contract SimpleAllocator is ISimpleAllocator { ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), keccak256( abi.encode( - keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"), + COMPACT_TYPEHASH, compact_.arbiter, compact_.sponsor, compact_.nonce, @@ -100,6 +77,53 @@ contract SimpleAllocator is ISimpleAllocator { emit Locked(compact_.sponsor, compact_.id, compact_.amount, compact_.expires); } + /// @inheritdoc ISimpleAllocator + function lockWithWitness(Compact calldata compact_, bytes32 typestringHash_, bytes32 witnessHash_) external { + bytes32 tokenHash = _checkAllocation(compact_); + + console.log("claimHash SimpleAllocator"); + // console.logBytes32(claimHash); + console.log("arbiter SimpleAllocator"); + console.logAddress(compact_.arbiter); + console.log("sponsor SimpleAllocator"); + console.logAddress(compact_.sponsor); + console.log("nonce SimpleAllocator"); + console.logUint(compact_.nonce); + console.log("expires SimpleAllocator"); + console.logUint(compact_.expires); + console.log("id SimpleAllocator"); + console.logUint(compact_.id); + console.log("amount SimpleAllocator"); + console.logUint(compact_.amount); + bytes32 digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + typestringHash_, // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)") + compact_.arbiter, + compact_.sponsor, + compact_.nonce, + compact_.expires, + compact_.id, + compact_.amount, + witnessHash_ + ) + ) + ) + ); + console.log("digest SimpleAllocator"); + console.logBytes32(digest); + + _claim[tokenHash] = compact_.expires; + _amount[tokenHash] = compact_.amount; + _nonce[tokenHash] = compact_.nonce; + _sponsor[digest] = tokenHash; + + emit Locked(compact_.sponsor, compact_.id, compact_.amount, compact_.expires); + } + /// @inheritdoc IAllocator function attest(address operator_, address from_, address, uint256 id_, uint256 amount_) external view returns (bytes4) { if (msg.sender != COMPACT_CONTRACT) { @@ -161,7 +185,7 @@ contract SimpleAllocator is ISimpleAllocator { ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), keccak256( abi.encode( - keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"), + COMPACT_TYPEHASH, compact_.arbiter, compact_.sponsor, compact_.nonce, @@ -177,10 +201,74 @@ contract SimpleAllocator is ISimpleAllocator { return (active, active ? expires : 0); } + /// @dev example of a witness type string input: + /// "uint256 witnessArgument" + /// @dev full typestring: + /// Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument) + function getTypestringHashForWitness(string calldata witness_) external pure returns (bytes32 typestringHash_) { + assembly { + let memoryOffset := mload(0x40) + mstore(memoryOffset, COMPACT_TYPESTRING_FRAGMENT_ONE) + mstore(add(memoryOffset, 0x20), COMPACT_TYPESTRING_FRAGMENT_TWO) + mstore(add(memoryOffset, 0x40), COMPACT_TYPESTRING_FRAGMENT_THREE) + mstore(add(memoryOffset, sub(0x60, 0x01)), shl(56, WITNESS_TYPESTRING)) + let witnessPointer := add(memoryOffset, add(sub(0x60, 0x01), 0x19)) + calldatacopy(witnessPointer, witness_.offset, witness_.length) + let witnessEnd := add(witnessPointer, witness_.length) + mstore8(witnessEnd, 0x29) + typestringHash_ := keccak256(memoryOffset, sub(add(witnessEnd, 0x01), memoryOffset)) + + mstore(0x40, add(or(witnessEnd, 0x1f), 0x20)) + } + return typestringHash_; + } + function _getTokenHash(uint256 id_, address sponsor_) internal pure returns (bytes32) { return keccak256(abi.encode(id_, sponsor_)); } + function _checkAllocation(Compact calldata compact_) internal view returns (bytes32) { + // Check msg.sender is sponsor + if (msg.sender != compact_.sponsor) { + revert InvalidCaller(msg.sender, compact_.sponsor); + } + bytes32 tokenHash = _getTokenHash(compact_.id, msg.sender); + // Check no lock is already active for this sponsor + if (_claim[tokenHash] > block.timestamp && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))) { + revert ClaimActive(compact_.sponsor); + } + // Check arbiter is valid + if (compact_.arbiter != ARBITER) { + revert InvalidArbiter(compact_.arbiter); + } + // Check expiration is not too soon or too late + if (compact_.expires < block.timestamp + MIN_WITHDRAWAL_DELAY || compact_.expires > block.timestamp + MAX_WITHDRAWAL_DELAY) { + revert InvalidExpiration(compact_.expires); + } + // Check expiration is not longer then the tokens forced withdrawal time + (,, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id); + if (compact_.expires > block.timestamp + _resetPeriodToSeconds(resetPeriod)) { + revert ForceWithdrawalAvailable(compact_.expires, block.timestamp + _resetPeriodToSeconds(resetPeriod)); + } + // Check expiration is not past an active force withdrawal + (, uint256 forcedWithdrawalExpiration) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); + if (forcedWithdrawalExpiration != 0 && forcedWithdrawalExpiration < compact_.expires) { + revert ForceWithdrawalAvailable(compact_.expires, forcedWithdrawalExpiration); + } + // Check nonce is not yet consumed + if (ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(compact_.nonce, address(this))) { + revert NonceAlreadyConsumed(compact_.nonce); + } + + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, compact_.id); + // Check balance is enough + if (balance < compact_.amount) { + revert InsufficientBalance(msg.sender, compact_.id, balance, compact_.amount); + } + + return tokenHash; + } + /// @dev copied from IdLib.sol function _resetPeriodToSeconds(ResetPeriod resetPeriod_) internal pure returns (uint256 duration) { assembly ("memory-safe") { diff --git a/src/interfaces/ISimpleAllocator.sol b/src/interfaces/ISimpleAllocator.sol index 12b21c0..cf716ad 100644 --- a/src/interfaces/ISimpleAllocator.sol +++ b/src/interfaces/ISimpleAllocator.sol @@ -43,6 +43,16 @@ interface ISimpleAllocator is IAllocator { /// @param compact_ The compact that contains the data about the lock function lock(Compact calldata compact_) external; + /// @notice Locks the tokens of an id for a claim with a witness + /// @dev Locks all tokens of a sponsor for an id with a witness + /// @dev example for the typeHash: + /// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)") + /// + /// @param compact_ The compact that contains the data about the lock + /// @param typeHash_ The type hash of the full compact, including the witness + /// @param witnessHash_ The witness hash of the witness + function lockWithWitness(Compact calldata compact_, bytes32 typeHash_,bytes32 witnessHash_) external; + /// @notice Checks if the tokens of a sponsor for an id are locked /// @param id_ The id of the token /// @param sponsor_ The address of the sponsor diff --git a/test/TheCompact.t.sol b/test/TheCompact.t.sol index a6e4d08..d42c695 100644 --- a/test/TheCompact.t.sol +++ b/test/TheCompact.t.sol @@ -4,12 +4,14 @@ 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 { SimpleAllocator } from "../src/examples/allocator/SimpleAllocator.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"; import { Scope } from "../src/types/Scope.sol"; import { CompactCategory } from "../src/types/CompactCategory.sol"; import { ISignatureTransfer } from "permit2/src/interfaces/ISignatureTransfer.sol"; +import { ISimpleAllocator } from "../src/interfaces/ISimpleAllocator.sol"; import { HashLib } from "../src/lib/HashLib.sol"; @@ -1140,6 +1142,68 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(claimant, id), amount); } + function test_claim_viaSimpleAllocator() public { + ResetPeriod resetPeriod = ResetPeriod.TenMinutes; + Scope scope = Scope.Multichain; + uint256 amount = 1e18; + uint256 nonce = 0; + uint256 expires = block.timestamp + 10; + address claimant = 0x1111111111111111111111111111111111111111; + address arbiter = 0x2222222222222222222222222222222222222222; + + // Contract registers as an allocator in the Compact contract on deployment + SimpleAllocator simpleAllocator = new SimpleAllocator(address(theCompact), arbiter, 5, 100); + + vm.prank(swapper); + uint256 id = theCompact.deposit{ value: amount }(address(simpleAllocator), 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); + + console.log("claimHash TheCompact test"); + console.logBytes32(claimHash); + console.log("arbiter SimpleAllocator"); + console.logAddress(arbiter); + console.log("sponsor SimpleAllocator"); + console.logAddress(swapper); + console.log("nonce SimpleAllocator"); + console.logUint(nonce); + console.log("expires SimpleAllocator"); + console.logUint(expires); + console.log("id SimpleAllocator"); + console.logUint(id); + console.log("amount SimpleAllocator"); + console.logUint(amount); + console.log("digest TheCompact test"); + console.logBytes32(digest); + + // Lock tokens + vm.prank(swapper); + vm.expectEmit(true, true, false, true); + emit ISimpleAllocator.Locked(swapper, id, amount, expires); + simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: swapper, nonce: nonce, id: id, expires: expires, amount: amount })); + + // Empty allocator signature, because the onchain allocator does not require a signature, only a lock + bytes memory allocatorSignature = ""; + + 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;