Skip to content

[SC-1] Implement FishnetWallet Core Contract (execute + EIP712 permit) #31

Description

@iamyxsh

[SC-1] Implement FishnetWallet Core Contract (execute + EIP712 permit verification)

Labels: smart-contract, solidity, priority:high, week-3-4
Assignee: Yash


Context

Per Section 10 of the Source of Truth, the FishnetWallet is a minimal Solidity contract with a single execute function gated by an EIP712 permit signature. The current contract (contracts/src/FishnetWallet.sol) is an empty 4-line skeleton. This issue covers the core contract logic.


1. FishnetPermit Struct (Section 4.3)

struct FishnetPermit {
    address wallet;       // smart wallet address
    uint64  chainId;      // chain the tx executes on
    uint256 nonce;        // replay protection (incremental)
    uint48  expiry;       // unix timestamp, short-lived (~5 min)
    address target;       // contract being called
    uint256 value;        // ETH/native value sent
    bytes32 calldataHash; // keccak256(calldata)
    bytes32 policyHash;   // hash of active policy version
}

2. State Variables

  • address public owner — wallet owner (deployer)
  • address public fishnetSigner — authorized Fishnet signer address
  • mapping(uint256 => bool) public usedNonces — replay protection
  • bool public paused — emergency pause flag

3. EIP712 Domain & Typehash

  • PERMIT_TYPEHASH:
    bytes32 constant PERMIT_TYPEHASH = keccak256(
        "FishnetPermit(address wallet,uint64 chainId,uint256 nonce,"
        "uint48 expiry,address target,uint256 value,"
        "bytes32 calldataHash,bytes32 policyHash)"
    );
  • EIP712 Domain:
    • name = "Fishnet"
    • version = "1"
    • chainId = block.chainid
    • verifyingContract = address(this)
  • DOMAIN_SEPARATOR — computed in constructor, cached

4. execute() Function

The core function — gated by permit signature:

function execute(
    address target,
    uint256 value,
    bytes calldata data,
    FishnetPermit calldata permit,
    bytes calldata signature
) external whenNotPaused
  • Validation checks (in order):
    1. require(block.timestamp <= permit.expiry, "permit expired")
    2. require(!usedNonces[permit.nonce], "nonce already used")
    3. require(permit.target == target, "target mismatch")
    4. require(permit.calldataHash == keccak256(data), "calldata mismatch")
    5. require(permit.wallet == address(this), "wallet mismatch")
    6. require(_verifySignature(permit, signature), "invalid signature")
  • Mark nonce as used: usedNonces[permit.nonce] = true
  • Execute call: (bool success, ) = target.call{value: value}(data)
  • Require success: require(success, "execution failed")
  • Emit event: ActionExecuted(target, value, permit.nonce, permit.policyHash)

5. _verifySignature() Internal Function

  • Compute struct hash: keccak256(abi.encode(PERMIT_TYPEHASH, permit.wallet, permit.chainId, permit.nonce, permit.expiry, permit.target, permit.value, permit.calldataHash, permit.policyHash))
  • Compute digest: keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash))
  • Recover signer via ecrecover(digest, v, r, s) (unpack signature bytes)
  • Return recoveredSigner == fishnetSigner

6. Owner Functions

  • setSigner(address _signer) external onlyOwner — rotate Fishnet signer, emit SignerUpdated
  • withdraw(address to) external onlyOwner — withdraw all ETH from wallet
  • pause() external onlyOwner — set paused = true, emit Paused
  • unpause() external onlyOwner — set paused = false, emit Unpaused
  • receive() external payable — accept ETH deposits

7. Modifiers

  • onlyOwnerrequire(msg.sender == owner, "not owner")
  • whenNotPausedrequire(!paused, "wallet paused")

8. Events

  • event ActionExecuted(address indexed target, uint256 value, uint256 nonce, bytes32 policyHash)
  • event SignerUpdated(address indexed oldSigner, address indexed newSigner)
  • event Paused(address account)
  • event Unpaused(address account)

Acceptance Criteria

  • Contract compiles with forge build (Solidity ^0.8.19)
  • execute() only succeeds with a valid EIP712 permit signed by fishnetSigner
  • All 6 validation checks revert with correct error messages
  • Nonces cannot be replayed
  • Owner can rotate signer, withdraw, and pause/unpause
  • Gas-efficient — minimal storage operations

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions