Skip to content

Commit 7078fc8

Browse files
authored
Merge pull request #6 from ethereumfollowprotocol/review
review
2 parents 6c78aa2 + ada843e commit 7078fc8

File tree

6 files changed

+555
-74
lines changed

6 files changed

+555
-74
lines changed

CLAUDE.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
The Ethereum Follow Protocol (EFP) ListRecordsV2 repository contains updated smart contracts for EFP's list minting and record management system. This is a Solidity project using the Foundry framework for smart contract development, with TypeScript tooling for ABI generation and testing.
8+
9+
## Common Commands
10+
11+
### Build and Development
12+
- `bun run build` - Compiles smart contracts using Foundry
13+
- `bun run test` - Runs all tests using Foundry with forked environment (`--fork-url ${ETH_RPC_URL} -vv`)
14+
- `bun run coverage` - Generates test coverage report and lcov file
15+
- `forge test --match-path test/specific.t.sol` - Run specific test file
16+
- `forge test --match-test testFunctionName` - Run specific test function
17+
18+
### Deployment
19+
- `bun run deploy` - Deploy to mainnet using `${ETH_RPC_URL}`
20+
- `bun run deploy:testnet` - Deploy to testnet using `${TESTNET_RPC_URL}`
21+
- `bun run deploy:local` - Deploy to local anvil instance
22+
- `bun run testnet` - Start local anvil fork on chain ID 8453
23+
24+
### ABI Generation
25+
- `bun wagmi generate` - Generate TypeScript ABIs, actions, and React hooks from compiled contracts
26+
27+
## Architecture Overview
28+
29+
### Core Contracts
30+
31+
**EFPListRecordsV2** (`src/EFPListRecordsV2.sol`): The main list records contract that stores and manages list operations and metadata. Key features:
32+
- **Slot-based access control**: Uses first 20 bytes of slot to match message sender address, preventing front-running of slot claims
33+
- **List operations storage**: Maintains a history of operations applied to each list
34+
- **Metadata management**: Key-value store for list metadata with manager-only access
35+
- **Manager system**: Claims management through `onlyListManager` modifier
36+
37+
**EFPListMinterV2** (`src/EFPListMinterV2.sol`): Handles minting of new lists with improved validation. Key features:
38+
- **Chain ID validation**: Ensures list records are stored on the intended chain when using native storage
39+
- **List storage location decoding**: Validates encoded storage locations before minting
40+
- **Primary list assignment**: Sets default lists for accounts via account metadata contract
41+
42+
### Contract Hierarchy
43+
44+
```
45+
ListMetadata (abstract)
46+
├── Manages metadata key-value pairs
47+
├── Implements manager claiming and validation
48+
└── Extends Pausable, Ownable
49+
50+
ListRecordsV2 (abstract)
51+
├── Extends ListMetadata
52+
├── Manages list operations history
53+
└── Provides batch operations
54+
55+
EFPListRecordsV2 (concrete)
56+
└── Final implementation with ENS reverse claiming
57+
```
58+
59+
### Key Dependencies
60+
61+
- **External contracts**: Interfaces to EFPListRegistry, EFPAccountMetadata
62+
- **OpenZeppelin**: Uses Ownable, Pausable, Context
63+
- **ENS integration**: ENSReverseClaimer for reverse name resolution
64+
- **Foundry libraries**: forge-std for testing and deployment
65+
66+
### Directory Structure
67+
68+
- `src/` - Main contract source files
69+
- `src/interfaces/` - Contract interfaces
70+
- `src/lib/` - Shared libraries (ENSReverseClaimer)
71+
- `script/` - Deployment scripts and utilities
72+
- `test/` - Foundry test files
73+
- `out/` - Compiled contract artifacts
74+
- `lib/` - Git submodules (forge-std, openzeppelin-contracts)
75+
76+
### Testing Approach
77+
78+
Tests use Foundry's testing framework with mainnet forking. Test files follow the pattern `*.t.sol` and are located in the `test/` directory. Coverage reports are generated using `forge coverage` with lcov output.
79+
80+
### Environment Setup
81+
82+
Requires `.env` file with:
83+
- `PRIVATE_KEY` - Deployment private key
84+
- `ETH_RPC_URL` - Mainnet RPC endpoint
85+
- `TESTNET_RPC_URL` - Testnet RPC endpoint
86+
- `ETHERSCAN_API_KEY` - For contract verification
87+
88+
### Important Security Features
89+
90+
1. **Slot validation**: EFPListRecordsV2 validates that the first 20 bytes of a slot match the caller's address
91+
2. **Chain ID checks**: EFPListMinterV2 verifies chain ID matches current chain for native storage
92+
3. **Manager-only operations**: All list modifications require manager privileges
93+
4. **Pausable contracts**: Admin can pause operations in emergency situations

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": "module",
77
"scripts": {
88
"build": "forge build",
9-
"test": "forge test --fork-url ${ETH_RPC_URL} -vv",
9+
"test": "forge test --match-path test/EFPListRecords.t.sol --fork-url ${ETH_RPC_URL} -vv && forge test --match-path test/EFPListMinter.t.sol --fork-url ${ETH_RPC_URL} -vv ",
1010
"coverage": "forge coverage --fork-url ${ETH_RPC_URL} --report lcov && lcov-viewer lcov -o ./coverage ./lcov.info",
1111
"deploy": "forge script script/deploy.s.sol --fork-url ${ETH_RPC_URL} --broadcast --private-key ${PRIVATE_KEY}",
1212
"deploy:testnet": "forge script script/deploy.s.sol --fork-url ${TESTNET_RPC_URL} --broadcast --private-key ${PRIVATE_KEY}",

src/EFPListMinterV2.sol

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ interface IEFPListRegistryERC721 is IEFPListRegistry {
2525
* EFP List metadata.
2626
*/
2727
contract EFPListMinterV2 is ENSReverseClaimer, Pausable {
28-
IEFPListRegistryERC721 public registry;
29-
IEFPAccountMetadata public accountMetadata;
28+
IEFPListRegistryERC721 public immutable registry;
29+
IEFPAccountMetadata public immutable accountMetadata;
3030
IEFPListRecords public listRecordsL1;
3131

32+
event Minted(string method, address indexed to, bytes listStorageLocation);
33+
3234
constructor(address _registryAddress, address _accountMetadataAddress, address _listRecordsL1) {
3335
registry = IEFPListRegistryERC721(_registryAddress);
3436
accountMetadata = IEFPAccountMetadata(_accountMetadataAddress);
@@ -70,25 +72,48 @@ contract EFPListMinterV2 is ENSReverseClaimer, Pausable {
7072
/////////////////////////////////////////////////////////////////////////////
7173

7274
/**
73-
* @dev Decode a list storage location with no metadata.
75+
* @dev Decode a list storage location
76+
* @param listStorageLocation The storage location of the list.
77+
* @return chain The chain ID of the list.
78+
* @return slot The slot of the list.
79+
* @return contractAddress The contract address of the list.
80+
*/
81+
function decodeLSL(bytes calldata listStorageLocation) public pure returns (uint256, uint256, address) {
82+
address contractAddress = _bytesToAddress(listStorageLocation, 34);
83+
uint256 chain = _bytesToUint(listStorageLocation, 2);
84+
uint256 slot = _bytesToUint(listStorageLocation, 54);
85+
return (chain, slot, contractAddress);
86+
}
87+
88+
/**
89+
* @dev Encode a list storage location. Note this has a fixed version and type.
90+
* @param chain The chain ID of the list.
91+
* @param slot The slot of the list.
92+
* @param contractAddress The contract address of the list.
93+
* @return The encoded list storage location.
94+
*/
95+
function encodeLSL(uint256 chain, uint256 slot, address contractAddress) public pure returns (bytes memory) {
96+
return abi.encodePacked(bytes1(0x01), bytes1(0x01), bytes32(chain), contractAddress, bytes32(slot));
97+
}
98+
99+
/**
100+
* @dev Validate and decode a list storage location
74101
* @param listStorageLocation The storage location of the list.
102+
* @return chain The chain ID of the list.
75103
* @return slot The slot of the list.
76104
* @return contractAddress The contract address of the list.
77105
*/
78-
function decodeL1ListStorageLocation(bytes calldata listStorageLocation) internal pure returns (uint256, uint256, address) {
106+
function validateAndDecodeLSL(bytes calldata listStorageLocation) internal pure returns (uint256, uint256, address) {
79107
// the list storage location is
80108
// - version (1 byte)
81-
// - list storate location type (1 byte)
109+
// - list storage location type (1 byte)
82110
// - chain id (32 bytes)
83111
// - contract address (20 bytes)
84112
// - slot (32 bytes)
85113
require(listStorageLocation.length == 1 + 1 + 32 + 20 + 32, 'EFPListMinter: invalid list storage location');
86114
require(listStorageLocation[0] == 0x01, 'EFPListMinter: invalid list storage location version');
87115
require(listStorageLocation[1] == 0x01, 'EFPListMinter: invalid list storage location type');
88-
address contractAddress = _bytesToAddress(listStorageLocation, 34);
89-
90-
uint256 chain = _bytesToUint(listStorageLocation, 2);
91-
uint256 slot = _bytesToUint(listStorageLocation, 54);
116+
(uint256 chain, uint256 slot, address contractAddress) = decodeLSL(listStorageLocation);
92117
return (chain, slot, contractAddress);
93118
}
94119

@@ -98,7 +123,7 @@ contract EFPListMinterV2 is ENSReverseClaimer, Pausable {
98123
*/
99124
function easyMint(bytes calldata listStorageLocation) public payable whenNotPaused {
100125
// validate the list storage location
101-
(uint256 chain, uint256 slot, address recordsContract) = decodeL1ListStorageLocation(listStorageLocation);
126+
(uint256 chain, uint256 slot, address recordsContract) = validateAndDecodeLSL(listStorageLocation);
102127

103128
uint256 tokenId = registry.totalSupply();
104129
uint256 currentChain = block.chainid;
@@ -107,6 +132,7 @@ contract EFPListMinterV2 is ENSReverseClaimer, Pausable {
107132
if (recordsContract == address(listRecordsL1) && currentChain == chain) {
108133
listRecordsL1.claimListManagerForAddress(slot, msg.sender);
109134
}
135+
emit Minted('easyMint', msg.sender, listStorageLocation);
110136
}
111137

112138
/**
@@ -116,7 +142,7 @@ contract EFPListMinterV2 is ENSReverseClaimer, Pausable {
116142
*/
117143
function easyMintTo(address to, bytes calldata listStorageLocation) public payable whenNotPaused {
118144
// validate the list storage location
119-
(uint256 chain, uint256 slot, address recordsContract) = decodeL1ListStorageLocation(listStorageLocation);
145+
(uint256 chain, uint256 slot, address recordsContract) = validateAndDecodeLSL(listStorageLocation);
120146

121147
uint256 tokenId = registry.totalSupply();
122148
uint256 currentChain = block.chainid;
@@ -125,6 +151,7 @@ contract EFPListMinterV2 is ENSReverseClaimer, Pausable {
125151
if (recordsContract == address(listRecordsL1) && currentChain == chain) {
126152
listRecordsL1.claimListManagerForAddress(slot, msg.sender);
127153
}
154+
emit Minted('easyMintTo', to, listStorageLocation);
128155
}
129156

130157
/**
@@ -133,10 +160,11 @@ contract EFPListMinterV2 is ENSReverseClaimer, Pausable {
133160
*/
134161
function mintPrimaryListNoMeta(bytes calldata listStorageLocation) public payable whenNotPaused {
135162
// validate the list storage location
136-
decodeL1ListStorageLocation(listStorageLocation);
163+
validateAndDecodeLSL(listStorageLocation);
137164
uint256 tokenId = registry.totalSupply();
138165
_setDefaultListForAccount(msg.sender, tokenId);
139166
registry.mintTo{value: msg.value}(msg.sender, listStorageLocation);
167+
emit Minted('mintPrimaryListNoMeta', msg.sender, listStorageLocation);
140168
}
141169

142170
/**
@@ -145,9 +173,10 @@ contract EFPListMinterV2 is ENSReverseClaimer, Pausable {
145173
*/
146174
function mintNoMeta(bytes calldata listStorageLocation) public payable whenNotPaused {
147175
// validate the list storage location
148-
decodeL1ListStorageLocation(listStorageLocation);
176+
validateAndDecodeLSL(listStorageLocation);
149177

150178
registry.mintTo{value: msg.value}(msg.sender, listStorageLocation);
179+
emit Minted('mintNoMeta', msg.sender, listStorageLocation);
151180
}
152181

153182
/**
@@ -157,9 +186,10 @@ contract EFPListMinterV2 is ENSReverseClaimer, Pausable {
157186
*/
158187
function mintToNoMeta(address to, bytes calldata listStorageLocation) public payable whenNotPaused {
159188
// validate the list storage location
160-
decodeL1ListStorageLocation(listStorageLocation);
189+
validateAndDecodeLSL(listStorageLocation);
161190

162191
registry.mintTo{value: msg.value}(to, listStorageLocation);
192+
emit Minted('mintToNoMeta', to, listStorageLocation);
163193
}
164194

165195
/**

src/EFPListRecordsV2.sol

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ abstract contract ListMetadata is IEFPListMetadata, Pausable, Ownable {
2020
// Data Structures
2121
///////////////////////////////////////////////////////////////////////////
2222

23-
/// @dev The key-value set for each token ID
24-
mapping(uint256 => mapping(string => bytes)) private values;
23+
/// @dev The key-value set for each slot
24+
mapping(uint256 slot => mapping(string key => bytes value)) private values;
2525

2626
/////////////////////////////////////////////////////////////////////////////
2727
// Pausable
@@ -61,30 +61,27 @@ abstract contract ListMetadata is IEFPListMetadata, Pausable, Ownable {
6161
/////////////////////////////////////////////////////////////////////////////
6262

6363
/**
64-
* @dev Retrieves metadata value for token ID and key.
65-
* @param tokenId The token Id to query.
64+
* @dev Retrieves metadata value for slot and key.
65+
* @param slot The slot to query.
6666
* @param key The key to query.
6767
* @return The associated value.
6868
*/
69-
function getMetadataValue(uint256 tokenId, string calldata key) external view returns (bytes memory) {
70-
return values[tokenId][key];
69+
function getMetadataValue(uint256 slot, string calldata key) external view returns (bytes memory) {
70+
return values[slot][key];
7171
}
7272

7373
/**
74-
* @dev Retrieves metadata values for token ID and keys.
75-
* @param tokenId The token Id to query.
74+
* @dev Retrieves metadata values for slot and keys.
75+
* @param slot The slot to query.
7676
* @param keys The keys to query.
7777
* @return The associated values.
7878
*/
79-
function getMetadataValues(uint256 tokenId, string[] calldata keys) external view returns (bytes[] memory) {
79+
function getMetadataValues(uint256 slot, string[] calldata keys) external view returns (bytes[] memory) {
8080
uint256 length = keys.length;
8181
bytes[] memory result = new bytes[](length);
82-
for (uint256 i = 0; i < length;) {
82+
for (uint256 i = 0; i < length; ++i) {
8383
string calldata key = keys[i];
84-
result[i] = values[tokenId][key];
85-
unchecked {
86-
++i;
87-
}
84+
result[i] = values[slot][key];
8885
}
8986
return result;
9087
}
@@ -94,8 +91,8 @@ abstract contract ListMetadata is IEFPListMetadata, Pausable, Ownable {
9491
/////////////////////////////////////////////////////////////////////////////
9592

9693
/**
97-
* @dev Sets metadata records for token ID with the unique key key to value,
98-
* overwriting anything previously stored for token ID and key. To clear a
94+
* @dev Sets metadata records for slot with the unique key key to value,
95+
* overwriting anything previously stored for slot and key. To clear a
9996
* field, set it to the empty string.
10097
* @param slot The slot corresponding to the list to update.
10198
* @param key The key to set.
@@ -107,8 +104,8 @@ abstract contract ListMetadata is IEFPListMetadata, Pausable, Ownable {
107104
}
108105

109106
/**
110-
* @dev Sets metadata records for token ID with the unique key key to value,
111-
* overwriting anything previously stored for token ID and key. To clear a
107+
* @dev Sets metadata records for slot with the unique key key to value,
108+
* overwriting anything previously stored for slot and key. To clear a
112109
* field, set it to the empty string. Only callable by the list manager.
113110
* @param slot The slot corresponding to the list to update.
114111
* @param key The key to set.
@@ -123,24 +120,21 @@ abstract contract ListMetadata is IEFPListMetadata, Pausable, Ownable {
123120
}
124121

125122
/**
126-
* @dev Sets an array of metadata records for a token ID. Each record is a
123+
* @dev Sets an array of metadata records for a slot. Each record is a
127124
* key/value pair.
128125
* @param slot The slot corresponding to the list to update.
129126
* @param records The records to set.
130127
*/
131128
function _setMetadataValues(uint256 slot, KeyValue[] calldata records) internal {
132129
uint256 length = records.length;
133-
for (uint256 i = 0; i < length;) {
130+
for (uint256 i = 0; i < length; ++i) {
134131
KeyValue calldata record = records[i];
135132
_setMetadataValue(slot, record.key, record.value);
136-
unchecked {
137-
++i;
138-
}
139133
}
140134
}
141135

142136
/**
143-
* @dev Sets an array of metadata records for a token ID. Each record is a
137+
* @dev Sets an array of metadata records for a slot. Each record is a
144138
* key/value pair. Only callable by the list manager.
145139
* @param slot The slot corresponding to the list to update.
146140
* @param records The records to set.
@@ -161,7 +155,7 @@ abstract contract ListMetadata is IEFPListMetadata, Pausable, Ownable {
161155
modifier onlyListManager(uint256 slot) {
162156
bytes memory existing = values[slot]['manager'];
163157
// if not set, claim for msg.sender
164-
if (existing.length != 20) {
158+
if (existing.length == 0) {
165159
_claimListManager(slot, msg.sender);
166160
} else {
167161
address existingManager = bytesToAddress(existing);
@@ -290,7 +284,7 @@ abstract contract ListRecordsV2 is IEFPListRecords, ListMetadata {
290284

291285
/// @notice Stores a sequence of operations for each list identified by its slot.
292286
/// @dev Each list can have multiple operations performed over time.
293-
mapping(uint256 => bytes[]) public listOps;
287+
mapping(uint256 slot => bytes[] listOps) public listOps;
294288

295289
///////////////////////////////////////////////////////////////////////////
296290
// List Operation Functions - Read
@@ -328,12 +322,8 @@ abstract contract ListRecordsV2 is IEFPListRecords, ListMetadata {
328322
}
329323

330324
bytes[] memory ops = new bytes[](end - start);
331-
for (uint256 i = start; i < end;) {
325+
for (uint256 i = start; i < end; ++i) {
332326
ops[i - start] = listOps[slot][i];
333-
334-
unchecked {
335-
++i;
336-
}
337327
}
338328
return ops;
339329
}
@@ -377,11 +367,8 @@ abstract contract ListRecordsV2 is IEFPListRecords, ListMetadata {
377367
*/
378368
function _applyListOps(uint256 slot, bytes[] calldata ops) internal {
379369
uint256 len = ops.length;
380-
for (uint256 i = 0; i < len;) {
370+
for (uint256 i = 0; i < len; ++i) {
381371
_applyListOp(slot, ops[i]);
382-
unchecked {
383-
++i;
384-
}
385372
}
386373
}
387374

0 commit comments

Comments
 (0)