Skip to content

Commit

Permalink
Merge pull request #118 from roycoprotocol/feat/erc1271-weiroll-wallet
Browse files Browse the repository at this point in the history
Make WeirollWallet ERC1271 Compliant
  • Loading branch information
corddry authored Oct 16, 2024
2 parents 1d827ac + 4f59204 commit d0375c3
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 3 deletions.
23 changes: 22 additions & 1 deletion src/WeirollWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
13 changes: 13 additions & 0 deletions src/interfaces/IERC1271.sol
Original file line number Diff line number Diff line change
@@ -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);
}
43 changes: 41 additions & 2 deletions test/WeirollWallet.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit d0375c3

Please sign in to comment.