generated from Uniswap/foundry-template
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'SimpleAllocator' into ERC7683Allocator
- Loading branch information
Showing
3 changed files
with
1,053 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.27; | ||
|
||
import {IAllocator} from '../interfaces/IAllocator.sol'; | ||
import {ISimpleAllocator} from '../interfaces/ISimpleAllocator.sol'; | ||
import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; | ||
import {ERC6909} from '@solady/tokens/ERC6909.sol'; | ||
import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; | ||
import {ResetPeriod} from '@uniswap/the-compact/lib/IdLib.sol'; | ||
import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; | ||
import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; | ||
|
||
contract SimpleAllocator is ISimpleAllocator { | ||
// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)") | ||
bytes32 constant COMPACT_TYPEHASH = 0xcdca950b17b5efc016b74b912d8527dfba5e404a688cbc3dab16cb943287fec2; | ||
|
||
address public immutable COMPACT_CONTRACT; | ||
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) internal _claim; | ||
/// @dev mapping of tokenHash to the amount of the lock | ||
mapping(bytes32 tokenHash => uint256 amount) internal _amount; | ||
/// @dev mapping of tokenHash to the nonce of the lock | ||
mapping(bytes32 tokenHash => uint256 nonce) internal _nonce; | ||
/// @dev mapping of the lock digest to the tokenHash of the lock | ||
mapping(bytes32 digest => bytes32 tokenHash) internal _sponsor; | ||
|
||
constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) { | ||
COMPACT_CONTRACT = compactContract_; | ||
MIN_WITHDRAWAL_DELAY = minWithdrawalDelay_; | ||
MAX_WITHDRAWAL_DELAY = maxWithdrawalDelay_; | ||
|
||
ITheCompact(COMPACT_CONTRACT).__registerAllocator(address(this), ''); | ||
} | ||
|
||
/// @inheritdoc ISimpleAllocator | ||
function lock(Compact calldata compact_) external { | ||
bytes32 tokenHash = _checkAllocation(compact_); | ||
|
||
bytes32 digest = keccak256( | ||
abi.encodePacked( | ||
bytes2(0x1901), | ||
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), | ||
keccak256( | ||
abi.encode( | ||
COMPACT_TYPEHASH, | ||
compact_.arbiter, | ||
compact_.sponsor, | ||
compact_.nonce, | ||
compact_.expires, | ||
compact_.id, | ||
compact_.amount | ||
) | ||
) | ||
) | ||
); | ||
|
||
_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) { | ||
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_); | ||
// 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]; | ||
} | ||
if (balance < fullAmount) { | ||
revert InsufficientBalance(from_, id_, balance, fullAmount); | ||
} | ||
|
||
return 0x1a808f91; | ||
} | ||
|
||
/// @inheritdoc IERC1271 | ||
/// @dev we trust the compact contract to check the nonce is not already consumed | ||
function isValidSignature(bytes32 hash, bytes calldata) external view returns (bytes4 magicValue) { | ||
// The hash is the digest of the compact | ||
bytes32 tokenHash = _sponsor[hash]; | ||
if (tokenHash == bytes32(0) || _claim[tokenHash] <= block.timestamp) { | ||
revert InvalidLock(hash, _claim[tokenHash]); | ||
} | ||
|
||
return IERC1271.isValidSignature.selector; | ||
} | ||
|
||
/// @inheritdoc 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 | ||
|| ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) | ||
) { | ||
return (0, 0); | ||
} | ||
|
||
return (_amount[tokenHash], expires); | ||
} | ||
|
||
/// @inheritdoc ISimpleAllocator | ||
function checkCompactLocked(Compact calldata compact_) external view returns (bool locked_, uint256 expires_) { | ||
bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor); | ||
bytes32 digest = keccak256( | ||
abi.encodePacked( | ||
bytes2(0x1901), | ||
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), | ||
keccak256( | ||
abi.encode( | ||
COMPACT_TYPEHASH, | ||
compact_.arbiter, | ||
compact_.sponsor, | ||
compact_.nonce, | ||
compact_.expires, | ||
compact_.id, | ||
compact_.amount | ||
) | ||
) | ||
) | ||
); | ||
uint256 expires = _claim[tokenHash]; | ||
bool active = _sponsor[digest] == tokenHash && expires > block.timestamp | ||
&& !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)); | ||
if (active) { | ||
(ForcedWithdrawalStatus status, uint256 forcedWithdrawalAvailableAt) = | ||
ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); | ||
if (status == ForcedWithdrawalStatus.Enabled && forcedWithdrawalAvailableAt < expires) { | ||
expires = forcedWithdrawalAvailableAt; | ||
active = expires > block.timestamp; | ||
} | ||
} | ||
return (active, active ? expires : 0); | ||
} | ||
|
||
function _getTokenHash(uint256 id_, address sponsor_) internal pure returns (bytes32) { | ||
return keccak256(abi.encode(id_, sponsor_)); | ||
} | ||
|
||
function _checkAllocation(Compact memory 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 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); | ||
} | ||
(, address allocator, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id); | ||
if (allocator != address(this)) { | ||
revert InvalidAllocator(allocator); | ||
} | ||
// Check expiration is not longer then the tokens forced withdrawal time | ||
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") { | ||
// Bitpacked durations in 24-bit segments: | ||
// 278d00 094890 015180 000f3c 000258 00003c 00000f 000001 | ||
// 30 days 7 days 1 day 1 hour 10 min 1 min 15 sec 1 sec | ||
let bitpacked := 0x278d00094890015180000f3c00025800003c00000f000001 | ||
|
||
// Shift right by period * 24 bits & mask the least significant 24 bits. | ||
duration := and(shr(mul(resetPeriod_, 24), bitpacked), 0xffffff) | ||
} | ||
return duration; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.27; | ||
|
||
import {IAllocator} from '../interfaces/IAllocator.sol'; | ||
import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; | ||
|
||
interface ISimpleAllocator is IAllocator { | ||
/// @notice Thrown if a claim is already active | ||
error ClaimActive(address sponsor); | ||
|
||
/// @notice Thrown if the caller is invalid | ||
error InvalidCaller(address caller, address expected); | ||
|
||
/// @notice Thrown if the nonce has already been consumed on the compact contract | ||
error NonceAlreadyConsumed(uint256 nonce); | ||
|
||
/// @notice Thrown if the sponsor does not have enough balance to lock the amount | ||
error InsufficientBalance(address sponsor, uint256 id, uint256 balance, uint256 expectedBalance); | ||
|
||
/// @notice Thrown if the provided expiration is not valid | ||
error InvalidExpiration(uint256 expires); | ||
|
||
/// @notice Thrown if the expiration is longer then the tokens forced withdrawal time | ||
error ForceWithdrawalAvailable(uint256 expires, uint256 forcedWithdrawalExpiration); | ||
|
||
/// @notice Thrown if the allocator is not the one expected | ||
error InvalidAllocator(address allocator); | ||
|
||
/// @notice Thrown if the provided lock is not available or expired | ||
/// @dev The expiration will be '0' if no lock is available | ||
error InvalidLock(bytes32 digest, uint256 expiration); | ||
|
||
/// @notice Emitted when a lock is successfully created | ||
/// @param sponsor The address of the sponsor | ||
/// @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 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 | ||
/// @param compact_ The compact that contains the data about the lock | ||
function lock(Compact calldata compact_) 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 | ||
/// @return amount_ The amount of the token that was available for locking (the full balance of the token will get locked) | ||
/// @return expires_ The expiration of the lock | ||
function checkTokensLocked(uint256 id_, address sponsor_) | ||
external | ||
view | ||
returns (uint256 amount_, uint256 expires_); | ||
|
||
/// @notice Checks if the a lock for the compact exists and is active | ||
/// @dev Also checks if the provided nonce has not yet been consumed on the compact contract | ||
/// @param compact_ The compact that contains the data about the lock | ||
/// @return locked_ Whether the compact is locked | ||
/// @return expires_ The expiration of the lock | ||
function checkCompactLocked(Compact calldata compact_) external view returns (bool locked_, uint256 expires_); | ||
} |
Oops, something went wrong.