Skip to content

Commit

Permalink
qualified split claim
Browse files Browse the repository at this point in the history
  • Loading branch information
0age committed Oct 14, 2024
1 parent 0009b64 commit 6285c02
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 64 deletions.
85 changes: 67 additions & 18 deletions src/TheCompact.sol
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,14 @@ contract TheCompact is ITheCompact, ERC6909, Extsload {
return _processSplitClaim(claimPayload, _withdraw);
}

function claim(QualifiedSplitClaim calldata claimPayload) external returns (bool) {
return _processQualifiedSplitClaim(claimPayload, _release);
}

function claimAndWithdraw(QualifiedSplitClaim calldata claimPayload) external returns (bool) {
return _processQualifiedSplitClaim(claimPayload, _withdraw);
}

function claim(BatchClaim calldata claimPayload) external returns (bool) {
return _processBatchClaim(claimPayload, _release);
}
Expand Down Expand Up @@ -704,37 +712,28 @@ contract TheCompact is ITheCompact, ERC6909, Extsload {
);
}

function _processSplitClaim(
SplitClaim calldata claimPayload,
function _verifyAndProcessSplitComponents(
address sponsor,
bytes32 messageHash,
uint256 id,
uint256 allocatedAmount,
SplitComponent[] calldata claimants,
function(address, address, uint256, uint256) internal returns (bool) operation
) internal returns (bool) {
claimPayload.expires.later();

uint256 id = claimPayload.id;
address allocator = id.toRegisteredAllocatorWithConsumed(claimPayload.nonce);

bytes32 messageHash = claimPayload.toMessageHash();
bytes32 domainSeparator = _INITIAL_DOMAIN_SEPARATOR.toLatest(_INITIAL_CHAIN_ID);
messageHash.signedBy(claimPayload.sponsor, claimPayload.sponsorSignature, domainSeparator);
messageHash.signedBy(allocator, claimPayload.allocatorSignature, domainSeparator);

uint256 totalClaims = claimPayload.claimants.length;
uint256 allocatedAmount = claimPayload.allocatedAmount;
uint256 totalClaims = claimants.length;
uint256 spentAmount = 0;
uint256 errorBuffer = (totalClaims == 0).asUint256();

unchecked {
for (uint256 i = 0; i < totalClaims; ++i) {
SplitComponent calldata component = claimPayload.claimants[i];
SplitComponent calldata component = claimants[i];
uint256 amount = component.amount;

uint256 updatedSpentAmount = amount + spentAmount;
errorBuffer |= (updatedSpentAmount < spentAmount).asUint256();
spentAmount = updatedSpentAmount;

emitAndOperate(
claimPayload.sponsor, component.claimant, id, messageHash, amount, operation
);
emitAndOperate(sponsor, component.claimant, id, messageHash, amount, operation);
}
}

Expand All @@ -752,6 +751,30 @@ contract TheCompact is ITheCompact, ERC6909, Extsload {
return true;
}

function _processSplitClaim(
SplitClaim calldata claimPayload,
function(address, address, uint256, uint256) internal returns (bool) operation
) internal returns (bool) {
claimPayload.expires.later();

uint256 id = claimPayload.id;
address allocator = id.toRegisteredAllocatorWithConsumed(claimPayload.nonce);

bytes32 messageHash = claimPayload.toMessageHash();
bytes32 domainSeparator = _INITIAL_DOMAIN_SEPARATOR.toLatest(_INITIAL_CHAIN_ID);
messageHash.signedBy(claimPayload.sponsor, claimPayload.sponsorSignature, domainSeparator);
messageHash.signedBy(allocator, claimPayload.allocatorSignature, domainSeparator);

return _verifyAndProcessSplitComponents(
claimPayload.sponsor,
messageHash,
id,
claimPayload.allocatedAmount,
claimPayload.claimants,
operation
);
}

function _processQualifiedClaim(
QualifiedClaim calldata claimPayload,
function(address, address, uint256, uint256) internal returns (bool) operation
Expand All @@ -778,6 +801,32 @@ contract TheCompact is ITheCompact, ERC6909, Extsload {
);
}

function _processQualifiedSplitClaim(
QualifiedSplitClaim calldata claimPayload,
function(address, address, uint256, uint256) internal returns (bool) operation
) internal returns (bool) {
claimPayload.expires.later();

uint256 id = claimPayload.id;
address allocator = id.toRegisteredAllocatorWithConsumed(claimPayload.nonce);

(bytes32 messageHash, bytes32 qualificationMessageHash) = claimPayload.toMessageHash();
bytes32 domainSeparator = _INITIAL_DOMAIN_SEPARATOR.toLatest(_INITIAL_CHAIN_ID);
messageHash.signedBy(claimPayload.sponsor, claimPayload.sponsorSignature, domainSeparator);
qualificationMessageHash.signedBy(
allocator, claimPayload.allocatorSignature, domainSeparator
);

return _verifyAndProcessSplitComponents(
claimPayload.sponsor,
messageHash,
id,
claimPayload.allocatedAmount,
claimPayload.claimants,
operation
);
}

function _processClaimWithWitness(
ClaimWithWitness calldata claimPayload,
function(address, address, uint256, uint256) internal returns (bool) operation
Expand Down
47 changes: 29 additions & 18 deletions src/lib/HashLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,28 @@ library HashLib {
qualificationMessageHash = toQualificationMessageHash(claim, messageHash, 0);
}

function usingQualifiedSplitClaim(
function (
QualifiedClaim calldata,
bytes32,
uint256
) internal pure returns (bytes32) fnIn
)
internal
pure
returns (
function(
QualifiedSplitClaim calldata,
bytes32,
uint256
) internal pure returns (bytes32) fnOut
)
{
assembly {
fnOut := fnIn
}
}

function toQualificationMessageHash(
QualifiedClaim calldata claim,
bytes32 messageHash,
Expand Down Expand Up @@ -308,35 +330,24 @@ library HashLib {
}
}

function toMessageHash(QualifiedSplitClaim memory claim)
function toMessageHash(QualifiedSplitClaim calldata claim)
internal
view
returns (bytes32 messageHash, bytes32 qualificationMessageHash)
{
assembly ("memory-safe") {
let m := mload(0x40) // Grab the free memory pointer; memory will be left dirtied.

// TODO: calldatacopy this whole chunk at once as part of calldata implementation
let sponsor := mload(claim)
let expires := mload(add(claim, 0x20))
let nonce := mload(add(claim, 0x40))
let id := mload(add(claim, 0x60))
let allocatedAmount := mload(add(claim, 0x80))

mstore(m, COMPACT_TYPEHASH)
mstore(add(m, 0x20), sponsor)
mstore(add(m, 0x40), expires)
mstore(add(m, 0x60), nonce)
mstore(add(m, 0x80), caller()) // arbiter: msg.sender
mstore(add(m, 0xa0), id)
mstore(add(m, 0xc0), allocatedAmount)
mstore(add(m, 0x20), caller()) // arbiter: msg.sender
calldatacopy(add(m, 0x40), add(claim, 0x40), 0x60) // sponsor, nonce, expires
mstore(add(m, 0xa0), calldataload(add(claim, 0xe0)))
mstore(add(m, 0xc0), calldataload(add(claim, 0x100)))
messageHash := keccak256(m, 0xe0)
}

// TODO: optimize once we're using calldata
qualificationMessageHash = keccak256(
abi.encodePacked(claim.qualificationTypehash, messageHash, claim.qualificationPayload)
);
qualificationMessageHash =
usingQualifiedSplitClaim(toQualificationMessageHash)(claim, messageHash, 0);
}

function toMessageHash(QualifiedSplitClaimWithWitness memory claim)
Expand Down
120 changes: 92 additions & 28 deletions test/TheCompact.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
QualifiedClaim,
ClaimWithWitness,
QualifiedClaimWithWitness,
SplitClaim
SplitClaim,
QualifiedSplitClaim
} from "../src/types/Claims.sol";
import { BatchTransfer, SplitBatchTransfer, BatchClaim } from "../src/types/BatchClaims.sol";

Expand All @@ -40,40 +41,13 @@ contract TheCompactTest is Test {
address swapper;
uint256 allocatorPrivateKey;
address allocator;
address dummyOracle;
address dummyBatchOracle;
bytes32 compactEIP712DomainHash = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 permit2EIP712DomainHash =
keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)");

function setUp() public {
address deployedDummyOracle;
address deployedDummyBatchOracle;
assembly {
// deploy a contract that always returns one word of 0's followed by one word of f's
// minimal "constructor" 0x600b5981380380925939f3... (11 bytes)
// runtime code (9 bytes):
// Op Opcode Name Stack
// 60 40 PUSH1 0x40 [0x40]
// 3D RETURNDATASIZE [0, 0x40]
// 3D RETURNDATASIZE [0, 0, 0x40]
// 19 NOT [type(uint256).max, 0, 0x40]
// 60 20 PUSH1 0x20 [0x20, type(uint256).max, 0, 0x40]
// 52 MSTORE [0, 0x40] (Memory at 0x20 set to type(uint256).max)
// F3 RETURN [] (Returns 0x40 bytes from memory starting at 0x00)
mstore(0, 0x600b5981380380925939f360403d3d19602052f3)
deployedDummyOracle := create(0, 12, 20)

// and this one returns [0, 0x40, 3, 0xfff, 0xfff, 0xfff]
mstore(0, 0x600b598138038092)
mstore(0x20, 0x5939f360c03d60406020600360403d1960603d1960803d1960a05252525252f3)
deployedDummyBatchOracle := create(0, 24, 40)
}
dummyOracle = deployedDummyOracle;
dummyBatchOracle = deployedDummyBatchOracle;

address permit2Deployer = address(0x4e59b44847b379578588920cA78FbF26c0B4956C);
address deployedPermit2Deployer;
address permit2DeployerDeployer = address(0x3fAB184622Dc19b6109349B94811493BF2a45362);
Expand Down Expand Up @@ -1162,6 +1136,96 @@ contract TheCompactTest is Test {
assertEq(theCompact.balanceOf(recipientTwo, id), amountTwo);
}

function test_qualifiedSplitClaim() public {
ResetPeriod resetPeriod = ResetPeriod.TenMinutes;
Scope scope = Scope.Multichain;
uint256 amount = 1e18;
uint256 nonce = 0;
uint256 expires = block.timestamp + 1000;
address arbiter = 0x2222222222222222222222222222222222222222;
address recipientOne = 0x1111111111111111111111111111111111111111;
address recipientTwo = 0x3333333333333333333333333333333333333333;
uint256 amountOne = 4e17;
uint256 amountTwo = 6e17;

vm.prank(allocator);
theCompact.__register(allocator, "");

vm.prank(swapper);
uint256 id = theCompact.deposit{ value: amount }(allocator, 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 qualificationTypehash =
keccak256("ExampleQualifiedClaim(bytes32 claimHash,uint256 qualifiedClaimArgument)");

uint256 qualifiedClaimArgument = 123;
bytes memory qualificationPayload = abi.encode(qualifiedClaimArgument);

bytes32 qualifiedClaimHash =
keccak256(abi.encode(qualificationTypehash, claimHash, qualifiedClaimArgument));

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

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

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

(r, vs) = vm.signCompact(allocatorPrivateKey, digest);
bytes memory allocatorSignature = abi.encodePacked(r, vs);

SplitComponent memory splitOne =
SplitComponent({ claimant: recipientOne, amount: amountOne });

SplitComponent memory splitTwo =
SplitComponent({ claimant: recipientTwo, amount: amountTwo });

SplitComponent[] memory recipients = new SplitComponent[](2);
recipients[0] = splitOne;
recipients[1] = splitTwo;

QualifiedSplitClaim memory claim = QualifiedSplitClaim(
allocatorSignature,
sponsorSignature,
swapper,
nonce,
expires,
qualificationTypehash,
qualificationPayload,
id,
amount,
recipients
);

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

assertEq(address(theCompact).balance, amount);
assertEq(recipientOne.balance, 0);
assertEq(recipientTwo.balance, 0);
assertEq(theCompact.balanceOf(swapper, id), 0);
assertEq(theCompact.balanceOf(recipientOne, id), amountOne);
assertEq(theCompact.balanceOf(recipientTwo, id), amountTwo);
}

function test_batchClaim() public {
uint256 amount = 1e18;
uint256 anotherAmount = 1e18;
Expand Down

0 comments on commit 6285c02

Please sign in to comment.