Skip to content

Commit 42c94dd

Browse files
ryanioCodySearsOS
andcommitted
Sync from opensea-devtools
Origin-SHA: a5992e398695b4cb4d0660f72b90a4b4c1ffd4d5 Co-authored-by: Cody <132297275+CodySearsOS@users.noreply.github.com>
1 parent b5a33ce commit 42c94dd

9 files changed

Lines changed: 789 additions & 2 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77
[submodule "lib/create2-helpers"]
88
path = lib/create2-helpers
99
url = https://github.com/emo-eth/create2-helpers
10+
[submodule "lib/shipyard-core"]
11+
path = lib/shipyard-core
12+
url = https://github.com/ProjectOpenSea/shipyard-core

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ Formatter: 120 char line width, 4-space tabs, no bracket spacing.
2222
|------|------|
2323
| `src/interfaces/IToolRegistry.sol` | Canonical interface — all public functions the registry exposes |
2424
| `src/interfaces/IAccessPredicate.sol` | Predicate interface — `hasAccess()` + `name()` |
25+
| `src/interfaces/IRequirementTypes.sol` | Requirement-type selectors (`IERC721Holding`, `IERC1155Holding`, `ISubscription`) for predicate `kind` values |
2526
| `src/ToolRegistry.sol` | Reference implementation |
2627
| `examples/` | Canonical predicate implementations (ERC721, ERC1155, Subscription, Composite) |
2728
| `test/` | Foundry tests; mocks live in `test/mocks/` for edge-case predicate behavior |
2829
| `script/Deploy.s.sol` | Deterministic CREATE2 deployment script |
2930
| `scripts/generate-test-vectors.ts` | TypeScript helper to produce ABI-encoded test inputs (its own pnpm project) |
31+
| `scripts/check-abi-sync.sh` | CI script verifying TypeScript ABIs in `tool-sdk` match Forge build output |
3032
| ERC-8257 spec (currently in review: https://github.com/ethereum/ERCs/pull/1723) | Standard — code must mirror its interface IDs and behavior |
3133
| `lib/` | Foundry dependencies via git submodules (forge-std, OpenZeppelin, create2-helpers) |
3234

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,19 @@ Reference predicates under `examples/` (not part of the canonical ERC). All mult
5151
| `ERC1155OwnerPredicate.sol` | Account owns ≥1 of any configured `(collection, tokenId)` pair across up to 10 ERC-1155 collections |
5252
| `SubscriptionPredicate.sol` | NFT-tier-with-expiration subscription model |
5353
| `CompositePredicate.sol` | Combines up to 3 leaf `IAccessPredicate` contracts under AND-all / OR-any with optional per-term negation, fail-closed on sub-call failure |
54+
| `TraitGatedPredicate.sol` | ERC-721 ownership + ERC-7496 dynamic trait value match. Supports a separate traits contract (e.g. a renderer). Configurable trait key and up to 32 allowed values per tool |
5455

5556
## Deploy
5657

57-
`script/Deploy.s.sol` deploys `ToolRegistry`, `ERC721OwnerPredicate`, `ERC1155OwnerPredicate`, and `SubscriptionPredicate` deterministically via the Arachnid keyless CREATE2 factory (pre-deployed at `0x4e59...956C` on every major chain). Re-running with the same salt is a no-op once the address is occupied; swapping in `_SALT` for a vanity salt later deploys the new address on chains that haven't seen it without disturbing existing chains.
58+
`script/Deploy.s.sol` deploys `ToolRegistry`, `ERC721OwnerPredicate`, `ERC1155OwnerPredicate`, `SubscriptionPredicate`, and `TraitGatedPredicate` deterministically via the Arachnid keyless CREATE2 factory (pre-deployed at `0x4e59...956C` on every major chain). Re-running with the same salt is a no-op once the address is occupied; swapping in `_SALT` for a vanity salt later deploys the new address on chains that haven't seen it without disturbing existing chains.
59+
60+
To deploy **only the TraitGatedPredicate** (without redeploying other contracts), use the standalone script:
61+
62+
```bash
63+
REGISTRY=0x265BB2DBFC0A8165C9A1941Eb1372F349baD2cf1 \
64+
NETWORKS=base forge script script/DeployTraitGatedPredicate.s.sol --sig "run()" -vvv \
65+
--account beta-deployer --sender $DEPLOYER --broadcast --verify
66+
```
5867

5968
### Live addresses (pre-beta, salt `bytes32(uint256(1))`)
6069

@@ -66,6 +75,7 @@ Canonical v0.2 deployments — same CREATE2 address on every supported chain.
6675
| `ERC721OwnerPredicate` (v0.2) | [`0xc8721c9A776958FfFfEb602DA1b708bf1D318379`](https://etherscan.io/address/0xc8721c9a776958ffffeb602da1b708bf1d318379#code) | Ethereum mainnet, Base |
6776
| `ERC1155OwnerPredicate` (v0.2) | [`0x77373Dc3c1AE9A1e937eF3e5E08F4807D47c7c11`](https://etherscan.io/address/0x77373dc3c1ae9a1e937ef3e5e08f4807d47c7c11#code) | Ethereum mainnet, Base |
6877
| `SubscriptionPredicate` (v0.2) | [`0xCBe0cd9B1d99d95Baa9c58f2767246C52e461f25`](https://etherscan.io/address/0xcbe0cd9b1d99d95baa9c58f2767246c52e461f25#code) | Ethereum mainnet, Base |
78+
| `TraitGatedPredicate` (v0.2) | [`0x10abF07CfA34Bf22372C57f27e8bd9C2DCF93fA1`](https://etherscan.io/address/0x10abf07cfa34bf22372c57f27e8bd9c2dcf93fa1#code) | Ethereum mainnet, Base |
6979

7080
Each contract advertises its identity onchain via `name()` and `version()` (registry) or `name()` (predicates). See the EIP draft for the version-string format.
7181

examples/TraitGatedPredicate.sol

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.25;
3+
4+
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
5+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
6+
import {AccessRequirement, IAccessPredicate, RequirementLogic} from "../src/interfaces/IAccessPredicate.sol";
7+
import {IERC7496Trait} from "../src/interfaces/IRequirementTypes.sol";
8+
import {IToolRegistry} from "../src/interfaces/IToolRegistry.sol";
9+
10+
/// @notice Minimal interface for ERC-7496 Dynamic Traits.
11+
interface IERC7496 {
12+
function getTraitValue(uint256 tokenId, bytes32 traitKey) external view returns (bytes32);
13+
}
14+
15+
/// @title TraitGatedPredicate
16+
/// @notice IAccessPredicate that gates tool access on ERC-721 ownership plus
17+
/// an ERC-7496 dynamic trait value. The traits contract may differ
18+
/// from the NFT contract (e.g. a separate renderer).
19+
/// @dev The caller must pass `abi.encode(uint256 tokenId)` as the `data`
20+
/// argument to `hasAccess`.
21+
contract TraitGatedPredicate is IAccessPredicate, ERC165 {
22+
uint256 public constant MAX_ALLOWED_VALUES = 32;
23+
24+
struct ToolTraitConfig {
25+
address collection;
26+
address traitsContract;
27+
bytes32 traitKey;
28+
bytes32[] allowedValues;
29+
}
30+
31+
IToolRegistry public immutable REGISTRY;
32+
33+
mapping(uint256 => ToolTraitConfig) private _configs;
34+
35+
event ToolTraitConfigured(
36+
uint256 indexed toolId,
37+
address indexed collection,
38+
address traitsContract,
39+
bytes32 traitKey,
40+
bytes32[] allowedValues
41+
);
42+
43+
error CallerIsNotToolCreator(uint256 toolId, address caller);
44+
error ZeroCollection();
45+
error ZeroTraitsContract();
46+
error CollectionNoCode(address collection);
47+
error TraitsContractNoCode(address traitsContract);
48+
error EmptyAllowedValues();
49+
error TooManyAllowedValues(uint256 count, uint256 max);
50+
error ZeroValueInAllowedValues(uint256 index);
51+
52+
constructor(address _registry) {
53+
require(_registry != address(0), "TraitGatedPredicate: zero registry");
54+
REGISTRY = IToolRegistry(_registry);
55+
}
56+
57+
/// @notice Configure which collection, traits contract, trait key, and
58+
/// allowed values gate a tool. Replaces any previous configuration.
59+
/// @dev Only the tool's creator (as recorded in the registry) may call this.
60+
/// @param toolId The tool to configure.
61+
/// @param collection The ERC-721 collection (for `ownerOf` checks).
62+
/// @param traitsContract The contract implementing ERC-7496 `getTraitValue`.
63+
/// May be the same as `collection` or a separate renderer.
64+
/// @param traitKey The trait key to query (e.g. `bytes32("tier")`).
65+
/// @param allowedValues The set of trait values that grant access.
66+
function configureToolTrait(
67+
uint256 toolId,
68+
address collection,
69+
address traitsContract,
70+
bytes32 traitKey,
71+
bytes32[] calldata allowedValues
72+
) external {
73+
if (REGISTRY.getToolConfig(toolId).creator != msg.sender) {
74+
revert CallerIsNotToolCreator(toolId, msg.sender);
75+
}
76+
77+
if (collection == address(0)) revert ZeroCollection();
78+
if (traitsContract == address(0)) revert ZeroTraitsContract();
79+
if (collection.code.length == 0) revert CollectionNoCode(collection);
80+
if (traitsContract.code.length == 0) revert TraitsContractNoCode(traitsContract);
81+
if (allowedValues.length == 0) revert EmptyAllowedValues();
82+
if (allowedValues.length > MAX_ALLOWED_VALUES) {
83+
revert TooManyAllowedValues(allowedValues.length, MAX_ALLOWED_VALUES);
84+
}
85+
for (uint256 i; i < allowedValues.length; ++i) {
86+
if (allowedValues[i] == bytes32(0)) revert ZeroValueInAllowedValues(i);
87+
}
88+
89+
_configs[toolId] = ToolTraitConfig({
90+
collection: collection,
91+
traitsContract: traitsContract,
92+
traitKey: traitKey,
93+
allowedValues: allowedValues
94+
});
95+
emit ToolTraitConfigured(toolId, collection, traitsContract, traitKey, allowedValues);
96+
}
97+
98+
function getToolTraitConfig(uint256 toolId) external view returns (ToolTraitConfig memory) {
99+
return _configs[toolId];
100+
}
101+
102+
/// @param data Must be `abi.encode(uint256 tokenId)`.
103+
function hasAccess(uint256 toolId, address account, bytes calldata data) external view override returns (bool) {
104+
ToolTraitConfig storage config = _configs[toolId];
105+
if (config.collection == address(0)) return false;
106+
if (config.collection.code.length == 0) return false;
107+
if (data.length != 32) return false;
108+
109+
uint256 tokenId = abi.decode(data, (uint256));
110+
111+
try IERC721(config.collection).ownerOf(tokenId) returns (address owner) {
112+
if (owner != account) return false;
113+
} catch {
114+
return false;
115+
}
116+
117+
address traits = config.traitsContract;
118+
if (traits.code.length == 0) return false;
119+
120+
try IERC7496(traits).getTraitValue(tokenId, config.traitKey) returns (bytes32 value) {
121+
bytes32[] storage allowed = config.allowedValues;
122+
uint256 len = allowed.length;
123+
for (uint256 i; i < len; ++i) {
124+
if (value == allowed[i]) return true;
125+
}
126+
return false;
127+
} catch {
128+
return false;
129+
}
130+
}
131+
132+
/// @inheritdoc IAccessPredicate
133+
function getRequirements(uint256 toolId)
134+
external
135+
view
136+
override
137+
returns (AccessRequirement[] memory requirements, RequirementLogic logic)
138+
{
139+
ToolTraitConfig storage config = _configs[toolId];
140+
if (config.collection == address(0)) {
141+
requirements = new AccessRequirement[](0);
142+
} else {
143+
requirements = new AccessRequirement[](1);
144+
requirements[0] = AccessRequirement({
145+
kind: type(IERC7496Trait).interfaceId,
146+
data: abi.encode(config.collection, config.traitsContract, config.traitKey, config.allowedValues),
147+
label: ""
148+
});
149+
}
150+
logic = RequirementLogic.AND;
151+
}
152+
153+
function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
154+
return interfaceId == type(IAccessPredicate).interfaceId || super.supportsInterface(interfaceId);
155+
}
156+
157+
/// @inheritdoc IAccessPredicate
158+
function name() external pure returns (string memory) {
159+
return "TraitGatedPredicate";
160+
}
161+
162+
function version() external pure returns (string memory) {
163+
return "0.1";
164+
}
165+
}

remappings.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
22
create2-helpers/=lib/create2-helpers/
3+
shipyard-core/=lib/shipyard-core/src/

script/Deploy.s.sol

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {ToolRegistry} from "../src/ToolRegistry.sol";
77
import {ERC721OwnerPredicate} from "../examples/ERC721OwnerPredicate.sol";
88
import {ERC1155OwnerPredicate} from "../examples/ERC1155OwnerPredicate.sol";
99
import {SubscriptionPredicate} from "../examples/SubscriptionPredicate.sol";
10+
import {TraitGatedPredicate} from "../examples/TraitGatedPredicate.sol";
1011

1112
/// @title Deploy
1213
/// @notice Deterministic CREATE2 deployment of `ToolRegistry` and the example
1314
/// `ERC721OwnerPredicate` / `ERC1155OwnerPredicate` /
14-
/// `SubscriptionPredicate` predicates via the Arachnid keyless factory.
15+
/// `SubscriptionPredicate` / `TraitGatedPredicate` predicates via the
16+
/// Arachnid keyless factory.
1517
/// @dev Deploys are idempotent: re-running with the same salt is a no-op once
1618
/// the address is occupied. Swap `_SALT` for a vanity salt later and
1719
/// re-run — the script will deploy at the new address on chains that
@@ -51,5 +53,10 @@ contract Deploy is BaseCreate2Script {
5153
abi.encodePacked(type(SubscriptionPredicate).creationCode, abi.encode(registryAddr));
5254
address predicateSubscription = _create2IfNotDeployed(deployer, _SALT, predicateSubscriptionInitCode);
5355
console2.log("SubscriptionPredicate: ", predicateSubscription);
56+
57+
bytes memory predicateTraitInitCode =
58+
abi.encodePacked(type(TraitGatedPredicate).creationCode, abi.encode(registryAddr));
59+
address predicateTrait = _create2IfNotDeployed(deployer, _SALT, predicateTraitInitCode);
60+
console2.log("TraitGatedPredicate: ", predicateTrait);
5461
}
5562
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.25;
3+
4+
import {BaseCreate2Script} from "create2-helpers/src/BaseCreate2Script.sol";
5+
import {console2} from "forge-std/console2.sol";
6+
import {TraitGatedPredicate} from "../examples/TraitGatedPredicate.sol";
7+
8+
/// @title DeployTraitGatedPredicate
9+
/// @notice Standalone deterministic CREATE2 deployment of `TraitGatedPredicate`
10+
/// against an existing `ToolRegistry`. Use this to deploy the predicate
11+
/// without redeploying the registry or other predicates.
12+
/// @dev Requires REGISTRY env var pointing to the deployed ToolRegistry address.
13+
/// Idempotent: re-running with the same salt is a no-op once the address
14+
/// is occupied.
15+
contract DeployTraitGatedPredicate is BaseCreate2Script {
16+
bytes32 private constant _SALT = bytes32(uint256(1));
17+
18+
function run() public {
19+
runOnNetworks(deploy, vm.envString("NETWORKS", ","));
20+
}
21+
22+
function deploy() public returns (address predicateAddr) {
23+
address registry = vm.envAddress("REGISTRY");
24+
console2.log("Using ToolRegistry: ", registry);
25+
26+
bytes memory initCode = abi.encodePacked(type(TraitGatedPredicate).creationCode, abi.encode(registry));
27+
predicateAddr = _create2IfNotDeployed(deployer, _SALT, initCode);
28+
console2.log("TraitGatedPredicate: ", predicateAddr);
29+
}
30+
}

src/interfaces/IRequirementTypes.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ interface IERC1155Holding {
2323
interface ISubscription {
2424
function subscription() external;
2525
}
26+
27+
/// @dev kind for ERC-7496 dynamic trait requirements.
28+
/// data = abi.encode(address collection, address traitsContract, bytes32 traitKey, bytes32[] allowedValues)
29+
interface IERC7496Trait {
30+
function erc7496Trait() external;
31+
}

0 commit comments

Comments
 (0)