Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/SpendPermissionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ contract SpendPermissionManager is EIP712 {
/// @notice MagicSpend singleton (https://github.com/coinbase/magic-spend).
address public immutable MAGIC_SPEND;

uint256 public immutable POST_OP_GAS = 0;

/// @notice Spend permission is approved.
mapping(bytes32 hash => bool approved) internal _isApproved;

Expand Down Expand Up @@ -468,6 +470,85 @@ contract SpendPermissionManager is EIP712 {
_transferFrom(spendPermission.token, spendPermission.account, spendPermission.spender, value);
}

function validatePaymasterUserOp(bytes calldata userOp, bytes32 userOpHash, uint256 maxCost) external returns (bytes memory context, uint256 validationData) {
(address sender, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas) = (address(0), 0, 0);
(SpendPermission memory spendPermission, uint48 start, uint48 end, uint160 value) = abi.decode(userOp[20:], (SpendPermission, uint48, uint48, uint160));

// check userOp sender is spender
if (sender != spendPermission.spender) revert();

// check spend permission token is native
if (spendPermission.token != NATIVE_TOKEN) revert();

// check start and end are valid period range
if (start < spendPermission.start) revert();
if (start - spendPermission.start % spendPermission.period != 0) revert();
if (start + spendPermission.period > spendPermission.end) {
if (end != spendPermission.end) revert();
} else if (start + spendPermission.period != end) revert();

// check value is gte max gas cost
if (value < maxCost) revert();

// check value is non-zero
if (value == 0) revert ZeroValue();

// check spend permission is approved and not revoked
if (!isValid(spendPermission)) revert UnauthorizedSpendPermission();

// check start and end are gte last updated period
bytes32 permissionHash = getHash(spendPermission);
PeriodSpend memory currentPeriod = _lastUpdatedPeriod[permissionHash];
if (currentPeriod.start < start) {
currentPeriod = PeriodSpend(start, end, 0);
} else if (currentPeriod.start != start) revert();

uint256 totalSpend = value + uint256(currentPeriod.spend);

// check total spend value does not overflow max value
if (totalSpend > type(uint160).max) revert SpendValueOverflow(totalSpend);

// check total spend value does not exceed spend permission
if (totalSpend > spendPermission.allowance) {
revert ExceededSpendPermission(totalSpend, spendPermission.allowance);
}

// update total spend for current period and emit event for incremental spend
currentPeriod.spend = uint160(totalSpend);
_lastUpdatedPeriod[permissionHash] = currentPeriod;
emit SpendPermissionUsed(
permissionHash,
spendPermission.account,
spendPermission.spender,
spendPermission.token,
PeriodSpend(currentPeriod.start, currentPeriod.end, uint160(value))
);

// pull tokens from account
_expectedReceiveAmount = value;
_execute({account: spendPermission.account, target: address(this), value: value, data: hex""});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to access storage on root account to validate permission manager is an owner to authenticate this call, which can only be allowed if the paymaster is staked via STO-033. However, this means that any account can damage the paymaster's reputation by creating malicious accounts that revert unexpectedly post-simulation.

_expectedReceiveAmount = 0;

// forward remainder value to spender if nonzero
SafeTransferLib.safeTransferETH(payable(spendPermission.spender), value - maxCost);

// return postOp context and valid time range
context = abi.encode(maxCost, maxFeePerGas, maxPriorityFeePerGas, spendPermission.spender);
validationData = uint256(start) << 48 | end;
return (context, validationData);
}

function postOp(uint8, bytes calldata context, uint256 actualCost) external {
(uint256 maxCost, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, address spender) = abi.decode(context, (uint256, uint256, uint256, address));
uint256 gasPrice = maxFeePerGas < maxPriorityFeePerGas + block.basefee ? maxFeePerGas : maxPriorityFeePerGas + block.basefee;
uint256 totalCost = actualCost + POST_OP_GAS * gasPrice;

// send context.maxCost - gasUsed to context.spender
if (maxCost - totalCost > 0) {
SafeTransferLib.safeTransferETH(payable(spender), maxCost - totalCost);
}
}

/// @notice Get if a spend permission is approved.
///
/// @param spendPermission Details of the spend permission.
Expand Down