Skip to content

Commit b8781cf

Browse files
authored
Merge pull request #42 from iamyxsh/feature/sc3
Feature/sc3
2 parents da0d607 + d9f55e0 commit b8781cf

13 files changed

Lines changed: 1957 additions & 167 deletions

contracts/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Fishnet Contracts
2+
3+
Smart contracts for the Fishnet permit-based wallet system.
4+
5+
## FishnetWallet
6+
7+
EIP-712 permit-gated smart wallet. The Fishnet backend signs permits authorizing on-chain actions, and any relayer can submit them.
8+
9+
### Build & Test
10+
11+
```bash
12+
cd contracts
13+
forge build
14+
forge test -vvv
15+
```
16+
17+
### EIP712 Compatibility Tests
18+
19+
The `EIP712Compatibility.t.sol` test suite proves encoding compatibility between the Rust backend signer (`crates/server/src/signer.rs`) and the Solidity contract:
20+
21+
- **Typehash match**: Raw keccak256 of the type string matches `PERMIT_TYPEHASH`
22+
- **Domain separator**: Field-by-field construction matches `DOMAIN_SEPARATOR()`
23+
- **Struct hash**: `abi.encode` padding for `uint64`/`uint48` matches Rust's manual padding
24+
- **End-to-end**: Full EIP-712 hash → sign → execute flow succeeds
25+
- **Signature format**: `r || s || v` (65 bytes) unpacking matches contract expectations
26+
27+
```bash
28+
forge test --match-contract EIP712Compatibility -vvv
29+
```
30+
31+
### Deployment
32+
33+
#### Local (Anvil)
34+
35+
```bash
36+
# Terminal 1
37+
anvil
38+
39+
# Terminal 2
40+
cd contracts
41+
SIGNER_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
42+
forge script script/Deploy.s.sol:DeployFishnetWallet \
43+
--rpc-url http://127.0.0.1:8545 \
44+
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
45+
--broadcast
46+
```
47+
48+
#### Base Sepolia
49+
50+
```bash
51+
cd contracts
52+
export BASE_SEPOLIA_RPC_URL="https://sepolia.base.org"
53+
export SIGNER_ADDRESS="<your-signer-address>"
54+
export BASESCAN_API_KEY="<your-api-key>"
55+
56+
forge script script/Deploy.s.sol:DeployFishnetWallet \
57+
--rpc-url base_sepolia \
58+
--private-key $DEPLOYER_PRIVATE_KEY \
59+
--broadcast \
60+
--verify
61+
```
62+
63+
### Environment Variables
64+
65+
| Variable | Required | Description |
66+
|----------|----------|-------------|
67+
| `SIGNER_ADDRESS` | Yes | Address of the Fishnet backend signer |
68+
| `OWNER_ADDRESS` | No | Wallet owner (defaults to deployer) |
69+
| `BASE_SEPOLIA_RPC_URL` | For testnet | Base Sepolia RPC endpoint |
70+
| `BASE_MAINNET_RPC_URL` | For mainnet | Base mainnet RPC endpoint |
71+
| `BASESCAN_API_KEY` | For verification | Basescan API key |
72+
73+
### Integration Test
74+
75+
Run the full Anvil-based E2E test (deploys, signs permit with `cast`, executes on-chain):
76+
77+
```bash
78+
bash scripts/sc3-integration-test.sh
79+
```
80+
81+
### Multi-Chain Deployments
82+
83+
Deployment artifacts are stored in `contracts/deployments/`:
84+
85+
```
86+
deployments/
87+
base-sepolia.json # Base Sepolia testnet
88+
base-mainnet.json # Base mainnet (future)
89+
arbitrum-sepolia.json # Arbitrum Sepolia (future)
90+
```
91+
92+
Each file contains: `wallet`, `signer`, `owner`, `chainId`, `deployBlock`, `timestamp`.
93+
94+
### EIP712 Encoding Notes
95+
96+
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.
97+
98+
Domain name is `"Fishnet"` (not `"FishnetPermit"`), version is `"1"`.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"wallet": "",
3+
"signer": "",
4+
"owner": "",
5+
"chainId": 421614,
6+
"deployBlock": 0,
7+
"timestamp": 0
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"wallet": "",
3+
"signer": "",
4+
"owner": "",
5+
"chainId": 8453,
6+
"deployBlock": 0,
7+
"timestamp": 0
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"wallet": "",
3+
"signer": "",
4+
"owner": "",
5+
"chainId": 84532,
6+
"deployBlock": 0,
7+
"timestamp": 0
8+
}

contracts/foundry.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,16 @@
22
src = "src"
33
out = "out"
44
libs = ["lib"]
5+
ffi = true
6+
7+
fs_permissions = [{ access = "read-write", path = "deployments" }]
8+
9+
[rpc_endpoints]
10+
base_sepolia = "${BASE_SEPOLIA_RPC_URL}"
11+
base_mainnet = "${BASE_MAINNET_RPC_URL}"
12+
arbitrum_sepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"
13+
localhost = "http://127.0.0.1:8545"
14+
15+
[etherscan]
16+
base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api", chain = 84532 }
17+
base_mainnet = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api", chain = 8453 }

contracts/script/Deploy.s.sol

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.19;
3+
4+
import {Script, console} from "forge-std/Script.sol";
5+
import {FishnetWallet} from "../src/FishnetWallet.sol";
6+
7+
contract DeployFishnetWallet is Script {
8+
function run() external {
9+
address signerAddress = vm.envAddress("SIGNER_ADDRESS");
10+
11+
vm.startBroadcast();
12+
13+
FishnetWallet wallet = new FishnetWallet(signerAddress);
14+
console.log("FishnetWallet deployed at:", address(wallet));
15+
console.log("Signer:", signerAddress);
16+
console.log("Owner:", msg.sender);
17+
console.log("Chain ID:", block.chainid);
18+
19+
vm.stopBroadcast();
20+
21+
// Write deployment info to JSON
22+
string memory networkName = _getNetworkName();
23+
string memory json = "deployment";
24+
vm.serializeAddress(json, "wallet", address(wallet));
25+
vm.serializeAddress(json, "signer", signerAddress);
26+
vm.serializeAddress(json, "owner", msg.sender);
27+
vm.serializeUint(json, "chainId", block.chainid);
28+
vm.serializeUint(json, "deployBlock", block.number);
29+
string memory finalJson = vm.serializeUint(json, "timestamp", block.timestamp);
30+
31+
string memory path = string.concat("deployments/", networkName, ".json");
32+
vm.writeJson(finalJson, path);
33+
console.log("Deployment info written to:", path);
34+
}
35+
36+
function _getNetworkName() internal view returns (string memory) {
37+
if (block.chainid == 84532) return "base-sepolia";
38+
if (block.chainid == 8453) return "base-mainnet";
39+
if (block.chainid == 421614) return "arbitrum-sepolia";
40+
if (block.chainid == 42161) return "arbitrum-one";
41+
if (block.chainid == 31337) return "localhost";
42+
return vm.toString(block.chainid);
43+
}
44+
}

0 commit comments

Comments
 (0)