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
6e5fda97
Unverified
Commit
6e5fda97
authored
Dec 14, 2022
by
mergify[bot]
Committed by
GitHub
Dec 14, 2022
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #4371 from ethereum-optimism/willc/optimist
feat(contracts-periphery): Add Optimist contract
parents
ad38dcc0
0e0546a1
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
592 additions
and
2 deletions
+592
-2
wet-apples-cheat.md
.changeset/wet-apples-cheat.md
+5
-0
Optimist.t.sol
...ontracts-periphery/contracts/foundry-tests/Optimist.t.sol
+429
-0
Optimist.sol
...ntracts-periphery/contracts/universal/op-nft/Optimist.sol
+157
-0
package.json
packages/contracts-periphery/package.json
+1
-2
No files found.
.changeset/wet-apples-cheat.md
0 → 100644
View file @
6e5fda97
---
'
@eth-optimism/contracts-periphery'
:
patch
---
Add optimist contract
packages/contracts-periphery/contracts/foundry-tests/Optimist.t.sol
0 → 100644
View file @
6e5fda97
//SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/* 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 { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
contract Optimist_Initializer is Test {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Initialized(uint8);
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;
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");
_initializeContracts();
}
function _initializeContracts() internal {
attestationStation = new AttestationStation();
vm.expectEmit(true, true, false, false);
emit Initialized(1);
optimist = new Optimist(name, symbol, alice_admin, attestationStation);
}
}
contract OptimistTest is Optimist_Initializer {
function setUp() public {
super._setUp();
_initializeContracts();
}
function test_optimist_initialize() 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(), "0.0.1");
}
/**
* @dev Bob should be able to mint an NFT if he is allowlisted
* by the attestation station and has a balance of 0
*/
function test_optimist_mint_happy_path() external {
// bob should start with 0 balance
assertEq(optimist.balanceOf(bob), 0);
// whitelist bob
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: bob,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
uint256 tokenId = uint256(uint160(bob));
vm.expectEmit(true, true, true, true);
emit Transfer(address(0), bob, tokenId);
bytes memory data = abi.encodeWithSelector(
attestationStation.attestations.selector,
alice_admin,
bob,
bytes32("optimist.can-mint")
);
vm.expectCall(address(attestationStation), data);
// mint an NFT
vm.prank(bob);
optimist.mint(bob);
// expect the NFT to be owned by bob
assertEq(optimist.ownerOf(256), bob);
assertEq(optimist.balanceOf(bob), 1);
}
/**
* @dev Sally should be able to mint a token on behalf of bob
*/
function test_optimist_mint_secondary_minter() external {
// whitelist bob
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: bob,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
bytes memory data = abi.encodeWithSelector(
attestationStation.attestations.selector,
alice_admin,
bob,
bytes32("optimist.can-mint")
);
vm.expectCall(address(attestationStation), data);
uint256 tokenId = uint256(uint160(bob));
vm.expectEmit(true, true, true, true);
emit Transfer(address(0), bob, tokenId);
// 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.balanceOf(bob), 1);
}
/**
* @dev Bob should not be able to mint an NFT if he is not whitelisted
*/
function test_optimist_mint_no_attestation() 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
*/
function test_optimist_mint_already_minted() external {
// whitelist bob
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: bob,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
// 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.balanceOf(bob), 1);
// attempt to mint again
vm.expectRevert("ERC721: token already minted");
optimist.mint(bob);
}
/**
* @dev The baseURI should be set by attestation station
* by the owner of contract alice_admin
*/
function test_optimist_baseURI() external {
// set baseURI
AttestationStation.AttestationData[]
memory attestationData = new AttestationStation.AttestationData[](1);
attestationData[0] = AttestationStation.AttestationData({
about: address(optimist),
key: bytes32("optimist.base-uri"),
val: bytes(base_uri)
});
bytes memory data = abi.encodeWithSelector(
attestationStation.attestations.selector,
alice_admin,
address(optimist),
bytes32("optimist.base-uri")
);
vm.expectCall(address(attestationStation), data);
vm.prank(alice_admin);
attestationStation.attest(attestationData);
// assert baseURI is set
assertEq(optimist.baseURI(), base_uri);
}
/**
* @dev The tokenURI should return the token uri
* for a minted token
*/
function test_optimist_token_uri() external {
// whitelist bob
// attest baseURI
AttestationStation.AttestationData[]
memory attestationData = new AttestationStation.AttestationData[](2);
// we are using true but it can be any non empty value
attestationData[0] = AttestationStation.AttestationData({
about: bob,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
// we are using true but it can be any non empty value
attestationData[1] = AttestationStation.AttestationData({
about: address(optimist),
key: bytes32("optimist.base-uri"),
val: bytes(base_uri)
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
// mint an NFT
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 {
// whitelist bob
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: bob,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
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
*/
function test_optimist_token_id_of_owner() external {
// whitelist bob
uint256 willTokenId = 1024;
address will = address(1024);
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: will,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
// mint as bob
optimist.mint(will);
assertEq(optimist.tokenIdOfAddress(will), willTokenId);
}
/**
* @dev It should revert if anybody attemps token transfer
*/
function test_optimist_sbt_transfer() external {
// whitelist bob
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: bob,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
// 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, 256);
// attempt to transfer to sally
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.prank(bob);
optimist.safeTransferFrom(bob, sally, 256);
// attempt to transfer to sally
vm.expectRevert(bytes("Optimist: soul bound token"));
vm.prank(bob);
optimist.safeTransferFrom(bob, sally, 256, bytes("0x"));
}
/**
* @dev It should revert if anybody attemps approve
*/
function test_optimist_sbt_approve() external {
// whitelist bob
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: bob,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
// 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), 256);
assertEq(optimist.getApproved(256), address(0));
}
/**
* @dev It should be able to burn token
*/
function test_optimist_burn() external {
// whitelist bob
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: bob,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
// mint as bob
vm.prank(bob);
optimist.mint(bob);
// burn as bob
vm.prank(bob);
optimist.burn(256);
// expect bob to have no balance now
assertEq(optimist.balanceOf(bob), 0);
}
/**
* @dev setApprovalForAll should revert as sbt
*/
function test_optimist_set_approval_for_all() external {
// whitelist bob
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: bob,
key: bytes32("optimist.can-mint"),
val: bytes("true")
});
vm.prank(alice_admin);
attestationStation.attest(attestationData);
// 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);
}
/**
* @dev should support erc721 interface
*/
function test_optimist_supports_interface() external {
bytes4 iface721 = type(IERC721).interfaceId;
// check that it supports erc721 interface
assertEq(optimist.supportsInterface(iface721), true);
}
}
packages/contracts-periphery/contracts/universal/op-nft/Optimist.sol
0 → 100644
View file @
6e5fda97
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Semver } from "@eth-optimism/contracts-bedrock/contracts/universal/Semver.sol";
import {
ERC721BurnableUpgradeable
} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import { AttestationStation } from "./AttestationStation.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
/**
* @title Optimist
* @dev Contract for Optimist SBT
* @notice The Optimist contract is a SBT representing real humans
* It uses attestations for its base URI and allowList
* This contract is meant to live on L2
* This contract is not yet audited
*/
contract Optimist is ERC721BurnableUpgradeable, Semver {
/**
* @notice The attestation station contract where owner makes attestations
*/
AttestationStation public immutable ATTESTATION_STATION;
/**
* @notice The attestor attests to the baseURI and allowList
*/
address public immutable ATTESTOR;
/**
* @notice Initialize the Optimist contract.
* @dev call initialize function
* @param _name The token name.
* @param _symbol The token symbol.
* @param _attestor The administrator address who makes attestations.
* @param _attestationStation The address of the attestation station contract.
*/
constructor(
string memory _name,
string memory _symbol,
address _attestor,
AttestationStation _attestationStation
) Semver(0, 0, 1) {
ATTESTOR = _attestor;
ATTESTATION_STATION = _attestationStation;
initialize(_name, _symbol);
}
/**
* @notice Initialize the Optimist contract.
* @dev Initializes the Optimist contract with the given parameters.
* @param _name The token name.
* @param _symbol The token symbol.
*/
function initialize(string memory _name, string memory _symbol) public initializer {
__ERC721_init(_name, _symbol);
__ERC721Burnable_init();
}
/**
* @notice Mint the Optimist token.
* @dev Mints the Optimist token to the give recipient address.
* Limits the number of tokens that can be minted to one per address.
* The tokenId is the uint256 of the recipient address.
* @param _recipient The 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 decimal tokenid for a given address
* @return uint256 decimal tokenId
*/
function baseURI() public view returns (string memory) {
return
string(
abi.encodePacked(
ATTESTATION_STATION.attestations(
ATTESTOR,
address(this),
bytes32("optimist.base-uri")
)
)
);
}
/**
* @notice Returns the URI for the token metadata.
* @dev The token URI will be stored at baseURI + '/' + tokenId + .json
* @param tokenId The token ID to query.
* @return The URI for the given token ID.
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
return
string(
abi.encodePacked(
baseURI(),
"/",
// convert tokenId to hex string formatted like an address (20)
Strings.toHexString(tokenId, 20),
".json"
)
);
}
/**
* @notice Returns whether an address is allowList
* @dev The allowList is an attestation by the admin of this contract
* @return boolean Whether the address is allowList
*/
function isOnAllowList(address _recipient) public view returns (bool) {
return
ATTESTATION_STATION
.attestations(ATTESTOR, _recipient, bytes32("optimist.can-mint"))
.length > 0;
}
/**
* @notice Returns decimal tokenid for a given address
* @return uint256 decimal tokenId
*/
function tokenIdOfAddress(address _owner) public pure returns (uint256) {
return uint256(uint160(_owner));
}
/**
* @notice Soulbound
* @dev Override function to prevent transfers of the Optimist token.
*/
function approve(address, uint256) public pure override {
revert("Optimist: soul bound token");
}
/**
* @notice Soulbound
* @dev Override function to prevent transfers of the Optimist token.
*/
function setApprovalForAll(address, bool) public virtual override {
revert("Optimist: soul bound token");
}
/**
* @notice (Internal) Soulbound
* @dev Override internal function to prevent transfers of the Optimist token.
* @param _from The address of the token sender.
* @param _to The 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");
}
}
packages/contracts-periphery/package.json
View file @
6e5fda97
...
...
@@ -19,7 +19,7 @@
"test"
:
"yarn test:contracts"
,
"test:contracts"
:
"hardhat test --show-stack-traces"
,
"test:forge"
:
"forge test"
,
"test:coverage"
:
"NODE_OPTIONS=--max_old_space_size=8192 hardhat coverage
&& istanbul check-coverage --statements 90 --branches 82 --functions 88 --lines 90
"
,
"test:coverage"
:
"NODE_OPTIONS=--max_old_space_size=8192 hardhat coverage"
,
"test:coverage:forge"
:
"forge coverage"
,
"test:slither"
:
"slither ."
,
"gas-snapshot"
:
"forge snapshot"
,
...
...
@@ -82,7 +82,6 @@
"hardhat"
:
"^2.9.6"
,
"hardhat-deploy"
:
"^0.11.10"
,
"hardhat-gas-reporter"
:
"^1.0.8"
,
"istanbul"
:
"^0.4.5"
,
"lint-staged"
:
"11.0.0"
,
"mocha"
:
"^10.0.0"
,
"mkdirp"
:
"^1.0.4"
,
...
...
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