-
Notifications
You must be signed in to change notification settings - Fork 1
Simple allocator #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
720896b
2efccfc
d19b528
872891b
c7c5e60
217a8c4
e268af4
6f8dff7
b524752
9ed5e40
d3d3cf1
abd027b
1b4281a
01417c6
ea8e05f
e4ebdeb
3c22b3f
69de0ee
8db0d7b
ca98b6a
04d1040
b8752bf
d7cccaa
eab3c92
e6d9d04
6f92df3
35ede04
d34ef61
f91a8cc
32dafd3
1bc6d91
fe0477a
a2fb41b
904d5d8
4bed9de
8cad10a
719699f
af49b8b
10162ae
273e632
0ac955d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
// 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_, true); | ||
|
||
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, address from_, address, uint256 id_, uint256 amount_) external view returns (bytes4) { | ||
if (msg.sender != COMPACT_CONTRACT) { | ||
revert InvalidCaller(msg.sender, COMPACT_CONTRACT); | ||
} | ||
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 this.attest.selector; | ||
} | ||
|
||
/// @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_, bool checkSponsor_) internal view returns (bytes32) { | ||
// Check msg.sender is sponsor | ||
if (checkSponsor_ && msg.sender != compact_.sponsor) { | ||
revert InvalidCaller(msg.sender, compact_.sponsor); | ||
} | ||
bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor); | ||
// 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(compact_.sponsor, compact_.id); | ||
// Check balance is enough | ||
if (balance < compact_.amount) { | ||
revert InsufficientBalance(compact_.sponsor, compact_.id, balance, compact_.amount); | ||
} | ||
|
||
return tokenHash; | ||
} | ||
|
||
/// @dev copied from IdLib.sol | ||
function _resetPeriodToSeconds(ResetPeriod resetPeriod_) internal pure returns (uint256 duration) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would it make sense to actually import IdLib and use it here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about that... Kind of felt a little overkill for these three lines of code. Also, I felt like it would make the contract easier accessible to someone who has not previously interacted with the Compact before. But definitely open to a different opinion! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. main reason i'd advise it is so that any upstream changes get incorporated; but agreed it's not strictly necessary! |
||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.27; | ||
|
||
import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; | ||
|
||
struct Claim { | ||
uint256 chainId; // Claim processing chain ID | ||
Compact compact; | ||
bytes sponsorSignature; // Authorization from the sponsor | ||
bytes allocatorSignature; // Authorization from the allocator | ||
} | ||
|
||
struct Mandate { | ||
// uint256 chainId; // (implicit arg, included in EIP712 payload). | ||
// address tribunal; // (implicit arg, included in EIP712 payload). | ||
address recipient; // Recipient of filled tokens. | ||
uint256 expires; // Mandate expiration timestamp. | ||
address token; // Fill token (address(0) for native). | ||
uint256 minimumAmount; // Minimum fill amount. | ||
uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. | ||
uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). | ||
uint256[] decayCurve; // Block durations, fill increases, & claim decreases. | ||
bytes32 salt; // Replay protection parameter. | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this also confirm that hasConsumedAllocatorNonce is false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think we need to do that, since we can confidently rely on the Compact contract to have checked the nonce consumption prior to the signature verification... Or did I miss something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aah yeah that check does happen prior; so i even think this check would always fail 🙃