-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
[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.chainidverifyingContract = 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):
require(block.timestamp <= permit.expiry, "permit expired")require(!usedNonces[permit.nonce], "nonce already used")require(permit.target == target, "target mismatch")require(permit.calldataHash == keccak256(data), "calldata mismatch")require(permit.wallet == address(this), "wallet mismatch")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, emitSignerUpdated -
withdraw(address to) external onlyOwner— withdraw all ETH from wallet -
pause() external onlyOwner— setpaused = true, emitPaused -
unpause() external onlyOwner— setpaused = false, emitUnpaused -
receive() external payable— accept ETH deposits
7. Modifiers
-
onlyOwner—require(msg.sender == owner, "not owner") -
whenNotPaused—require(!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 byfishnetSigner- 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
Reactions are currently unavailable