Skip to content

Commit 49ff3c7

Browse files
committed
Lock compact with witness
1 parent 47b5b28 commit 49ff3c7

File tree

3 files changed

+201
-39
lines changed

3 files changed

+201
-39
lines changed

src/examples/allocator/SimpleAllocator.sol

+127-39
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,21 @@ import { ITheCompact } from "src/interfaces/ITheCompact.sol";
99
import { ISimpleAllocator } from "src/interfaces/ISimpleAllocator.sol";
1010
import { Compact } from "src/types/EIP712Types.sol";
1111
import { ResetPeriod } from "src/lib/IdLib.sol";
12+
import { console } from "forge-std/console.sol";
1213

1314
contract SimpleAllocator is ISimpleAllocator {
15+
// abi.decode(bytes("Compact(address arbiter,address "), (bytes32))
16+
bytes32 constant COMPACT_TYPESTRING_FRAGMENT_ONE = 0x436f6d70616374286164647265737320617262697465722c6164647265737320;
17+
// abi.decode(bytes("sponsor,uint256 nonce,uint256 ex"), (bytes32))
18+
bytes32 constant COMPACT_TYPESTRING_FRAGMENT_TWO = 0x73706f6e736f722c75696e74323536206e6f6e63652c75696e74323536206578;
19+
// abi.decode(bytes("pires,uint256 id,uint256 amount)"), (bytes32))
20+
bytes32 constant COMPACT_TYPESTRING_FRAGMENT_THREE = 0x70697265732c75696e743235362069642c75696e7432353620616d6f756e7429;
21+
// uint200(abi.decode(bytes(",Witness witness)Witness("), (bytes25)))
22+
uint200 constant WITNESS_TYPESTRING = 0x2C5769746E657373207769746E657373295769746E65737328;
23+
24+
// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)")
25+
bytes32 constant COMPACT_TYPEHASH = 0xcdca950b17b5efc016b74b912d8527dfba5e404a688cbc3dab16cb943287fec2;
26+
1427
address public immutable COMPACT_CONTRACT;
1528
address public immutable ARBITER;
1629
uint256 public immutable MIN_WITHDRAWAL_DELAY;
@@ -36,51 +49,15 @@ contract SimpleAllocator is ISimpleAllocator {
3649

3750
/// @inheritdoc ISimpleAllocator
3851
function lock(Compact calldata compact_) external {
39-
// Check msg.sender is sponsor
40-
if (msg.sender != compact_.sponsor) {
41-
revert InvalidCaller(msg.sender, compact_.sponsor);
42-
}
43-
bytes32 tokenHash = _getTokenHash(compact_.id, msg.sender);
44-
// Check no lock is already active for this sponsor
45-
if (_claim[tokenHash] > block.timestamp && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))) {
46-
revert ClaimActive(compact_.sponsor);
47-
}
48-
// Check arbiter is valid
49-
if (compact_.arbiter != ARBITER) {
50-
revert InvalidArbiter(compact_.arbiter);
51-
}
52-
// Check expiration is not too soon or too late
53-
if (compact_.expires < block.timestamp + MIN_WITHDRAWAL_DELAY || compact_.expires > block.timestamp + MAX_WITHDRAWAL_DELAY) {
54-
revert InvalidExpiration(compact_.expires);
55-
}
56-
// Check expiration is not longer then the tokens forced withdrawal time
57-
(,, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id);
58-
if (compact_.expires > block.timestamp + _resetPeriodToSeconds(resetPeriod)) {
59-
revert ForceWithdrawalAvailable(compact_.expires, block.timestamp + _resetPeriodToSeconds(resetPeriod));
60-
}
61-
// Check expiration is not past an active force withdrawal
62-
(, uint256 forcedWithdrawalExpiration) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id);
63-
if (forcedWithdrawalExpiration != 0 && forcedWithdrawalExpiration < compact_.expires) {
64-
revert ForceWithdrawalAvailable(compact_.expires, forcedWithdrawalExpiration);
65-
}
66-
// Check nonce is not yet consumed
67-
if (ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(compact_.nonce, address(this))) {
68-
revert NonceAlreadyConsumed(compact_.nonce);
69-
}
70-
71-
uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, compact_.id);
72-
// Check balance is enough
73-
if (balance < compact_.amount) {
74-
revert InsufficientBalance(msg.sender, compact_.id, balance, compact_.amount);
75-
}
52+
bytes32 tokenHash = _checkAllocation(compact_);
7653

