Commit 39c3b5b6 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

contracts: remove op-nft (#12950)

* contracts: remove op-nft

Remove the `op-nft` package from the contracts package. None of these
contracts are maintained and can be moved to another repo if necessary.
The purpose of this commit is to reduce compilation time. Less files
to compile means less compilation time.

* fix: semver lock step - ran pre-pr.

* fix: use added/modified filter for heavy fuzz

Updates the heavy fuzz filter for CI to only check for added or
modified contracts, now excluding deleted or moved contracts.

---------
Co-authored-by: default avatarBlaine Malone <blainemalone01@gmail.com>
Co-authored-by: default avatarKelvin Fichter <kelvinfichter@gmail.com>
parent f496b075
...@@ -1359,7 +1359,7 @@ workflows: ...@@ -1359,7 +1359,7 @@ workflows:
# Heavily fuzz any fuzz tests within added or modified test files. # Heavily fuzz any fuzz tests within added or modified test files.
name: contracts-bedrock-tests-heavy-fuzz-modified name: contracts-bedrock-tests-heavy-fuzz-modified
test_parallelism: 1 test_parallelism: 1
test_list: git diff origin/develop...HEAD --name-only -- './test/**/*.t.sol' | sed 's|packages/contracts-bedrock/||' test_list: git diff origin/develop...HEAD --name-only --diff-filter=AM -- './test/**/*.t.sol' | sed 's|packages/contracts-bedrock/||'
test_timeout: 1h test_timeout: 1h
test_profile: ciheavy test_profile: ciheavy
- contracts-bedrock-coverage - contracts-bedrock-coverage
......
[
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "about",
"type": "address"
},
{
"internalType": "bytes32",
"name": "key",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "val",
"type": "bytes"
}
],
"internalType": "struct AttestationStation.AttestationData[]",
"name": "_attestations",
"type": "tuple[]"
}
],
"name": "attest",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_about",
"type": "address"
},
{
"internalType": "bytes32",
"name": "_key",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "_val",
"type": "bytes"
}
],
"name": "attest",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "attestations",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "about",
"type": "address"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "key",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "bytes",
"name": "val",
"type": "bytes"
}
],
"name": "AttestationCreated",
"type": "event"
}
]
\ No newline at end of file
[
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_symbol",
"type": "string"
},
{
"internalType": "address",
"name": "_baseURIAttestor",
"type": "address"
},
{
"internalType": "contract AttestationStation",
"name": "_attestationStation",
"type": "address"
},
{
"internalType": "contract OptimistAllowlist",
"name": "_optimistAllowlist",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "ATTESTATION_STATION",
"outputs": [
{
"internalType": "contract AttestationStation",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "BASE_URI_ATTESTATION_KEY",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "BASE_URI_ATTESTOR",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "OPTIMIST_ALLOWLIST",
"outputs": [
{
"internalType": "contract OptimistAllowlist",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "baseURI",
"outputs": [
{
"internalType": "string",
"name": "uri_",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "burn",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_symbol",
"type": "string"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_recipient",
"type": "address"
}
],
"name": "isOnAllowList",
"outputs": [
{
"internalType": "bool",
"name": "allowed_",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_recipient",
"type": "address"
}
],
"name": "mint",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "tokenIdOfAddress",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "uri_",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint8",
"name": "version",
"type": "uint8"
}
],
"name": "Initialized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
}
]
\ No newline at end of file
[
{
"inputs": [
{
"internalType": "contract AttestationStation",
"name": "_attestationStation",
"type": "address"
},
{
"internalType": "address",
"name": "_allowlistAttestor",
"type": "address"
},
{
"internalType": "address",
"name": "_coinbaseQuestAttestor",
"type": "address"
},
{
"internalType": "address",
"name": "_optimistInviter",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "ALLOWLIST_ATTESTOR",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ATTESTATION_STATION",
"outputs": [
{
"internalType": "contract AttestationStation",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "COINBASE_QUEST_ATTESTOR",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "COINBASE_QUEST_ELIGIBLE_ATTESTATION_KEY",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "OPTIMIST_CAN_MINT_ATTESTATION_KEY",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "OPTIMIST_INVITER",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_claimer",
"type": "address"
}
],
"name": "isAllowedToMint",
"outputs": [
{
"internalType": "bool",
"name": "allowed_",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
}
]
\ No newline at end of file
[
{
"inputs": [
{
"internalType": "address",
"name": "_inviteGranter",
"type": "address"
},
{
"internalType": "contract AttestationStation",
"name": "_attestationStation",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "ATTESTATION_STATION",
"outputs": [
{
"internalType": "contract AttestationStation",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CAN_INVITE_ATTESTATION_KEY",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CLAIMABLE_INVITE_TYPEHASH",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "EIP712_VERSION",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "INVITE_GRANTER",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MIN_COMMITMENT_PERIOD",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_claimer",
"type": "address"
},
{
"components": [
{
"internalType": "address",
"name": "issuer",
"type": "address"
},
{
"internalType": "bytes32",
"name": "nonce",
"type": "bytes32"
}
],
"internalType": "struct OptimistInviter.ClaimableInvite",
"name": "_claimableInvite",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "_signature",
"type": "bytes"
}
],
"name": "claimInvite",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "_commitment",
"type": "bytes32"
}
],
"name": "commitInvite",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "commitmentTimestamps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "inviteCounts",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "_accounts",
"type": "address[]"
},
{
"internalType": "uint256",
"name": "_inviteCount",
"type": "uint256"
}
],
"name": "setInviteCounts",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "usedNonces",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "version",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint8",
"name": "version",
"type": "uint8"
}
],
"name": "Initialized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "issuer",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "claimer",
"type": "address"
}
],
"name": "InviteClaimed",
"type": "event"
}
]
\ No newline at end of file
...@@ -183,22 +183,6 @@ ...@@ -183,22 +183,6 @@
"initCodeHash": "0xefc6ed9e325c2d614ea0d28c3eabfff1b345f7c6054e90253c6a091c29508267", "initCodeHash": "0xefc6ed9e325c2d614ea0d28c3eabfff1b345f7c6054e90253c6a091c29508267",
"sourceCodeHash": "0xaa08a61448f485b277af57251d2089cc6a80ce0a763bf7184d48ffed5034ef69" "sourceCodeHash": "0xaa08a61448f485b277af57251d2089cc6a80ce0a763bf7184d48ffed5034ef69"
}, },
"src/periphery/op-nft/AttestationStation.sol": {
"initCodeHash": "0x2e665d9ee554430980f64bcb6d2611a1cb03dbacfd58bb0d6f5d32951a267bde",
"sourceCodeHash": "0xe0bc805b22c7d04b5a9444cddd4c0e1bcb3006c69c03610494277ab2cc83f553"
},
"src/periphery/op-nft/Optimist.sol": {
"initCodeHash": "0x8fccdef5fb6e6d51215b39acc449faad8ba15416699c9b3af77866f4297805a3",
"sourceCodeHash": "0xfa9354827b642803e10415ed30ca789be1bd23d88fac14f7adaa65c6eb1c1643"
},
"src/periphery/op-nft/OptimistAllowlist.sol": {
"initCodeHash": "0x166dd3fc18cb238895f2faa7fdd635af48ce2c54e21ed2d6dae857c3731c4d6c",
"sourceCodeHash": "0x3a5f61046f729c9a70274b8b2a739382987ec5eb77705b259e8a3210a5f43462"
},
"src/periphery/op-nft/OptimistInviter.sol": {
"initCodeHash": "0x28dfa6676702a7abd19609cc773158d1f958210bc0a38c008d67a002dc1df862",
"sourceCodeHash": "0x3a0a294932d6deba043f6a2b46b4e8477ee96e7fb054d7e7229a43ce4352c68d"
},
"src/safe/DeputyGuardianModule.sol": { "src/safe/DeputyGuardianModule.sol": {
"initCodeHash": "0xd95e562f395d4eb6e332f4474dffab660ada9e9da7c79f58fb6052278e0904df", "initCodeHash": "0xd95e562f395d4eb6e332f4474dffab660ada9e9da7c79f58fb6052278e0904df",
"sourceCodeHash": "0x45daabe094de0287e244e6fea4f1887b9adc09b07c47dc77361b1678645a1470" "sourceCodeHash": "0x45daabe094de0287e244e6fea4f1887b9adc09b07c47dc77361b1678645a1470"
......
[
{
"bytes": "32",
"label": "attestations",
"offset": 0,
"slot": "0",
"type": "mapping(address => mapping(address => mapping(bytes32 => bytes)))"
}
]
\ No newline at end of file
[
{
"bytes": "1",
"label": "_initialized",
"offset": 0,
"slot": "0",
"type": "uint8"
},
{
"bytes": "1",
"label": "_initializing",
"offset": 1,
"slot": "0",
"type": "bool"
},
{
"bytes": "1600",
"label": "__gap",
"offset": 0,
"slot": "1",
"type": "uint256[50]"
},
{
"bytes": "1600",
"label": "__gap",
"offset": 0,
"slot": "51",
"type": "uint256[50]"
},
{
"bytes": "32",
"label": "_name",
"offset": 0,
"slot": "101",
"type": "string"
},
{
"bytes": "32",
"label": "_symbol",
"offset": 0,
"slot": "102",
"type": "string"
},
{
"bytes": "32",
"label": "_owners",
"offset": 0,
"slot": "103",
"type": "mapping(uint256 => address)"
},
{
"bytes": "32",
"label": "_balances",
"offset": 0,
"slot": "104",
"type": "mapping(address => uint256)"
},
{
"bytes": "32",
"label": "_tokenApprovals",
"offset": 0,
"slot": "105",
"type": "mapping(uint256 => address)"
},
{
"bytes": "32",
"label": "_operatorApprovals",
"offset": 0,
"slot": "106",
"type": "mapping(address => mapping(address => bool))"
},
{
"bytes": "1408",
"label": "__gap",
"offset": 0,
"slot": "107",
"type": "uint256[44]"
},
{
"bytes": "1600",
"label": "__gap",
"offset": 0,
"slot": "151",
"type": "uint256[50]"
}
]
\ No newline at end of file
[
{
"bytes": "1",
"label": "_initialized",
"offset": 0,
"slot": "0",
"type": "uint8"
},
{
"bytes": "1",
"label": "_initializing",
"offset": 1,
"slot": "0",
"type": "bool"
},
{
"bytes": "32",
"label": "_HASHED_NAME",
"offset": 0,
"slot": "1",
"type": "bytes32"
},
{
"bytes": "32",
"label": "_HASHED_VERSION",
"offset": 0,
"slot": "2",
"type": "bytes32"
},
{
"bytes": "1600",
"label": "__gap",
"offset": 0,
"slot": "3",
"type": "uint256[50]"
},
{
"bytes": "32",
"label": "commitmentTimestamps",
"offset": 0,
"slot": "53",
"type": "mapping(bytes32 => uint256)"
},
{
"bytes": "32",
"label": "usedNonces",
"offset": 0,
"slot": "54",
"type": "mapping(address => mapping(bytes32 => bool))"
},
{
"bytes": "32",
"label": "inviteCounts",
"offset": 0,
"slot": "55",
"type": "mapping(address => uint256)"
}
]
\ No newline at end of file
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { ISemver } from "src/universal/interfaces/ISemver.sol";
/// @title AttestationStation
/// @author Optimism Collective
/// @author Gitcoin
/// @notice Where attestations live.
contract AttestationStation is ISemver {
/// @notice Struct representing data that is being attested.
/// @custom:field about Address for which the attestation is about.
/// @custom:field key A bytes32 key for the attestation.
/// @custom:field val The attestation as arbitrary bytes.
struct AttestationData {
address about;
bytes32 key;
bytes val;
}
/// @notice Maps addresses to attestations. Creator => About => Key => Value.
mapping(address => mapping(address => mapping(bytes32 => bytes))) public attestations;
/// @notice Emitted when Attestation is created.
/// @param creator Address that made the attestation.
/// @param about Address attestation is about.
/// @param key Key of the attestation.
/// @param val Value of the attestation.
event AttestationCreated(address indexed creator, address indexed about, bytes32 indexed key, bytes val);
/// @notice Semantic version.
/// @custom:semver 1.2.1-beta.1
string public constant version = "1.2.1-beta.1";
/// @notice Allows anyone to create an attestation.
/// @param _about Address that the attestation is about.
/// @param _key A key used to namespace the attestation.
/// @param _val An arbitrary value stored as part of the attestation.
function attest(address _about, bytes32 _key, bytes memory _val) public {
attestations[msg.sender][_about][_key] = _val;
emit AttestationCreated(msg.sender, _about, _key, _val);
}
/// @notice Allows anyone to create attestations.
/// @param _attestations An array of AttestationData structs.
function attest(AttestationData[] calldata _attestations) external {
uint256 length = _attestations.length;
for (uint256 i = 0; i < length;) {
AttestationData memory attestation = _attestations[i];
attest(attestation.about, attestation.key, attestation.val);
unchecked {
++i;
}
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { ISemver } from "src/universal/interfaces/ISemver.sol";
import { ERC721BurnableUpgradeable } from
"@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import { AttestationStation } from "src/periphery/op-nft/AttestationStation.sol";
import { OptimistAllowlist } from "src/periphery/op-nft/OptimistAllowlist.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
/// @author Optimism Collective
/// @author Gitcoin
/// @title Optimist
/// @notice A Soul Bound Token for real humans only(tm).
contract Optimist is ERC721BurnableUpgradeable, ISemver {
/// @notice Attestation key used by the attestor to attest the baseURI.
bytes32 public constant BASE_URI_ATTESTATION_KEY = bytes32("optimist.base-uri");
/// @notice Attestor who attests to baseURI.
address public immutable BASE_URI_ATTESTOR;
/// @notice Address of the AttestationStation contract.
AttestationStation public immutable ATTESTATION_STATION;
/// @notice Address of the OptimistAllowlist contract.
OptimistAllowlist public immutable OPTIMIST_ALLOWLIST;
/// @notice Semantic version.
/// @custom:semver 2.1.1-beta.1
string public constant version = "2.1.1-beta.1";
/// @param _name Token name.
/// @param _symbol Token symbol.
/// @param _baseURIAttestor Address of the baseURI attestor.
/// @param _attestationStation Address of the AttestationStation contract.
/// @param _optimistAllowlist Address of the OptimistAllowlist contract
constructor(
string memory _name,
string memory _symbol,
address _baseURIAttestor,
AttestationStation _attestationStation,
OptimistAllowlist _optimistAllowlist
) {
BASE_URI_ATTESTOR = _baseURIAttestor;
ATTESTATION_STATION = _attestationStation;
OPTIMIST_ALLOWLIST = _optimistAllowlist;
initialize(_name, _symbol);
}
/// @notice Initializes the Optimist contract.
/// @param _name Token name.
/// @param _symbol Token symbol.
function initialize(string memory _name, string memory _symbol) public initializer {
__ERC721_init(_name, _symbol);
__ERC721Burnable_init();
}
/// @notice Allows an address to mint an Optimist NFT. Token ID is the uint256 representation
/// of the recipient's address. Recipients must be permitted to mint, eventually anyone
/// will be able to mint. One token per address.
/// @param _recipient Address of the token recipient.
function mint(address _recipient) public {
require(isOnAllowList(_recipient), "Optimist: address is not on allowList");
_safeMint(_recipient, tokenIdOfAddress(_recipient));
}
/// @notice Returns the baseURI for all tokens.
/// @return uri_ BaseURI for all tokens.
function baseURI() public view returns (string memory uri_) {
uri_ = string(
abi.encodePacked(
ATTESTATION_STATION.attestations(BASE_URI_ATTESTOR, address(this), bytes32("optimist.base-uri"))
)
);
}
/// @notice Returns the token URI for a given token by ID
/// @param _tokenId Token ID to query.
/// @return uri_ Token URI for the given token by ID.
function tokenURI(uint256 _tokenId) public view virtual override returns (string memory uri_) {
uri_ = string(
abi.encodePacked(
baseURI(),
"/",
// Properly format the token ID as a 20 byte hex string (address).
Strings.toHexString(_tokenId, 20),
".json"
)
);
}
/// @notice Checks OptimistAllowlist to determine whether a given address is allowed to mint
/// the Optimist NFT. Since the Optimist NFT will also be used as part of the
/// Citizens House, mints are currently restricted. Eventually anyone will be able
/// to mint.
/// @return allowed_ Whether or not the address is allowed to mint yet.
function isOnAllowList(address _recipient) public view returns (bool allowed_) {
allowed_ = OPTIMIST_ALLOWLIST.isAllowedToMint(_recipient);
}
/// @notice Returns the token ID for the token owned by a given address. This is the uint256
/// representation of the given address.
/// @return Token ID for the token owned by the given address.
function tokenIdOfAddress(address _owner) public pure returns (uint256) {
return uint256(uint160(_owner));
}
/// @notice Disabled for the Optimist NFT (Soul Bound Token).
function approve(address, uint256) public pure override {
revert("Optimist: soul bound token");
}
/// @notice Disabled for the Optimist NFT (Soul Bound Token).
function setApprovalForAll(address, bool) public virtual override {
revert("Optimist: soul bound token");
}
/// @notice Prevents transfers of the Optimist NFT (Soul Bound Token).
/// @param _from Address of the token sender.
/// @param _to Address of the token recipient.
function _beforeTokenTransfer(address _from, address _to, uint256) internal virtual override {
require(_from == address(0) || _to == address(0), "Optimist: soul bound token");
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { ISemver } from "src/universal/interfaces/ISemver.sol";
import { AttestationStation } from "src/periphery/op-nft/AttestationStation.sol";
import { OptimistConstants } from "src/periphery/op-nft/libraries/OptimistConstants.sol";
/// @title OptimistAllowlist
/// @notice Source of truth for whether an address is able to mint an Optimist NFT.
/// isAllowedToMint function checks various signals to return boolean value
/// for whether an address is eligible or not.
contract OptimistAllowlist is ISemver {
/// @notice Attestation key used by the AllowlistAttestor to manually add addresses to the
/// allowlist.
bytes32 public constant OPTIMIST_CAN_MINT_ATTESTATION_KEY = bytes32("optimist.can-mint");
/// @notice Attestation key used by Coinbase to issue attestations for Quest participants.
bytes32 public constant COINBASE_QUEST_ELIGIBLE_ATTESTATION_KEY = bytes32("coinbase.quest-eligible");
/// @notice Address of the AttestationStation contract.
AttestationStation public immutable ATTESTATION_STATION;
/// @notice Attestor that issues 'optimist.can-mint' attestations.
address public immutable ALLOWLIST_ATTESTOR;
/// @notice Attestor that issues 'coinbase.quest-eligible' attestations.
address public immutable COINBASE_QUEST_ATTESTOR;
/// @notice Address of OptimistInviter contract that issues 'optimist.can-mint-from-invite'
/// attestations.
address public immutable OPTIMIST_INVITER;
/// @notice Semantic version.
/// @custom:semver 1.1.1-beta.1
string public constant version = "1.1.1-beta.1";
/// @param _attestationStation Address of the AttestationStation contract.
/// @param _allowlistAttestor Address of the allowlist attestor.
/// @param _coinbaseQuestAttestor Address of the Coinbase Quest attestor.
/// @param _optimistInviter Address of the OptimistInviter contract.
constructor(
AttestationStation _attestationStation,
address _allowlistAttestor,
address _coinbaseQuestAttestor,
address _optimistInviter
) {
ATTESTATION_STATION = _attestationStation;
ALLOWLIST_ATTESTOR = _allowlistAttestor;
COINBASE_QUEST_ATTESTOR = _coinbaseQuestAttestor;
OPTIMIST_INVITER = _optimistInviter;
}
/// @notice Checks whether a given address is allowed to mint the Optimist NFT yet. Since the
/// Optimist NFT will also be used as part of the Citizens House, mints are currently
/// restricted. Eventually anyone will be able to mint.
/// Currently, address is allowed to mint if it satisfies any of the following:
/// 1) Has a valid 'optimist.can-mint' attestation from the allowlist attestor.
/// 2) Has a valid 'coinbase.quest-eligible' attestation from Coinbase Quest attestor
/// 3) Has a valid 'optimist.can-mint-from-invite' attestation from the OptimistInviter
/// contract.
/// @param _claimer Address to check.
/// @return allowed_ Whether or not the address is allowed to mint yet.
function isAllowedToMint(address _claimer) public view returns (bool allowed_) {
allowed_ = _hasAttestationFromAllowlistAttestor(_claimer) || _hasAttestationFromCoinbaseQuestAttestor(_claimer)
|| _hasAttestationFromOptimistInviter(_claimer);
}
/// @notice Checks whether an address has a valid 'optimist.can-mint' attestation from the
/// allowlist attestor.
/// @param _claimer Address to check.
/// @return valid_ Whether or not the address has a valid attestation.
function _hasAttestationFromAllowlistAttestor(address _claimer) internal view returns (bool valid_) {
// Expected attestation value is bytes32("true")
valid_ = _hasValidAttestation(ALLOWLIST_ATTESTOR, _claimer, OPTIMIST_CAN_MINT_ATTESTATION_KEY);
}
/// @notice Checks whether an address has a valid attestation from the Coinbase attestor.
/// @param _claimer Address to check.
/// @return valid_ Whether or not the address has a valid attestation.
function _hasAttestationFromCoinbaseQuestAttestor(address _claimer) internal view returns (bool valid_) {
// Expected attestation value is bytes32("true")
valid_ = _hasValidAttestation(COINBASE_QUEST_ATTESTOR, _claimer, COINBASE_QUEST_ELIGIBLE_ATTESTATION_KEY);
}
/// @notice Checks whether an address has a valid attestation from the OptimistInviter contract.
/// @param _claimer Address to check.
/// @return valid_ Whether or not the address has a valid attestation.
function _hasAttestationFromOptimistInviter(address _claimer) internal view returns (bool valid_) {
// Expected attestation value is the inviter's address
valid_ = _hasValidAttestation(
OPTIMIST_INVITER, _claimer, OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY
);
}
/// @notice Checks whether an address has a valid truthy attestation.
/// Any attestation val other than bytes32("") is considered truthy.
/// @param _creator Address that made the attestation.
/// @param _about Address attestation is about.
/// @param _key Key of the attestation.
/// @return valid_ Whether or not the address has a valid truthy attestation.
function _hasValidAttestation(address _creator, address _about, bytes32 _key) internal view returns (bool valid_) {
valid_ = ATTESTATION_STATION.attestations(_creator, _about, _key).length > 0;
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { OptimistConstants } from "src/periphery/op-nft/libraries/OptimistConstants.sol";
import { ISemver } from "src/universal/interfaces/ISemver.sol";
import { AttestationStation } from "src/periphery/op-nft/AttestationStation.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol";
/// @custom:upgradeable
/// @title OptimistInviter
/// @notice OptimistInviter issues "optimist.can-invite" and "optimist.can-mint-from-invite"
/// attestations. Accounts that have invites can issue signatures that allow other
/// accounts to claim an invite. The invitee uses a claim and reveal flow to claim the
/// invite to an address of their choosing.
///
/// Parties involved:
/// 1) INVITE_GRANTER: trusted account that can allow accounts to issue invites
/// 2) issuer: account that is allowed to issue invites
/// 3) claimer: account that receives the invites
///
/// Flow:
/// 1) INVITE_GRANTER calls _setInviteCount to allow an issuer to issue a certain number
/// of invites, and also creates a "optimist.can-invite" attestation for the issuer
/// 2) Off-chain, the issuer signs (EIP-712) a ClaimableInvite to produce a signature
/// 3) Off-chain, invite issuer sends the plaintext ClaimableInvite and the signature
/// to the recipient
/// 4) claimer chooses an address they want to receive the invite on
/// 5) claimer commits the hash of the address they want to receive the invite on and the
/// received signature keccak256(abi.encode(addressToReceiveTo, receivedSignature))
/// using the commitInvite function
/// 6) claimer waits for the MIN_COMMITMENT_PERIOD to pass.
/// 7) claimer reveals the plaintext ClaimableInvite and the signature using the
/// claimInvite function, receiving the "optimist.can-mint-from-invite" attestation
contract OptimistInviter is ISemver, EIP712Upgradeable {
/// @notice Emitted when an invite is claimed.
/// @param issuer Address that issued the signature.
/// @param claimer Address that claimed the invite.
event InviteClaimed(address indexed issuer, address indexed claimer);
/// @notice Version used for the EIP712 domain separator. This version is separated from the
/// contract semver because the EIP712 domain separator is used to sign messages, and
/// changing the domain separator invalidates all existing signatures. We should only
/// bump this version if we make a major change to the signature scheme.
string public constant EIP712_VERSION = "1.0.0";
/// @notice EIP712 typehash for the ClaimableInvite type.
bytes32 public constant CLAIMABLE_INVITE_TYPEHASH = keccak256("ClaimableInvite(address issuer,bytes32 nonce)");
/// @notice Attestation key for that signals that an account was allowed to issue invites
bytes32 public constant CAN_INVITE_ATTESTATION_KEY = bytes32("optimist.can-invite");
/// @notice Granter who can set accounts' invite counts.
address public immutable INVITE_GRANTER;
/// @notice Address of the AttestationStation contract.
AttestationStation public immutable ATTESTATION_STATION;
/// @notice Minimum age of a commitment (in seconds) before it can be revealed using
/// claimInvite. Currently set to 60 seconds.
///
/// Prevents an attacker from front-running a commitment by taking the signature in the
/// claimInvite call and quickly committing and claiming it before the the claimer's
/// transaction succeeds. With this, frontrunning a commitment requires that an attacker
/// be able to prevent the honest claimer's claimInvite transaction from being included
/// for this long.
uint256 public constant MIN_COMMITMENT_PERIOD = 60;
/// @notice Struct that represents a claimable invite that will be signed by the issuer.
/// @custom:field issuer Address that issued the signature. Reason this is explicitly included,
/// and not implicitly assumed to be the recovered address from the
/// signature is that the issuer may be using a ERC-1271 compatible
/// contract wallet, where the recovered address is not the same as the
/// issuer, or the signature is not an ECDSA signature at all.
/// @custom:field nonce Pseudorandom nonce to prevent replay attacks.
struct ClaimableInvite {
address issuer;
bytes32 nonce;
}
/// @notice Maps from hashes to the timestamp when they were committed.
mapping(bytes32 => uint256) public commitmentTimestamps;
/// @notice Maps from addresses to nonces to whether or not they have been used.
mapping(address => mapping(bytes32 => bool)) public usedNonces;
/// @notice Maps from addresses to number of invites they have.
mapping(address => uint256) public inviteCounts;
/// @notice Semantic version.
/// @custom:semver 1.1.1-beta.1
string public constant version = "1.1.1-beta.1";
/// @param _inviteGranter Address of the invite granter.
/// @param _attestationStation Address of the AttestationStation contract.
constructor(address _inviteGranter, AttestationStation _attestationStation) {
INVITE_GRANTER = _inviteGranter;
ATTESTATION_STATION = _attestationStation;
}
/// @notice Initializes this contract, setting the EIP712 context.
/// Only update the EIP712_VERSION when there is a change to the signature scheme.
/// After the EIP712 version is changed, any signatures issued off-chain but not
/// claimed yet will no longer be accepted by the claimInvite function. Please make
/// sure to notify the issuers that they must re-issue their invite signatures.
/// @param _name Contract name.
function initialize(string memory _name) public initializer {
__EIP712_init(_name, EIP712_VERSION);
}
/// @notice Allows invite granter to set the number of invites an address has.
/// @param _accounts An array of accounts to update the invite counts of.
/// @param _inviteCount Number of invites to set to.
function setInviteCounts(address[] calldata _accounts, uint256 _inviteCount) public {
// Only invite granter can grant invites
require(msg.sender == INVITE_GRANTER, "OptimistInviter: only invite granter can grant invites");
uint256 length = _accounts.length;
AttestationStation.AttestationData[] memory attestations = new AttestationStation.AttestationData[](length);
for (uint256 i; i < length;) {
// Set invite count for account to _inviteCount
inviteCounts[_accounts[i]] = _inviteCount;
// Create an attestation for posterity that the account is allowed to create invites
attestations[i] = AttestationStation.AttestationData({
about: _accounts[i],
key: CAN_INVITE_ATTESTATION_KEY,
val: bytes("true")
});
unchecked {
++i;
}
}
ATTESTATION_STATION.attest(attestations);
}
/// @notice Allows anyone (but likely the claimer) to commit a received signature along with the
/// address to claim to.
///
/// Before calling this function, the claimer should have received a signature from the
/// issuer off-chain. The claimer then calls this function with the hash of the
/// claimer's address and the received signature. This is necessary to prevent
/// front-running when the invitee is claiming the invite. Without a commit and reveal
/// scheme, anyone who is watching the mempool can take the signature being submitted
/// and front run the transaction to claim the invite to their own address.
///
/// The same commitment can only be made once, and the function reverts if the
/// commitment has already been made. This prevents griefing where a malicious party can
/// prevent the original claimer from being able to claimInvite.
/// @param _commitment A hash of the claimer and signature concatenated.
/// keccak256(abi.encode(_claimer, _signature))
function commitInvite(bytes32 _commitment) public {
// Check that the commitment hasn't already been made. This prevents griefing where
// a malicious party continuously re-submits the same commitment, preventing the original
// claimer from claiming their invite by resetting the minimum commitment period.
require(commitmentTimestamps[_commitment] == 0, "OptimistInviter: commitment already made");
commitmentTimestamps[_commitment] = block.timestamp;
}
/// @notice Allows anyone to reveal a commitment and claim an invite.
/// The hash, keccak256(abi.encode(_claimer, _signature)), should have been already
/// committed using commitInvite. Before issuing the "optimist.can-mint-from-invite"
/// attestation, this function checks that
/// 1) the hash corresponding to the _claimer and the _signature was committed
/// 2) MIN_COMMITMENT_PERIOD has passed since the commitment was made.
/// 3) the _signature is signed correctly by the issuer
/// 4) the _signature hasn't already been used to claim an invite before
/// 5) the _signature issuer has not used up all of their invites
/// This function doesn't require that the _claimer is calling this function.
/// @param _claimer Address that will be granted the invite.
/// @param _claimableInvite ClaimableInvite struct containing the issuer and nonce.
/// @param _signature Signature signed over the claimable invite.
function claimInvite(address _claimer, ClaimableInvite calldata _claimableInvite, bytes memory _signature) public {
uint256 commitmentTimestamp = commitmentTimestamps[keccak256(abi.encode(_claimer, _signature))];
// Make sure the claimer and signature have been committed.
require(commitmentTimestamp > 0, "OptimistInviter: claimer and signature have not been committed yet");
// Check that MIN_COMMITMENT_PERIOD has passed since the commitment was made.
require(
commitmentTimestamp + MIN_COMMITMENT_PERIOD <= block.timestamp,
"OptimistInviter: minimum commitment period has not elapsed yet"
);
// Generate a EIP712 typed data hash to compare against the signature.
bytes32 digest = _hashTypedDataV4(
keccak256(abi.encode(CLAIMABLE_INVITE_TYPEHASH, _claimableInvite.issuer, _claimableInvite.nonce))
);
// Uses SignatureChecker, which supports both regular ECDSA signatures from EOAs as well as
// ERC-1271 signatures from contract wallets or multi-sigs. This means that if the issuer
// wants to revoke a signature, they can use a smart contract wallet to issue the signature,
// then invalidate the signature after issuing it.
require(
SignatureChecker.isValidSignatureNow(_claimableInvite.issuer, digest, _signature),
"OptimistInviter: invalid signature"
);
// The issuer's signature commits to a nonce to prevent replay attacks.
// This checks that the nonce has not been used for this issuer before. The nonces are
// scoped to the issuer address, so the same nonce can be used by different issuers without
// clashing.
require(
usedNonces[_claimableInvite.issuer][_claimableInvite.nonce] == false,
"OptimistInviter: nonce has already been used"
);
// Set the nonce as used for the issuer so that it cannot be replayed.
usedNonces[_claimableInvite.issuer][_claimableInvite.nonce] = true;
// Failing this check means that the issuer has used up all of their existing invites.
require(inviteCounts[_claimableInvite.issuer] > 0, "OptimistInviter: issuer has no invites");
// Reduce the issuer's invite count by 1. Can be unchecked because we check above that
// count is > 0.
unchecked {
--inviteCounts[_claimableInvite.issuer];
}
// Create the attestation that the claimer can mint from the issuer's invite.
// The invite issuer is included in the data of the attestation.
ATTESTATION_STATION.attest(
_claimer,
OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY,
abi.encode(_claimableInvite.issuer)
);
emit InviteClaimed(_claimableInvite.issuer, _claimer);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/// @title OptimistConstants
/// @notice Library for storing Optimist related constants that are shared in multiple contracts.
library OptimistConstants {
/// @notice Attestation key issued by OptimistInviter allowing the attested account to mint.
bytes32 internal constant OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY = bytes32("optimist.can-mint-from-invite");
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { OptimistInviter } from "src/periphery/op-nft/OptimistInviter.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/// @notice Simple helper contract that helps with testing flow and signature for
/// OptimistInviter contract. Made this a separate contract instead of including
/// in OptimistInviter.t.sol for reusability.
contract OptimistInviterHelper {
/// @notice EIP712 typehash for the ClaimableInvite type.
bytes32 public constant CLAIMABLE_INVITE_TYPEHASH = keccak256("ClaimableInvite(address issuer,bytes32 nonce)");
/// @notice EIP712 typehash for the EIP712Domain type that is included as part of the signature.
bytes32 public constant EIP712_DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
/// @notice Address of OptimistInviter contract we are testing.
OptimistInviter public optimistInviter;
/// @notice OptimistInviter contract name. Used to construct the EIP-712 domain.
string public name;
/// @notice Keeps track of current nonce to generate new nonces for each invite.
uint256 public currentNonce;
constructor(OptimistInviter _optimistInviter, string memory _name) {
optimistInviter = _optimistInviter;
name = _name;
}
/// @notice Returns the hash of the struct ClaimableInvite.
/// @param _claimableInvite ClaimableInvite struct to hash.
/// @return EIP-712 typed struct hash.
function getClaimableInviteStructHash(OptimistInviter.ClaimableInvite memory _claimableInvite)
public
pure
returns (bytes32)
{
return keccak256(abi.encode(CLAIMABLE_INVITE_TYPEHASH, _claimableInvite.issuer, _claimableInvite.nonce));
}
/// @notice Returns a bytes32 nonce that should change everytime. In practice, people should use
/// pseudorandom nonces.
/// @return Nonce that should be used as part of ClaimableInvite.
function consumeNonce() public returns (bytes32) {
return bytes32(keccak256(abi.encode(currentNonce++)));
}
/// @notice Returns a ClaimableInvite with the issuer and current nonce.
/// @param _issuer Issuer to include in the ClaimableInvite.
/// @return ClaimableInvite that can be hashed & signed.
function getClaimableInviteWithNewNonce(address _issuer) public returns (OptimistInviter.ClaimableInvite memory) {
return OptimistInviter.ClaimableInvite(_issuer, consumeNonce());
}
/// @notice Computes the EIP712 digest with default correct parameters.
/// @param _claimableInvite ClaimableInvite struct to hash.
/// @return EIP-712 compatible digest.
function getDigest(OptimistInviter.ClaimableInvite calldata _claimableInvite) public view returns (bytes32) {
return getDigestWithEIP712Domain(
_claimableInvite,
bytes(name),
bytes(optimistInviter.EIP712_VERSION()),
block.chainid,
address(optimistInviter)
);
}
/// @notice Computes the EIP712 digest with the given domain parameters.
/// Used for testing that different domain parameters fail.
/// @param _claimableInvite ClaimableInvite struct to hash.
/// @param _name Contract name to use in the EIP712 domain.
/// @param _version Contract version to use in the EIP712 domain.
/// @param _chainid Chain ID to use in the EIP712 domain.
/// @param _verifyingContract Address to use in the EIP712 domain.
/// @return EIP-712 compatible digest.
function getDigestWithEIP712Domain(
OptimistInviter.ClaimableInvite calldata _claimableInvite,
bytes memory _name,
bytes memory _version,
uint256 _chainid,
address _verifyingContract
)
public
pure
returns (bytes32)
{
bytes32 domainSeparator = keccak256(
abi.encode(EIP712_DOMAIN_TYPEHASH, keccak256(_name), keccak256(_version), _chainid, _verifyingContract)
);
return ECDSA.toTypedDataHash(domainSeparator, getClaimableInviteStructHash(_claimableInvite));
}
}
//SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/* Testing utilities */
import { Test } from "forge-std/Test.sol";
import { AttestationStation } from "src/periphery/op-nft/AttestationStation.sol";
contract AttestationStation_Initializer is Test {
address alice_attestor = address(128);
address bob = address(256);
address sally = address(512);
function setUp() public {
// Give alice and bob some ETH
vm.deal(alice_attestor, 1 ether);
vm.label(alice_attestor, "alice_attestor");
vm.label(bob, "bob");
vm.label(sally, "sally");
}
}
contract AttestationStationTest is AttestationStation_Initializer {
event AttestationCreated(address indexed creator, address indexed about, bytes32 indexed key, bytes val);
function test_attest_individual_succeeds() external {
AttestationStation attestationStation = new AttestationStation();
vm.expectEmit(true, true, true, true);
emit AttestationCreated(alice_attestor, bob, bytes32("foo"), bytes("bar"));
vm.prank(alice_attestor);
attestationStation.attest({ _about: bob, _key: bytes32("foo"), _val: bytes("bar") });
}
function test_attest_single_succeeds() external {
AttestationStation attestationStation = new AttestationStation();
AttestationStation.AttestationData[] memory attestationDataArr = new AttestationStation.AttestationData[](1);
// alice is going to attest about bob
AttestationStation.AttestationData memory attestationData = AttestationStation.AttestationData({
about: bob,
key: bytes32("test-key:string"),
val: bytes("test-value")
});
// assert the attestation starts empty
assertEq(attestationStation.attestations(alice_attestor, attestationData.about, attestationData.key), "");
// make attestation
vm.prank(alice_attestor);
attestationDataArr[0] = attestationData;
attestationStation.attest(attestationDataArr);
// assert the attestation is there
assertEq(
attestationStation.attestations(alice_attestor, attestationData.about, attestationData.key),
attestationData.val
);
bytes memory new_val = bytes("new updated value");
// make a new attestations to same about and key
attestationData =
AttestationStation.AttestationData({ about: attestationData.about, key: attestationData.key, val: new_val });
vm.prank(alice_attestor);
attestationDataArr[0] = attestationData;
attestationStation.attest(attestationDataArr);
// assert the attestation is updated
assertEq(
attestationStation.attestations(alice_attestor, attestationData.about, attestationData.key),
attestationData.val
);
}
function test_attest_bulk_succeeds() external {
AttestationStation attestationStation = new AttestationStation();
vm.prank(alice_attestor);
AttestationStation.AttestationData[] memory attestationData = new AttestationStation.AttestationData[](3);
attestationData[0] = AttestationStation.AttestationData({
about: bob,
key: bytes32("test-key:string"),
val: bytes("test-value")
});
attestationData[1] =
AttestationStation.AttestationData({ about: bob, key: bytes32("test-key2"), val: bytes("test-value2") });
attestationData[2] = AttestationStation.AttestationData({
about: sally,
key: bytes32("test-key:string"),
val: bytes("test-value3")
});
attestationStation.attest(attestationData);
// assert the attestations are there
assertEq(
attestationStation.attestations(alice_attestor, attestationData[0].about, attestationData[0].key),
attestationData[0].val
);
assertEq(
attestationStation.attestations(alice_attestor, attestationData[1].about, attestationData[1].key),
attestationData[1].val
);
assertEq(
attestationStation.attestations(alice_attestor, attestationData[2].about, attestationData[2].key),
attestationData[2].val
);
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.2 <0.9.0;
// Testing utilities
import { Test } from "forge-std/Test.sol";
import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol";
import { AttestationStation } from "src/periphery/op-nft/AttestationStation.sol";
import { Optimist } from "src/periphery/op-nft/Optimist.sol";
import { OptimistAllowlist } from "src/periphery/op-nft/OptimistAllowlist.sol";
import { OptimistInviter } from "src/periphery/op-nft/OptimistInviter.sol";
import { OptimistInviterHelper } from "test/mocks/OptimistInviterHelper.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
library Multicall {
bytes internal constant code =
hex"6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c0033";
address internal constant addr = 0xcA11bde05977b3631167028862bE2a173976CA11;
}
contract Optimist_Initializer is Test {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Initialized(uint8);
event AttestationCreated(address indexed creator, address indexed about, bytes32 indexed key, bytes val);
string constant name = "Optimist name";
string constant symbol = "OPTIMISTSYMBOL";
string constant base_uri = "https://storageapi.fleek.co/6442819a1b05-bucket/optimist-nft/attributes";
AttestationStation attestationStation;
Optimist optimist;
OptimistAllowlist optimistAllowlist;
OptimistInviter optimistInviter;
// Helps with EIP-712 signature generation
OptimistInviterHelper optimistInviterHelper;
// To test multicall for claiming and minting in one call
IMulticall3 multicall3;
address internal carol_baseURIAttestor;
address internal alice_allowlistAttestor;
address internal eve_inviteGranter;
address internal ted_coinbaseAttestor;
address internal bob;
address internal sally;
/// @notice BaseURI attestor sets the baseURI of the Optimist NFT.
function _attestBaseURI(string memory _baseUri) internal {
bytes32 baseURIAttestationKey = optimist.BASE_URI_ATTESTATION_KEY();
AttestationStation.AttestationData[] memory attestationData = new AttestationStation.AttestationData[](1);
attestationData[0] =
AttestationStation.AttestationData(address(optimist), baseURIAttestationKey, bytes(_baseUri));
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(carol_baseURIAttestor, address(optimist), baseURIAttestationKey, bytes(_baseUri));
vm.prank(carol_baseURIAttestor);
attestationStation.attest(attestationData);
}
/// @notice Allowlist attestor creates an attestation for an address.
function _attestAllowlist(address _about) internal {
bytes32 attestationKey = optimistAllowlist.OPTIMIST_CAN_MINT_ATTESTATION_KEY();
AttestationStation.AttestationData[] memory attestationData = new AttestationStation.AttestationData[](1);
// we are using true but it can be any non empty value
attestationData[0] =
AttestationStation.AttestationData({ about: _about, key: attestationKey, val: bytes("true") });
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(alice_allowlistAttestor, _about, attestationKey, bytes("true"));
vm.prank(alice_allowlistAttestor);
attestationStation.attest(attestationData);
assertTrue(optimist.isOnAllowList(_about));
}
/// @notice Coinbase Quest attestor creates an attestation for an address.
function _attestCoinbaseQuest(address _about) internal {
bytes32 attestationKey = optimistAllowlist.COINBASE_QUEST_ELIGIBLE_ATTESTATION_KEY();
AttestationStation.AttestationData[] memory attestationData = new AttestationStation.AttestationData[](1);
// we are using true but it can be any non empty value
attestationData[0] =
AttestationStation.AttestationData({ about: _about, key: attestationKey, val: bytes("true") });
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(ted_coinbaseAttestor, _about, attestationKey, bytes("true"));
vm.prank(ted_coinbaseAttestor);
attestationStation.attest(attestationData);
assertTrue(optimist.isOnAllowList(_about));
}
/// @notice Issues invite, then claims it using the claimer's address.
function _inviteAndClaim(address _about) internal {
uint256 inviterPrivateKey = 0xbeefbeef;
address inviter = vm.addr(inviterPrivateKey);
address[] memory addresses = new address[](1);
addresses[0] = inviter;
vm.prank(eve_inviteGranter);
// grant invites to Inviter;
optimistInviter.setInviteCounts(addresses, 3);
// issue a new invite
OptimistInviter.ClaimableInvite memory claimableInvite =
optimistInviterHelper.getClaimableInviteWithNewNonce(inviter);
// EIP-712 sign with Inviter's private key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(inviterPrivateKey, optimistInviterHelper.getDigest(claimableInvite));
bytes memory signature = abi.encodePacked(r, s, v);
bytes32 hashedCommit = keccak256(abi.encode(_about, signature));
// commit the invite
vm.prank(_about);
optimistInviter.commitInvite(hashedCommit);
// wait minimum commitment period
vm.warp(optimistInviter.MIN_COMMITMENT_PERIOD() + block.timestamp);
// reveal and claim the invite
optimistInviter.claimInvite(_about, claimableInvite, signature);
assertTrue(optimist.isOnAllowList(_about));
}
/// @notice Mocks the allowlistAttestor to always return true for a given address.
function _mockAllowlistTrueFor(address _claimer) internal {
vm.mockCall(
address(optimistAllowlist), abi.encodeCall(OptimistAllowlist.isAllowedToMint, (_claimer)), abi.encode(true)
);
assertTrue(optimist.isOnAllowList(_claimer));
}
/// @notice Returns address as uint256.
function _getTokenId(address _owner) internal pure returns (uint256) {
return uint256(uint160(address(_owner)));
}
function setUp() public {
carol_baseURIAttestor = makeAddr("carol_baseURIAttestor");
alice_allowlistAttestor = makeAddr("alice_allowlistAttestor");
eve_inviteGranter = makeAddr("eve_inviteGranter");
ted_coinbaseAttestor = makeAddr("ted_coinbaseAttestor");
bob = makeAddr("bob");
sally = makeAddr("sally");
_initializeContracts();
}
function _initializeContracts() internal {
attestationStation = new AttestationStation();
vm.expectEmit(true, true, false, false);
emit Initialized(1);
optimistInviter =
new OptimistInviter({ _inviteGranter: eve_inviteGranter, _attestationStation: attestationStation });
optimistInviter.initialize("OptimistInviter");
// Initialize the helper which helps sign EIP-712 signatures
optimistInviterHelper = new OptimistInviterHelper(optimistInviter, "OptimistInviter");
optimistAllowlist = new OptimistAllowlist({
_attestationStation: attestationStation,
_allowlistAttestor: alice_allowlistAttestor,
_coinbaseQuestAttestor: ted_coinbaseAttestor,
_optimistInviter: address(optimistInviter)
});
optimist = new Optimist({
_name: name,
_symbol: symbol,
_baseURIAttestor: carol_baseURIAttestor,
_attestationStation: attestationStation,
_optimistAllowlist: optimistAllowlist
});
multicall3 = IMulticall3(Multicall.addr);
vm.etch(Multicall.addr, Multicall.code);
}
}
contract OptimistTest is Optimist_Initializer {
/// @notice Check that constructor and initializer parameters are correctly set.
function test_initialize_succeeds() external view {
// expect name to be set
assertEq(optimist.name(), name);
// expect symbol to be set
assertEq(optimist.symbol(), symbol);
// expect attestationStation to be set
assertEq(address(optimist.ATTESTATION_STATION()), address(attestationStation));
assertEq(optimist.BASE_URI_ATTESTOR(), carol_baseURIAttestor);
}
/// @notice Bob should be able to mint an NFT if he is allowlisted
/// by the allowlistAttestor and has a balance of 0.
function test_mint_afterAllowlistAttestation_succeeds() external {
// bob should start with 0 balance
assertEq(optimist.balanceOf(bob), 0);
// allowlist bob
_attestAllowlist(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
// Check that the OptimistAllowlist is checked
bytes memory data = abi.encodeCall(OptimistAllowlist.isAllowedToMint, (bob));
vm.expectCall(address(optimistAllowlist), data);
// mint an NFT and expect mint transfer event to be emitted
vm.expectEmit(true, true, true, true);
emit Transfer(address(0), bob, _getTokenId(bob));
vm.prank(bob);
optimist.mint(bob);
// expect the NFT to be owned by bob
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
}
/// @notice Bob should be able to mint an NFT if he claimed an invite through OptimistInviter
/// and has a balance of 0.
function test_mint_afterInviteClaimed_succeeds() external {
// bob should start with 0 balance
assertEq(optimist.balanceOf(bob), 0);
// bob claims an invite
_inviteAndClaim(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
// Check that the OptimistAllowlist is checked
bytes memory data = abi.encodeCall(OptimistAllowlist.isAllowedToMint, (bob));
vm.expectCall(address(optimistAllowlist), data);
// mint an NFT and expect mint transfer event to be emitted
vm.expectEmit(true, true, true, true);
emit Transfer(address(0), bob, _getTokenId(bob));
vm.prank(bob);
optimist.mint(bob);
// expect the NFT to be owned by bob
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
}
/// @notice Bob should be able to mint an NFT if he has an attestation from Coinbase Quest
/// attestor and has a balance of 0.
function test_mint_afterCoinbaseQuestAttestation_succeeds() external {
// bob should start with 0 balance
assertEq(optimist.balanceOf(bob), 0);
// bob receives attestation from Coinbase Quest attestor
_attestCoinbaseQuest(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
// Check that the OptimistAllowlist is checked
bytes memory data = abi.encodeCall(OptimistAllowlist.isAllowedToMint, (bob));
vm.expectCall(address(optimistAllowlist), data);
// mint an NFT and expect mint transfer event to be emitted
vm.expectEmit(true, true, true, true);
emit Transfer(address(0), bob, _getTokenId(bob));
vm.prank(bob);
optimist.mint(bob);
// expect the NFT to be owned by bob
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
}
/// @notice Multiple valid attestations should allow Bob to mint.
function test_mint_afterMultipleAttestations_succeeds() external {
// bob should start with 0 balance
assertEq(optimist.balanceOf(bob), 0);
// bob receives attestation from Coinbase Quest attestor
_attestCoinbaseQuest(bob);
// allowlist bob
_attestAllowlist(bob);
// bob claims an invite
_inviteAndClaim(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
// Check that the OptimistAllowlist is checked
bytes memory data = abi.encodeCall(OptimistAllowlist.isAllowedToMint, (bob));
vm.expectCall(address(optimistAllowlist), data);
// mint an NFT and expect mint transfer event to be emitted
vm.expectEmit(true, true, true, true);
emit Transfer(address(0), bob, _getTokenId(bob));
vm.prank(bob);
optimist.mint(bob);
// expect the NFT to be owned by bob
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
}
/// @notice Sally should be able to mint a token on behalf of bob.
function test_mint_secondaryMinter_succeeds() external {
_mockAllowlistTrueFor(bob);
vm.expectEmit(true, true, true, true);
emit Transfer(address(0), bob, _getTokenId(bob));
// mint as sally instead of bob
vm.prank(sally);
optimist.mint(bob);
// expect the NFT to be owned by bob
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
}
/// @notice Bob should not be able to mint an NFT if he is not allowlisted.
function test_mint_forNonAllowlistedClaimer_reverts() external {
vm.prank(bob);
vm.expectRevert("Optimist: address is not on allowList");
optimist.mint(bob);
}
/// @notice Bob's tx should revert if he already minted.
function test_mint_forAlreadyMintedClaimer_reverts() external {
_attestAllowlist(bob);
// mint initial nft with bob
vm.prank(bob);
optimist.mint(bob);
// expect the NFT to be owned by bob
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
// attempt to mint again
vm.expectRevert("ERC721: token already minted");
optimist.mint(bob);
}
/// @notice The baseURI should be set by attestation station by the baseURIAttestor.
function test_baseURI_returnsCorrectBaseURI_succeeds() external {
_attestBaseURI(base_uri);
bytes memory data = abi.encodeCall(
attestationStation.attestations,
(carol_baseURIAttestor, address(optimist), optimist.BASE_URI_ATTESTATION_KEY())
);
vm.expectCall(address(attestationStation), data);
vm.prank(carol_baseURIAttestor);
// assert baseURI is set
assertEq(optimist.baseURI(), base_uri);
}
/// @notice tokenURI should return the token uri for a minted token.
function test_tokenURI_returnsCorrectTokenURI_succeeds() external {
// we are using true but it can be any non empty value
_attestBaseURI(base_uri);
// mint an NFT
_mockAllowlistTrueFor(bob);
vm.prank(bob);
optimist.mint(bob);
// assert tokenURI is set
assertEq(optimist.baseURI(), base_uri);
assertEq(
optimist.tokenURI(_getTokenId(bob)),
"https://storageapi.fleek.co/6442819a1b05-bucket/optimist-nft/attributes/0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e.json"
);
}
/// @notice Should return the token id of the owner.
function test_tokenIdOfAddress_returnsOwnerID_succeeds() external {
uint256 willTokenId = 1024;
address will = address(1024);
_mockAllowlistTrueFor(will);
optimist.mint(will);
assertEq(optimist.tokenIdOfAddress(will), willTokenId);
}
/// @notice transferFrom should revert since Optimist is a SBT.
function test_transferFrom_soulbound_reverts() external {
_mockAllowlistTrueFor(bob);
// mint as bob
vm.prank(bob);
optimist.mint(bob);
// attempt to transfer to sally
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.prank(bob);
optimist.transferFrom(bob, sally, _getTokenId(bob));
// attempt to transfer to sally
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.prank(bob);
optimist.safeTransferFrom(bob, sally, _getTokenId(bob));
// attempt to transfer to sally
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.prank(bob);
optimist.safeTransferFrom(bob, sally, _getTokenId(bob), bytes("0x"));
}
/// @notice approve should revert since Optimist is a SBT.
function test_approve_soulbound_reverts() external {
_mockAllowlistTrueFor(bob);
// mint as bob
vm.prank(bob);
optimist.mint(bob);
// attempt to approve sally
vm.prank(bob);
vm.expectRevert("Optimist: soul bound token");
optimist.approve(address(attestationStation), _getTokenId(bob));
assertEq(optimist.getApproved(_getTokenId(bob)), address(0));
}
/// @notice setApprovalForAll should revert since Optimist is a SBT.
function test_setApprovalForAll_soulbound_reverts() external {
_mockAllowlistTrueFor(bob);
// mint as bob
vm.prank(bob);
optimist.mint(bob);
vm.prank(alice_allowlistAttestor);
vm.expectRevert(bytes("Optimist: soul bound token"));
optimist.setApprovalForAll(alice_allowlistAttestor, true);
// expect approval amount to stil be 0
assertEq(optimist.getApproved(_getTokenId(bob)), address(0));
// isApprovedForAll should return false
assertEq(optimist.isApprovedForAll(alice_allowlistAttestor, alice_allowlistAttestor), false);
}
/// @notice Only owner should be able to burn token.
function test_burn_byOwner_succeeds() external {
_mockAllowlistTrueFor(bob);
// mint as bob
vm.prank(bob);
optimist.mint(bob);
// burn as bob
vm.prank(bob);
optimist.burn(_getTokenId(bob));
// expect bob to have no balance now
assertEq(optimist.balanceOf(bob), 0);
}
/// @notice Non-owner attempting to burn token should revert.
function test_burn_byNonOwner_reverts() external {
_mockAllowlistTrueFor(bob);
// mint as bob
vm.prank(bob);
optimist.mint(bob);
vm.expectRevert("ERC721: caller is not token owner nor approved");
// burn as Sally
vm.prank(sally);
optimist.burn(_getTokenId(bob));
// expect bob to have still have the token
assertEq(optimist.balanceOf(bob), 1);
}
/// @notice Should support ERC-721 interface.
function test_supportsInterface_returnsCorrectInterfaceForERC721_succeeds() external view {
bytes4 iface721 = type(IERC721).interfaceId;
// check that it supports ERC-721 interface
assertEq(optimist.supportsInterface(iface721), true);
}
/// @notice Checking that multi-call using the invite & claim flow works correctly, since the
/// frontend will be making multicalls to improve UX. The OptimistInviter.claimInvite
/// and Optimist.mint will be batched
function test_multicall_batchingClaimAndMint_succeeds() external {
uint256 inviterPrivateKey = 0xbeefbeef;
address inviter = vm.addr(inviterPrivateKey);
address[] memory addresses = new address[](1);
addresses[0] = inviter;
vm.prank(eve_inviteGranter);
// grant invites to Inviter;
optimistInviter.setInviteCounts(addresses, 3);
// issue a new invite
OptimistInviter.ClaimableInvite memory claimableInvite =
optimistInviterHelper.getClaimableInviteWithNewNonce(inviter);
// EIP-712 sign with Inviter's private key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(inviterPrivateKey, optimistInviterHelper.getDigest(claimableInvite));
bytes memory signature = abi.encodePacked(r, s, v);
bytes32 hashedCommit = keccak256(abi.encode(bob, signature));
// commit the invite
vm.prank(bob);
optimistInviter.commitInvite(hashedCommit);
// wait minimum commitment period
vm.warp(optimistInviter.MIN_COMMITMENT_PERIOD() + block.timestamp);
IMulticall3.Call3[] memory calls = new IMulticall3.Call3[](2);
// First call is to claim the invite, receiving the attestation
calls[0] = IMulticall3.Call3({
target: address(optimistInviter),
callData: abi.encodeCall(OptimistInviter.claimInvite, (bob, claimableInvite, signature)),
allowFailure: false
});
// Second call is to mint the Optimist NFT
calls[1] = IMulticall3.Call3({
target: address(optimist),
callData: abi.encodeCall(Optimist.mint, (bob)),
allowFailure: false
});
multicall3.aggregate3(calls);
assertTrue(optimist.isOnAllowList(bob));
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
// Testing utilities
import { Test } from "forge-std/Test.sol";
import { AttestationStation } from "src/periphery/op-nft/AttestationStation.sol";
import { OptimistAllowlist } from "src/periphery/op-nft/OptimistAllowlist.sol";
import { OptimistInviter } from "src/periphery/op-nft/OptimistInviter.sol";
import { OptimistInviterHelper } from "test/mocks/OptimistInviterHelper.sol";
import { OptimistConstants } from "src/periphery/op-nft/libraries/OptimistConstants.sol";
contract OptimistAllowlist_Initializer is Test {
event AttestationCreated(address indexed creator, address indexed about, bytes32 indexed key, bytes val);
address internal alice_allowlistAttestor;
address internal sally_coinbaseQuestAttestor;
address internal ted;
uint256 internal bobPrivateKey;
address internal bob;
AttestationStation attestationStation;
OptimistAllowlist optimistAllowlist;
OptimistInviter optimistInviter;
// Helps with EIP-712 signature generation
OptimistInviterHelper optimistInviterHelper;
function setUp() public {
alice_allowlistAttestor = makeAddr("alice_allowlistAttestor");
sally_coinbaseQuestAttestor = makeAddr("sally_coinbaseQuestAttestor");
ted = makeAddr("ted");
bobPrivateKey = 0xB0B0B0B0;
bob = vm.addr(bobPrivateKey);
vm.label(bob, "bob");
// Give alice and bob and sally some ETH
vm.deal(alice_allowlistAttestor, 1 ether);
vm.deal(sally_coinbaseQuestAttestor, 1 ether);
vm.deal(bob, 1 ether);
vm.deal(ted, 1 ether);
_initializeContracts();
}
function attestAllowlist(address _about) internal {
AttestationStation.AttestationData[] memory attestationData = new AttestationStation.AttestationData[](1);
// we are using true but it can be any non empty value
attestationData[0] = AttestationStation.AttestationData({
about: _about,
key: optimistAllowlist.OPTIMIST_CAN_MINT_ATTESTATION_KEY(),
val: bytes("true")
});
vm.prank(alice_allowlistAttestor);
attestationStation.attest(attestationData);
}
function attestCoinbaseQuest(address _about) internal {
AttestationStation.AttestationData[] memory attestationData = new AttestationStation.AttestationData[](1);
// we are using true but it can be any non empty value
attestationData[0] = AttestationStation.AttestationData({
about: _about,
key: optimistAllowlist.COINBASE_QUEST_ELIGIBLE_ATTESTATION_KEY(),
val: bytes("true")
});
vm.prank(sally_coinbaseQuestAttestor);
attestationStation.attest(attestationData);
}
function inviteAndClaim(address claimer) internal {
address[] memory addresses = new address[](1);
addresses[0] = bob;
vm.prank(alice_allowlistAttestor);
// grant invites to Bob;
optimistInviter.setInviteCounts(addresses, 3);
// issue a new invite
OptimistInviter.ClaimableInvite memory claimableInvite =
optimistInviterHelper.getClaimableInviteWithNewNonce(bob);
// EIP-712 sign with Bob's private key
bytes memory signature = _getSignature(bobPrivateKey, optimistInviterHelper.getDigest(claimableInvite));
bytes32 hashedCommit = keccak256(abi.encode(claimer, signature));
// commit the invite
vm.prank(claimer);
optimistInviter.commitInvite(hashedCommit);
// wait minimum commitment period
vm.warp(optimistInviter.MIN_COMMITMENT_PERIOD() + block.timestamp);
// reveal and claim the invite
optimistInviter.claimInvite(claimer, claimableInvite, signature);
}
/// @notice Get signature as a bytes blob, since SignatureChecker takes arbitrary signature blobs.
function _getSignature(uint256 _signingPrivateKey, bytes32 _digest) internal pure returns (bytes memory) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(_signingPrivateKey, _digest);
bytes memory signature = abi.encodePacked(r, s, v);
return signature;
}
function _initializeContracts() internal {
attestationStation = new AttestationStation();
optimistInviter = new OptimistInviter(alice_allowlistAttestor, attestationStation);
optimistInviter.initialize("OptimistInviter");
optimistAllowlist = new OptimistAllowlist(
attestationStation, alice_allowlistAttestor, sally_coinbaseQuestAttestor, address(optimistInviter)
);
optimistInviterHelper = new OptimistInviterHelper(optimistInviter, "OptimistInviter");
}
}
contract OptimistAllowlistTest is OptimistAllowlist_Initializer {
function test_constructor_succeeds() external view {
// expect attestationStation to be set
assertEq(address(optimistAllowlist.ATTESTATION_STATION()), address(attestationStation));
assertEq(optimistAllowlist.ALLOWLIST_ATTESTOR(), alice_allowlistAttestor);
assertEq(optimistAllowlist.COINBASE_QUEST_ATTESTOR(), sally_coinbaseQuestAttestor);
assertEq(address(optimistAllowlist.OPTIMIST_INVITER()), address(optimistInviter));
}
/// @notice Base case, a account without any relevant attestations should not be able to mint.
function test_isAllowedToMint_withoutAnyAttestations_fails() external view {
assertFalse(optimistAllowlist.isAllowedToMint(bob));
}
/// @notice After receiving a valid allowlist attestation, the account should be able to mint.
function test_isAllowedToMint_fromAllowlistAttestor_succeeds() external {
attestAllowlist(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
}
/// @notice After receiving a valid attestation from the Coinbase Quest attestor,
/// the account should be able to mint.
function test_isAllowedToMint_fromCoinbaseQuestAttestor_succeeds() external {
attestCoinbaseQuest(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
}
/// @notice Account that received an attestation from the OptimistInviter contract by going
/// through the claim invite flow should be able to mint.
function test_isAllowedToMint_fromInvite_succeeds() external {
inviteAndClaim(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
}
/// @notice Attestation from the wrong allowlist attestor should not allow minting.
function test_isAllowedToMint_fromWrongAllowlistAttestor_fails() external {
// Ted is not the allowlist attestor
vm.prank(ted);
attestationStation.attest(bob, optimistAllowlist.OPTIMIST_CAN_MINT_ATTESTATION_KEY(), bytes("true"));
assertFalse(optimistAllowlist.isAllowedToMint(bob));
}
/// @notice Coinbase quest attestation from wrong attestor should not allow minting.
function test_isAllowedToMint_fromWrongCoinbaseQuestAttestor_fails() external {
// Ted is not the coinbase quest attestor
vm.prank(ted);
attestationStation.attest(bob, optimistAllowlist.COINBASE_QUEST_ELIGIBLE_ATTESTATION_KEY(), bytes("true"));
assertFalse(optimistAllowlist.isAllowedToMint(bob));
}
/// @notice Claiming an invite on the non-official OptimistInviter contract should not allow
/// minting.
function test_isAllowedToMint_fromWrongOptimistInviter_fails() external {
vm.prank(ted);
attestationStation.attest(bob, OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY, bytes("true"));
assertFalse(optimistAllowlist.isAllowedToMint(bob));
}
/// @notice Having multiple signals, even if one is invalid, should still allow minting.
function test_isAllowedToMint_withMultipleAttestations_succeeds() external {
attestAllowlist(bob);
attestCoinbaseQuest(bob);
inviteAndClaim(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
// A invalid attestation, as Ted is not allowlist attestor
vm.prank(ted);
attestationStation.attest(bob, optimistAllowlist.OPTIMIST_CAN_MINT_ATTESTATION_KEY(), bytes("true"));
// Since Bob has at least one valid attestation, he should be allowed to mint
assertTrue(optimistAllowlist.isAllowedToMint(bob));
}
/// @notice Having falsy attestation value should not allow minting.
function test_isAllowedToMint_fromAllowlistAttestorWithFalsyValue_fails() external {
// First sends correct attestation
attestAllowlist(bob);
bytes32 key = optimistAllowlist.OPTIMIST_CAN_MINT_ATTESTATION_KEY();
vm.expectEmit(true, true, true, false);
emit AttestationCreated(alice_allowlistAttestor, bob, key, bytes("dsafsds"));
// Invalidates existing attestation
vm.prank(alice_allowlistAttestor);
attestationStation.attest(bob, key, bytes(""));
assertFalse(optimistAllowlist.isAllowedToMint(bob));
}
/// @notice Having falsy attestation value from Coinbase attestor should not allow minting.
function test_isAllowedToMint_fromCoinbaseQuestAttestorWithFalsyValue_fails() external {
// First sends correct attestation
attestAllowlist(bob);
bytes32 key = optimistAllowlist.OPTIMIST_CAN_MINT_ATTESTATION_KEY();
vm.expectEmit(true, true, true, true);
emit AttestationCreated(alice_allowlistAttestor, bob, key, bytes(""));
// Invalidates existing attestation
vm.prank(alice_allowlistAttestor);
attestationStation.attest(bob, key, bytes(""));
assertFalse(optimistAllowlist.isAllowedToMint(bob));
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
// Testing utilities
import { Test } from "forge-std/Test.sol";
import { AttestationStation } from "src/periphery/op-nft/AttestationStation.sol";
import { OptimistInviter } from "src/periphery/op-nft/OptimistInviter.sol";
import { Optimist } from "src/periphery/op-nft/Optimist.sol";
import { TestERC1271Wallet } from "test/mocks/TestERC1271Wallet.sol";
import { OptimistInviterHelper } from "test/mocks/OptimistInviterHelper.sol";
import { OptimistConstants } from "src/periphery/op-nft/libraries/OptimistConstants.sol";
contract OptimistInviter_Initializer is Test {
event InviteClaimed(address indexed issuer, address indexed claimer);
event Initialized(uint8 version);
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event AttestationCreated(address indexed creator, address indexed about, bytes32 indexed key, bytes val);
bytes32 EIP712_DOMAIN_TYPEHASH;
address internal alice_inviteGranter;
address internal sally;
address internal ted;
address internal eve;
address internal bob;
uint256 internal bobPrivateKey;
address internal carol;
uint256 internal carolPrivateKey;
TestERC1271Wallet carolERC1271Wallet;
AttestationStation attestationStation;
OptimistInviter optimistInviter;
OptimistInviterHelper optimistInviterHelper;
function setUp() public {
alice_inviteGranter = makeAddr("alice_inviteGranter");
sally = makeAddr("sally");
ted = makeAddr("ted");
eve = makeAddr("eve");
bobPrivateKey = 0xB0B0B0B0;
bob = vm.addr(bobPrivateKey);
carolPrivateKey = 0xC0C0C0C0;
carol = vm.addr(carolPrivateKey);
carolERC1271Wallet = new TestERC1271Wallet(carol);
// Give alice and bob and sally some ETH
vm.deal(alice_inviteGranter, 1 ether);
vm.deal(bob, 1 ether);
vm.deal(sally, 1 ether);
vm.deal(ted, 1 ether);
vm.deal(eve, 1 ether);
EIP712_DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
_initializeContracts();
}
/// @notice Instantiates an AttestationStation, and an OptimistInviter.
function _initializeContracts() internal {
attestationStation = new AttestationStation();
optimistInviter = new OptimistInviter(alice_inviteGranter, attestationStation);
vm.expectEmit(true, true, true, true, address(optimistInviter));
emit Initialized(1);
optimistInviter.initialize("OptimistInviter");
optimistInviterHelper = new OptimistInviterHelper(optimistInviter, "OptimistInviter");
}
function _passMinCommitmentPeriod() internal {
vm.warp(optimistInviter.MIN_COMMITMENT_PERIOD() + block.timestamp);
}
/// @notice Returns a user's current invite count, as stored in the AttestationStation.
function _getInviteCount(address _issuer) internal view returns (uint256) {
return optimistInviter.inviteCounts(_issuer);
}
/// @notice Returns true if claimer has the proper attestation from OptimistInviter to mint.
function _hasMintAttestation(address _claimer) internal view returns (bool) {
bytes memory attestation = attestationStation.attestations(
address(optimistInviter), _claimer, OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY
);
return attestation.length > 0;
}
/// @notice Get signature as a bytes blob, since SignatureChecker takes arbitrary signature blobs.
function _getSignature(uint256 _signingPrivateKey, bytes32 _digest) internal pure returns (bytes memory) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(_signingPrivateKey, _digest);
bytes memory signature = abi.encodePacked(r, s, v);
return signature;
}
/// @notice Signs a claimable invite with the given private key and returns the signature using
/// correct EIP712 domain separator.
function _issueInviteAs(uint256 _privateKey)
internal
returns (OptimistInviter.ClaimableInvite memory, bytes memory)
{
return _issueInviteWithEIP712Domain(
_privateKey,
bytes("OptimistInviter"),
bytes(optimistInviter.EIP712_VERSION()),
block.chainid,
address(optimistInviter)
);
}
/// @notice Signs a claimable invite with the given private key and returns the signature using
/// the given EIP712 domain separator. This assumes that the issuer's address is the
/// corresponding public key to _issuerPrivateKey.
function _issueInviteWithEIP712Domain(
uint256 _issuerPrivateKey,
bytes memory _eip712Name,
bytes memory _eip712Version,
uint256 _eip712Chainid,
address _eip712VerifyingContract
)
internal
returns (OptimistInviter.ClaimableInvite memory, bytes memory)
{
address issuer = vm.addr(_issuerPrivateKey);
OptimistInviter.ClaimableInvite memory claimableInvite =
optimistInviterHelper.getClaimableInviteWithNewNonce(issuer);
return (
claimableInvite,
_getSignature(
_issuerPrivateKey,
optimistInviterHelper.getDigestWithEIP712Domain(
claimableInvite, _eip712Name, _eip712Version, _eip712Chainid, _eip712VerifyingContract
)
)
);
}
/// @notice Commits a signature and claimer address to the OptimistInviter contract.
function _commitInviteAs(address _as, bytes memory _signature) internal {
vm.prank(_as);
bytes32 hashedSignature = keccak256(abi.encode(_as, _signature));
optimistInviter.commitInvite(hashedSignature);
// Check that the commitment was stored correctly
assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp);
}
/// @notice Signs a claimable invite with the given private key. The claimer commits then claims
/// the invite. Checks that all expected events are emitted and that state is updated
/// correctly. Returns the signature and invite for use in tests.
function _issueThenClaimShouldSucceed(
uint256 _issuerPrivateKey,
address _claimer
)
internal
returns (OptimistInviter.ClaimableInvite memory, bytes memory)
{
address issuer = vm.addr(_issuerPrivateKey);
uint256 prevInviteCount = _getInviteCount(issuer);
(OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) =
_issueInviteAs(_issuerPrivateKey);
_commitInviteAs(_claimer, signature);
// The hash(claimer ++ signature) should be committed
assertEq(optimistInviter.commitmentTimestamps(keccak256(abi.encode(_claimer, signature))), block.timestamp);
_passMinCommitmentPeriod();
// OptimistInviter should issue a new attestation allowing claimer to mint
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
_claimer,
OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY,
abi.encode(issuer)
);
// Should emit an event indicating that the invite was claimed
vm.expectEmit(true, false, false, false, address(optimistInviter));
emit InviteClaimed(issuer, _claimer);
vm.prank(_claimer);
optimistInviter.claimInvite(_claimer, claimableInvite, signature);
// The nonce that issuer used should be marked as used
assertTrue(optimistInviter.usedNonces(issuer, claimableInvite.nonce));
// Issuer should have one less invite
assertEq(prevInviteCount - 1, _getInviteCount(issuer));
// Claimer should have the mint attestation from the OptimistInviter contract
assertTrue(_hasMintAttestation(_claimer));
return (claimableInvite, signature);
}
/// @notice Issues 3 invites to the given address. Checks that all expected events are emitted
/// and that state is updated correctly.
function _grantInvitesTo(address _to) internal {
address[] memory addresses = new address[](1);
addresses[0] = _to;
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter), _to, optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true")
);
vm.prank(alice_inviteGranter);
optimistInviter.setInviteCounts(addresses, 3);
assertEq(_getInviteCount(_to), 3);
}
}
contract OptimistInviterTest is OptimistInviter_Initializer {
function test_initialize_succeeds() external view {
// expect attestationStation to be set
assertEq(address(optimistInviter.ATTESTATION_STATION()), address(attestationStation));
assertEq(optimistInviter.INVITE_GRANTER(), alice_inviteGranter);
}
/// @notice Alice the admin should be able to give Bob, Sally, and Carol 3 invites, and the
/// OptimistInviter contract should increment invite counts on inviteCounts and issue
/// 'optimist.can-invite' attestations.
function test_grantInvites_adminAddingInvites_succeeds() external {
address[] memory addresses = new address[](3);
addresses[0] = bob;
addresses[1] = sally;
addresses[2] = address(carolERC1271Wallet);
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter), bob, optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true")
);
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter), sally, optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true")
);
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
address(carolERC1271Wallet),
optimistInviter.CAN_INVITE_ATTESTATION_KEY(),
bytes("true")
);
vm.prank(alice_inviteGranter);
optimistInviter.setInviteCounts(addresses, 3);
assertEq(_getInviteCount(bob), 3);
assertEq(_getInviteCount(sally), 3);
assertEq(_getInviteCount(address(carolERC1271Wallet)), 3);
}
/// @notice Bob, who is not the invite granter, should not be able to issue invites.
function test_grantInvites_nonAdminAddingInvites_reverts() external {
address[] memory addresses = new address[](2);
addresses[0] = bob;
addresses[1] = sally;
vm.expectRevert("OptimistInviter: only invite granter can grant invites");
vm.prank(bob);
optimistInviter.setInviteCounts(addresses, 3);
}
/// @notice Sally should be able to commit an invite given by by Bob.
function test_commitInvite_committingForYourself_succeeds() external {
_grantInvitesTo(bob);
(, bytes memory signature) = _issueInviteAs(bobPrivateKey);
vm.prank(sally);
bytes32 hashedSignature = keccak256(abi.encode(sally, signature));
optimistInviter.commitInvite(hashedSignature);
assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp);
}
/// @notice Sally should be able to Bob's for a different claimer, Eve.
function test_commitInvite_committingForSomeoneElse_succeeds() external {
_grantInvitesTo(bob);
(, bytes memory signature) = _issueInviteAs(bobPrivateKey);
vm.prank(sally);
bytes32 hashedSignature = keccak256(abi.encode(eve, signature));
optimistInviter.commitInvite(hashedSignature);
assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp);
}
/// @notice Attempting to commit the same hash twice should revert. This prevents griefing.
function test_commitInvite_committingSameHashTwice_reverts() external {
_grantInvitesTo(bob);
(, bytes memory signature) = _issueInviteAs(bobPrivateKey);
vm.prank(sally);
bytes32 hashedSignature = keccak256(abi.encode(eve, signature));
optimistInviter.commitInvite(hashedSignature);
assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp);
vm.expectRevert("OptimistInviter: commitment already made");
optimistInviter.commitInvite(hashedSignature);
}
/// @notice Bob issues signature, and Sally claims the invite. Bob's invite count should be
/// decremented, and Sally should be able to mint.
function test_claimInvite_succeeds() external {
_grantInvitesTo(bob);
_issueThenClaimShouldSucceed(bobPrivateKey, sally);
}
/// @notice Bob issues signature, and Ted commits the invite for Sally. Eve claims for Sally.
function test_claimInvite_claimForSomeoneElse_succeeds() external {
_grantInvitesTo(bob);
(OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteAs(bobPrivateKey);
vm.prank(ted);
optimistInviter.commitInvite(keccak256(abi.encode(sally, signature)));
_passMinCommitmentPeriod();
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
sally,
OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY,
abi.encode(bob)
);
// Should emit an event indicating that the invite was claimed
vm.expectEmit(true, true, true, true, address(optimistInviter));
emit InviteClaimed(bob, sally);
vm.prank(eve);
optimistInviter.claimInvite(sally, claimableInvite, signature);
assertEq(_getInviteCount(bob), 2);
assertTrue(_hasMintAttestation(sally));
assertFalse(_hasMintAttestation(eve));
}
function test_claimInvite_claimBeforeMinCommitmentPeriod_reverts() external {
_grantInvitesTo(bob);
(OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteAs(bobPrivateKey);
_commitInviteAs(sally, signature);
// Some time passes, but not enough to meet the minimum commitment period
vm.warp(block.timestamp + 10);
vm.expectRevert("OptimistInviter: minimum commitment period has not elapsed yet");
vm.prank(sally);
optimistInviter.claimInvite(sally, claimableInvite, signature);
}
/// @notice Signature issued for previous versions of the contract should fail.
function test_claimInvite_usingSignatureIssuedForDifferentVersion_reverts() external {
_grantInvitesTo(bob);
(OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteWithEIP712Domain(
bobPrivateKey, "OptimismInviter", "0.9.1", block.chainid, address(optimistInviter)
);
_commitInviteAs(sally, signature);
_passMinCommitmentPeriod();
vm.expectRevert("OptimistInviter: invalid signature");
vm.prank(sally);
optimistInviter.claimInvite(sally, claimableInvite, signature);
}
/// @notice Replay attack for signature issued for contract on different chain (ie. mainnet)
/// should fail.
function test_claimInvite_usingSignatureIssuedForDifferentChain_reverts() external {
_grantInvitesTo(bob);
(OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteWithEIP712Domain(
bobPrivateKey, "OptimismInviter", bytes(optimistInviter.EIP712_VERSION()), 1, address(optimistInviter)
);
_commitInviteAs(sally, signature);
_passMinCommitmentPeriod();
vm.expectRevert("OptimistInviter: invalid signature");
vm.prank(sally);
optimistInviter.claimInvite(sally, claimableInvite, signature);
}
/// @notice Replay attack for signature issued for instantiation of the OptimistInviter contract
/// on a different address should fail.
function test_claimInvite_usingSignatureIssuedForDifferentContract_reverts() external {
_grantInvitesTo(bob);
(OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteWithEIP712Domain(
bobPrivateKey, "OptimismInviter", bytes(optimistInviter.EIP712_VERSION()), block.chainid, address(0xBEEF)
);
_commitInviteAs(sally, signature);
_passMinCommitmentPeriod();
vm.expectRevert("OptimistInviter: invalid signature");
vm.prank(sally);
optimistInviter.claimInvite(sally, claimableInvite, signature);
}
/// @notice Attempting to claim again using the same signature again should fail.
function test_claimInvite_replayingUsedNonce_reverts() external {
_grantInvitesTo(bob);
(OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) =
_issueThenClaimShouldSucceed(bobPrivateKey, sally);
// Sally tries to claim the invite using the same signature
vm.expectRevert("OptimistInviter: nonce has already been used");
vm.prank(sally);
optimistInviter.claimInvite(sally, claimableInvite, signature);
// Carol tries to claim the invite using the same signature
_commitInviteAs(carol, signature);
_passMinCommitmentPeriod();
vm.expectRevert("OptimistInviter: nonce has already been used");
vm.prank(carol);
optimistInviter.claimInvite(carol, claimableInvite, signature);
}
/// @notice Issuing signatures through a contract that implements ERC1271 should succeed (ie.
/// Gnosis Safe or other smart contract wallets). Carol is using a ERC1271 contract
/// wallet that is simply backed by her private key.
function test_claimInvite_usingERC1271Wallet_succeeds() external {
_grantInvitesTo(address(carolERC1271Wallet));
OptimistInviter.ClaimableInvite memory claimableInvite =
optimistInviterHelper.getClaimableInviteWithNewNonce(address(carolERC1271Wallet));
bytes memory signature = _getSignature(carolPrivateKey, optimistInviterHelper.getDigest(claimableInvite));
// Sally tries to claim the invite
_commitInviteAs(sally, signature);
_passMinCommitmentPeriod();
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
sally,
OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY,
abi.encode(address(carolERC1271Wallet))
);
vm.prank(sally);
optimistInviter.claimInvite(sally, claimableInvite, signature);
assertEq(_getInviteCount(address(carolERC1271Wallet)), 2);
}
/// @notice Claimer must commit the signature before claiming the invite. Sally attempts to
/// claim the Bob's invite without committing the signature first.
function test_claimInvite_withoutCommittingHash_reverts() external {
_grantInvitesTo(bob);
(OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteAs(bobPrivateKey);
vm.expectRevert("OptimistInviter: claimer and signature have not been committed yet");
vm.prank(sally);
optimistInviter.claimInvite(sally, claimableInvite, signature);
}
/// @notice Using a signature that doesn't correspond to the claimable invite should fail.
function test_claimInvite_withIncorrectSignature_reverts() external {
_grantInvitesTo(carol);
_grantInvitesTo(bob);
(OptimistInviter.ClaimableInvite memory bobClaimableInvite, bytes memory bobSignature) =
_issueInviteAs(bobPrivateKey);
(, bytes memory carolSignature) = _issueInviteAs(carolPrivateKey);
_commitInviteAs(sally, bobSignature);
_commitInviteAs(sally, carolSignature);
_passMinCommitmentPeriod();
vm.expectRevert("OptimistInviter: invalid signature");
vm.prank(sally);
optimistInviter.claimInvite(sally, bobClaimableInvite, carolSignature);
}
/// @notice Attempting to use a signature from a issuer who never was granted invites should
/// fail.
function test_claimInvite_whenIssuerNeverReceivedInvites_reverts() external {
// Bob was never granted any invites, but issues an invite for Eve
(OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteAs(bobPrivateKey);
_commitInviteAs(sally, signature);
_passMinCommitmentPeriod();
vm.expectRevert("OptimistInviter: issuer has no invites");
vm.prank(sally);
optimistInviter.claimInvite(sally, claimableInvite, signature);
}
/// @notice Attempting to use a signature from a issuer who has no more invites should fail.
/// Bob has 3 invites, but issues 4 invites for Sally, Carol, Ted, and Eve. Only the
/// first 3 invites should be claimable. The last claimer, Eve, should not be able to
/// claim the invite.
function test_claimInvite_whenIssuerHasNoInvitesLeft_reverts() external {
_grantInvitesTo(bob);
_issueThenClaimShouldSucceed(bobPrivateKey, sally);
_issueThenClaimShouldSucceed(bobPrivateKey, carol);
_issueThenClaimShouldSucceed(bobPrivateKey, ted);
assertEq(_getInviteCount(bob), 0);
(OptimistInviter.ClaimableInvite memory claimableInvite4, bytes memory signature4) =
_issueInviteAs(bobPrivateKey);
_commitInviteAs(eve, signature4);
_passMinCommitmentPeriod();
vm.expectRevert("OptimistInviter: issuer has no invites");
vm.prank(eve);
optimistInviter.claimInvite(eve, claimableInvite4, signature4);
assertEq(_getInviteCount(bob), 0);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment