Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permit3 with 6492 and hooks #68

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions interfaces/IWalletPermit3Utility.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title IWalletPermit3Utility
/// @notice Interface for wallet permit3 utility functionality
interface IWalletPermit3Utility {
/// @notice Sends native tokens (ETH) from the account to Permit3
/// @dev Implementation must handle native token spending logic
/// @param account Address of the account to spend from
/// @param value Amount of native tokens to spend
function spendNativeToken(address account, uint256 value) external;
}
78 changes: 78 additions & 0 deletions src/CoinbaseSmartWalletHooks.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {MagicSpend} from "magic-spend/MagicSpend.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {CoinbaseSmartWallet} from "smart-wallet/CoinbaseSmartWallet.sol";

import {IHooks} from "./HooksForwarder.sol";
import {Permit3} from "./Permit3.sol";
import {SpendPermission} from "./SpendPermission.sol";

contract CoinbaseSmartWalletHooks is IHooks {
Permit3 immutable PERMIT3;
address immutable MAGIC_SPEND;

error SpendTokenWithdrawAssetMismatch(address spendToken, address withdrawAsset);
error SpendValueWithdrawAmountMismatch(uint256 spendValue, uint256 withdrawAmount);
error InvalidWithdrawRequestNonce(uint128 noncePostfix, uint128 permissionHashPostfix);

constructor(address permit3, address magicSpend) {
PERMIT3 = Permit3(payable(permit3));
MAGIC_SPEND = magicSpend;
}

function preSpend(SpendPermission calldata spendPermission, uint256 value, bytes calldata hookData) external {
if (msg.sender != address(PERMIT3.HOOKS_FORWARDER())) revert();

// withdraw from magic spend if hookData present
if (hookData.length > 0) {
MagicSpend.WithdrawRequest memory withdrawRequest = abi.decode(hookData, (MagicSpend.WithdrawRequest));

// check spend token and withdraw asset are the same
if (
!(spendPermission.token == PERMIT3.NATIVE_TOKEN() && withdrawRequest.asset == address(0))
&& spendPermission.token != withdrawRequest.asset
) {
revert SpendTokenWithdrawAssetMismatch(spendPermission.token, withdrawRequest.asset);
}

// check spend value is not less than withdraw request amount
if (withdrawRequest.amount > value) {
revert SpendValueWithdrawAmountMismatch(value, withdrawRequest.amount);
}

// check withdraw request nonce postfix matches spend permission hash postfix.
bytes32 permissionHash = PERMIT3.getHash(spendPermission);
if (uint128(withdrawRequest.nonce) != uint128(uint256(permissionHash))) {
revert InvalidWithdrawRequestNonce(uint128(withdrawRequest.nonce), uint128(uint256(permissionHash)));
}

_execute({
account: spendPermission.account,
target: MAGIC_SPEND,
value: 0,
data: abi.encodeWithSelector(MagicSpend.withdraw.selector, withdrawRequest)
});
}

if (spendPermission.token == PERMIT3.NATIVE_TOKEN()) {
// call account to send native token to this contract
_execute({account: spendPermission.account, target: address(PERMIT3), value: value, data: hex""});
} else {
// set allowance for this contract to spend exact value on behalf of account
_execute({
account: spendPermission.account,
target: spendPermission.token,
value: 0,
data: abi.encodeWithSelector(IERC20.approve.selector, address(PERMIT3), value)
});
}
}

function postSpend(SpendPermission calldata spendPermission, uint256 value, bytes calldata hookData) external {}

function _execute(address account, address target, uint256 value, bytes memory data) internal virtual {
CoinbaseSmartWallet(payable(account)).execute({target: target, value: value, data: data});
}
}
130 changes: 130 additions & 0 deletions src/CoinbaseSmartWalletPermit3Utility.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../interfaces/IWalletPermit3Utility.sol";
import "./Permit3.sol";
import {console2} from "forge-std/console2.sol";

import {PublicERC6492Validator} from "./PublicERC6492Validator.sol";
import {IERC1271} from "openzeppelin-contracts/contracts/interfaces/IERC1271.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {CoinbaseSmartWallet} from "smart-wallet/CoinbaseSmartWallet.sol";

/// @title CoinbaseSmartWalletPermit3Utility
/// @notice Utility contract for handling Permit3 integrations with Coinbase Smart Wallet
contract CoinbaseSmartWalletPermit3Utility is IWalletPermit3Utility {
/// @notice The Permit3 contract instance
Permit3 public immutable permit3;

/// @notice Magic value indicating a valid signature (from IERC1271)
bytes4 constant MAGICVALUE = 0x1626ba7e;

/// @notice Magic value indicating a failed signature (from IERC1271)
bytes4 constant FAILVALUE = 0xffffffff;

/// @notice ERC-7528 native token address convention (https://eips.ethereum.org/EIPS/eip-7528).
address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

/// @notice Only allows the Permit3 contract to call the function
error OnlyPermit3();

/// @notice The account is not the current account being processed in Permit3
error InvalidAccount();

/// @notice The hash is not the current permission hash being processed in Permit3
error InvalidHash();

/// @notice The signature is not valid
error InvalidSignature();

/// @notice Constructor to set the Permit3 contract address
/// @param _permit3 Address of the Permit3 contract
constructor(Permit3 _permit3) {
permit3 = _permit3;
}

/// @notice Ensures only the Permit3 contract can call the function
modifier onlyPermit3() {
if (msg.sender != address(permit3)) revert OnlyPermit3();
_;
}

/// @notice Implementation of IERC1271 isValidSignature
/// @dev Behavior depends on whether we need to intentionally trigger a failure to invoke the ERC-6492 prepareData
/// flow.
/// If the criteria required to process the given token type is not already met, we fail intentionally.
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
console2.log("isValidSignature from utility");
console2.log("Hash passed to utility:", uint256(hash));

// Get current spend permission values from Permit3
address currentAccount = permit3.getCurrentAccount();
address currentToken = permit3.getCurrentToken();
bytes32 rawPermissionHash = permit3.getCurrentPermissionHash();

console2.log("Current permission hash from Permit3:", uint256(rawPermissionHash));
bytes32 expectedHash = CoinbaseSmartWallet(payable(currentAccount)).replaySafeHash(rawPermissionHash);
console2.log("Expected replay-safe hash:", uint256(expectedHash));

if (hash != expectedHash) revert InvalidHash();

if (currentToken == NATIVE_TOKEN) {
console2.log("Native token case");
// Native token case
// check if the utility contract is registered for the current account
if (permit3.accountToUtility(currentAccount) != address(this)) {
console2.log("Utility contract not registered - returning 0xffffffff");
return FAILVALUE;
}
} else {
console2.log("ERC20 case");
// ERC20 case
// check if the ERC20 token is approved for infinite spending
if (IERC20(currentToken).allowance(currentAccount, address(permit3)) < type(uint256).max) {
console2.log("ERC20 token not approved for infinite spending - returning 0xffffffff");
return FAILVALUE;
}
}

return CoinbaseSmartWallet(payable(permit3.getCurrentAccount())).isValidSignature(rawPermissionHash, signature);
}

/// TODO: how do we secure this? the caller will be the PublicERC6492Validator, so we can't access control
/// on sender since that contract isn't secure.
/// @notice Approves an ERC20 token for infinite spending by Permit3
/// @dev Calls execute on the CoinbaseSmartWallet to approve Permit3 for infinite spending
function approveERC20() external {
bytes memory approvalCalldata =
abi.encodeWithSelector(IERC20.approve.selector, address(permit3), type(uint256).max);

// Call execute on the CoinbaseSmartWallet
CoinbaseSmartWallet(payable(permit3.getCurrentAccount())).execute(
permit3.getCurrentToken(), // target (the ERC20 token contract)
0, // value (no ETH needed for approve)
approvalCalldata
);
}

/// TODO: how do we secure this? the caller will be the PublicERC6492Validator, so we can't access control
/// on sender since that contract isn't secure.
/// @notice Registers this utility contract with Permit3
function registerPermit3Utility() external {
console2.log("registerPermit3Utility from utility");
console2.log("Current account:", permit3.getCurrentAccount());
bytes memory registerCalldata = abi.encodeWithSelector(Permit3.registerPermit3Utility.selector, address(this));

CoinbaseSmartWallet(payable(permit3.getCurrentAccount())).execute(address(permit3), 0, registerCalldata);
}

/// @notice Sends native tokens (ETH) from the account to Permit3
/// @dev Calls execute on the CoinbaseSmartWallet to send ETH to Permit3
/// @inheritdoc IWalletPermit3Utility
function spendNativeToken(address account, uint256 value) external override onlyPermit3 {
// if (account != permit3.getCurrentAccount()) revert InvalidAccount();
CoinbaseSmartWallet(payable(account)).execute(
address(permit3), // target (the Permit3 contract)
value, // value (amount of ETH to send)
"" // data (empty since we're just sending ETH)
);
}
}
36 changes: 36 additions & 0 deletions src/HooksForwarder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol";

import {SpendPermission} from "./SpendPermission.sol";

interface IHooks {
function preSpend(SpendPermission calldata spendPermission, uint256 value, bytes calldata hookData) external;
function postSpend(SpendPermission calldata spendPermission, uint256 value, bytes calldata hookData) external;
}

/// @notice Separate calls to arbitrary hooks contracts from Permit3 for security.
/// @dev Permit3 is likely to receive infinite allowances from EOAs, so arbitrary external calls from that
/// address are vulnerable.
contract HooksForwarder {
address immutable PERMIT3;

constructor(address permit3) {
PERMIT3 = permit3;
}

function preSpend(SpendPermission calldata spendPermission, uint256 value, address hooks, bytes calldata hookData)
external
{
if (msg.sender != PERMIT3) revert();
IHooks(hooks).preSpend(spendPermission, value, hookData);
}

function postSpend(SpendPermission calldata spendPermission, uint256 value, address hooks, bytes calldata hookData)
external
{
if (msg.sender != PERMIT3) revert();
IHooks(hooks).postSpend(spendPermission, value, hookData);
}
}
Loading