Skip to content

Commit fb8f8e5

Browse files
authored
Merge pull request #5 from Uniswap/SimpleAllocator
Simple allocator
2 parents 324782a + 0ac955d commit fb8f8e5

14 files changed

+2895
-31
lines changed

.github/workflows/semgrep.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
jobs:
1212
semgrep:
1313
name: semgrep/ci
14-
runs-on: ubuntu-20.04
14+
runs-on: ubuntu-latest
1515
env:
1616
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
1717
container:

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Install Foundry
2525
uses: foundry-rs/foundry-toolchain@v1
2626
with:
27-
version: nightly
27+
version: stable
2828

2929
- name: Run Forge build
3030
run: |

foundry.toml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ libs = ["lib"]
55
optimizer = true
66
optimizer_runs = 999999
77
via_ir = true
8-
solc = "0.8.27"
8+
solc = "0.8.28"
99
verbosity = 2
1010
ffi = true
11+
evm_version = "cancun"
1112
fs_permissions = [
1213
{ access = "read-write", path = ".forge-snapshots"},
1314
{ access = "read", path = "script/" }
1415
]
15-
1616
remappings = [
1717
"forge-std=lib/forge-std/src",
1818
"@openzeppelin/contracts=lib/openzeppelin-contracts/contracts",
@@ -21,13 +21,15 @@ remappings = [
2121
"@solady=lib/solady/src",
2222
]
2323

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

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

3234
[profile.default.fuzz]
3335
runs = 1000

src/allocators/ERC7683Allocator.sol

Lines changed: 365 additions & 0 deletions
Large diffs are not rendered by default.

src/allocators/SimpleAllocator.sol

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {IAllocator} from '../interfaces/IAllocator.sol';
6+
import {ISimpleAllocator} from '../interfaces/ISimpleAllocator.sol';
7+
import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol';
8+
import {ERC6909} from '@solady/tokens/ERC6909.sol';
9+
import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol';
10+
import {ResetPeriod} from '@uniswap/the-compact/lib/IdLib.sol';
11+
import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol';
12+
import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol';
13+
14+
contract SimpleAllocator is ISimpleAllocator {
15+
// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)")
16+
bytes32 constant COMPACT_TYPEHASH = 0xcdca950b17b5efc016b74b912d8527dfba5e404a688cbc3dab16cb943287fec2;
17+
18+
address public immutable COMPACT_CONTRACT;
19+
uint256 public immutable MIN_WITHDRAWAL_DELAY;
20+
uint256 public immutable MAX_WITHDRAWAL_DELAY;
21+
22+
/// @dev mapping of tokenHash to the expiration of the lock
23+
mapping(bytes32 tokenHash => uint256 expiration) internal _claim;
24+
/// @dev mapping of tokenHash to the amount of the lock
25+
mapping(bytes32 tokenHash => uint256 amount) internal _amount;
26+
/// @dev mapping of tokenHash to the nonce of the lock
27+
mapping(bytes32 tokenHash => uint256 nonce) internal _nonce;
28+
/// @dev mapping of the lock digest to the tokenHash of the lock
29+
mapping(bytes32 digest => bytes32 tokenHash) internal _sponsor;
30+
31+
constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) {
32+
COMPACT_CONTRACT = compactContract_;
33+
MIN_WITHDRAWAL_DELAY = minWithdrawalDelay_;
34+
MAX_WITHDRAWAL_DELAY = maxWithdrawalDelay_;
35+
36+
ITheCompact(COMPACT_CONTRACT).__registerAllocator(address(this), '');
37+
}
38+
39+
/// @inheritdoc ISimpleAllocator
40+
function lock(Compact calldata compact_) external {
41+
bytes32 tokenHash = _checkAllocation(compact_, true);
42+
43+
bytes32 digest = keccak256(
44+
abi.encodePacked(
45+
bytes2(0x1901),
46+
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(),
47+
keccak256(
48+
abi.encode(
49+
COMPACT_TYPEHASH,
50+
compact_.arbiter,
51+
compact_.sponsor,
52+
compact_.nonce,
53+
compact_.expires,
54+
compact_.id,
55+
compact_.amount
56+
)
57+
)
58+
)
59+
);
60+
61+
_claim[tokenHash] = compact_.expires;
62+
_amount[tokenHash] = compact_.amount;
63+
_nonce[tokenHash] = compact_.nonce;
64+
_sponsor[digest] = tokenHash;
65+
66+
emit Locked(compact_.sponsor, compact_.id, compact_.amount, compact_.expires);
67+
}
68+
69+
/// @inheritdoc IAllocator
70+
function attest(address, address from_, address, uint256 id_, uint256 amount_) external view returns (bytes4) {
71+
if (msg.sender != COMPACT_CONTRACT) {
72+
revert InvalidCaller(msg.sender, COMPACT_CONTRACT);
73+
}
74+
uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(from_, id_);
75+
// Check unlocked balance
76+
bytes32 tokenHash = _getTokenHash(id_, from_);
77+
78+
uint256 fullAmount = amount_;
79+
if (_claim[tokenHash] > block.timestamp) {
80+
// Lock is still active, add the locked amount if the nonce has not yet been consumed
81+
fullAmount += ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))
82+
? 0
83+
: _amount[tokenHash];
84+
}
85+
if (balance < fullAmount) {
86+
revert InsufficientBalance(from_, id_, balance, fullAmount);
87+
}
88+
89+
return this.attest.selector;
90+
}
91+
92+
/// @inheritdoc IERC1271
93+
/// @dev we trust the compact contract to check the nonce is not already consumed
94+
function isValidSignature(bytes32 hash, bytes calldata) external view returns (bytes4 magicValue) {
95+
// The hash is the digest of the compact
96+
bytes32 tokenHash = _sponsor[hash];
97+
if (tokenHash == bytes32(0) || _claim[tokenHash] <= block.timestamp) {
98+
revert InvalidLock(hash, _claim[tokenHash]);
99+
}
100+
101+
return IERC1271.isValidSignature.selector;
102+
}
103+
104+
/// @inheritdoc ISimpleAllocator
105+
function checkTokensLocked(uint256 id_, address sponsor_)
106+
external
107+
view
108+
returns (uint256 amount_, uint256 expires_)
109+
{
110+
bytes32 tokenHash = _getTokenHash(id_, sponsor_);
111+
uint256 expires = _claim[tokenHash];
112+
if (
113+
expires <= block.timestamp
114+
|| ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))
115+
) {
116+
return (0, 0);
117+
}
118+
119+
return (_amount[tokenHash], expires);
120+
}
121+
122+
/// @inheritdoc ISimpleAllocator
123+
function checkCompactLocked(Compact calldata compact_) external view returns (bool locked_, uint256 expires_) {
124+
bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor);
125+
bytes32 digest = keccak256(
126+
abi.encodePacked(
127+
bytes2(0x1901),
128+
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(),
129+
keccak256(
130+
abi.encode(
131+
COMPACT_TYPEHASH,
132+
compact_.arbiter,
133+
compact_.sponsor,
134+
compact_.nonce,
135+
compact_.expires,
136+
compact_.id,
137+
compact_.amount
138+
)
139+
)
140+
)
141+
);
142+
uint256 expires = _claim[tokenHash];
143+
bool active = _sponsor[digest] == tokenHash && expires > block.timestamp
144+
&& !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this));
145+
if (active) {
146+
(ForcedWithdrawalStatus status, uint256 forcedWithdrawalAvailableAt) =
147+
ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id);
148+
if (status == ForcedWithdrawalStatus.Enabled && forcedWithdrawalAvailableAt < expires) {
149+
expires = forcedWithdrawalAvailableAt;
150+
active = expires > block.timestamp;
151+
}
152+
}
153+
return (active, active ? expires : 0);
154+
}
155+
156+
function _getTokenHash(uint256 id_, address sponsor_) internal pure returns (bytes32) {
157+
return keccak256(abi.encode(id_, sponsor_));
158+
}
159+
160+
function _checkAllocation(Compact memory compact_, bool checkSponsor_) internal view returns (bytes32) {
161+
// Check msg.sender is sponsor
162+
if (checkSponsor_ && msg.sender != compact_.sponsor) {
163+
revert InvalidCaller(msg.sender, compact_.sponsor);
164+
}
165+
bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor);
166+
// Check no lock is already active for this sponsor
167+
if (
168+
_claim[tokenHash] > block.timestamp
169+
&& !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))
170+
) {
171+
revert ClaimActive(compact_.sponsor);
172+
}
173+
// Check expiration is not too soon or too late
174+
if (
175+
compact_.expires < block.timestamp + MIN_WITHDRAWAL_DELAY
176+
|| compact_.expires > block.timestamp + MAX_WITHDRAWAL_DELAY
177+
) {
178+
revert InvalidExpiration(compact_.expires);
179+
}
180+
(, address allocator, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id);
181+
if (allocator != address(this)) {
182+
revert InvalidAllocator(allocator);
183+
}
184+
// Check expiration is not longer then the tokens forced withdrawal time
185+
if (compact_.expires > block.timestamp + _resetPeriodToSeconds(resetPeriod)) {
186+
revert ForceWithdrawalAvailable(compact_.expires, block.timestamp + _resetPeriodToSeconds(resetPeriod));
187+
}
188+
// Check expiration is not past an active force withdrawal
189+
(, uint256 forcedWithdrawalExpiration) =
190+
ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id);
191+
if (forcedWithdrawalExpiration != 0 && forcedWithdrawalExpiration < compact_.expires) {
192+
revert ForceWithdrawalAvailable(compact_.expires, forcedWithdrawalExpiration);
193+
}
194+
// Check nonce is not yet consumed
195+
if (ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(compact_.nonce, address(this))) {
196+
revert NonceAlreadyConsumed(compact_.nonce);
197+
}
198+
199+
uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(compact_.sponsor, compact_.id);
200+
// Check balance is enough
201+
if (balance < compact_.amount) {
202+
revert InsufficientBalance(compact_.sponsor, compact_.id, balance, compact_.amount);
203+
}
204+
205+
return tokenHash;
206+
}
207+
208+
/// @dev copied from IdLib.sol
209+
function _resetPeriodToSeconds(ResetPeriod resetPeriod_) internal pure returns (uint256 duration) {
210+
assembly ("memory-safe") {
211+
// Bitpacked durations in 24-bit segments:
212+
// 278d00 094890 015180 000f3c 000258 00003c 00000f 000001
213+
// 30 days 7 days 1 day 1 hour 10 min 1 min 15 sec 1 sec
214+
let bitpacked := 0x278d00094890015180000f3c00025800003c00000f000001
215+
216+
// Shift right by period * 24 bits & mask the least significant 24 bits.
217+
duration := and(shr(mul(resetPeriod_, 24), bitpacked), 0xffffff)
218+
}
219+
return duration;
220+
}
221+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol';
6+
7+
struct Claim {
8+
uint256 chainId; // Claim processing chain ID
9+
Compact compact;
10+
bytes sponsorSignature; // Authorization from the sponsor
11+
bytes allocatorSignature; // Authorization from the allocator
12+
}
13+
14+
struct Mandate {
15+
// uint256 chainId; // (implicit arg, included in EIP712 payload).
16+
// address tribunal; // (implicit arg, included in EIP712 payload).
17+
address recipient; // Recipient of filled tokens.
18+
uint256 expires; // Mandate expiration timestamp.
19+
address token; // Fill token (address(0) for native).
20+
uint256 minimumAmount; // Minimum fill amount.
21+
uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in.
22+
uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline).
23+
uint256[] decayCurve; // Block durations, fill increases, & claim decreases.
24+
bytes32 salt; // Replay protection parameter.
25+
}

0 commit comments

Comments
 (0)