Skip to content

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

@iamyxsh

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

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions