Skip to content

Commit 4f75e13

Browse files
authored
Merge pull request #40 from iamyxsh/feature/sc1
feat: implemented FishnetWallet Core Contract (execute + EIP712 permi…
2 parents e839434 + 0710962 commit 4f75e13

6 files changed

Lines changed: 1107 additions & 2 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "contracts/lib/forge-std"]
2+
path = contracts/lib/forge-std
3+
url = https://github.com/foundry-rs/forge-std

contracts/foundry.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"lib/forge-std": {
3+
"tag": {
4+
"name": "v1.15.0",
5+
"rev": "0844d7e1fc5e60d77b68e469bff60265f236c398"
6+
}
7+
}
8+
}

contracts/lib/forge-std

Submodule forge-std added at 0844d7e

contracts/src/FishnetWallet.sol

Lines changed: 175 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,177 @@
11
// SPDX-License-Identifier: MIT
2-
pragma solidity ^0.8.28;
2+
pragma solidity ^0.8.19;
33

4-
contract FishnetWallet {}
4+
contract FishnetWallet {
5+
error PermitExpired();
6+
error WrongChain();
7+
error NonceUsed();
8+
error TargetMismatch();
9+
error ValueMismatch();
10+
error CalldataMismatch();
11+
error WalletMismatch();
12+
error InvalidSignature();
13+
error InvalidSignatureLength();
14+
error ExecutionFailed();
15+
error NotOwner();
16+
error WalletPaused();
17+
error ZeroAddress();
18+
error WithdrawFailed();
19+
20+
// Slot 0: owner (20 bytes) + paused (1 byte) packed
21+
address public owner;
22+
bool public paused;
23+
address public fishnetSigner;
24+
mapping(uint256 => bool) public usedNonces;
25+
26+
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
27+
uint256 private immutable _CACHED_CHAIN_ID;
28+
29+
bytes32 internal constant PERMIT_TYPEHASH = keccak256(
30+
"FishnetPermit(address wallet,uint64 chainId,uint256 nonce,"
31+
"uint48 expiry,address target,uint256 value,"
32+
"bytes32 calldataHash,bytes32 policyHash)"
33+
);
34+
35+
struct FishnetPermit {
36+
address wallet;
37+
uint64 chainId;
38+
uint256 nonce;
39+
uint48 expiry;
40+
address target;
41+
uint256 value;
42+
bytes32 calldataHash;
43+
bytes32 policyHash;
44+
}
45+
46+
event ActionExecuted(address indexed target, uint256 value, uint256 nonce, bytes32 policyHash);
47+
event SignerUpdated(address indexed oldSigner, address indexed newSigner);
48+
event Paused(address account);
49+
event Unpaused(address account);
50+
event Withdrawn(address indexed to, uint256 amount);
51+
52+
modifier onlyOwner() {
53+
if (msg.sender != owner) revert NotOwner();
54+
_;
55+
}
56+
57+
modifier whenNotPaused() {
58+
if (paused) revert WalletPaused();
59+
_;
60+
}
61+
62+
constructor(address _fishnetSigner) {
63+
if (_fishnetSigner == address(0)) revert ZeroAddress();
64+
owner = msg.sender;
65+
fishnetSigner = _fishnetSigner;
66+
67+
_CACHED_CHAIN_ID = block.chainid;
68+
_CACHED_DOMAIN_SEPARATOR = _computeDomainSeparator();
69+
}
70+
71+
function DOMAIN_SEPARATOR() public view returns (bytes32) {
72+
if (block.chainid == _CACHED_CHAIN_ID) {
73+
return _CACHED_DOMAIN_SEPARATOR;
74+
}
75+
return _computeDomainSeparator();
76+
}
77+
78+
function _computeDomainSeparator() internal view returns (bytes32) {
79+
return keccak256(
80+
abi.encode(
81+
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
82+
keccak256("Fishnet"),
83+
keccak256("1"),
84+
block.chainid,
85+
address(this)
86+
)
87+
);
88+
}
89+
90+
function execute(
91+
address target,
92+
uint256 value,
93+
bytes calldata data,
94+
FishnetPermit calldata permit,
95+
bytes calldata signature
96+
) external whenNotPaused {
97+
if (block.timestamp > permit.expiry) revert PermitExpired();
98+
if (permit.chainId != block.chainid) revert WrongChain();
99+
if (usedNonces[permit.nonce]) revert NonceUsed();
100+
if (permit.target != target) revert TargetMismatch();
101+
if (permit.value != value) revert ValueMismatch();
102+
if (permit.calldataHash != keccak256(data)) revert CalldataMismatch();
103+
if (permit.wallet != address(this)) revert WalletMismatch();
104+
if (!_verifySignature(permit, signature)) revert InvalidSignature();
105+
106+
usedNonces[permit.nonce] = true;
107+
108+
(bool success, ) = target.call{value: value}(data);
109+
if (!success) revert ExecutionFailed();
110+
111+
emit ActionExecuted(target, value, permit.nonce, permit.policyHash);
112+
}
113+
114+
function _verifySignature(
115+
FishnetPermit calldata permit,
116+
bytes calldata signature
117+
) internal view returns (bool) {
118+
if (signature.length != 65) revert InvalidSignatureLength();
119+
120+
bytes32 structHash = keccak256(
121+
abi.encode(
122+
PERMIT_TYPEHASH,
123+
permit.wallet,
124+
permit.chainId,
125+
permit.nonce,
126+
permit.expiry,
127+
permit.target,
128+
permit.value,
129+
permit.calldataHash,
130+
permit.policyHash
131+
)
132+
);
133+
134+
bytes32 digest = keccak256(
135+
abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), structHash)
136+
);
137+
138+
bytes32 r;
139+
bytes32 s;
140+
uint8 v;
141+
assembly {
142+
let ptr := signature.offset
143+
r := calldataload(ptr)
144+
s := calldataload(add(ptr, 32))
145+
v := byte(0, calldataload(add(ptr, 64)))
146+
}
147+
148+
address recoveredSigner = ecrecover(digest, v, r, s);
149+
return recoveredSigner == fishnetSigner;
150+
}
151+
152+
function setSigner(address _signer) external onlyOwner {
153+
if (_signer == address(0)) revert ZeroAddress();
154+
address oldSigner = fishnetSigner;
155+
fishnetSigner = _signer;
156+
emit SignerUpdated(oldSigner, _signer);
157+
}
158+
159+
function withdraw(address to) external onlyOwner {
160+
uint256 balance = address(this).balance;
161+
(bool success, ) = to.call{value: balance}("");
162+
if (!success) revert WithdrawFailed();
163+
emit Withdrawn(to, balance);
164+
}
165+
166+
function pause() external onlyOwner {
167+
paused = true;
168+
emit Paused(msg.sender);
169+
}
170+
171+
function unpause() external onlyOwner {
172+
paused = false;
173+
emit Unpaused(msg.sender);
174+
}
175+
176+
receive() external payable {}
177+
}

0 commit comments

Comments
 (0)