Skip to content

Commit

Permalink
Lock compact with witness
Browse files Browse the repository at this point in the history
  • Loading branch information
vimageDE committed Dec 20, 2024
1 parent 47b5b28 commit 49ff3c7
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 39 deletions.
166 changes: 127 additions & 39 deletions src/examples/allocator/SimpleAllocator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,21 @@ import { ITheCompact } from "src/interfaces/ITheCompact.sol";
import { ISimpleAllocator } from "src/interfaces/ISimpleAllocator.sol";
import { Compact } from "src/types/EIP712Types.sol";
import { ResetPeriod } from "src/lib/IdLib.sol";
import { console } from "forge-std/console.sol";

contract SimpleAllocator is ISimpleAllocator {
// abi.decode(bytes("Compact(address arbiter,address "), (bytes32))
bytes32 constant COMPACT_TYPESTRING_FRAGMENT_ONE = 0x436f6d70616374286164647265737320617262697465722c6164647265737320;
// abi.decode(bytes("sponsor,uint256 nonce,uint256 ex"), (bytes32))
bytes32 constant COMPACT_TYPESTRING_FRAGMENT_TWO = 0x73706f6e736f722c75696e74323536206e6f6e63652c75696e74323536206578;
// abi.decode(bytes("pires,uint256 id,uint256 amount)"), (bytes32))
bytes32 constant COMPACT_TYPESTRING_FRAGMENT_THREE = 0x70697265732c75696e743235362069642c75696e7432353620616d6f756e7429;
// uint200(abi.decode(bytes(",Witness witness)Witness("), (bytes25)))
uint200 constant WITNESS_TYPESTRING = 0x2C5769746E657373207769746E657373295769746E65737328;

// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)")
bytes32 constant COMPACT_TYPEHASH = 0xcdca950b17b5efc016b74b912d8527dfba5e404a688cbc3dab16cb943287fec2;

address public immutable COMPACT_CONTRACT;
address public immutable ARBITER;
uint256 public immutable MIN_WITHDRAWAL_DELAY;
Expand All @@ -36,51 +49,15 @@ contract SimpleAllocator is ISimpleAllocator {

/// @inheritdoc ISimpleAllocator
function lock(Compact calldata compact_) external {
// 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 arbiter is valid
if (compact_.arbiter != ARBITER) {
revert InvalidArbiter(compact_.arbiter);
}
// 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);
}
// Check expiration is not longer then the tokens forced withdrawal time
(,, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id);
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);
}
bytes32 tokenHash = _checkAllocation(compact_);

