Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
0c3b866b
Unverified
Commit
0c3b866b
authored
Apr 04, 2023
by
Madhur Shrimal
Committed by
GitHub
Apr 04, 2023
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'ethereum-optimism:develop' into madhur/tx-metrics
parents
7c2dfa62
44eec0de
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
516 additions
and
159 deletions
+516
-159
.gitmodules
.gitmodules
+5
-0
main.go
op-chain-ops/cmd/rollover/main.go
+22
-8
util.go
op-chain-ops/util/util.go
+4
-4
.gitmodules
packages/contracts-periphery/.gitmodules
+0
-0
MulticallContractCompiler.t.sol
...y/contracts/foundry-tests/MulticallContractCompiler.t.sol
+11
-0
Optimist.t.sol
...ontracts-periphery/contracts/foundry-tests/Optimist.t.sol
+440
-130
Optimist.sol
...ntracts-periphery/contracts/universal/op-nft/Optimist.sol
+29
-17
foundry.toml
packages/contracts-periphery/foundry.toml
+4
-0
multicall
packages/contracts-periphery/lib/multicall
+1
-0
No files found.
.gitmodules
View file @
0c3b866b
[submodule "tests"]
[submodule "tests"]
path = l2geth/tests/testdata
path = l2geth/tests/testdata
url = https://github.com/ethereum/tests
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
op-chain-ops/cmd/rollover/main.go
View file @
0c3b866b
...
@@ -9,21 +9,25 @@ import (
...
@@ -9,21 +9,25 @@ import (
"sync"
"sync"
"time"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
"github.com/ethereum-optimism/optimism/op-chain-ops/util"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
legacy_bindings
"github.com/ethereum-optimism/optimism/op-bindings/legacy-bindings"
legacy_bindings
"github.com/ethereum-optimism/optimism/op-bindings/legacy-bindings"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum-optimism/optimism/op-chain-ops/util"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
)
func
main
()
{
func
main
()
{
log
.
Root
()
.
SetHandler
(
log
.
StreamHandler
(
os
.
Stderr
,
log
.
TerminalFormat
(
isatty
.
IsTerminal
(
os
.
Stderr
.
Fd
()))))
app
:=
cli
.
NewApp
()
app
:=
cli
.
NewApp
()
app
.
Name
=
"rollover"
app
.
Name
=
"rollover"
app
.
Usage
=
"Commands for assisting in the rollover of the system"
app
.
Usage
=
"Commands for assisting in the rollover of the system"
...
@@ -149,6 +153,9 @@ func main() {
...
@@ -149,6 +153,9 @@ func main() {
return
err
return
err
}
}
log
.
Info
(
"Remaining deposits that must be submitted"
,
"count"
,
finalPending
)
log
.
Info
(
"Remaining deposits that must be submitted"
,
"count"
,
finalPending
)
if
finalPending
.
Cmp
(
common
.
Big0
)
==
0
{
log
.
Info
(
"All deposits have been batch submitted"
)
}
return
nil
return
nil
},
},
},
},
...
@@ -183,11 +190,11 @@ func main() {
...
@@ -183,11 +190,11 @@ func main() {
log
.
Info
(
"Waiting for CanonicalTransactionChain"
)
log
.
Info
(
"Waiting for CanonicalTransactionChain"
)
wg
.
Add
(
1
)
wg
.
Add
(
1
)
go
waitForTotalElements
(
&
wg
,
ctc
,
clients
.
L2Client
)
go
waitForTotalElements
(
&
wg
,
ctc
,
clients
.
L2Client
,
"CanonicalTransactionChain"
)
log
.
Info
(
"Waiting for StateCommitmentChain"
)
log
.
Info
(
"Waiting for StateCommitmentChain"
)
wg
.
Add
(
1
)
wg
.
Add
(
1
)
go
waitForTotalElements
(
&
wg
,
scc
,
clients
.
L2Client
)
go
waitForTotalElements
(
&
wg
,
scc
,
clients
.
L2Client
,
"StateCommitmentChain"
)
wg
.
Wait
()
wg
.
Wait
()
log
.
Info
(
"All batches have been submitted"
)
log
.
Info
(
"All batches have been submitted"
)
...
@@ -210,7 +217,7 @@ type RollupContract interface {
...
@@ -210,7 +217,7 @@ type RollupContract interface {
}
}
// waitForTotalElements will poll to see
// waitForTotalElements will poll to see
func
waitForTotalElements
(
wg
*
sync
.
WaitGroup
,
contract
RollupContract
,
client
*
ethclient
.
Client
)
{
func
waitForTotalElements
(
wg
*
sync
.
WaitGroup
,
contract
RollupContract
,
client
*
ethclient
.
Client
,
name
string
)
{
defer
wg
.
Done
()
defer
wg
.
Done
()
for
{
for
{
...
@@ -228,9 +235,16 @@ func waitForTotalElements(wg *sync.WaitGroup, contract RollupContract, client *e
...
@@ -228,9 +235,16 @@ func waitForTotalElements(wg *sync.WaitGroup, contract RollupContract, client *e
}
}
if
totalElements
.
Uint64
()
==
bn
{
if
totalElements
.
Uint64
()
==
bn
{
log
.
Info
(
"Total elements matches block number"
,
"name"
,
name
,
"count"
,
bn
)
return
return
}
}
log
.
Info
(
"Waiting for elements to be submitted"
,
"count"
,
totalElements
.
Uint64
()
-
bn
,
"height"
,
bn
,
"total-elements"
,
totalElements
.
Uint64
())
log
.
Info
(
"Waiting for elements to be submitted"
,
"name"
,
name
,
"count"
,
totalElements
.
Uint64
()
-
bn
,
"height"
,
bn
,
"total-elements"
,
totalElements
.
Uint64
(),
)
time
.
Sleep
(
3
*
time
.
Second
)
time
.
Sleep
(
3
*
time
.
Second
)
}
}
...
...
op-chain-ops/util/util.go
View file @
0c3b866b
...
@@ -39,21 +39,21 @@ func NewClients(ctx *cli.Context) (*Clients, error) {
...
@@ -39,21 +39,21 @@ func NewClients(ctx *cli.Context) (*Clients, error) {
l1RpcURL
:=
ctx
.
String
(
"l1-rpc-url"
)
l1RpcURL
:=
ctx
.
String
(
"l1-rpc-url"
)
l1Client
,
err
:=
ethclient
.
Dial
(
l1RpcURL
)
l1Client
,
err
:=
ethclient
.
Dial
(
l1RpcURL
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
fmt
.
Errorf
(
"cannot dial L1: %w"
,
err
)
}
}
l1ChainID
,
err
:=
l1Client
.
ChainID
(
context
.
Background
())
l1ChainID
,
err
:=
l1Client
.
ChainID
(
context
.
Background
())
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
fmt
.
Errorf
(
"cannot fetch L1 chainid: %w"
,
err
)
}
}
l2RpcURL
:=
ctx
.
String
(
"l2-rpc-url"
)
l2RpcURL
:=
ctx
.
String
(
"l2-rpc-url"
)
l2Client
,
err
:=
ethclient
.
Dial
(
l2RpcURL
)
l2Client
,
err
:=
ethclient
.
Dial
(
l2RpcURL
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
fmt
.
Errorf
(
"cannot dial L2: %w"
,
err
)
}
}
l2ChainID
,
err
:=
l2Client
.
ChainID
(
context
.
Background
())
l2ChainID
,
err
:=
l2Client
.
ChainID
(
context
.
Background
())
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
fmt
.
Errorf
(
"cannot fetch L2 chainid: %w"
,
err
)
}
}
l1RpcClient
,
err
:=
rpc
.
DialContext
(
context
.
Background
(),
l1RpcURL
)
l1RpcClient
,
err
:=
rpc
.
DialContext
(
context
.
Background
(),
l1RpcURL
)
...
...
packages/contracts-periphery/.gitmodules
0 → 100644
View file @
0c3b866b
packages/contracts-periphery/contracts/foundry-tests/MulticallContractCompiler.t.sol
0 → 100644
View file @
0c3b866b
// 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 {
}
packages/contracts-periphery/contracts/foundry-tests/Optimist.t.sol
View file @
0c3b866b
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: MIT
pragma solidity
0.8.15
;
pragma solidity
>=0.6.2 <0.9.0
;
/* Testing utilities */
/* Testing utilities */
import { Test } from "forge-std/Test.sol";
import { Test } from "forge-std/Test.sol";
import { AttestationStation } from "../universal/op-nft/AttestationStation.sol";
import { AttestationStation } from "../universal/op-nft/AttestationStation.sol";
import { Optimist } from "../universal/op-nft/Optimist.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 { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.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 {
contract Optimist_Initializer is Test {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Initialized(uint8);
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 name = "Optimist name";
string constant symbol = "OPTIMISTSYMBOL";
string constant symbol = "OPTIMISTSYMBOL";
string constant base_uri =
string constant base_uri =
"https://storageapi.fleek.co/6442819a1b05-bucket/optimist-nft/attributes";
"https://storageapi.fleek.co/6442819a1b05-bucket/optimist-nft/attributes";
AttestationStation attestationStation;
AttestationStation attestationStation;
Optimist optimist;
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[]
AttestationStation.AttestationData[]
memory attestationData = new AttestationStation.AttestationData[](1);
memory attestationData = new AttestationStation.AttestationData[](1);
attestationData[0] = AttestationStation.AttestationData(
attestationData[0] = AttestationStation.AttestationData(
address(optimist),
address(optimist),
b
ytes32("optimist.base-uri")
,
b
aseURIAttestationKey
,
bytes(_baseUri)
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);
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[]
AttestationStation.AttestationData[]
memory attestationData = new AttestationStation.AttestationData[](1);
memory attestationData = new AttestationStation.AttestationData[](1);
// we are using true but it can be any non empty value
// we are using true but it can be any non empty value
attestationData[0] = AttestationStation.AttestationData({
attestationData[0] = AttestationStation.AttestationData({
about: _about,
about: _about,
key:
bytes32("optimist.can-mint")
,
key:
attestationKey
,
val: bytes("true")
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);
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 {
function setUp() public {
// Give alice and bob and sally some ETH
carol_baseURIAttestor = makeAddr("carol_baseURIAttestor");
vm.deal(alice_admin, 1 ether);
alice_allowlistAttestor = makeAddr("alice_allowlistAttestor");
vm.deal(bob, 1 ether);
eve_inviteGranter = makeAddr("eve_inviteGranter");
vm.deal(sally, 1 ether);
ted_coinbaseAttestor = makeAddr("ted_coinbaseAttestor");
bob = makeAddr("bob");
vm.label(alice_admin, "alice_admin");
sally = makeAddr("sally");
vm.label(bob, "bob");
vm.label(sally, "sally");
_initializeContracts();
_initializeContracts();
}
}
...
@@ -63,99 +207,207 @@ contract Optimist_Initializer is Test {
...
@@ -63,99 +207,207 @@ contract Optimist_Initializer is Test {
attestationStation = new AttestationStation();
attestationStation = new AttestationStation();
vm.expectEmit(true, true, false, false);
vm.expectEmit(true, true, false, false);
emit Initialized(1);
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 {
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
// expect name to be set
assertEq(optimist.name(), name);
assertEq(optimist.name(), name);
// expect symbol to be set
// expect symbol to be set
assertEq(optimist.symbol(), symbol);
assertEq(optimist.symbol(), symbol);
// expect attestationStation to be set
// expect attestationStation to be set
assertEq(address(optimist.ATTESTATION_STATION()), address(attestationStation));
assertEq(address(optimist.ATTESTATION_STATION()), address(attestationStation));
assertEq(optimist.
ATTESTOR(), alice_admin
);
assertEq(optimist.
BASE_URI_ATTESTOR(), carol_baseURIAttestor
);
assertEq(optimist.version(), "
1
.0.0");
assertEq(optimist.version(), "
2
.0.0");
}
}
/**
/**
* @
dev
Bob should be able to mint an NFT if he is allowlisted
* @
notice
Bob should be able to mint an NFT if he is allowlisted
*
by the attestation station and has a balance of 0
*
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
// bob should start with 0 balance
assertEq(optimist.balanceOf(bob), 0);
assertEq(optimist.balanceOf(bob), 0);
// whitelist bob
// allowlist bob
attestAllowlist(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);
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(
// expect the NFT to be owned by bob
attestationStation.attestations.selector,
assertEq(optimist.ownerOf(_getTokenId(bob)), bob);
alice_admin,
assertEq(optimist.balanceOf(bob), 1);
bob,
}
bytes32("optimist.can-mint")
);
/**
vm.expectCall(address(attestationStation), data);
* @notice Bob should be able to mint an NFT if he claimed an invite through OptimistInviter
// mint an NFT
* 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);
vm.prank(bob);
optimist.mint(bob);
optimist.mint(bob);
// expect the NFT to be owned by 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);
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 {
function test_mint_afterCoinbaseQuestAttestation_succeeds() external {
attestAllowlist(bob);
// bob should start with 0 balance
assertEq(optimist.balanceOf(bob), 0);
bytes memory data = abi.encodeWithSelector(
// bob receives attestation from Coinbase Quest attestor
attestationStation.attestations.selector,
_attestCoinbaseQuest(bob);
alice_admin,
bob,
assertTrue(optimistAllowlist.isAllowedToMint(bob));
bytes32("optimist.can-mint")
);
// Check that the OptimistAllowlist is checked
vm.expectCall(address(attestationStation), data);
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);
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
// mint as sally instead of bob
vm.prank(sally);
vm.prank(sally);
optimist.mint(bob);
optimist.mint(bob);
// expect the NFT to be owned by 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);
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.prank(bob);
vm.expectRevert("Optimist: address is not on allowList");
vm.expectRevert("Optimist: address is not on allowList");
optimist.mint(bob);
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 {
function test_
mint_forAlreadyMintedClaimer_reverts
() external {
attestAllowlist(bob);
_
attestAllowlist(bob);
// mint initial nft with bob
// mint initial nft with bob
vm.prank(bob);
vm.prank(bob);
optimist.mint(bob);
optimist.mint(bob);
// expect the NFT to be owned by 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);
assertEq(optimist.balanceOf(bob), 1);
// attempt to mint again
// attempt to mint again
...
@@ -164,82 +416,52 @@ contract OptimistTest is Optimist_Initializer {
...
@@ -164,82 +416,52 @@ contract OptimistTest is Optimist_Initializer {
}
}
/**
/**
* @dev The baseURI should be set by attestation station
* @notice The baseURI should be set by attestation station by the baseURIAttestor.
* by the owner of contract alice_admin
*/
*/
function test_
optimist_baseURI
() external {
function test_
baseURI_returnsCorrectBaseURI_succeeds
() external {
attestBaseuri
(base_uri);
_attestBaseURI
(base_uri);
bytes memory data = abi.encodeWithSelector(
bytes memory data = abi.encodeWithSelector(
attestationStation.attestations.selector,
attestationStation.attestations.selector,
alice_admin
,
carol_baseURIAttestor
,
address(optimist),
address(optimist),
bytes32("optimist.base-uri"
)
optimist.BASE_URI_ATTESTATION_KEY(
)
);
);
vm.expectCall(address(attestationStation), data);
vm.expectCall(address(attestationStation), data);
vm.prank(
alice_admin
);
vm.prank(
carol_baseURIAttestor
);
// assert baseURI is set
// assert baseURI is set
assertEq(optimist.baseURI(), base_uri);
assertEq(optimist.baseURI(), base_uri);
}
}
/**
/**
* @dev The tokenURI should return the token uri
* @notice tokenURI should return the token uri for a minted token.
* for a minted token
*/
*/
function test_optimist_token_uri() external {
function test_tokenURI_returnsCorrectTokenURI_succeeds() external {
attestAllowlist(bob);
// we are using true but it can be any non empty value
// we are using true but it can be any non empty value
attestBaseuri
(base_uri);
_attestBaseURI
(base_uri);
// mint an NFT
// mint an NFT
_mockAllowlistTrueFor(bob);
vm.prank(bob);
vm.prank(bob);
optimist.mint(bob);
optimist.mint(bob);
// assert tokenURI is set
// assert tokenURI is set
assertEq(optimist.baseURI(), base_uri);
assertEq(optimist.baseURI(), base_uri);
assertEq(
assertEq(
optimist.tokenURI(256),
optimist.tokenURI(_getTokenId(bob)),
// solhint-disable-next-line max-line-length
"https://storageapi.fleek.co/6442819a1b05-bucket/optimist-nft/attributes/0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e.json"
"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")
);
);
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 {
function test_tokenIdOfAddress_returnsOwnerID_succeeds() external {
// whitelist bob
uint256 willTokenId = 1024;
uint256 willTokenId = 1024;
address will = address(1024);
address will = address(1024);
attestAllowlist
(will);
_mockAllowlistTrueFor
(will);
optimist.mint(will);
optimist.mint(will);
...
@@ -247,10 +469,10 @@ contract OptimistTest is Optimist_Initializer {
...
@@ -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 {
function test_
transferFrom_reverts
() external {
attestAllowlist
(bob);
_mockAllowlistTrueFor
(bob);
// mint as bob
// mint as bob
vm.prank(bob);
vm.prank(bob);
...
@@ -259,23 +481,23 @@ contract OptimistTest is Optimist_Initializer {
...
@@ -259,23 +481,23 @@ contract OptimistTest is Optimist_Initializer {
// attempt to transfer to sally
// attempt to transfer to sally
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.prank(bob);
vm.prank(bob);
optimist.transferFrom(bob, sally,
256
);
optimist.transferFrom(bob, sally,
_getTokenId(bob)
);
// attempt to transfer to sally
// attempt to transfer to sally
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.prank(bob);
vm.prank(bob);
optimist.safeTransferFrom(bob, sally,
256
);
optimist.safeTransferFrom(bob, sally,
_getTokenId(bob)
);
// attempt to transfer to sally
// attempt to transfer to sally
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.prank(bob);
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 {
function test_
approve_reverts
() external {
attestAllowlist
(bob);
_mockAllowlistTrueFor
(bob);
// mint as bob
// mint as bob
vm.prank(bob);
vm.prank(bob);
...
@@ -284,16 +506,38 @@ contract OptimistTest is Optimist_Initializer {
...
@@ -284,16 +506,38 @@ contract OptimistTest is Optimist_Initializer {
// attempt to approve sally
// attempt to approve sally
vm.prank(bob);
vm.prank(bob);
vm.expectRevert("Optimist: soul bound token");
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 {
function test_
burn_byOwner_succeeds
() external {
attestAllowlist
(bob);
_mockAllowlistTrueFor
(bob);
// mint as bob
// mint as bob
vm.prank(bob);
vm.prank(bob);
...
@@ -301,37 +545,103 @@ contract OptimistTest is Optimist_Initializer {
...
@@ -301,37 +545,103 @@ contract OptimistTest is Optimist_Initializer {
// burn as bob
// burn as bob
vm.prank(bob);
vm.prank(bob);
optimist.burn(
256
);
optimist.burn(
_getTokenId(bob)
);
// expect bob to have no balance now
// expect bob to have no balance now
assertEq(optimist.balanceOf(bob), 0);
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 {
function test_
burn_byNonOwner_reverts
() external {
attestAllowlist
(bob);
_mockAllowlistTrueFor
(bob);
// mint as bob
// mint as bob
vm.prank(bob);
vm.prank(bob);
optimist.mint(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
vm.expectRevert("ERC721: caller is not token owner nor approved");
assertEq(optimist.getApproved(256), address(0));
// burn as Sally
// isApprovedForAll should return false
vm.prank(sally);
assertEq(optimist.isApprovedForAll(alice_admin, alice_admin), false);
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;
bytes4 iface721 = type(IERC721).interfaceId;
// check that it supports
erc
721 interface
// check that it supports
ERC-
721 interface
assertEq(optimist.supportsInterface(iface721), true);
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);
}
}
}
packages/contracts-periphery/contracts/universal/op-nft/Optimist.sol
View file @
0c3b866b
...
@@ -6,6 +6,7 @@ import {
...
@@ -6,6 +6,7 @@ import {
ERC721BurnableUpgradeable
ERC721BurnableUpgradeable
} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import { AttestationStation } from "./AttestationStation.sol";
import { AttestationStation } from "./AttestationStation.sol";
import { OptimistAllowlist } from "./OptimistAllowlist.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
/**
/**
...
@@ -15,31 +16,44 @@ 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).
* @notice A Soul Bound Token for real humans only(tm).
*/
*/
contract Optimist is ERC721BurnableUpgradeable, Semver {
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.
* @notice Address of the AttestationStation contract.
*/
*/
AttestationStation public immutable ATTESTATION_STATION;
AttestationStation public immutable ATTESTATION_STATION;
/**
/**
* @notice A
ttestor who attests to baseURI and allowlis
t.
* @notice A
ddress of the OptimistAllowlist contrac
t.
*/
*/
address public immutable ATTESTOR
;
OptimistAllowlist public immutable OPTIMIST_ALLOWLIST
;
/**
/**
* @custom:semver
1
.0.0
* @custom:semver
2
.0.0
* @param _name Token name.
* @param _name Token name.
* @param _symbol Token symbol.
* @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 _attestationStation Address of the AttestationStation contract.
* @param _optimistAllowlist Address of the OptimistAllowlist contract
*/
*/
constructor(
constructor(
string memory _name,
string memory _name,
string memory _symbol,
string memory _symbol,
address _attestor,
address _baseURIAttestor,
AttestationStation _attestationStation
AttestationStation _attestationStation,
) Semver(1, 0, 0) {
OptimistAllowlist _optimistAllowlist
ATTESTOR = _attestor;
) Semver(2, 0, 0) {
BASE_URI_ATTESTOR = _baseURIAttestor;
ATTESTATION_STATION = _attestationStation;
ATTESTATION_STATION = _attestationStation;
OPTIMIST_ALLOWLIST = _optimistAllowlist;
initialize(_name, _symbol);
initialize(_name, _symbol);
}
}
...
@@ -76,7 +90,7 @@ contract Optimist is ERC721BurnableUpgradeable, Semver {
...
@@ -76,7 +90,7 @@ contract Optimist is ERC721BurnableUpgradeable, Semver {
string(
string(
abi.encodePacked(
abi.encodePacked(
ATTESTATION_STATION.attestations(
ATTESTATION_STATION.attestations(
ATTESTOR,
BASE_URI_
ATTESTOR,
address(this),
address(this),
bytes32("optimist.base-uri")
bytes32("optimist.base-uri")
)
)
...
@@ -105,17 +119,15 @@ contract Optimist is ERC721BurnableUpgradeable, Semver {
...
@@ -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
* @notice Checks OptimistAllowlist to determine whether a given address is allowed to mint
* Optimist NFT will also be used as part of the Citizens House, mints are currently
* the Optimist NFT. Since the Optimist NFT will also be used as part of the
* restricted. Eventually anyone will be able to mint.
* Citizens House, mints are currently restricted. Eventually anyone will be able
* to mint.
*
*
* @return Whether or not the address is allowed to mint yet.
* @return Whether or not the address is allowed to mint yet.
*/
*/
function isOnAllowList(address _recipient) public view returns (bool) {
function isOnAllowList(address _recipient) public view returns (bool) {
return
return OPTIMIST_ALLOWLIST.isAllowedToMint(_recipient);
ATTESTATION_STATION
.attestations(ATTESTOR, _recipient, bytes32("optimist.can-mint"))
.length > 0;
}
}
/**
/**
...
...
packages/contracts-periphery/foundry.toml
View file @
0c3b866b
...
@@ -16,9 +16,13 @@ remappings = [
...
@@ -16,9 +16,13 @@ remappings = [
'@rari-capital/solmate/=node_modules/@rari-capital/solmate'
,
'@rari-capital/solmate/=node_modules/@rari-capital/solmate'
,
'forge-std/=node_modules/forge-std/src'
,
'forge-std/=node_modules/forge-std/src'
,
'ds-test/=node_modules/ds-test/src'
,
'ds-test/=node_modules/ds-test/src'
,
'multicall/=lib/multicall'
,
'@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/'
,
'@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/'
,
'@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/'
,
'@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/'
,
'@eth-optimism/contracts-bedrock/=../../node_modules/@eth-optimism/contracts-bedrock'
,
'@eth-optimism/contracts-bedrock/=../../node_modules/@eth-optimism/contracts-bedrock'
,
]
]
# The metadata hash can be removed from the bytecode by setting "none"
# The metadata hash can be removed from the bytecode by setting "none"
bytecode_hash
=
"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"
}
]
multicall
@
a1fa0644
Subproject commit a1fa0644fa412cd3237ef7081458ecb2ffad7dbe
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment