diff --git a/src/examples/allocator/SimpleAllocator.sol b/src/examples/allocator/SimpleAllocator.sol index 6833194..2516231 100644 --- a/src/examples/allocator/SimpleAllocator.sol +++ b/src/examples/allocator/SimpleAllocator.sol @@ -12,10 +12,10 @@ import { ResetPeriod } from "src/lib/IdLib.sol"; contract SimpleAllocator is ISimpleAllocator { - address private immutable _COMPACT_CONTRACT; - address private immutable _ARBITER; - uint256 private immutable _MIN_WITHDRAWAL_DELAY; - uint256 private immutable _MAX_WITHDRAWAL_DELAY; + address public immutable COMPACT_CONTRACT; + address public immutable ARBITER; + uint256 public immutable MIN_WITHDRAWAL_DELAY; + uint256 public immutable MAX_WITHDRAWAL_DELAY; /// @dev mapping of tokenHash to the expiration of the lock mapping(bytes32 tokenHash => uint256 expiration) private _claim; @@ -27,10 +27,12 @@ contract SimpleAllocator is ISimpleAllocator { mapping(bytes32 digest => bytes32 tokenHash) private _sponsor; constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) { - _COMPACT_CONTRACT = compactContract_; - _ARBITER = arbiter_; - _MIN_WITHDRAWAL_DELAY = minWithdrawalDelay_; - _MAX_WITHDRAWAL_DELAY = maxWithdrawalDelay_; + COMPACT_CONTRACT = compactContract_; + ARBITER = arbiter_; + MIN_WITHDRAWAL_DELAY = minWithdrawalDelay_; + MAX_WITHDRAWAL_DELAY = maxWithdrawalDelay_; + + ITheCompact(COMPACT_CONTRACT).__registerAllocator(address(this), ""); } /// @inheritdoc ISimpleAllocator @@ -40,47 +42,43 @@ contract SimpleAllocator is ISimpleAllocator { revert InvalidCaller(msg.sender, compact_.sponsor); } bytes32 tokenHash = _getTokenHash(compact_.id, msg.sender); - // Check if the claim is already active - if (_claim[tokenHash] > block.timestamp && !ITheCompact(_COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))) { - revert ClaimActive(compact_.sponsor); - } - // Check no lock is active for this sponsor - if (_claim[tokenHash] > block.timestamp) { + // 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) { + 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) { + 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); + (,, 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); + (, 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))) { + if (ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(compact_.nonce, address(this))) { revert NonceAlreadyConsumed(compact_.nonce); } - uint256 balance = ERC6909(_COMPACT_CONTRACT).balanceOf(msg.sender, compact_.id); + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, compact_.id); // Check balance is enough if (balance < compact_.amount) { - revert InsufficientBalance(msg.sender, compact_.id); + revert InsufficientBalance(msg.sender, compact_.id, balance, compact_.amount); } bytes32 digest = keccak256( abi.encodePacked( bytes2(0x1901), - ITheCompact(_COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), keccak256( abi.encode( keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"), @@ -97,32 +95,32 @@ contract SimpleAllocator is ISimpleAllocator { _claim[tokenHash] = compact_.expires; _amount[tokenHash] = compact_.amount; + _nonce[tokenHash] = compact_.nonce; _sponsor[digest] = tokenHash; - _nonce[digest] = compact_.nonce; 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) { - revert InvalidCaller(msg.sender, _COMPACT_CONTRACT); + if (msg.sender != COMPACT_CONTRACT) { + revert InvalidCaller(msg.sender, COMPACT_CONTRACT); } // For a transfer, the sponsor is the arbiter if (operator_ != from_) { revert InvalidCaller(operator_, from_); } - uint256 balance = ERC6909(_COMPACT_CONTRACT).balanceOf(from_, id_); + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(from_, id_); // Check unlocked balance bytes32 tokenHash = _getTokenHash(id_, from_); uint256 fullAmount = amount_; if(_claim[tokenHash] > block.timestamp) { // Lock is still active, add the locked amount if the nonce has not yet been consumed - fullAmount += ITheCompact(_COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) ? 0 : _amount[tokenHash]; + fullAmount += ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) ? 0 : _amount[tokenHash]; } if( balance < fullAmount) { - revert InsufficientBalance(from_, id_); + revert InsufficientBalance(from_, id_, balance, fullAmount); } return 0x1a808f91; @@ -144,7 +142,7 @@ contract SimpleAllocator is ISimpleAllocator { function checkTokensLocked(uint256 id_, address sponsor_) external view returns (uint256 amount_, uint256 expires_) { bytes32 tokenHash = _getTokenHash(id_, sponsor_); uint256 expires = _claim[tokenHash]; - if (expires <= block.timestamp) { + if (expires <= block.timestamp || ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))) { return (0, 0); } @@ -154,14 +152,14 @@ contract SimpleAllocator is ISimpleAllocator { /// @inheritdoc ISimpleAllocator function checkCompactLocked(Compact calldata compact_) external view returns (bool locked_, uint256 expires_) { // TODO: Check the force unlock time in the compact contract and adapt expires_ if needed - if (compact_.arbiter != _ARBITER) { + if (compact_.arbiter != ARBITER) { revert InvalidArbiter(compact_.arbiter); } bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor); bytes32 digest = keccak256( abi.encodePacked( bytes2(0x1901), - ITheCompact(_COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), keccak256( abi.encode( keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"), @@ -176,7 +174,8 @@ contract SimpleAllocator is ISimpleAllocator { ) ); uint256 expires = _claim[tokenHash]; - return (_sponsor[digest] == tokenHash && expires > block.timestamp && !ITheCompact(_COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)), expires); + bool active = _sponsor[digest] == tokenHash && expires > block.timestamp && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)); + return (active, active ? expires : 0); } function _getTokenHash(uint256 id_, address sponsor_) internal pure returns (bytes32) { diff --git a/src/interfaces/ISimpleAllocator.sol b/src/interfaces/ISimpleAllocator.sol index 1f018fc..a730057 100644 --- a/src/interfaces/ISimpleAllocator.sol +++ b/src/interfaces/ISimpleAllocator.sol @@ -21,7 +21,7 @@ interface ISimpleAllocator is IAllocator { error NonceAlreadyConsumed(uint256 nonce); /// @notice Thrown if the sponsor does not have enough balance to lock the amount - error InsufficientBalance(address sponsor, uint256 id); + error InsufficientBalance(address sponsor, uint256 id, uint256 balance, uint256 expectedBalance); /// @notice Thrown if the provided expiration is not valid error InvalidExpiration(uint256 expires); @@ -38,7 +38,7 @@ interface ISimpleAllocator is IAllocator { /// @param id The id of the token /// @param amount The amount of the token that was available for locking (the full balance of the token will get locked) /// @param expires The expiration of the lock - event Locked(address sponsor, uint256 id, uint256 amount, uint256 expires); + event Locked(address indexed sponsor, uint256 indexed id, uint256 amount, uint256 expires); /// @notice Locks the tokens of an id for a claim /// @dev Locks all tokens of a sponsor for an id diff --git a/src/test/TheCompactMock.sol b/src/test/TheCompactMock.sol index 8905324..a4f7360 100644 --- a/src/test/TheCompactMock.sol +++ b/src/test/TheCompactMock.sol @@ -6,36 +6,52 @@ import { IAllocator } from "src/interfaces/IAllocator.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"; +import { ForcedWithdrawalStatus } from "src/types/ForcedWithdrawalStatus.sol"; +import { ResetPeriod } from "src/types/ResetPeriod.sol"; +import { Scope } from "src/types/Scope.sol"; +import { console2 } from "forge-std/console2.sol"; + contract TheCompactMock is ERC6909 { using IdLib for uint96; using IdLib for uint256; using IdLib for address; + // Mock Variables + uint32 private constant DEFAULT_RESET_PERIOD = 60; + ResetPeriod private constant DEFAULT_RESET_PERIOD_TYPE = ResetPeriod.OneMinute; + Scope private constant DEFAULT_SCOPE = Scope.Multichain; + address private DEFAULT_ALLOCATOR; + + // Mock State + mapping(uint256 id => address token) public tokens; mapping(uint256 nonce => bool consumed) public consumedNonces; mapping(address allocator => bool registered) public registeredAllocators; + mapping(address user => uint256 availableAt) public forcedWithdrawalStatus; function __registerAllocator(address allocator, bytes calldata) external returns (uint96) { registeredAllocators[allocator] = true; + DEFAULT_ALLOCATOR = allocator; return 0; } function deposit(address token, uint256 amount, address allocator) external { ERC20(token).transferFrom(msg.sender, address(this), amount); uint256 id = _getTokenId(token, allocator); + tokens[id] = token; _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); + _transfer(address(0), 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); + _transfer(address(0), from, to, id, amount); } function withdraw(address token, uint256 amount, address allocator) external { @@ -52,10 +68,47 @@ contract TheCompactMock is ERC6909 { return true; } + function hasConsumedAllocatorNonce(uint256 nonce, address) external view returns (bool) { + return consumedNonces[nonce]; + } + + function getLockDetails(uint256 id) external view returns (address, address, ResetPeriod, Scope) { + return (tokens[id], DEFAULT_ALLOCATOR, DEFAULT_RESET_PERIOD_TYPE, DEFAULT_SCOPE); + } + + function enableForceWithdrawal(uint256) external returns (uint256) { + forcedWithdrawalStatus[msg.sender] = block.timestamp + DEFAULT_RESET_PERIOD; + return block.timestamp + DEFAULT_RESET_PERIOD; + } + + function disableForceWithdrawal(uint256) external returns (bool) { + forcedWithdrawalStatus[msg.sender] = 0; + return true; + } + + function getForcedWithdrawalStatus(address sponsor, uint256) external view returns (ForcedWithdrawalStatus, uint256) { + uint256 expires = forcedWithdrawalStatus[sponsor]; + return (expires == 0 ? ForcedWithdrawalStatus.Disabled : ForcedWithdrawalStatus.Enabled, expires); + } + function getTokenId(address token, address allocator) external pure returns (uint256) { return _getTokenId(token, allocator); } + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return + keccak256( + abi.encode( + // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, + keccak256("The Compact"), + keccak256("0"), + block.chainid, + address(this) + ) + ); + } + function name( uint256 // id ) public view virtual override returns (string memory) { diff --git a/test/ServerAllocator.t.sol b/test/ServerAllocator.t.sol index 82843a5..ec45d6d 100644 --- a/test/ServerAllocator.t.sol +++ b/test/ServerAllocator.t.sol @@ -60,7 +60,7 @@ abstract contract CreateHash is Test { // return keccak256( // abi.encodePacked( // "\x19\x01", // backslash is needed to escape the character - // _domainSeperator(verifyingContract), + // _domainSeparator(verifyingContract), // keccak256(abi.encode(keccak256(bytes(ALLOCATOR_TYPE)), data.hash)) // ) // ); @@ -71,7 +71,7 @@ abstract contract CreateHash is Test { return keccak256( abi.encodePacked( "\x19\x01", // backslash is needed to escape the character - _domainSeperator(verifyingContract), + _domainSeparator(verifyingContract), keccak256(abi.encode(COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount)) ) ); @@ -81,7 +81,7 @@ abstract contract CreateHash is Test { return keccak256( abi.encodePacked( "\x19\x01", // backslash is needed to escape the character - _domainSeperator(verifyingContract), + _domainSeparator(verifyingContract), keccak256(abi.encode(keccak256(bytes(REGISTER_ATTESTATION_TYPE)), data.signer, data.attestationHash, data.expiration, data.nonce)) ) ); @@ -92,13 +92,13 @@ abstract contract CreateHash is Test { return keccak256( abi.encodePacked( "\x19\x01", // backslash is needed to escape the character - _domainSeperator(verifyingContract), + _domainSeparator(verifyingContract), keccak256(abi.encode(keccak256(bytes(NONCE_CONSUMPTION_TYPE)), data.signer, data.nonces, data.attestations)) ) ); } - function _domainSeperator(address verifyingContract) internal view returns (bytes32) { + function _domainSeparator(address verifyingContract) internal view returns (bytes32) { return keccak256(abi.encode(keccak256(bytes(EIP712_DOMAIN_TYPE)), keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, verifyingContract)); } diff --git a/test/SimpleAllocator.t.sol b/test/SimpleAllocator.t.sol new file mode 100644 index 0000000..b14555d --- /dev/null +++ b/test/SimpleAllocator.t.sol @@ -0,0 +1,692 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { Test } from "forge-std/Test.sol"; +import { SimpleAllocator } from "src/examples/allocator/SimpleAllocator.sol"; +import { ITheCompact } from "src/interfaces/ITheCompact.sol"; +import { ISimpleAllocator } from "src/interfaces/ISimpleAllocator.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 { ERC6909 } from "lib/solady/src/tokens/ERC6909.sol"; +import { console } from "forge-std/console.sol"; +import { IERC1271 } from "lib/permit2/src/interfaces/IERC1271.sol"; +import { ForcedWithdrawalStatus } from "src/types/ForcedWithdrawalStatus.sol"; + +abstract contract MocksSetup is Test { + address user; + uint256 userPK; + address attacker; + uint256 attackerPK; + address arbiter; + ERC20Mock usdc; + TheCompactMock compactContract; + SimpleAllocator simpleAllocator; + uint256 usdcId; + + uint256 defaultResetPeriod = 60; + uint256 defaultAmount = 1000; + uint256 defaultNonce = 1; + uint256 defaultExpiration; + function setUp() public virtual { + arbiter = makeAddr("arbiter"); + usdc = new ERC20Mock("USDC", "USDC"); + compactContract = new TheCompactMock(); + simpleAllocator = new SimpleAllocator(address(compactContract), arbiter, 5, 100); + usdcId = compactContract.getTokenId(address(usdc), address(simpleAllocator)); + (user, userPK) = makeAddrAndKey("user"); + (attacker, attackerPK) = makeAddrAndKey("attacker"); + } +} + +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 + // EIP712 domain type + string name = "The Compact"; + string version = "0"; + + 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 + _domainSeparator(verifyingContract), + keccak256(abi.encode(COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount)) + ) + ); + } + + function _domainSeparator(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); + } +} + +abstract contract Deposited is MocksSetup { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), defaultAmount, address(simpleAllocator)); + + vm.stopPrank(); + } +} + +abstract contract Locked is Deposited { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + + vm.stopPrank(); + } +} + +contract SimpleAllocator_Lock is MocksSetup { + function test_revert_InvalidCaller() public { + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, user, attacker)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: attacker, + nonce: 1, + id: usdcId, + expires: block.timestamp + 1, + amount: 1000 + })); + } + function test_revert_ClaimActive() public { + vm.startPrank(user); + + // Mint, approve and deposit + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), defaultAmount, address(simpleAllocator)); + + // Successfully locked + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: defaultAmount + })); + + vm.warp(block.timestamp + defaultResetPeriod - 1); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ClaimActive.selector, user)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce + 1, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: defaultAmount + })); + } + function test_revert_InvalidArbiter(address falseArbiter_) public { + vm.assume(falseArbiter_ != arbiter); + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidArbiter.selector, falseArbiter_)); + simpleAllocator.lock(Compact({ + arbiter: falseArbiter_, + sponsor: user, + nonce: 1, + id: usdcId, + expires: block.timestamp + 1, + amount: 1000 + })); + } + function test_revert_InvalidExpiration_tooShort(uint128 delay_) public { + vm.assume(delay_ < simpleAllocator.MIN_WITHDRAWAL_DELAY()); + uint256 expiration = vm.getBlockTimestamp() + delay_; + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidExpiration.selector, expiration)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: vm.getBlockTimestamp() + delay_, + amount: 1000 + })); + } + function test_revert_InvalidExpiration_tooLong(uint128 delay_) public { + vm.assume(delay_ > simpleAllocator.MAX_WITHDRAWAL_DELAY()); + uint256 expiration = vm.getBlockTimestamp() + delay_; + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidExpiration.selector, expiration)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: vm.getBlockTimestamp() + delay_, + amount: 1000 + })); + } + function test_revert_ForceWithdrawalAvailable_ExpirationLongerThenResetPeriod(uint32 delay_) public { + vm.assume(delay_ > simpleAllocator.MIN_WITHDRAWAL_DELAY()); + vm.assume(delay_ < simpleAllocator.MAX_WITHDRAWAL_DELAY()); + vm.assume(delay_ > defaultResetPeriod); + + uint256 expiration = vm.getBlockTimestamp() + delay_; + uint256 maxExpiration = vm.getBlockTimestamp() + defaultResetPeriod; + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, maxExpiration)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: expiration, + amount: 1000 + })); + } + function test_revert_ForceWithdrawalAvailable_ScheduledForceWithdrawal() public { + + vm.startPrank(user); + compactContract.enableForceWithdrawal(usdcId); + + // move time forward + vm.warp(vm.getBlockTimestamp() + 1); + + // This expiration should be fine, if the force withdrawal was not enabled + uint256 expiration = vm.getBlockTimestamp() + defaultResetPeriod; + // check force withdrawal + (ForcedWithdrawalStatus status, uint256 expires) = compactContract.getForcedWithdrawalStatus(user, usdcId); + assertEq(status == ForcedWithdrawalStatus.Enabled, true); + assertEq(expires, expiration - 1); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, expiration - 1)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: expiration, + amount: 1000 + })); + } + function test_revert_ForceWithdrawalAvailable_ActiveForceWithdrawal() public { + vm.startPrank(user); + compactContract.enableForceWithdrawal(usdcId); + + // move time forward + uint256 forceWithdrawalTimestamp = vm.getBlockTimestamp() + defaultResetPeriod; + vm.warp(forceWithdrawalTimestamp); + + // This expiration should be fine, if the force withdrawal was not enabled + uint256 expiration = vm.getBlockTimestamp() + defaultResetPeriod; + // check force withdrawal + (ForcedWithdrawalStatus status, uint256 expires) = compactContract.getForcedWithdrawalStatus(user, usdcId); + assertEq(status == ForcedWithdrawalStatus.Enabled, true); + assertEq(expires, forceWithdrawalTimestamp); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, forceWithdrawalTimestamp)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: expiration, + amount: 1000 + })); + } + function test_revert_NonceAlreadyConsumed(uint256 nonce_) public { + vm.startPrank(user); + uint256[] memory nonces = new uint256[](1); + nonces[0] = nonce_; + compactContract.consume(nonces); + assertEq(compactContract.hasConsumedAllocatorNonce(nonce_, address(simpleAllocator)), true); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.NonceAlreadyConsumed.selector, nonce_)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: nonce_, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: 1000 + })); + } + function test_revert_InsufficientBalance(uint256 balance_,uint256 amount_) public { + vm.assume(balance_ < amount_); + + vm.startPrank(user); + + // Mint, approve and deposit + usdc.mint(user, balance_); + usdc.approve(address(compactContract), balance_); + compactContract.deposit(address(usdc), balance_, address(simpleAllocator)); + + // Check balance + assertEq(compactContract.balanceOf(user, usdcId), balance_); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InsufficientBalance.selector, user, usdcId, balance_, amount_)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: amount_ + })); + } + function test_successfullyLocked(uint256 nonce_, uint128 amount_, uint32 delay_) public { + vm.assume(delay_ > simpleAllocator.MIN_WITHDRAWAL_DELAY()); + vm.assume(delay_ < simpleAllocator.MAX_WITHDRAWAL_DELAY()); + vm.assume(delay_ <= defaultResetPeriod); + + vm.startPrank(user); + + // Mint, approve and deposit + usdc.mint(user, amount_); + usdc.approve(address(compactContract), amount_); + compactContract.deposit(address(usdc), amount_, address(simpleAllocator)); + + + // Check no lock exists + (uint256 amountBefore, uint256 expiresBefore) = simpleAllocator.checkTokensLocked(usdcId, user); + + assertEq(amountBefore, 0); + assertEq(expiresBefore, 0); + + uint256 expiration = vm.getBlockTimestamp() + delay_; + vm.expectEmit(true, true, false, true); + emit ISimpleAllocator.Locked(user, usdcId, amount_, expiration); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: nonce_, + id: usdcId, + expires: expiration, + amount: amount_ + })); + + // Check lock exists + (uint256 amountAfter, uint256 expiresAfter) = simpleAllocator.checkTokensLocked(usdcId, user); + + assertEq(amountAfter, amount_); + assertEq(expiresAfter, expiration); + } + function test_successfullyLocked_AfterNonceConsumption(uint256 nonce_, uint256 noncePrev_, uint128 amount_, uint32 delay_) public { + vm.assume(delay_ > simpleAllocator.MIN_WITHDRAWAL_DELAY()); + vm.assume(delay_ < simpleAllocator.MAX_WITHDRAWAL_DELAY()); + vm.assume(delay_ <= defaultResetPeriod); + vm.assume(noncePrev_ != nonce_); + + vm.startPrank(user); + + // Mint, approve and deposit + usdc.mint(user, amount_); + usdc.approve(address(compactContract), amount_); + compactContract.deposit(address(usdc), amount_, address(simpleAllocator)); + + // Create a previous lock + uint256 expirationPrev = vm.getBlockTimestamp() + delay_; + vm.expectEmit(true, true, false, true); + emit ISimpleAllocator.Locked(user, usdcId, amount_, expirationPrev); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: noncePrev_, + id: usdcId, + expires: expirationPrev, + amount: amount_ + })); + + // Check a previous lock exists + (uint256 amountBefore, uint256 expiresBefore) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amountBefore, amount_); + assertEq(expiresBefore, expirationPrev); + + + // Check for revert if previous nonce not consumed + uint256 expiration = vm.getBlockTimestamp() + delay_; + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ClaimActive.selector, user)); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: nonce_, + id: usdcId, + expires: expiration, + amount: amount_ + })); + + // Consume previous nonce + uint256[] memory nonces = new uint256[](1); + nonces[0] = noncePrev_; + vm.stopPrank(); + vm.prank(address(simpleAllocator)); + compactContract.consume(nonces); + + vm.prank(user); + + vm.expectEmit(true, true, false, true); + emit ISimpleAllocator.Locked(user, usdcId, amount_, expiration); + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: nonce_, + id: usdcId, + expires: expiration, + amount: amount_ + })); + + // Check lock exists + (uint256 amountAfter, uint256 expiresAfter) = simpleAllocator.checkTokensLocked(usdcId, user); + + assertEq(amountAfter, amount_); + assertEq(expiresAfter, expiration); + } +} + +contract SimpleAllocator_Attest is Deposited { + function test_revert_InvalidCaller_NotCompact() public { + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, address(compactContract))); + simpleAllocator.attest(address(user), address(user), address(usdc), usdcId, defaultAmount); + } + function test_revert_InvalidCaller_FromNotOperator() public { + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, user)); + compactContract.transfer(user, attacker, defaultAmount, address(usdc), address(simpleAllocator)); + } + function test_revert_InsufficientBalance_NoActiveLock(uint128 falseAmount_) public { + vm.assume(falseAmount_ > defaultAmount); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InsufficientBalance.selector, user, usdcId, defaultAmount, falseAmount_)); + compactContract.transfer(user, attacker, falseAmount_, address(usdc), address(simpleAllocator)); + } + function test_revert_InsufficientBalance_ActiveLock() public { + vm.startPrank(user); + + // Lock a single token + uint256 defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: 1 + })); + + // At this point, the deposited defaultAmount is not fully available anymore, because one of the tokens was locked + + // Revert if we try to transfer all of the deposited tokens + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InsufficientBalance.selector, user, usdcId, defaultAmount, defaultAmount + 1)); + compactContract.transfer(user, attacker, defaultAmount, address(usdc), address(simpleAllocator)); + } + function test_successfullyAttested(uint32 lockedAmount_, uint32 transferAmount_) public { + vm.assume(uint256(transferAmount_) + uint256(lockedAmount_) <= defaultAmount); + + address otherUser = makeAddr("otherUser"); + + vm.startPrank(user); + // Lock tokens + uint256 defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: lockedAmount_ + })); + + vm.expectEmit(true, true, true, true); + emit ERC6909.Transfer(address(0), user, otherUser, usdcId, transferAmount_); + compactContract.transfer(user, otherUser, transferAmount_, address(usdc), address(simpleAllocator)); + + // Check that the other user has the tokens + assertEq(compactContract.balanceOf(otherUser, usdcId), transferAmount_); + assertEq(compactContract.balanceOf(user, usdcId), defaultAmount - transferAmount_); + + } +} + +contract SimpleAllocator_IsValidSignature is Deposited, CreateHash { + function test_revert_InvalidLock_NoActiveLock() public { + bytes32 digest = _hashCompact(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: defaultAmount + }), address(compactContract)); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidLock.selector, digest, 0)); + simpleAllocator.isValidSignature(digest, ""); + } + function test_revert_InvalidLock_ExpiredLock() public { + vm.startPrank(user); + + // Lock tokens + uint256 defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + + // Move time forward so lock has expired + vm.warp(block.timestamp + defaultResetPeriod); + + bytes32 digest = _hashCompact(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }), address(compactContract)); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidLock.selector, digest, defaultExpiration)); + simpleAllocator.isValidSignature(digest, ""); + + } + function test_successfullyValidated() public { + vm.startPrank(user); + + // Lock tokens + uint256 defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + + // Move time forward so lock has expired + vm.warp(block.timestamp + defaultResetPeriod - 1); + + bytes32 digest = _hashCompact(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }), address(compactContract)); + + bytes4 selector = simpleAllocator.isValidSignature(digest, ""); + assertEq(selector,IERC1271.isValidSignature.selector); + } +} + +contract SimpleAllocator_CheckTokensLocked is Locked { + function test_checkTokensLocked_NoActiveLock() public { + address otherUser = makeAddr("otherUser"); + (uint256 amount, uint256 expires) = simpleAllocator.checkTokensLocked(usdcId, otherUser); + assertEq(amount, 0); + assertEq(expires, 0); + } + function test_checkTokensLocked_ExpiredLock() public { + (uint256 amount, uint256 expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, defaultAmount); + assertEq(expires, defaultExpiration); + + vm.warp(defaultExpiration); + + (amount, expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, 0); + assertEq(expires, 0); + } + function test_checkTokensLocked_NonceConsumed() public { + (uint256 amount, uint256 expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, defaultAmount); + assertEq(expires, defaultExpiration); + + uint256[] memory nonces = new uint256[](1); + nonces[0] = defaultNonce; + vm.prank(address(simpleAllocator)); + compactContract.consume(nonces); + + (amount, expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, 0); + assertEq(expires, 0); + } + function test_checkTokensLocked_ActiveLock() public { + vm.warp(defaultExpiration - 1); + + (uint256 amount, uint256 expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, defaultAmount); + assertEq(expires, defaultExpiration); + } + function test_checkCompactLocked_revert_InvalidArbiter() public { + address otherArbiter = makeAddr("otherArbiter"); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidArbiter.selector, otherArbiter)); + simpleAllocator.checkCompactLocked(Compact({ + arbiter: otherArbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + } + function test_checkCompactLocked_NoActiveLock() public { + address otherUser = makeAddr("otherUser"); + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked(Compact({ + arbiter: arbiter, + sponsor: otherUser, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + assertEq(locked, false); + assertEq(expires, 0); + } + function test_checkCompactLocked_ExpiredLock() public { + // Confirm that a lock is previously active + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + assertEq(locked, true); + assertEq(expires, defaultExpiration); + + // Move time forward so lock has expired + vm.warp(defaultExpiration); + + // Check that the lock is no longer active + (locked, expires) = simpleAllocator.checkCompactLocked(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + assertEq(locked, false); + assertEq(expires, 0); + } + function test_checkCompactLocked_NonceConsumed() public { + // Confirm that a lock is previously active + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + assertEq(locked, true); + assertEq(expires, defaultExpiration); + + // Consume nonce + uint256[] memory nonces = new uint256[](1); + nonces[0] = defaultNonce; + vm.prank(address(simpleAllocator)); + compactContract.consume(nonces); + + // Check that the lock is no longer active + (locked, expires) = simpleAllocator.checkCompactLocked(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + assertEq(locked, false); + assertEq(expires, 0); + } + + function test_checkCompactLocked_successfully() public { + // Move time forward to last second before expiration + vm.warp(defaultExpiration - 1); + + // Confirm that a lock is active + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked(Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + })); + assertEq(locked, true); + assertEq(expires, defaultExpiration); + } +}