diff --git a/src/WeirollWallet.sol b/src/WeirollWallet.sol index f3f6c24..c7d69d5 100644 --- a/src/WeirollWallet.sol +++ b/src/WeirollWallet.sol @@ -3,12 +3,20 @@ pragma solidity ^0.8.0; import { VM } from "lib/weiroll/contracts/VM.sol"; import { Clone } from "lib/clones-with-immutable-args/src/Clone.sol"; +import { IERC1271 } from "src/interfaces/IERC1271.sol"; +import { ECDSA } from "lib/solady/src/utils/ECDSA.sol"; /// @title WeirollWallet /// @author Royco /// @notice WeirollWallet implementation contract. /// @notice Implements a simple smart contract wallet that can execute Weiroll VM commands -contract WeirollWallet is Clone, VM { +contract WeirollWallet is IERC1271, Clone, VM { + // Returned to indicate a valid ERC1271 signature + bytes4 internal constant ERC1271_MAGIC_VALUE = 0x1626ba7e; // bytes4(keccak256("isValidSignature(bytes32,bytes)") + + // Returned to indicate an invalid ERC1271 signature + bytes4 internal constant INVALID_SIGNATURE = 0x00000000; + /// @notice Let the Weiroll Wallet receive ether directly if needed receive() external payable { } /// @notice Also allow a fallback with no logic if erroneous data is provided @@ -139,4 +147,17 @@ contract WeirollWallet is Clone, VM { emit WeirollWalletExecutedManually(); return result; } + + /// @notice Check if signature is valid for this contract + /// @dev Signature is valid if the signer is the owner of this wallet + /// @param digest Hash of the message to validate the signature against + /// @param signature Signature produced for the provided digest + function isValidSignature(bytes32 digest, bytes calldata signature) external view returns (bytes4) { + // Modify digest to include the chainId and address of this wallet to prevent replay attacks + bytes32 walletSpecificDigest = keccak256(abi.encode(digest, block.chainid, address(this))); + // Check if signature was produced by owner of this wallet + // Don't revert on failure. Simply return INVALID_SIGNATURE. + if (ECDSA.tryRecover(walletSpecificDigest, signature) == owner()) return ERC1271_MAGIC_VALUE; + else return INVALID_SIGNATURE; + } } diff --git a/src/interfaces/IERC1271.sol b/src/interfaces/IERC1271.sol new file mode 100644 index 0000000..1150c04 --- /dev/null +++ b/src/interfaces/IERC1271.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +/// @title IERC1271 +/// @notice Interface defined by EIP-1271 +/// @dev Interface for verifying contract account signatures +interface IERC1271 { + /// @notice Returns whether the provided signature is valid for the provided data + /// @dev Returns 0x1626ba7e (magic value) when function passes. + /// @param digest Hash of the message to validate the signature against + /// @param signature Signature produced for the provided digest + function isValidSignature(bytes32 digest, bytes memory signature) external view returns (bytes4); +} diff --git a/test/WeirollWallet.t.sol b/test/WeirollWallet.t.sol index 29267ec..322896a 100644 --- a/test/WeirollWallet.t.sol +++ b/test/WeirollWallet.t.sol @@ -5,6 +5,7 @@ import { WeirollWallet } from "src/WeirollWallet.sol"; import { ClonesWithImmutableArgs } from "lib/clones-with-immutable-args/src/ClonesWithImmutableArgs.sol"; import { Test } from "forge-std/Test.sol"; import { VM } from "lib/weiroll/contracts/VM.sol"; +import { ECDSA } from "lib/solady/src/utils/ECDSA.sol"; contract WeirollWalletTest is Test { using ClonesWithImmutableArgs for address; @@ -39,8 +40,9 @@ contract WeirollWalletTest is Test { public returns (WeirollWallet) { - return - WeirollWallet(payable(WEIROLL_WALLET_IMPLEMENTATION.clone(abi.encodePacked(_owner, _recipeMarketHub, _amount, _lockedUntil, _isForfeitable, _marketHash)))); + return WeirollWallet( + payable(WEIROLL_WALLET_IMPLEMENTATION.clone(abi.encodePacked(_owner, _recipeMarketHub, _amount, _lockedUntil, _isForfeitable, _marketHash))) + ); } function testWalletInitialization() public view { @@ -152,6 +154,43 @@ contract WeirollWalletTest is Test { // Check if the balance increased assertEq(address(wallet).balance, initialBalance + 1 ether); } + + function testIsValidSignatureFuzz(uint256 ownerPrivateKey, bytes32 digest, bytes memory signature) public { + // Avoid using zero private key and have it be in the correct range for secp256k1 SKs + vm.assume( + ownerPrivateKey != 0 && ownerPrivateKey < 115_792_089_237_316_195_423_570_985_008_687_907_852_837_564_279_074_904_382_605_163_141_518_161_494_337 + ); + // Initialize owner address from the private key + owner = vm.addr(ownerPrivateKey); + + // Create a new wallet with the fuzzed owner + wallet = createWallet(owner, address(mockRecipeMarketHub), AMOUNT, lockedUntil, true, marketHash); + + // Modify digest for replay protection + bytes32 walletSpecificDigest = keccak256(abi.encode(digest, block.chainid, address(wallet))); + + // Create a valid signature from the owner + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, walletSpecificDigest); + bytes memory validSignature = abi.encodePacked(r, s, v); + + // Test valid signature + bytes4 validResult = wallet.isValidSignature(digest, validSignature); + bytes4 expectedMagicValue = 0x1626ba7e; // ERC1271_MAGIC_VALUE + assertEq(validResult, expectedMagicValue); + + // Skip invalid signature lengths + if (signature.length != 64 && signature.length != 65) { + return; + } + + // Test invalid signature + bytes4 invalidResult = wallet.isValidSignature(digest, signature); + assertEq(invalidResult, 0x00000000); // INVALID_SIGNATURE + + // Ensure the owner address is correctly recovered + address recoveredSigner = ECDSA.recover(walletSpecificDigest, validSignature); + assertEq(recoveredSigner, owner); + } } contract MockRecipeMarketHub {