Commit 44eec0de authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #5324 from ethereum-optimism/03-30-update_Optimist_to_use_allowlist

feat(contracts-periphery): update Optimist contract to use OptimistAllowlist
parents 915036aa c713ce3a
[submodule "tests"]
path = l2geth/tests/testdata
url = https://github.com/ethereum/tests
[submodule "packages/contracts-periphery/lib/multicall"]
path = packages/contracts-periphery/lib/multicall
url = https://github.com/mds1/multicall
[submodule "lib/multicall"]
branch = v3.1.0
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { Multicall3 } from "multicall/src/Multicall3.sol";
/**
* Just exists so we can compile this contract.
*/
contract MulticallContractCompiler {
}
//SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
pragma solidity >=0.6.2 <0.9.0;
/* Testing utilities */
import { Test } from "forge-std/Test.sol";
import { AttestationStation } from "../universal/op-nft/AttestationStation.sol";
import { Optimist } from "../universal/op-nft/Optimist.sol";
import { OptimistAllowlist } from "../universal/op-nft/OptimistAllowlist.sol";
import { OptimistInviter } from "../universal/op-nft/OptimistInviter.sol";
import { OptimistInviterHelper } from "../testing/helpers/OptimistInviterHelper.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
interface IMulticall3 {
struct Call3 {
address target;
bool allowFailure;
bytes callData;
}
struct Result {
bool success;
bytes returnData;
}
function aggregate3(Call3[] calldata calls)
external
payable
returns (Result[] memory returnData);
}
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
);
address constant alice_admin = address(128);
address constant bob = address(256);
address constant sally = address(512);
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;
function attestBaseuri(string memory _baseUri) internal {
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),
bytes32("optimist.base-uri"),
baseURIAttestationKey,
bytes(_baseUri)
);
vm.prank(alice_admin);
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));
}
function attestAllowlist(address _about) internal {
/**
* @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: bytes32("optimist.can-mint"),
key: attestationKey,
val: bytes("true")
});
vm.prank(alice_admin);
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.encodeWithSelector(OptimistAllowlist.isAllowedToMint.selector, _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 {
// Give alice and bob and sally some ETH
vm.deal(alice_admin, 1 ether);
vm.deal(bob, 1 ether);
vm.deal(sally, 1 ether);
vm.label(alice_admin, "alice_admin");
vm.label(bob, "bob");
vm.label(sally, "sally");
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();
}
......@@ -63,99 +207,207 @@ contract Optimist_Initializer is Test {
attestationStation = new AttestationStation();
vm.expectEmit(true, true, false, false);
emit Initialized(1);
optimist = new Optimist(name, symbol, alice_admin, attestationStation);
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
});
// address test = deployCode("Multicall3.sol");
multicall3 = IMulticall3(deployCode("Multicall3.sol"));
}
}
contract OptimistTest is Optimist_Initializer {
function test_optimist_initialize() external {
/**
* @notice Check that constructor and initializer parameters are correctly set.
*/
function test_initialize_success() external {
// 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.ATTESTOR(), alice_admin);
assertEq(optimist.version(), "1.0.0");
assertEq(optimist.BASE_URI_ATTESTOR(), carol_baseURIAttestor);
assertEq(optimist.version(), "2.0.0");
}
/**
* @dev Bob should be able to mint an NFT if he is allowlisted
* by the attestation station and has a balance of 0
* @notice Bob should be able to mint an NFT if he is allowlisted
* by the allowlistAttestor and has a balance of 0.
*/
function test_optimist_mint_happy_path() external {
function test_mint_afterAllowlistAttestation_succeeds() external {
// bob should start with 0 balance
assertEq(optimist.balanceOf(bob), 0);
// whitelist bob
attestAllowlist(bob);
// allowlist bob
_attestAllowlist(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
// Check that the OptimistAllowlist is checked
bytes memory data = abi.encodeWithSelector(optimistAllowlist.isAllowedToMint.selector, bob);
vm.expectCall(address(optimistAllowlist), data);
uint256 tokenId = uint256(uint160(bob));
// mint an NFT and expect mint transfer event to be emitted
vm.expectEmit(true, true, true, true);
emit Transfer(address(0), bob, tokenId);
emit Transfer(address(0), bob, _getTokenId(bob));
vm.prank(bob);
optimist.mint(bob);
bytes memory data = abi.encodeWithSelector(
attestationStation.attestations.selector,
alice_admin,
bob,
bytes32("optimist.can-mint")
);
vm.expectCall(address(attestationStation), data);
// mint an NFT
// 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.encodeWithSelector(optimistAllowlist.isAllowedToMint.selector, 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(256), bob);
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
}
/**
* @dev Sally should be able to mint a token on behalf of bob
* @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_optimist_mint_secondary_minter() external {
attestAllowlist(bob);
function test_mint_afterCoinbaseQuestAttestation_succeeds() external {
// bob should start with 0 balance
assertEq(optimist.balanceOf(bob), 0);
bytes memory data = abi.encodeWithSelector(
attestationStation.attestations.selector,
alice_admin,
bob,
bytes32("optimist.can-mint")
);
vm.expectCall(address(attestationStation), data);
// bob receives attestation from Coinbase Quest attestor
_attestCoinbaseQuest(bob);
assertTrue(optimistAllowlist.isAllowedToMint(bob));
// Check that the OptimistAllowlist is checked
bytes memory data = abi.encodeWithSelector(optimistAllowlist.isAllowedToMint.selector, 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.encodeWithSelector(optimistAllowlist.isAllowedToMint.selector, 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);
uint256 tokenId = uint256(uint160(bob));
vm.expectEmit(true, true, true, true);
emit Transfer(address(0), bob, tokenId);
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(256), bob);
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
}
/**
* @dev Bob should not be able to mint an NFT if he is not whitelisted
* @notice Bob should not be able to mint an NFT if he is not allowlisted.
*/
function test_optimist_mint_no_attestation() external {
function test_mint_forNonAllowlistedClaimer_reverts() external {
vm.prank(bob);
vm.expectRevert("Optimist: address is not on allowList");
optimist.mint(bob);
}
/**
* @dev Bob's tx should revert if he already minted
* @notice Bob's tx should revert if he already minted.
*/
function test_optimist_mint_already_minted() external {
attestAllowlist(bob);
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(256), bob);
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
// attempt to mint again
......@@ -164,82 +416,52 @@ contract OptimistTest is Optimist_Initializer {
}
/**
* @dev The baseURI should be set by attestation station
* by the owner of contract alice_admin
* @notice The baseURI should be set by attestation station by the baseURIAttestor.
*/
function test_optimist_baseURI() external {
attestBaseuri(base_uri);
function test_baseURI_returnsCorrectBaseURI_succeeds() external {
_attestBaseURI(base_uri);
bytes memory data = abi.encodeWithSelector(
attestationStation.attestations.selector,
alice_admin,
carol_baseURIAttestor,
address(optimist),
bytes32("optimist.base-uri")
optimist.BASE_URI_ATTESTATION_KEY()
);
vm.expectCall(address(attestationStation), data);
vm.prank(alice_admin);
vm.prank(carol_baseURIAttestor);
// assert baseURI is set
assertEq(optimist.baseURI(), base_uri);
}
/**
* @dev The tokenURI should return the token uri
* for a minted token
* @notice tokenURI should return the token uri for a minted token.
*/
function test_optimist_token_uri() external {
attestAllowlist(bob);
function test_tokenURI_returnsCorrectTokenURI_succeeds() external {
// we are using true but it can be any non empty value
attestBaseuri(base_uri);
_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(256),
// solhint-disable-next-line max-line-length
"https://storageapi.fleek.co/6442819a1b05-bucket/optimist-nft/attributes/0x0000000000000000000000000000000000000100.json"
);
}
/**
* @dev Should return a boolean of if the address is whitelisted
*/
function test_optimist_is_on_allow_list() external {
attestAllowlist(bob);
bytes memory data = abi.encodeWithSelector(
attestationStation.attestations.selector,
alice_admin,
bob,
bytes32("optimist.can-mint")
optimist.tokenURI(_getTokenId(bob)),
"https://storageapi.fleek.co/6442819a1b05-bucket/optimist-nft/attributes/0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e.json"
);
vm.expectCall(address(attestationStation), data);
// assert bob is whitelisted
assertEq(optimist.isOnAllowList(bob), true);
data = abi.encodeWithSelector(
attestationStation.attestations.selector,
alice_admin,
sally,
bytes32("optimist.can-mint")
);
vm.expectCall(address(attestationStation), data);
// assert sally is not whitelisted
assertEq(optimist.isOnAllowList(sally), false);
}
/**
* @dev Should return the token id of the owner
* @notice Should return the token id of the owner.
*/
function test_optimist_token_id_of_owner() external {
// whitelist bob
function test_tokenIdOfAddress_returnsOwnerID_succeeds() external {
uint256 willTokenId = 1024;
address will = address(1024);
attestAllowlist(will);
_mockAllowlistTrueFor(will);
optimist.mint(will);
......@@ -247,10 +469,10 @@ contract OptimistTest is Optimist_Initializer {
}
/**
* @dev It should revert if anybody attemps token transfer
* @notice transferFrom should revert since Optimist is a SBT.
*/
function test_optimist_sbt_transfer() external {
attestAllowlist(bob);
function test_transferFrom_reverts() external {
_mockAllowlistTrueFor(bob);
// mint as bob
vm.prank(bob);
......@@ -259,23 +481,23 @@ contract OptimistTest is Optimist_Initializer {
// attempt to transfer to sally
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.prank(bob);
optimist.transferFrom(bob, sally, 256);
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, 256);
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, 256, bytes("0x"));
optimist.safeTransferFrom(bob, sally, _getTokenId(bob), bytes("0x"));
}
/**
* @dev It should revert if anybody attemps approve
* @notice approve should revert since Optimist is a SBT.
*/
function test_optimist_sbt_approve() external {
attestAllowlist(bob);
function test_approve_reverts() external {
_mockAllowlistTrueFor(bob);
// mint as bob
vm.prank(bob);
......@@ -284,16 +506,38 @@ contract OptimistTest is Optimist_Initializer {
// attempt to approve sally
vm.prank(bob);
vm.expectRevert("Optimist: soul bound token");
optimist.approve(address(attestationStation), 256);
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_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);
assertEq(optimist.getApproved(256), address(0));
// 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
);
}
/**
* @dev It should be able to burn token
* @notice Only owner should be able to burn token.
*/
function test_optimist_burn() external {
attestAllowlist(bob);
function test_burn_byOwner_succeeds() external {
_mockAllowlistTrueFor(bob);
// mint as bob
vm.prank(bob);
......@@ -301,37 +545,103 @@ contract OptimistTest is Optimist_Initializer {
// burn as bob
vm.prank(bob);
optimist.burn(256);
optimist.burn(_getTokenId(bob));
// expect bob to have no balance now
assertEq(optimist.balanceOf(bob), 0);
}
/**
* @dev setApprovalForAll should revert as sbt
* @notice Non-owner attempting to burn token should revert.
*/
function test_optimist_set_approval_for_all() external {
attestAllowlist(bob);
function test_burn_byNonOwner_reverts() external {
_mockAllowlistTrueFor(bob);
// mint as bob
vm.prank(bob);
optimist.mint(bob);
vm.prank(alice_admin);
vm.expectRevert(bytes("Optimist: soul bound token"));
optimist.setApprovalForAll(alice_admin, true);
// expect approval amount to stil be 0
assertEq(optimist.getApproved(256), address(0));
// isApprovedForAll should return false
assertEq(optimist.isApprovedForAll(alice_admin, alice_admin), false);
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);
}
/**
* @dev should support erc721 interface
* @notice Should support ERC-721 interface.
*/
function test_optimist_supports_interface() external {
function test_supportsInterface_returnsCorrectInterfaceForERC721_succeeds() external {
bytes4 iface721 = type(IERC721).interfaceId;
// check that it supports erc721 interface
// 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.encodeWithSelector(
optimistInviter.claimInvite.selector,
bob,
claimableInvite,
signature
),
allowFailure: false
});
// Second call is to mint the Optimist NFT
calls[1] = IMulticall3.Call3({
target: address(optimist),
callData: abi.encodeWithSelector(optimist.mint.selector, bob),
allowFailure: false
});
multicall3.aggregate3(calls);
assertTrue(optimist.isOnAllowList(bob));
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
assertEq(optimist.balanceOf(bob), 1);
}
}
......@@ -6,6 +6,7 @@ import {
ERC721BurnableUpgradeable
} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import { AttestationStation } from "./AttestationStation.sol";
import { OptimistAllowlist } from "./OptimistAllowlist.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
/**
......@@ -15,31 +16,44 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
* @notice A Soul Bound Token for real humans only(tm).
*/
contract Optimist is ERC721BurnableUpgradeable, Semver {
/**
* @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 Attestor who attests to baseURI and allowlist.
* @notice Address of the OptimistAllowlist contract.
*/
address public immutable ATTESTOR;
OptimistAllowlist public immutable OPTIMIST_ALLOWLIST;
/**
* @custom:semver 1.0.0
* @custom:semver 2.0.0
* @param _name Token name.
* @param _symbol Token symbol.
* @param _attestor Address of the attestor.
* @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 _attestor,
AttestationStation _attestationStation
) Semver(1, 0, 0) {
ATTESTOR = _attestor;
address _baseURIAttestor,
AttestationStation _attestationStation,
OptimistAllowlist _optimistAllowlist
) Semver(2, 0, 0) {
BASE_URI_ATTESTOR = _baseURIAttestor;
ATTESTATION_STATION = _attestationStation;
OPTIMIST_ALLOWLIST = _optimistAllowlist;
initialize(_name, _symbol);
}
......@@ -76,7 +90,7 @@ contract Optimist is ERC721BurnableUpgradeable, Semver {
string(
abi.encodePacked(
ATTESTATION_STATION.attestations(
ATTESTOR,
BASE_URI_ATTESTOR,
address(this),
bytes32("optimist.base-uri")
)
......@@ -105,17 +119,15 @@ contract Optimist is ERC721BurnableUpgradeable, Semver {
}
/**
* @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.
* @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 Whether or not the address is allowed to mint yet.
*/
function isOnAllowList(address _recipient) public view returns (bool) {
return
ATTESTATION_STATION
.attestations(ATTESTOR, _recipient, bytes32("optimist.can-mint"))
.length > 0;
return OPTIMIST_ALLOWLIST.isAllowedToMint(_recipient);
}
/**
......
......@@ -16,9 +16,13 @@ remappings = [
'@rari-capital/solmate/=node_modules/@rari-capital/solmate',
'forge-std/=node_modules/forge-std/src',
'ds-test/=node_modules/ds-test/src',
'multicall/=lib/multicall',
'@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/',
'@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/',
'@eth-optimism/contracts-bedrock/=../../node_modules/@eth-optimism/contracts-bedrock',
]
# The metadata hash can be removed from the bytecode by setting "none"
bytecode_hash = "none"
libs = ["node_modules", "lib"]
# Required to use `deployCode` to deploy the multicall contract which has incompatible version
fs_permissions = [{ access = "read", path = "./forge-artifacts/Multicall3.sol/Multicall3.json"}]
Subproject commit a1fa0644fa412cd3237ef7081458ecb2ffad7dbe
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