Skip to content

Commit

Permalink
[SCW-116] Add EIP-6492 init code deployment for pre-deploy signatures…
Browse files Browse the repository at this point in the history
… (#80)

* call eip6492 signature checker

* working on e2e test...

* complete e2e test

* add test for 6492-compliant signature from already deployed account

* fix failing test

* cleanup

* add test for permitAndSpend and ability to init w spendpermission as owner

* review comments

* fix test name
  • Loading branch information
amiecorso authored Oct 16, 2024
1 parent 2aeacc3 commit f5fd5cc
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 8 deletions.
12 changes: 8 additions & 4 deletions src/SpendPermissionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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";
import {EIP712} from "solady/utils/EIP712.sol";
import {SignatureCheckerLib} from "solady/utils/SignatureCheckerLib.sol";

/// @title SpendPermissionManager
///
Expand Down Expand Up @@ -153,17 +154,20 @@ contract SpendPermissionManager is EIP712 {

/// @notice Approve a spend permission via a signature from the account.
///
/// @dev Compatible with ERC-6492 signatures (https://eips.ethereum.org/EIPS/eip-6492)
/// @dev Accounts are automatically deployed if init code present in signature.
///
/// @param spendPermission Details of the spend permission.
/// @param signature Signed approval from the user.
function permit(SpendPermission memory spendPermission, bytes memory signature) public {
// validate signature over spend permission data
// validate signature over spend permission data and optionally deploy account
if (
IERC1271(spendPermission.account).isValidSignature(getHash(spendPermission), signature)
!= IERC1271.isValidSignature.selector
!SignatureCheckerLib.isValidERC6492SignatureNowAllowSideEffects(
spendPermission.account, getHash(spendPermission), signature
)
) {
revert UnauthorizedSpendPermission();
}

_approve(spendPermission);
}

Expand Down
45 changes: 43 additions & 2 deletions test/base/SpendPermissionManagerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ import {SpendPermissionManager} from "../../src/SpendPermissionManager.sol";
import {MockSpendPermissionManager} from "../mocks/MockSpendPermissionManager.sol";
import {Base} from "./Base.sol";

import {MockCoinbaseSmartWallet} from "../mocks/MockCoinbaseSmartWallet.sol";
import {CoinbaseSmartWallet} from "smart-wallet/CoinbaseSmartWallet.sol";
import {CoinbaseSmartWalletFactory} from "smart-wallet/CoinbaseSmartWalletFactory.sol";

contract SpendPermissionManagerBase is Base {
address constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

bytes32 constant EIP6492_MAGIC_VALUE = 0x6492649264926492649264926492649264926492649264926492649264926492;
bytes32 constant CBSW_MESSAGE_TYPEHASH = keccak256("CoinbaseSmartWalletMessage(bytes32 hash)");
MockSpendPermissionManager mockSpendPermissionManager;
CoinbaseSmartWalletFactory mockCoinbaseSmartWalletFactory;

function _initializeSpendPermissionManager() internal {
_initialize(); // Base
mockSpendPermissionManager = new MockSpendPermissionManager();
mockCoinbaseSmartWalletFactory = new CoinbaseSmartWalletFactory(address(account));
}

/**
Expand All @@ -36,12 +43,46 @@ contract SpendPermissionManagerBase is Base {
uint256 ownerIndex
) internal view returns (bytes memory) {
bytes32 spendPermissionHash = mockSpendPermissionManager.getHash(spendPermission);
bytes32 replaySafeHash = account.replaySafeHash(spendPermissionHash);
bytes32 replaySafeHash =
CoinbaseSmartWallet(payable(spendPermission.account)).replaySafeHash(spendPermissionHash);
bytes memory signature = _sign(ownerPk, replaySafeHash);
bytes memory wrappedSignature = _applySignatureWrapper(ownerIndex, signature);
return wrappedSignature;
}

function _signSpendPermission6492(
SpendPermissionManager.SpendPermission memory spendPermission,
uint256 ownerPk,
uint256 ownerIndex,
bytes[] memory allInitialOwners
) internal view returns (bytes memory) {
bytes32 spendPermissionHash = mockSpendPermissionManager.getHash(spendPermission);
// construct replaySafeHash without relying on the account contract being deployed
bytes32 cbswDomainSeparator = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes("Coinbase Smart Wallet")),
keccak256(bytes("1")),
block.chainid,
spendPermission.account
)
);
bytes32 replaySafeHash = keccak256(
abi.encodePacked(
"\x19\x01", cbswDomainSeparator, keccak256(abi.encode(CBSW_MESSAGE_TYPEHASH, spendPermissionHash))
)
);
bytes memory signature = _sign(ownerPk, replaySafeHash);
bytes memory wrappedSignature = _applySignatureWrapper(ownerIndex, signature);

// wrap inner sig in 6492 format ======================
address factory = address(mockCoinbaseSmartWalletFactory);
bytes memory factoryCallData = abi.encodeWithSignature("createAccount(bytes[],uint256)", allInitialOwners, 0);
bytes memory eip6492Signature = abi.encode(factory, factoryCallData, wrappedSignature);
eip6492Signature = abi.encodePacked(eip6492Signature, EIP6492_MAGIC_VALUE);
return eip6492Signature;
}

function _safeAddUint48(uint48 a, uint48 b) internal pure returns (uint48 c) {
bool overflow = uint256(a) + uint256(b) > type(uint48).max;
return overflow ? type(uint48).max : a + b;
Expand Down
2 changes: 1 addition & 1 deletion test/src/SpendPermissions/getCurrentPeriod.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ contract GetCurrentPeriodTest is SpendPermissionManagerBase {
vm.assume(end > 0);
vm.assume(start < end);
vm.assume(period > 0);
vm.assume(period <= end - start);
vm.assume(period < end - start);
vm.assume(allowance > 0);
vm.assume(spend <= allowance);

Expand Down
83 changes: 83 additions & 0 deletions test/src/SpendPermissions/permit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.23;
import {SpendPermissionManager} from "../../../src/SpendPermissionManager.sol";

import {SpendPermissionManagerBase} from "../../base/SpendPermissionManagerBase.sol";
import {CoinbaseSmartWallet} from "smart-wallet/CoinbaseSmartWallet.sol";

contract PermitTest is SpendPermissionManagerBase {
function setUp() public {
Expand Down Expand Up @@ -157,4 +158,86 @@ contract PermitTest is SpendPermissionManagerBase {
});
mockSpendPermissionManager.permit(spendPermission, signature);
}

function test_permit_success_erc6492SignaturePreDeploy(
uint128 ownerPk,
address spender,
address token,
uint48 start,
uint48 end,
uint48 period,
uint160 allowance
) public {
vm.assume(start > 0);
vm.assume(start < end);
vm.assume(period > 0);
vm.assume(allowance > 0);
vm.assume(ownerPk != 0);
// generate the counterfactual address for the account
address ownerAddress = vm.addr(ownerPk);
bytes[] memory owners = new bytes[](1);
owners[0] = abi.encode(ownerAddress);
address counterfactualAccount = mockCoinbaseSmartWalletFactory.getAddress(owners, 0);

// create a 6492-compliant signature for the spend permission
SpendPermissionManager.SpendPermission memory spendPermission = SpendPermissionManager.SpendPermission({
account: counterfactualAccount,
spender: spender,
token: token,
start: start,
end: end,
period: period,
allowance: allowance
});
bytes memory signature = _signSpendPermission6492(spendPermission, ownerPk, 0, owners);
// verify that the account isn't deployed yet
vm.assertEq(counterfactualAccount.code.length, 0);

// submit the spend permission with the signature, see permit succeed
mockSpendPermissionManager.permit(spendPermission, signature);

// verify that the account is now deployed (has code) and that a call to isValidSignature returns true
vm.assertGt(counterfactualAccount.code.length, 0);
vm.assertTrue(mockSpendPermissionManager.isApproved(spendPermission));
}

function test_permit_success_erc6492SignatureAlreadyDeployed(
uint128 ownerPk,
address spender,
address token,
uint48 start,
uint48 end,
uint48 period,
uint160 allowance
) public {
vm.assume(start > 0);
vm.assume(start < end);
vm.assume(period > 0);
vm.assume(allowance > 0);
vm.assume(ownerPk != 0);
// generate the counterfactual address for the account
address ownerAddress = vm.addr(ownerPk);
bytes[] memory owners = new bytes[](1);
owners[0] = abi.encode(ownerAddress);
address counterfactualAccount = mockCoinbaseSmartWalletFactory.getAddress(owners, 0);
// deploy the account already
mockCoinbaseSmartWalletFactory.createAccount(owners, 0);
// create a 6492-compliant signature for the spend permission
SpendPermissionManager.SpendPermission memory spendPermission = SpendPermissionManager.SpendPermission({
account: counterfactualAccount,
spender: spender,
token: token,
start: start,
end: end,
period: period,
allowance: allowance
});
bytes memory signature = _signSpendPermission6492(spendPermission, ownerPk, 0, owners);
// verify that the account is already deployed
vm.assertGt(counterfactualAccount.code.length, 0);

// submit the spend permission with the signature, see permit succeed
mockSpendPermissionManager.permit(spendPermission, signature);
vm.assertTrue(mockSpendPermissionManager.isApproved(spendPermission));
}
}
59 changes: 58 additions & 1 deletion test/src/SpendPermissions/permitAndSpend.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ contract PermitAndSpendTest is SpendPermissionManagerBase {

function test_permitAndSpend_revert_invalidSender(
address sender,
address account,
address spender,
address recipient,
uint48 start,
Expand Down Expand Up @@ -223,4 +222,62 @@ contract PermitAndSpendTest is SpendPermissionManagerBase {
assertEq(usage.end, _safeAddUint48(start, period));
assertEq(usage.spend, spend);
}

function test_permitAndSpend_success_ether_erc6492PreDeploy(
uint128 ownerPk,
address spender,
address recipient,
uint48 start,
uint48 end,
uint48 period,
uint160 allowance,
uint160 spend
) public {
assumePayable(recipient);
vm.assume(ownerPk != 0);
vm.assume(start > 0);
vm.assume(end > 0);
vm.assume(start < end);
vm.assume(period > 0);
vm.assume(spend > 0);
vm.assume(allowance > 0);
vm.assume(allowance >= spend);
address ownerAddress = vm.addr(ownerPk);
bytes[] memory owners = new bytes[](2);
owners[0] = abi.encode(ownerAddress);
owners[1] = abi.encode(address(mockSpendPermissionManager));
address counterfactualAccount = mockCoinbaseSmartWalletFactory.getAddress(owners, 0);
vm.assume(recipient != counterfactualAccount); // otherwise balance checks can fail

// create a 6492-compliant signature for the spend permission
SpendPermissionManager.SpendPermission memory spendPermission = SpendPermissionManager.SpendPermission({
account: counterfactualAccount,
spender: spender,
token: NATIVE_TOKEN,
start: start,
end: end,
period: period,
allowance: allowance
});
vm.deal(counterfactualAccount, allowance);
vm.deal(recipient, 0);
assertEq(counterfactualAccount.balance, allowance);
assertEq(recipient.balance, 0);

bytes memory signature = _signSpendPermission6492(spendPermission, ownerPk, 0, owners);
// verify that the account isn't deployed yet
vm.assertEq(counterfactualAccount.code.length, 0);

vm.warp(start);

vm.startPrank(spender);
mockSpendPermissionManager.permitAndSpend(spendPermission, signature, recipient, spend);

assertEq(counterfactualAccount.balance, allowance - spend);
assertEq(recipient.balance, spend);
SpendPermissionManager.PeriodSpend memory usage = mockSpendPermissionManager.getCurrentPeriod(spendPermission);
assertEq(usage.start, start);
assertEq(usage.end, _safeAddUint48(start, period));
assertEq(usage.spend, spend);
}
}

0 comments on commit f5fd5cc

Please sign in to comment.