From 7824b00e81217a42a2a1a45174974e780e22a82d Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Wed, 12 Feb 2025 23:11:15 -0800 Subject: [PATCH 1/3] Add EOA-supported SpendPermissionManager --- src/EOASpendPermissionManager.sol | 29 +++++++++++++++++++++++++++++ src/SpendPermissionManager.sol | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/EOASpendPermissionManager.sol diff --git a/src/EOASpendPermissionManager.sol b/src/EOASpendPermissionManager.sol new file mode 100644 index 0000000..623439b --- /dev/null +++ b/src/EOASpendPermissionManager.sol @@ -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); + } + } +} diff --git a/src/SpendPermissionManager.sol b/src/SpendPermissionManager.sol index bbd6d9a..95c3d71 100644 --- a/src/SpendPermissionManager.sol +++ b/src/SpendPermissionManager.sol @@ -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; From ea7fdbbd8d00fdf54ec8842a3fb28f3ad2ba9676 Mon Sep 17 00:00:00 2001 From: Amie Corso Date: Fri, 21 Mar 2025 16:11:36 -0700 Subject: [PATCH 2/3] add draft ERC7579SpendPermissionManager --- src/ERC7579SpendPermissionManager.sol | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/ERC7579SpendPermissionManager.sol diff --git a/src/ERC7579SpendPermissionManager.sol b/src/ERC7579SpendPermissionManager.sol new file mode 100644 index 0000000..56d527a --- /dev/null +++ b/src/ERC7579SpendPermissionManager.sol @@ -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); + } + } +} From d4ddee01d54a7b3ce59440c0f433cca073ab38b2 Mon Sep 17 00:00:00 2001 From: Amie Corso Date: Fri, 21 Mar 2025 16:20:05 -0700 Subject: [PATCH 3/3] add draft spend permission manager interface --- interfaces/ISpendPermissionManager.sol | 148 +++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 interfaces/ISpendPermissionManager.sol diff --git a/interfaces/ISpendPermissionManager.sol b/interfaces/ISpendPermissionManager.sol new file mode 100644 index 0000000..0dc292f --- /dev/null +++ b/interfaces/ISpendPermissionManager.sol @@ -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 + ); +}