Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Fishnet Contracts

Smart contracts for the Fishnet permit-based wallet system.

## FishnetWallet

EIP-712 permit-gated smart wallet. The Fishnet backend signs permits authorizing on-chain actions, and any relayer can submit them.

### Build & Test

```bash
cd contracts
forge build
forge test -vvv
```

### EIP712 Compatibility Tests

The `EIP712Compatibility.t.sol` test suite proves encoding compatibility between the Rust backend signer (`crates/server/src/signer.rs`) and the Solidity contract:

- **Typehash match**: Raw keccak256 of the type string matches `PERMIT_TYPEHASH`
- **Domain separator**: Field-by-field construction matches `DOMAIN_SEPARATOR()`
- **Struct hash**: `abi.encode` padding for `uint64`/`uint48` matches Rust's manual padding
- **End-to-end**: Full EIP-712 hash → sign → execute flow succeeds
- **Signature format**: `r || s || v` (65 bytes) unpacking matches contract expectations

```bash
forge test --match-contract EIP712Compatibility -vvv
```

### Deployment

#### Local (Anvil)

```bash
# Terminal 1
anvil

# Terminal 2
cd contracts
SIGNER_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
forge script script/Deploy.s.sol:DeployFishnetWallet \
--rpc-url http://127.0.0.1:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--broadcast
```

#### Base Sepolia

```bash
cd contracts
export BASE_SEPOLIA_RPC_URL="https://sepolia.base.org"
export SIGNER_ADDRESS="<your-signer-address>"
export BASESCAN_API_KEY="<your-api-key>"

forge script script/Deploy.s.sol:DeployFishnetWallet \
--rpc-url base_sepolia \
--private-key $DEPLOYER_PRIVATE_KEY \
--broadcast \
--verify
```

### Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `SIGNER_ADDRESS` | Yes | Address of the Fishnet backend signer |
| `OWNER_ADDRESS` | No | Wallet owner (defaults to deployer) |
| `BASE_SEPOLIA_RPC_URL` | For testnet | Base Sepolia RPC endpoint |
| `BASE_MAINNET_RPC_URL` | For mainnet | Base mainnet RPC endpoint |
| `BASESCAN_API_KEY` | For verification | Basescan API key |

### Integration Test

Run the full Anvil-based E2E test (deploys, signs permit with `cast`, executes on-chain):

```bash
bash scripts/sc3-integration-test.sh
```

### Multi-Chain Deployments

Deployment artifacts are stored in `contracts/deployments/`:

```
deployments/
base-sepolia.json # Base Sepolia testnet
base-mainnet.json # Base mainnet (future)
arbitrum-sepolia.json # Arbitrum Sepolia (future)
```

Each file contains: `wallet`, `signer`, `owner`, `chainId`, `deployBlock`, `timestamp`.

### EIP712 Encoding Notes

The permit typehash uses `uint64 chainId` and `uint48 expiry` (not `uint256`). Both Solidity's `abi.encode` and Rust's manual big-endian padding produce identical 32-byte left-padded values for these smaller types, ensuring cross-stack compatibility.

Domain name is `"Fishnet"` (not `"FishnetPermit"`), version is `"1"`.
8 changes: 8 additions & 0 deletions contracts/deployments/arbitrum-sepolia.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"wallet": "",
"signer": "",
"owner": "",
"chainId": 421614,
"deployBlock": 0,
"timestamp": 0
}
8 changes: 8 additions & 0 deletions contracts/deployments/base-mainnet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"wallet": "",
"signer": "",
"owner": "",
"chainId": 8453,
"deployBlock": 0,
"timestamp": 0
}
8 changes: 8 additions & 0 deletions contracts/deployments/base-sepolia.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"wallet": "",
"signer": "",
"owner": "",
"chainId": 84532,
"deployBlock": 0,
"timestamp": 0
}
13 changes: 13 additions & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,16 @@
src = "src"
out = "out"
libs = ["lib"]
ffi = true

fs_permissions = [{ access = "read-write", path = "deployments" }]

[rpc_endpoints]
base_sepolia = "${BASE_SEPOLIA_RPC_URL}"
base_mainnet = "${BASE_MAINNET_RPC_URL}"
arbitrum_sepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"
localhost = "http://127.0.0.1:8545"

[etherscan]
base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api", chain = 84532 }
base_mainnet = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api", chain = 8453 }
44 changes: 44 additions & 0 deletions contracts/script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Script, console} from "forge-std/Script.sol";
import {FishnetWallet} from "../src/FishnetWallet.sol";

contract DeployFishnetWallet is Script {
function run() external {
address signerAddress = vm.envAddress("SIGNER_ADDRESS");

vm.startBroadcast();

FishnetWallet wallet = new FishnetWallet(signerAddress);
console.log("FishnetWallet deployed at:", address(wallet));
console.log("Signer:", signerAddress);
console.log("Owner:", msg.sender);
console.log("Chain ID:", block.chainid);

vm.stopBroadcast();

// Write deployment info to JSON
string memory networkName = _getNetworkName();
string memory json = "deployment";
vm.serializeAddress(json, "wallet", address(wallet));
vm.serializeAddress(json, "signer", signerAddress);
vm.serializeAddress(json, "owner", msg.sender);
vm.serializeUint(json, "chainId", block.chainid);
vm.serializeUint(json, "deployBlock", block.number);
string memory finalJson = vm.serializeUint(json, "timestamp", block.timestamp);

string memory path = string.concat("deployments/", networkName, ".json");
vm.writeJson(finalJson, path);
console.log("Deployment info written to:", path);
}

function _getNetworkName() internal view returns (string memory) {
if (block.chainid == 84532) return "base-sepolia";
if (block.chainid == 8453) return "base-mainnet";
if (block.chainid == 421614) return "arbitrum-sepolia";
if (block.chainid == 42161) return "arbitrum-one";
if (block.chainid == 31337) return "localhost";
return vm.toString(block.chainid);
}
}
Loading