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
0e0546a1
Commit
0e0546a1
authored
Dec 13, 2022
by
Will Cory
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add optimist contract
parent
ad38dcc0
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 @
0e0546a1
---
'
@eth-optimism/contracts-periphery'
:
patch
---
Add optimist contract
packages/contracts-periphery/contracts/foundry-tests/Optimist.t.sol
0 → 100644
View file @
0e0546a1
//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 @
0e0546a1
// 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 @
0e0546a1
...
...
@@ -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