diff --git a/contracts/IPAccountImpl.sol b/contracts/IPAccountImpl.sol index 249a9cb9..c49b2a98 100644 --- a/contracts/IPAccountImpl.sol +++ b/contracts/IPAccountImpl.sol @@ -5,6 +5,7 @@ import { IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol" import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { IERC6551Account } from "erc6551/interfaces/IERC6551Account.sol"; import { IERC6551Executable } from "erc6551/interfaces/IERC6551Executable.sol"; @@ -248,6 +249,35 @@ contract IPAccountImpl is ERC6551, IPAccountStorage, IIPAccount { return isValidSigner(signer, address(uint160(uint256(extraData))), context); } + /// @dev Returns whether the `signature` is valid for the `hash. + function _erc1271IsValidSignature(bytes32 hash, bytes calldata signature) internal view override returns (bool) { + uint8 v = uint8(signature[64]); + address signer; + + // Smart contract signature + if (v == 0) { + // Signer address encoded in r + signer = address(uint160(uint256(bytes32(signature[:32])))); + + // Allow recursive signature verification + if (!this.isValidSigner(signer, address(0), "") && signer != address(this)) { + return false; + } + + // Signature offset encoded in s + bytes calldata _signature = signature[uint256(bytes32(signature[32:64])):]; + + return SignatureChecker.isValidERC1271SignatureNow(signer, hash, _signature); + } + + ECDSA.RecoverError _error; + (signer, _error, ) = ECDSA.tryRecover(hash, signature); + + if (_error != ECDSA.RecoverError.NoError) return false; + + return this.isValidSigner(signer, address(0), ""); + } + /// @dev Override Solady EIP712 function and return EIP712 domain name for IPAccount. function _domainNameAndVersion() internal view override returns (string memory name, string memory version) { name = "Story Protocol IP Account"; diff --git a/test/foundry/IPAccountMetaTx.t.sol b/test/foundry/IPAccountMetaTx.t.sol index 60721a51..86d1943a 100644 --- a/test/foundry/IPAccountMetaTx.t.sol +++ b/test/foundry/IPAccountMetaTx.t.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.26; import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { ERC6551 } from "@solady/src/accounts/ERC6551.sol"; +import { ERC1271 } from "@solady/src/accounts/ERC1271.sol"; import { IIPAccount } from "../../contracts/interfaces/IIPAccount.sol"; import { MetaTx } from "../../contracts/lib/MetaTx.sol"; @@ -188,6 +190,76 @@ contract IPAccountMetaTxTest is BaseTest { assertEq(ipAccount.state(), expectedState); } + function test_IPAccount_isValidSignature() public { + uint256 tokenId = 100; + + mockNFT.mintId(owner, tokenId); + + address account = ipAssetRegistry.register(block.chainid, address(mockNFT), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 setPermissionState = keccak256( + abi.encode( + ipAccount.state(), + abi.encodeWithSignature( + "execute(address,uint256,bytes)", + address(accessController), + 0, + abi.encodeWithSignature( + "setPermission(address,address,address,bytes4,uint8)", + address(ipAccount), + address(metaTxModule), + address(module), + bytes4(0), + AccessPermission.ALLOW + ) + ) + ) + ); + bytes32 expectedState = keccak256( + abi.encode( + setPermissionState, + abi.encodeWithSignature( + "execute(address,uint256,bytes)", + address(module), + 0, + abi.encodeWithSignature("executeSuccessfully(string)", "test") + ) + ) + ); + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(accessController), + value: 0, + data: abi.encodeWithSignature( + "setPermission(address,address,address,bytes4,uint8)", + address(ipAccount), + address(metaTxModule), + address(module), + bytes4(0), + AccessPermission.ALLOW + ), + nonce: setPermissionState, + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + bytes memory signature = abi.encodePacked(r, s, v); + assertEq( + ERC6551(payable(address(ipAccount))).isValidSignature(digest, signature), + ERC1271.isValidSignature.selector + ); + } + function test_IPAccount_setPermissionWithSignatureThenCallAccessControlledModule() public { uint256 tokenId = 100;