7754
bytes32 digest = keccak256(
7855
abi.encodePacked(
7956
bytes2(0x1901),
8057
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(),
8158
keccak256(
8259
abi.encode(
83-
keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"),
60+
COMPACT_TYPEHASH,
8461
compact_.arbiter,
8562
compact_.sponsor,
8663
compact_.nonce,
@@ -100,6 +77,53 @@ contract SimpleAllocator is ISimpleAllocator {
10077
emit Locked(compact_.sponsor, compact_.id, compact_.amount, compact_.expires);
10178
}
10279

80+
/// @inheritdoc ISimpleAllocator
81+
function lockWithWitness(Compact calldata compact_, bytes32 typestringHash_, bytes32 witnessHash_) external {
82+
bytes32 tokenHash = _checkAllocation(compact_);
83+
84+
console.log("claimHash SimpleAllocator");
85+
// console.logBytes32(claimHash);
86+
console.log("arbiter SimpleAllocator");
87+
console.logAddress(compact_.arbiter);
88+
console.log("sponsor SimpleAllocator");
89+
console.logAddress(compact_.sponsor);
90+
console.log("nonce SimpleAllocator");
91+
console.logUint(compact_.nonce);
92+
console.log("expires SimpleAllocator");
93+
console.logUint(compact_.expires);
94+
console.log("id SimpleAllocator");
95+
console.logUint(compact_.id);
96+
console.log("amount SimpleAllocator");
97+
console.logUint(compact_.amount);
98+
bytes32 digest = keccak256(
99+
abi.encodePacked(
100+
bytes2(0x1901),
101+
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(),
102+
keccak256(
103+
abi.encode(
104+
typestringHash_, // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)")
105+
compact_.arbiter,
106+
compact_.sponsor,
107+
compact_.nonce,
108+
compact_.expires,
109+
compact_.id,
110+
compact_.amount,
111+
witnessHash_
112+
)
113+
)
114+
)
115+
);
116+
console.log("digest SimpleAllocator");
117+
console.logBytes32(digest);
118+
119+
_claim[tokenHash] = compact_.expires;
120+
_amount[tokenHash] = compact_.amount;
121+
_nonce[tokenHash] = compact_.nonce;
122+
_sponsor[digest] = tokenHash;
123+
124+
emit Locked(compact_.sponsor, compact_.id, compact_.amount, compact_.expires);
125+
}
126+
103127
/// @inheritdoc IAllocator
104128
function attest(address operator_, address from_, address, uint256 id_, uint256 amount_) external view returns (bytes4) {
105129
if (msg.sender != COMPACT_CONTRACT) {
@@ -161,7 +185,7 @@ contract SimpleAllocator is ISimpleAllocator {
161185
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(),
162186
keccak256(
163187
abi.encode(
164-
keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"),
188+
COMPACT_TYPEHASH,
165189
compact_.arbiter,
166190
compact_.sponsor,
167191
compact_.nonce,
@@ -177,10 +201,74 @@ contract SimpleAllocator is ISimpleAllocator {
177201
return (active, active ? expires : 0);
178202
}
179203

204+
/// @dev example of a witness type string input:
205+
/// "uint256 witnessArgument"
206+
/// @dev full typestring:
207+
/// Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)
208+
function getTypestringHashForWitness(string calldata witness_) external pure returns (bytes32 typestringHash_) {
209+
assembly {
210+
let memoryOffset := mload(0x40)
211+
mstore(memoryOffset, COMPACT_TYPESTRING_FRAGMENT_ONE)
212+
mstore(add(memoryOffset, 0x20), COMPACT_TYPESTRING_FRAGMENT_TWO)
213+
mstore(add(memoryOffset, 0x40), COMPACT_TYPESTRING_FRAGMENT_THREE)
214+
mstore(add(memoryOffset, sub(0x60, 0x01)), shl(56, WITNESS_TYPESTRING))
215+
let witnessPointer := add(memoryOffset, add(sub(0x60, 0x01), 0x19))
216+
calldatacopy(witnessPointer, witness_.offset, witness_.length)
217+
let witnessEnd := add(witnessPointer, witness_.length)
218+
mstore8(witnessEnd, 0x29)
219+
typestringHash_ := keccak256(memoryOffset, sub(add(witnessEnd, 0x01), memoryOffset))
220+
221+
mstore(0x40, add(or(witnessEnd, 0x1f), 0x20))
222+
}
223+
return typestringHash_;
224+
}
225+
180226
function _getTokenHash(uint256 id_, address sponsor_) internal pure returns (bytes32) {
181227
return keccak256(abi.encode(id_, sponsor_));
182228
}
183229

230+
function _checkAllocation(Compact calldata compact_) internal view returns (bytes32) {
231+
// Check msg.sender is sponsor
232+
if (msg.sender != compact_.sponsor) {
233+
revert InvalidCaller(msg.sender, compact_.sponsor);
234+
}
235+
bytes32 tokenHash = _getTokenHash(compact_.id, msg.sender);
236+
// Check no lock is already active for this sponsor
237+
if (_claim[tokenHash] > block.timestamp && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))) {
238+
revert ClaimActive(compact_.sponsor);
239+
}
240+
// Check arbiter is valid
241+
if (compact_.arbiter != ARBITER) {
242+
revert InvalidArbiter(compact_.arbiter);
243+
}
244+
// Check expiration is not too soon or too late
245+
if (compact_.expires < block.timestamp + MIN_WITHDRAWAL_DELAY || compact_.expires > block.timestamp + MAX_WITHDRAWAL_DELAY) {
246+
revert InvalidExpiration(compact_.expires);
247+
}
248+
// Check expiration is not longer then the tokens forced withdrawal time
249+
(,, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id);
250+
if (compact_.expires > block.timestamp + _resetPeriodToSeconds(resetPeriod)) {
251+
revert ForceWithdrawalAvailable(compact_.expires, block.timestamp + _resetPeriodToSeconds(resetPeriod));
252+
}
253+
// Check expiration is not past an active force withdrawal
254+
(, uint256 forcedWithdrawalExpiration) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id);
255+
if (forcedWithdrawalExpiration != 0 && forcedWithdrawalExpiration < compact_.expires) {
256+
revert ForceWithdrawalAvailable(compact_.expires, forcedWithdrawalExpiration);
257+
}
258+
// Check nonce is not yet consumed
259+
if (ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(compact_.nonce, address(this))) {
260+
revert NonceAlreadyConsumed(compact_.nonce);
261+
}
262+
263+
uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, compact_.id);
264+
// Check balance is enough
265+
if (balance < compact_.amount) {
266+
revert InsufficientBalance(msg.sender, compact_.id, balance, compact_.amount);
267+
}
268+
269+
return tokenHash;
270+
}
271+
184272
/// @dev copied from IdLib.sol
185273
function _resetPeriodToSeconds(ResetPeriod resetPeriod_) internal pure returns (uint256 duration) {
186274
assembly ("memory-safe") {

src/interfaces/ISimpleAllocator.sol

+10
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ interface ISimpleAllocator is IAllocator {
4343
/// @param compact_ The compact that contains the data about the lock
4444
function lock(Compact calldata compact_) external;
4545

46+
/// @notice Locks the tokens of an id for a claim with a witness
47+
/// @dev Locks all tokens of a sponsor for an id with a witness
48+
/// @dev example for the typeHash:
49+
/// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)")
50+
///
51+
/// @param compact_ The compact that contains the data about the lock
52+
/// @param typeHash_ The type hash of the full compact, including the witness
53+
/// @param witnessHash_ The witness hash of the witness
54+
function lockWithWitness(Compact calldata compact_, bytes32 typeHash_,bytes32 witnessHash_) external;
55+
4656
/// @notice Checks if the tokens of a sponsor for an id are locked
4757
/// @param id_ The id of the token
4858
/// @param sponsor_ The address of the sponsor

test/TheCompact.t.sol

+64
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ pragma solidity ^0.8.13;
44
import { Test, console } from "forge-std/Test.sol";
55
import { TheCompact } from "../src/TheCompact.sol";
66
import { ServerAllocator } from "../src/examples/allocator/ServerAllocator.sol";
7+
import { SimpleAllocator } from "../src/examples/allocator/SimpleAllocator.sol";
78
import { MockERC20 } from "../lib/solady/test/utils/mocks/MockERC20.sol";
89
import { Compact, BatchCompact, Segment } from "../src/types/EIP712Types.sol";
910
import { ResetPeriod } from "../src/types/ResetPeriod.sol";
1011
import { Scope } from "../src/types/Scope.sol";
1112
import { CompactCategory } from "../src/types/CompactCategory.sol";
1213
import { ISignatureTransfer } from "permit2/src/interfaces/ISignatureTransfer.sol";
14+
import { ISimpleAllocator } from "../src/interfaces/ISimpleAllocator.sol";
1315

1416
import { HashLib } from "../src/lib/HashLib.sol";
1517

@@ -1140,6 +1142,68 @@ contract TheCompactTest is Test {
11401142
assertEq(theCompact.balanceOf(claimant, id), amount);
11411143
}
11421144

1145+
function test_claim_viaSimpleAllocator() public {
1146+
ResetPeriod resetPeriod = ResetPeriod.TenMinutes;
1147+
Scope scope = Scope.Multichain;
1148+
uint256 amount = 1e18;
1149+
uint256 nonce = 0;
1150+
uint256 expires = block.timestamp + 10;
1151+
address claimant = 0x1111111111111111111111111111111111111111;
1152+
address arbiter = 0x2222222222222222222222222222222222222222;
1153+
1154+
// Contract registers as an allocator in the Compact contract on deployment
1155+
SimpleAllocator simpleAllocator = new SimpleAllocator(address(theCompact), arbiter, 5, 100);
1156+
1157+
vm.prank(swapper);
1158+
uint256 id = theCompact.deposit{ value: amount }(address(simpleAllocator), resetPeriod, scope, swapper);
1159+
assertEq(theCompact.balanceOf(swapper, id), amount);
1160+
1161+
bytes32 claimHash = keccak256(abi.encode(keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"), arbiter, swapper, nonce, expires, id, amount));
1162+
1163+
bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash));
1164+
1165+
(bytes32 r_sponsor, bytes32 vs_sponsor) = vm.signCompact(swapperPrivateKey, digest);
1166+
bytes memory sponsorSignature = abi.encodePacked(r_sponsor, vs_sponsor);
1167+
1168+
console.log("claimHash TheCompact test");
1169+
console.logBytes32(claimHash);
1170+
console.log("arbiter SimpleAllocator");
1171+
console.logAddress(arbiter);
1172+
console.log("sponsor SimpleAllocator");
1173+
console.logAddress(swapper);
1174+
console.log("nonce SimpleAllocator");
1175+
console.logUint(nonce);
1176+
console.log("expires SimpleAllocator");
1177+
console.logUint(expires);
1178+
console.log("id SimpleAllocator");
1179+
console.logUint(id);
1180+
console.log("amount SimpleAllocator");
1181+
console.logUint(amount);
1182+
console.log("digest TheCompact test");
1183+
console.logBytes32(digest);
1184+
1185+
// Lock tokens
1186+
vm.prank(swapper);
1187+
vm.expectEmit(true, true, false, true);
1188+
emit ISimpleAllocator.Locked(swapper, id, amount, expires);
1189+
simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: swapper, nonce: nonce, id: id, expires: expires, amount: amount }));
1190+
1191+
// Empty allocator signature, because the onchain allocator does not require a signature, only a lock
1192+
bytes memory allocatorSignature = "";
1193+
1194+
BasicClaim memory claim = BasicClaim(allocatorSignature, sponsorSignature, swapper, nonce, expires, id, amount, claimant, amount);
1195+
1196+
vm.prank(arbiter);
1197+
bool status = theCompact.claim(claim);
1198+
vm.snapshotGasLastCall("claim");
1199+
assert(status);
1200+
1201+
assertEq(address(theCompact).balance, amount);
1202+
assertEq(claimant.balance, 0);
1203+
assertEq(theCompact.balanceOf(swapper, id), 0);
1204+
assertEq(theCompact.balanceOf(claimant, id), amount);
1205+
}
1206+
11431207
function test_registerAndClaim() public {
11441208
ResetPeriod resetPeriod = ResetPeriod.TenMinutes;
11451209
Scope scope = Scope.Multichain;

0 commit comments

Comments
 (0)