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 all 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
2 changes: 1 addition & 1 deletion .github/workflows/semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
jobs:
semgrep:
name: semgrep/ci
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
container:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable

- name: Run Forge build
run: |
Expand Down
18 changes: 10 additions & 8 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ libs = ["lib"]
optimizer = true
optimizer_runs = 999999
via_ir = true
solc = "0.8.27"
solc = "0.8.28"
verbosity = 2
ffi = true
evm_version = "cancun"
fs_permissions = [
{ access = "read-write", path = ".forge-snapshots"},
{ access = "read", path = "script/" }
]

remappings = [
"forge-std=lib/forge-std/src",
"@openzeppelin/contracts=lib/openzeppelin-contracts/contracts",
Expand All @@ -21,13 +21,15 @@ remappings = [
"@solady=lib/solady/src",
]

additional_compiler_profiles = [
{ name = "test", via_ir = false }
]
[profile.ci]
inherit = "default"
optimizer_runs = 200 # Override optimizer runs to reduce the compact contract sizes
bytecode_hash = 'none'

compilation_restrictions = [
{ paths = "test/**", via_ir = false }
]
[profile.pr]
inherit = "default"
optimizer_runs = 200 # Override optimizer runs to reduce the compact contract sizes
bytecode_hash = 'none'

[profile.default.fuzz]
runs = 1000
Expand Down
365 changes: 365 additions & 0 deletions src/allocators/ERC7683Allocator.sol

Large diffs are not rendered by default.

221 changes: 221 additions & 0 deletions src/allocators/SimpleAllocator.sol
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) {
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_, 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) {
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;
}
}
25 changes: 25 additions & 0 deletions src/allocators/types/TribunalStructs.sol
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.
}
Loading
Loading