Skip to content

Commit

Permalink
Add merkle proof hash generation
Browse files Browse the repository at this point in the history
  • Loading branch information
ilikesymmetry committed Oct 9, 2024
1 parent 9670fb0 commit 24cd653
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 16 deletions.
50 changes: 40 additions & 10 deletions src/SpendPermissions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ contract SpendPermissions {
uint160 allowance;
}

/// @notice A signed permit to approve a recurring allowance.
struct SignedPermit {
/// @dev Recurring allowance parameters.
RecurringAllowance recurringAllowance;
/// @dev User signature to validate via EIP-1271.
bytes signature;
/// @dev Optional Merkle proof from batch signing of many recurring allowances.
bytes32[] proof;
}

/// @notice Cycle parameters and spend usage.
struct CycleUsage {
/// @dev Start time of the cycle (unix seconds).
Expand Down Expand Up @@ -150,26 +160,25 @@ contract SpendPermissions {
/// @param recipient Address to withdraw tokens to.
/// @param value Amount of token attempting to withdraw (wei).
function withdraw(bytes calldata context, address recipient, uint160 value) external {
(RecurringAllowance memory recurringAllowance, bytes memory signature) =
abi.decode(context, (RecurringAllowance, bytes));
permit(recurringAllowance, signature);
withdraw(recurringAllowance, recipient, value);
SignedPermit memory signedPermit = abi.decode(context, (SignedPermit));
permit(signedPermit);
withdraw(signedPermit.recurringAllowance, recipient, value);
}

/// @notice Approve a recurring allowance via a signature from the account.
///
/// @param recurringAllowance Details of the recurring allowance.
/// @param signature Signed hash of the recurring allowance data.
function permit(RecurringAllowance memory recurringAllowance, bytes memory signature) public {
/// @param signedPermit user-approved recurring allowance with optional Merkle proof if batch signed.
function permit(SignedPermit memory signedPermit) public {
// validate signature over recurring allowance data
if (
IERC1271(recurringAllowance.account).isValidSignature(getHash(recurringAllowance), signature)
!= IERC1271.isValidSignature.selector
IERC1271(signedPermit.recurringAllowance.account).isValidSignature(
getRoot(signedPermit.recurringAllowance, signedPermit.proof), signedPermit.signature
) != IERC1271.isValidSignature.selector
) {
revert UnauthorizedRecurringAllowance();
}

_approve(recurringAllowance);
_approve(signedPermit.recurringAllowance);
}

/// @notice Withdraw tokens using a recurring allowance.
Expand Down Expand Up @@ -197,6 +206,27 @@ contract SpendPermissions {
return keccak256(abi.encode(recurringAllowance, block.chainid, address(this)));
}

/// @notice Hash a RecurringAllowance struct for signing.
///
/// @dev Prevent phishing permits by making the hash incompatible with EIP-191/712.
/// @dev Include chainId and contract address in hash for cross-chain and cross-contract replay protection.
///
/// @param recurringAllowance Details of the recurring allowance.
/// @param proof Merkle branch matching this recurring allowance to reconstruct root.
///
/// @return root Merkle root of a batch of recurring allowances.
function getRoot(RecurringAllowance memory recurringAllowance, bytes32[] memory proof)
public
view
returns (bytes32 root)
{
root = getHash(recurringAllowance);
uint256 len = proof.length;
for (uint256 i = 0; i < len; i++) {
root = keccak256(abi.encode(root, proof[i]));
}
}

/// @notice Return if recurring allowance is authorized i.e. approved and not revoked.
///
/// @param recurringAllowance Details of the recurring allowance.
Expand Down
10 changes: 6 additions & 4 deletions src/SpendPermissionsPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ contract SpendPermissionsPaymaster is SpendPermissions, Ownable2Step, IPaymaster
requireSender(entryPoint())
returns (bytes memory postOpContext, uint256 validationData)
{
(RecurringAllowance memory recurringAllowance, bytes memory signature, uint256 withdrawAmount) =
abi.decode(userOp.paymasterAndData[20:], (RecurringAllowance, bytes, uint256));
// todo allow passing signature for first-time allowance use
(SignedPermit memory signedPermit, uint256 withdrawAmount) =
abi.decode(userOp.paymasterAndData[20:], (SignedPermit, uint256));
RecurringAllowance memory recurringAllowance = signedPermit.recurringAllowance;

// require withdraw amount not less than max gas cost
if (withdrawAmount < maxGasCost) {
Expand All @@ -74,8 +76,8 @@ contract SpendPermissionsPaymaster is SpendPermissions, Ownable2Step, IPaymaster
}

// apply permit if signature length non-zero
if (signature.length > 0) {
permit(recurringAllowance, signature);
if (signedPermit.signature.length > 0) {
permit(signedPermit);
}

// check total spend value does not overflow max value
Expand Down
1 change: 1 addition & 0 deletions test/src/SpendPermissions/Debug.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ contract DebugTest is Test, Base {
}

function test_withdraw(address recipient) public {
vm.assume(recipient != address(spendPermissions));
SpendPermissions.RecurringAllowance memory recurringAllowance = _createRecurringAllowance();

vm.prank(address(account));
Expand Down
9 changes: 7 additions & 2 deletions test/src/SpendPermissionsPaymaster/Debug.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ contract DebugTest is Test, Base {

vm.assertEq(account.isValidSignature(hash, signature), IERC1271.isValidSignature.selector);

bytes memory paymasterData = abi.encode(recurringAllowance, signature, allowance);
SpendPermissions.SignedPermit memory signedPermit =
SpendPermissions.SignedPermit(recurringAllowance, signature, new bytes32[](0));
bytes memory paymasterData = abi.encode(signedPermit, allowance);

userOp.paymasterAndData = abi.encodePacked(address(spendPermissions), paymasterData);

Expand Down Expand Up @@ -79,7 +81,10 @@ contract DebugTest is Test, Base {

vm.assertEq(account.isValidSignature(hash, signature), IERC1271.isValidSignature.selector);

spendPermissions.permit(recurringAllowance, signature);
SpendPermissions.SignedPermit memory signedPermit =
SpendPermissions.SignedPermit(recurringAllowance, signature, new bytes32[](0));

spendPermissions.permit(signedPermit);

vm.assertTrue(spendPermissions.isAuthorized(recurringAllowance));

Expand Down

0 comments on commit 24cd653

Please sign in to comment.