diff --git a/src/PermitEnforcer.sol b/src/PermitEnforcer.sol new file mode 100644 index 0000000..2d19cea --- /dev/null +++ b/src/PermitEnforcer.sol @@ -0,0 +1,66 @@ +pragma solidity ^0.8.23; + +import {ISignatureEnforcer} from "./interfaces/ISignatureEnforcer.sol"; +import {MessageHashUtils} from "src/DelegationManagerBatch.sol"; + +struct PermitTerms { + address owner; + address spender; + uint256 maximum; +} + +struct PermitArgs { + uint256 value; + uint256 nonce; + uint256 deadline; +} + +interface IUSDC { + function balanceOf(address account) external view returns (uint256); + function mint(address to, uint256 amount) external; + function configureMinter(address minter, uint256 minterAllowedAmount) external; + function masterMinter() external view returns (address); + function DOMAIN_SEPARATOR() external view returns (bytes32); + function permit(address owner, address spender, uint256 value, uint256 deadline, bytes memory signature) external; + function nonces(address owner) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} +// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") + +bytes32 constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + +contract PermitEnforcer is ISignatureEnforcer { + IUSDC immutable usdc; + + constructor(IUSDC _usdc) { + usdc = _usdc; + } + + function checkSignatureVerification( // temporary name + bytes calldata _terms, + bytes calldata _args, + address _requestor, // address of the contract that called initial DELEGATOR.isValidSignature(bytes32 hash, bytes calldata signature) + bytes32 _messageHash, // hash that was given on initial DELEGATOR.isValidSignature(bytes32 hash, bytes calldata signature) + bytes32 _delegateionHash, + address _delegator, + address _redeemer + ) external view returns (bool) { + PermitTerms memory pt = parseTerms(_terms); + PermitArgs memory pa = parseArgs(_args); + require(pt.maximum >= pa.value, "value exceeds maximum"); + bytes32 generatedTypedDataHash = MessageHashUtils.toTypedDataHash( + usdc.DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, pt.owner, pt.spender, pa.value, pa.nonce, pa.deadline)) + ); + require(generatedTypedDataHash == _messageHash, "message hash does not match args/terms"); + return _requestor == address(usdc); + } + + function parseTerms(bytes calldata _terms) internal pure returns (PermitTerms memory) { + return abi.decode(_terms, (PermitTerms)); + } + + function parseArgs(bytes calldata _args) internal pure returns (PermitArgs memory) { + return abi.decode(_args, (PermitArgs)); + } +} diff --git a/src/SignatureRequestorEnforcer.sol b/src/SignatureRequestorEnforcer.sol deleted file mode 100644 index 80258d4..0000000 --- a/src/SignatureRequestorEnforcer.sol +++ /dev/null @@ -1,18 +0,0 @@ -pragma solidity ^0.8.23; - -import {ISignatureEnforcer} from "./interfaces/ISignatureEnforcer.sol"; - -contract SignatureRequestorEnforcer is ISignatureEnforcer { - function checkSignatureVerification( // temporary name - bytes calldata _terms, - bytes calldata _args, - address _requestor, // address of the contract that called initial DELEGATOR.isValidSignature(bytes32 hash, bytes calldata signature) - bytes32 _messageHash, // hash that was given on initial DELEGATOR.isValidSignature(bytes32 hash, bytes calldata signature) - bytes32 _delegateionHash, - address _delegator, - address _redeemer - ) external view returns (bool) { - address allowedRequestor = abi.decode(_terms, (address)); - return _requestor == allowedRequestor; - } -} diff --git a/test/DMYi.t.sol b/test/DMYi.t.sol index 0e82db8..c2107b1 100644 --- a/test/DMYi.t.sol +++ b/test/DMYi.t.sol @@ -15,7 +15,7 @@ import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; import "forge-std/console.sol"; import {ECDSA} from "solady/utils/ECDSA.sol"; import {ExecMode, ExecLib} from "kernel/src/utils/ExecLib.sol"; -import {SignatureRequestorEnforcer} from "src/SignatureRequestorEnforcer.sol"; +import {IUSDC, PERMIT_TYPEHASH, PermitEnforcer, PermitTerms, PermitArgs} from "src/PermitEnforcer.sol"; contract MockCallee { mapping(address caller => uint256) public barz; @@ -39,20 +39,6 @@ contract MockERC20 is ERC20 { } } -// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") -bytes32 constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; - -interface IUSDC { - function balanceOf(address account) external view returns (uint256); - function mint(address to, uint256 amount) external; - function configureMinter(address minter, uint256 minterAllowedAmount) external; - function masterMinter() external view returns (address); - function DOMAIN_SEPARATOR() external view returns (bytes32); - function permit(address owner, address spender, uint256 value, uint256 deadline, bytes memory signature) external; - function nonces(address owner) external view returns (uint256); - function allowance(address owner, address spender) external view returns (uint256); -} - contract DMTest is Test { DelegationManagerBatch public dm; SubAccountFactory public factory; @@ -97,9 +83,9 @@ contract DMTest is Test { } function testSessionKeyUseOnlyUSDCPermit() external { - SignatureRequestorEnforcer signatureRequestorEnforcer = new SignatureRequestorEnforcer(); //// USDC contract address on mainnet IUSDC usdc = IUSDC(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + PermitEnforcer signatureRequestorEnforcer = new PermitEnforcer(usdc); // spoof .configureMinter() call with the master minter account vm.prank(usdc.masterMinter()); @@ -109,6 +95,8 @@ contract DMTest is Test { usdc.mint(address(this), 1000e6); vm.stopPrank(); + address spender = makeAddr("Spender"); + Delegation[] memory d = new Delegation[](2); Caveat[] memory e = new Caveat[](0); d[1] = Delegation({ @@ -121,8 +109,17 @@ contract DMTest is Test { }); console.log("Master : ", master); // delegate usdc permit signature to session + uint256 allowance = 1000; Caveat[] memory c = new Caveat[](1); - c[0] = Caveat({enforcer: address(signatureRequestorEnforcer), terms: abi.encode(address(usdc)), args: hex""}); + PermitTerms memory pt = PermitTerms({owner: address(subAccount), spender: spender, maximum: 10000}); + PermitArgs memory pa = PermitArgs({value: 1000, nonce: usdc.nonces(owner), deadline: block.timestamp + 1000}); + + bytes32 permitHash = MessageHashUtils.toTypedDataHash( + usdc.DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, pt.owner, pt.spender, pa.value, pa.nonce, pa.deadline)) + ); + + c[0] = Caveat({enforcer: address(signatureRequestorEnforcer), terms: abi.encode(pt), args: abi.encode(pa)}); d[0] = Delegation({ delegate: session, delegator: master, @@ -133,18 +130,69 @@ contract DMTest is Test { }); d[0].signature = signDelegation(d[0], masterKey); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKey, permitHash); + usdc.permit( + address(subAccount), + spender, + allowance, + block.timestamp + 1000, + abi.encode(d, bytes.concat(r, s, bytes1(v))) + ); + + assertEq(usdc.allowance(address(subAccount), spender), allowance); + } + + function testSessionKeyUseOnlyUSDCPermitExceedsMaximum() external { + //// USDC contract address on mainnet + IUSDC usdc = IUSDC(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + PermitEnforcer signatureRequestorEnforcer = new PermitEnforcer(usdc); + + // spoof .configureMinter() call with the master minter account + vm.prank(usdc.masterMinter()); + // allow this test contract to mint USDC + usdc.configureMinter(address(this), type(uint256).max); + // mint $1000 USDC to the test contract (or an external user) + usdc.mint(address(this), 1000e6); + vm.stopPrank(); + address spender = makeAddr("Spender"); + + Delegation[] memory d = new Delegation[](2); + Caveat[] memory e = new Caveat[](0); + d[1] = Delegation({ + delegate: master, + delegator: address(subAccount), + authority: ROOT_AUTHORITY, + caveats: e, + salt: 0, + signature: hex"" + }); + console.log("Master : ", master); + // delegate usdc permit signature to session uint256 allowance = 1000; + Caveat[] memory c = new Caveat[](1); + PermitTerms memory pt = PermitTerms({owner: address(subAccount), spender: spender, maximum: 10000}); + PermitArgs memory pa = + PermitArgs({value: pt.maximum + 1, nonce: usdc.nonces(owner), deadline: block.timestamp + 1000}); + bytes32 permitHash = MessageHashUtils.toTypedDataHash( usdc.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - PERMIT_TYPEHASH, address(subAccount), spender, allowance, usdc.nonces(owner), block.timestamp + 1000 - ) - ) + keccak256(abi.encode(PERMIT_TYPEHASH, pt.owner, pt.spender, pa.value, pa.nonce, pa.deadline)) ); + c[0] = Caveat({enforcer: address(signatureRequestorEnforcer), terms: abi.encode(pt), args: abi.encode(pa)}); + d[0] = Delegation({ + delegate: session, + delegator: master, + authority: dm.getDelegationHash(d[1]), + caveats: c, + salt: 0, + signature: hex"" + }); + d[0].signature = signDelegation(d[0], masterKey); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sessionKey, permitHash); + vm.expectRevert(); usdc.permit( address(subAccount), spender, @@ -153,7 +201,7 @@ contract DMTest is Test { abi.encode(d, bytes.concat(r, s, bytes1(v))) ); - assertEq(usdc.allowance(address(subAccount), spender), allowance); + assertEq(usdc.allowance(address(subAccount), spender), 0); } function signDelegation(Delegation memory delegation, uint256 key) internal returns (bytes memory) {