Skip to content

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

Merged
merged 41 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
720896b
SimpleWitnessAllocator
mgretzke Jan 10, 2025
2efccfc
Merge pull request #1 from Uniswap/main
mgretzke Jan 10, 2025
d19b528
Simple ERC7683 Allocator
mgretzke Jan 10, 2025
872891b
readme
mgretzke Jan 10, 2025
c7c5e60
SimpleERC7683 updates
mgretzke Jan 15, 2025
217a8c4
RoutingAllocator
mgretzke Jan 15, 2025
e268af4
ERC8683Allocator contract and tests
mgretzke Feb 25, 2025
6f8dff7
routing allocator fixed
mgretzke Feb 25, 2025
b524752
Removed Arbiter from constructor
mgretzke Feb 25, 2025
9ed5e40
resolve tests
mgretzke Feb 26, 2025
d3d3cf1
forge fmt
mgretzke Feb 27, 2025
abd027b
removed old contracts
mgretzke Feb 27, 2025
1b4281a
improved coverage
mgretzke Feb 27, 2025
01417c6
use stable foundry version
mgretzke Feb 27, 2025
ea8e05f
remove compilation restrictions
mgretzke Feb 27, 2025
e4ebdeb
readme fixed
mgretzke Feb 27, 2025
3c22b3f
bound fuzz tests
mgretzke Feb 28, 2025
69de0ee
bytecode_hash = "none" to fix theCompact size
mgretzke Feb 28, 2025
8db0d7b
Merge branch 'main' into ERC7683Allocator
mgretzke Feb 28, 2025
ca98b6a
contract + interface + tests
mgretzke Feb 28, 2025
04d1040
format fix
mgretzke Feb 28, 2025
b8752bf
Merge branch 'main' into ERC7683Allocator
mgretzke Feb 28, 2025
d7cccaa
deleted simpleWitnessAllocator contracts
mgretzke Feb 28, 2025
eab3c92
Merge branch 'SimpleAllocator' into ERC7683Allocator
mgretzke Feb 28, 2025
e6d9d04
format fix
mgretzke Feb 28, 2025
6f92df3
fixed EIP712 compatibility
mgretzke Mar 4, 2025
35ede04
fixed ubuntu version
mgretzke Mar 4, 2025
d34ef61
Changed Mandate data to be Tribunal ready
mgretzke Mar 4, 2025
f91a8cc
Removed sponsor == operator requirement
mgretzke Mar 5, 2025
32dafd3
use this.attest.selector
mgretzke Mar 5, 2025
1bc6d91
moved actual nonce to least significant bits
mgretzke Mar 5, 2025
fe0477a
using bitmap for nonces
mgretzke Mar 6, 2025
a2fb41b
enable relayed locks in child contracts
mgretzke Mar 6, 2025
904d5d8
Merged SimpleAllocator
mgretzke Mar 6, 2025
4bed9de
fixed relayed openFor call
mgretzke Mar 6, 2025
8cad10a
added decayCurve to Mandate
mgretzke Mar 21, 2025
719699f
additional input params
mgretzke Mar 21, 2025
af49b8b
removed claimant from originData
mgretzke Mar 21, 2025
10162ae
Moved RDA params into qualification data
mgretzke Mar 21, 2025
273e632
Merge pull request #7 from Uniswap/mandate-reverse-dutch-auction
0age Mar 27, 2025
0ac955d
Merge pull request #6 from Uniswap/ERC7683Allocator
mgretzke Apr 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions src/allocators/SimpleAllocator.sol
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) {
Copy link
Contributor

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?

Copy link
Collaborator Author

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?

Copy link
Contributor

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 🙃

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to actually import IdLib and use it here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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!

Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
2 changes: 1 addition & 1 deletion src/interfaces/IAllocator.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol';

interface IAllocator is IERC1271 {
// Called on standard transfers; must return this function selector (0x1a808f91).
Expand Down
62 changes: 62 additions & 0 deletions src/interfaces/ISimpleAllocator.sol
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_);
}
4 changes: 2 additions & 2 deletions src/test/ERC20Mock.sol
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol';

contract ERC20Mock is ERC20 {
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) { }
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}

function mint(address to, uint256 amount) external {
_mint(to, amount);
Expand Down
Loading
Loading