bytes32 digest = keccak256(
abi.encodePacked(
bytes2(0x1901),
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"),
COMPACT_TYPEHASH,
compact_.arbiter,
compact_.sponsor,
compact_.nonce,
Expand All @@ -100,6 +77,53 @@ contract SimpleAllocator is ISimpleAllocator {
emit Locked(compact_.sponsor, compact_.id, compact_.amount, compact_.expires);
}

/// @inheritdoc ISimpleAllocator
function lockWithWitness(Compact calldata compact_, bytes32 typestringHash_, bytes32 witnessHash_) external {
bytes32 tokenHash = _checkAllocation(compact_);

console.log("claimHash SimpleAllocator");
// console.logBytes32(claimHash);
console.log("arbiter SimpleAllocator");
console.logAddress(compact_.arbiter);
console.log("sponsor SimpleAllocator");
console.logAddress(compact_.sponsor);
console.log("nonce SimpleAllocator");
console.logUint(compact_.nonce);
console.log("expires SimpleAllocator");
console.logUint(compact_.expires);
console.log("id SimpleAllocator");
console.logUint(compact_.id);
console.log("amount SimpleAllocator");
console.logUint(compact_.amount);
bytes32 digest = keccak256(
abi.encodePacked(
bytes2(0x1901),
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
typestringHash_, // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)")
compact_.arbiter,
compact_.sponsor,
compact_.nonce,
compact_.expires,
compact_.id,
compact_.amount,
witnessHash_
)
)
)
);
console.log("digest SimpleAllocator");
console.logBytes32(digest);

_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) {
Expand Down Expand Up @@ -161,7 +185,7 @@ contract SimpleAllocator is ISimpleAllocator {
ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)"),
COMPACT_TYPEHASH,
compact_.arbiter,
compact_.sponsor,
compact_.nonce,
Expand All @@ -177,10 +201,74 @@ contract SimpleAllocator is ISimpleAllocator {
return (active, active ? expires : 0);
}

/// @dev example of a witness type string input:
/// "uint256 witnessArgument"
/// @dev full typestring:
/// Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)
function getTypestringHashForWitness(string calldata witness_) external pure returns (bytes32 typestringHash_) {
assembly {
let memoryOffset := mload(0x40)
mstore(memoryOffset, COMPACT_TYPESTRING_FRAGMENT_ONE)
mstore(add(memoryOffset, 0x20), COMPACT_TYPESTRING_FRAGMENT_TWO)
mstore(add(memoryOffset, 0x40), COMPACT_TYPESTRING_FRAGMENT_THREE)
mstore(add(memoryOffset, sub(0x60, 0x01)), shl(56, WITNESS_TYPESTRING))
let witnessPointer := add(memoryOffset, add(sub(0x60, 0x01), 0x19))
calldatacopy(witnessPointer, witness_.offset, witness_.length)
let witnessEnd := add(witnessPointer, witness_.length)
mstore8(witnessEnd, 0x29)
typestringHash_ := keccak256(memoryOffset, sub(add(witnessEnd, 0x01), memoryOffset))

mstore(0x40, add(or(witnessEnd, 0x1f), 0x20))
}
return typestringHash_;
}

function _getTokenHash(uint256 id_, address sponsor_) internal pure returns (bytes32) {
return keccak256(abi.encode(id_, sponsor_));
}

function _checkAllocation(Compact calldata 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 arbiter is valid
if (compact_.arbiter != ARBITER) {
revert InvalidArbiter(compact_.arbiter);
}
// 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);
}
// Check expiration is not longer then the tokens forced withdrawal time
(,, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id);
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) {
assembly ("memory-safe") {
Expand Down
10 changes: 10 additions & 0 deletions src/interfaces/ISimpleAllocator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ interface ISimpleAllocator is IAllocator {
/// @param compact_ The compact that contains the data about the lock
function lock(Compact calldata compact_) external;

/// @notice Locks the tokens of an id for a claim with a witness
/// @dev Locks all tokens of a sponsor for an id with a witness
/// @dev example for the typeHash:
/// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)")
///
/// @param compact_ The compact that contains the data about the lock
/// @param typeHash_ The type hash of the full compact, including the witness
/// @param witnessHash_ The witness hash of the witness
function lockWithWitness(Compact calldata compact_, bytes32 typeHash_,bytes32 witnessHash_) 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
Expand Down
64 changes: 64 additions & 0 deletions test/TheCompact.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ pragma solidity ^0.8.13;
import { Test, console } from "forge-std/Test.sol";
import { TheCompact } from "../src/TheCompact.sol";
import { ServerAllocator } from "../src/examples/allocator/ServerAllocator.sol";
import { SimpleAllocator } from "../src/examples/allocator/SimpleAllocator.sol";
import { MockERC20 } from "../lib/solady/test/utils/mocks/MockERC20.sol";
import { Compact, BatchCompact, Segment } from "../src/types/EIP712Types.sol";
import { ResetPeriod } from "../src/types/ResetPeriod.sol";
import { Scope } from "../src/types/Scope.sol";
import { CompactCategory } from "../src/types/CompactCategory.sol";
import { ISignatureTransfer } from "permit2/src/interfaces/ISignatureTransfer.sol";
import { ISimpleAllocator } from "../src/interfaces/ISimpleAllocator.sol";

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

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

function test_claim_viaSimpleAllocator() public {
ResetPeriod resetPeriod = ResetPeriod.TenMinutes;
Scope scope = Scope.Multichain;
uint256 amount = 1e18;
uint256 nonce = 0;
uint256 expires = block.timestamp + 10;
address claimant = 0x1111111111111111111111111111111111111111;
address arbiter = 0x2222222222222222222222222222222222222222;

// Contract registers as an allocator in the Compact contract on deployment
SimpleAllocator simpleAllocator = new SimpleAllocator(address(theCompact), arbiter, 5, 100);

vm.prank(swapper);
uint256 id = theCompact.deposit{ value: amount }(address(simpleAllocator), resetPeriod, scope, swapper);
assertEq(theCompact.balanceOf(swapper, id), amount);

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));

bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash));

(bytes32 r_sponsor, bytes32 vs_sponsor) = vm.signCompact(swapperPrivateKey, digest);
bytes memory sponsorSignature = abi.encodePacked(r_sponsor, vs_sponsor);

console.log("claimHash TheCompact test");
console.logBytes32(claimHash);
console.log("arbiter SimpleAllocator");
console.logAddress(arbiter);
console.log("sponsor SimpleAllocator");
console.logAddress(swapper);
console.log("nonce SimpleAllocator");
console.logUint(nonce);
console.log("expires SimpleAllocator");
console.logUint(expires);
console.log("id SimpleAllocator");
console.logUint(id);
console.log("amount SimpleAllocator");
console.logUint(amount);
console.log("digest TheCompact test");
console.logBytes32(digest);

// Lock tokens
vm.prank(swapper);
vm.expectEmit(true, true, false, true);
emit ISimpleAllocator.Locked(swapper, id, amount, expires);
simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: swapper, nonce: nonce, id: id, expires: expires, amount: amount }));

// Empty allocator signature, because the onchain allocator does not require a signature, only a lock
bytes memory allocatorSignature = "";

BasicClaim memory claim = BasicClaim(allocatorSignature, sponsorSignature, swapper, nonce, expires, id, amount, claimant, amount);

vm.prank(arbiter);
bool status = theCompact.claim(claim);
vm.snapshotGasLastCall("claim");
assert(status);

assertEq(address(theCompact).balance, amount);
assertEq(claimant.balance, 0);
assertEq(theCompact.balanceOf(swapper, id), 0);
assertEq(theCompact.balanceOf(claimant, id), amount);
}

function test_registerAndClaim() public {
ResetPeriod resetPeriod = ResetPeriod.TenMinutes;
Scope scope = Scope.Multichain;
Expand Down

0 comments on commit 49ff3c7

Please sign in to comment.