Skip to content
Draft
Show file tree
Hide file tree
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
148 changes: 148 additions & 0 deletions interfaces/ISpendPermissionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {MagicSpend} from "magic-spend/MagicSpend.sol";

/**
* @title ISpendPermissionManager
* @notice Interface for spend permission managers that handle token spending permissions
* @dev All spend permission managers must implement these core functions
*/
interface ISpendPermissionManager {
/// @notice A spend permission for an external entity to be able to spend an account's tokens.
struct SpendPermission {
/// @dev Smart account this spend permission is valid for.
address account;
/// @dev Entity that can spend `account`'s tokens.
address spender;
/// @dev Token address (ERC-7528 native token or ERC-20 contract).
address token;
/// @dev Maximum allowed value to spend within each `period`.
uint160 allowance;
/// @dev Time duration for resetting used `allowance` on a recurring basis (seconds).
uint48 period;
/// @dev Timestamp this spend permission is valid starting at (inclusive, unix seconds).
uint48 start;
/// @dev Timestamp this spend permission is valid until (exclusive, unix seconds).
uint48 end;
/// @dev Arbitrary data to differentiate unique spend permissions with otherwise identical fields.
uint256 salt;
/// @dev Arbitrary data to attach to a spend permission which may be consumed by the `spender`.
bytes extraData;
}

/// @notice Period parameters and spend usage.
struct PeriodSpend {
/// @dev Timstamp this period starts at (inclusive, unix seconds).
uint48 start;
/// @dev Timestamp this period ends before (exclusive, unix seconds).
uint48 end;
/// @dev Accumulated spend amount for the period.
uint160 spend;
}

/// @notice Approve a spend permission via a direct call from the account.
/// @param spendPermission Details of the spend permission.
/// @return approved True if spend permission is approved and not revoked.
function approve(SpendPermission calldata spendPermission) external returns (bool);

/// @notice Approve a spend permission via a signature from the account.
/// @param spendPermission Details of the spend permission.
/// @param signature Signed approval from the user.
/// @return approved True if spend permission is approved and not revoked.
function approveWithSignature(SpendPermission calldata spendPermission, bytes calldata signature)
external
returns (bool);

/// @notice Revoke a spend permission to disable its use indefinitely.
/// @param spendPermission Details of the spend permission.
function revoke(SpendPermission calldata spendPermission) external;

/// @notice Revoke a spend permission to disable its use indefinitely.
/// @param spendPermission Details of the spend permission.
function revokeAsSpender(SpendPermission calldata spendPermission) external;

/// @notice Spend tokens using a spend permission, transferring them from `account` to `spender`.
/// @param spendPermission Details of the spend permission.
/// @param value Amount of token attempting to spend.
function spend(SpendPermission memory spendPermission, uint160 value) external;

/// @notice Spend tokens using a spend permission and atomically call MagicSpend to fund the account.
/// @param spendPermission Details of the spend permission.
/// @param value Amount of token attempting to spend.
/// @param withdrawRequest Request to withdraw tokens from MagicSpend into the account.
function spendWithWithdraw(
SpendPermission memory spendPermission,
uint160 value,
MagicSpend.WithdrawRequest memory withdrawRequest
) external;

/// @notice Get if a spend permission is approved.
/// @param spendPermission Details of the spend permission.
/// @return approved True if spend permission is approved.
function isApproved(SpendPermission memory spendPermission) external view returns (bool);

/// @notice Get if a spend permission is revoked.
/// @param spendPermission Details of the spend permission.
/// @return revoked True if spend permission is revoked.
function isRevoked(SpendPermission memory spendPermission) external view returns (bool);

/// @notice Get if spend permission is approved and not revoked.
/// @param spendPermission Details of the spend permission.
/// @return valid True if spend permission is approved and not revoked.
function isValid(SpendPermission memory spendPermission) external view returns (bool);

/// @notice Get last updated period for a spend permission.
/// @param spendPermission Details of the spend permission.
/// @return lastUpdatedPeriod Last updated period for the spend permission.
function getLastUpdatedPeriod(SpendPermission memory spendPermission) external view returns (PeriodSpend memory);

/// @notice Get start, end, and spend of the current period.
/// @param spendPermission Details of the spend permission
/// @return currentPeriod Currently active period with cumulative spend (struct)
function getCurrentPeriod(SpendPermission memory spendPermission) external view returns (PeriodSpend memory);

/// @notice Hash a SpendPermission struct for signing in accordance with EIP-712
/// @param spendPermission Details of the spend permission.
/// @return hash Hash of the spend permission.
function getHash(SpendPermission memory spendPermission) external view returns (bytes32);

/// @notice ERC-7528 native token address convention
function NATIVE_TOKEN() external view returns (address);

/// @notice MagicSpend singleton address
function MAGIC_SPEND() external view returns (address);

/// @notice PublicERC6492Validator contract address
function PUBLIC_ERC6492_VALIDATOR() external view returns (address);

/**
* @dev Emitted when a spend permission is approved
* @param hash Unique hash representing the spend permission
* @param spendPermission Details of the spend permission
*/
event SpendPermissionApproved(bytes32 indexed hash, SpendPermission spendPermission);

/**
* @dev Emitted when a spend permission is revoked
* @param hash Unique hash representing the spend permission
* @param spendPermission Details of the spend permission
*/
event SpendPermissionRevoked(bytes32 indexed hash, SpendPermission spendPermission);

/**
* @dev Emitted when a spend permission is used
* @param hash Unique hash representing the spend permission
* @param account Account that had its tokens spent via the spend permission
* @param spender Entity that spent `account`'s tokens
* @param token Address of token spent via the spend permission
* @param periodSpend Start and end of the current period with the new incremental spend
*/
event SpendPermissionUsed(
bytes32 indexed hash,
address indexed account,
address indexed spender,
address token,
PeriodSpend periodSpend
);
}
29 changes: 29 additions & 0 deletions src/EOASpendPermissionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import {PublicERC6492Validator} from "./PublicERC6492Validator.sol";
import {SpendPermissionManager} from "./SpendPermissionManager.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

contract EOASpendPermissionManager is SpendPermissionManager {
using SafeERC20 for IERC20;

error NativeTokenNotSupported();

constructor(PublicERC6492Validator publicERC6492Validator, address magicSpend)
SpendPermissionManager(publicERC6492Validator, magicSpend)
{}

/// @inheritdoc SpendPermissionManager
/// @dev Because EOAs cannot be called into, native token not supported and assumes existing infinite ERC20
/// allowance
function _transferFrom(address token, address account, address recipient, uint256 value) internal override {
if (token == NATIVE_TOKEN) {
revert NativeTokenNotSupported();
} else {
// use allowance to transfer from account to recipient, which will revert if transfer fails
IERC20(token).safeTransferFrom(account, recipient, value);
}
}
}
54 changes: 54 additions & 0 deletions src/ERC7579SpendPermissionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import {PublicERC6492Validator} from "./PublicERC6492Validator.sol";
import {SpendPermissionManager} from "./SpendPermissionManager.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

interface IERC7579Execution {
function execute(bytes32 mode, bytes calldata executionCalldata) external;
}

/**
* @title ERC7579SpendPermissionManager
* @notice A spend permission manager that supports ERC7579 modular smart accounts
* @dev Implements token transfers through ERC7579's execution interface
*/
contract ERC7579SpendPermissionManager is SpendPermissionManager {
using SafeERC20 for IERC20;

// Constant for ERC7579 execution mode (single call, revert on failure)
bytes32 public constant SINGLE_CALL_MODE = bytes32(uint256(0x00));

constructor(PublicERC6492Validator publicERC6492Validator, address magicSpend)
SpendPermissionManager(publicERC6492Validator, magicSpend)
{}

/// @inheritdoc SpendPermissionManager
function _transferFrom(address token, address account, address recipient, uint256 value) internal override {
if (token == NATIVE_TOKEN) {
// For native token transfers, we need to encode a call to transfer ETH
bytes memory callData = abi.encodePacked(recipient);
bytes memory executionCalldata = abi.encodePacked(account, value, callData);

// Execute the transfer through the ERC7579 account
IERC7579Execution(account).execute(SINGLE_CALL_MODE, executionCalldata);
} else {
// For ERC20 transfers, we encode a call to transferFrom
bytes memory callData = abi.encodeWithSelector(
IERC20.transferFrom.selector,
account,
recipient,
value
);

// Pack the execution data according to ERC7579 spec:
// target (token address), value (0), and callData
bytes memory executionCalldata = abi.encodePacked(token, uint256(0), callData);

// Execute the transfer through the ERC7579 account
IERC7579Execution(account).execute(SINGLE_CALL_MODE, executionCalldata);
}
}
}
2 changes: 1 addition & 1 deletion src/SpendPermissionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ contract SpendPermissionManager is EIP712 {
/// @param account Address to transfer from.
/// @param recipient Address to transfer to.
/// @param value Amount to transfer.
function _transferFrom(address token, address account, address recipient, uint256 value) internal {
function _transferFrom(address token, address account, address recipient, uint256 value) internal virtual {
if (token == NATIVE_TOKEN) {
// set flag to allow contract to receive expected amount of native token
_expectedReceiveAmount = value;
Expand Down