Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 5.0.0 #13

Merged
merged 3 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
on: [push]

name: Test

jobs:
check:
name: Transient Labs Story Inscriptions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Run unit tests
run: make test_suite
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ test_suite:
forge test --use 0.8.18
forge test --use 0.8.19
forge test --use 0.8.20

fuzz_test:
forge test --fuzz-runs 10000
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ You can write whatever you want down here!
```

## 5. ERC-165 Support
The Story Contract supports ERC-165. The Interface ID is `0x0d23ecb9`
The Story Contract supports ERC-165. The Interface ID is `0x2464f17b`

## 6. Gas Cost
Based on local testing, the gas cost of a 5000 word story (a research paper) costs `694795 gas`. At 100 gwei gas, this coverts to a gas cost of `0.0694795 ETH`. This is extrememly gas efficient. Stories will also likely be much shorter in length and submitted when gas is lower.
Expand All @@ -65,4 +65,4 @@ This codebase is provided on an "as is" and "as available" basis.
We do not give any warranties and will not be liable for any loss incurred through any use of this codebase.

## License
This code is copyright Transient Labs, Inc 2022 and is licensed under the MIT license.
This code is copyright Transient Labs, Inc 2023 and is licensed under the MIT license.
23 changes: 20 additions & 3 deletions src/IStory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ error TokenDoesNotExist();
/// @dev caller is not the token owner
error NotTokenOwner();

/// @dev caller is not the token creator
error NotTokenCreator();
/// @dev caller is not the creator
error NotCreator();

/// @dev caller is not a story admin
error NotStoryAdmin();
Expand All @@ -25,13 +25,21 @@ error NotStoryAdmin();
//////////////////////////////////////////////////////////////////////////*/

/// @title Story Contract Interface
/// @dev interface id: 0x2464f17b
/// @author transientlabs.xyz
/// @custom:version 4.0.2
/// @custom:version 5.0.0
interface IStory {
/*//////////////////////////////////////////////////////////////////////////
Events
//////////////////////////////////////////////////////////////////////////*/

/// @notice event describing a collection story getting added to a contract
/// @dev this event stories creator stories on chain in the event log that apply to an entire collection
/// @param creatorAddress - the address of the creator of the collection
/// @param creatorName - string representation of the creator's name
/// @param story - the story written and attached to the collection
event CollectionStory(address indexed creatorAddress, string creatorName, string story);

/// @notice event describing a creator story getting added to a token
/// @dev this events stores creator stories on chain in the event log
/// @param tokenId - the token id to which the story is attached
Expand All @@ -52,6 +60,15 @@ interface IStory {
Story Functions
//////////////////////////////////////////////////////////////////////////*/

/// @notice function to let the creator add a story to the collection they have created
/// @dev depending on the implementation, this function may be restricted in various ways, such as
/// limiting the number of times the creator may write a story.
/// @dev this function MUST emit the CollectionStory event each time it is called
/// @dev this function MUST implement logic to restrict access to only the creator
/// @param creatorName - string representation of the creator's name
/// @param story - the story written and attached to the token id
function addCollectionStory(string calldata creatorName, string calldata story) external;

/// @notice function to let the creator add a story to any token they have created
/// @dev depending on the implementation, this function may be restricted in various ways, such as
/// limiting the number of times the creator may write a story.
Expand Down
23 changes: 17 additions & 6 deletions src/StoryContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pragma solidity ^0.8.17;
//////////////////////////////////////////////////////////////////////////*/

import {ERC165} from "openzeppelin/utils/introspection/ERC165.sol";
import {IStory, StoryNotEnabled, TokenDoesNotExist, NotTokenOwner, NotTokenCreator, NotStoryAdmin} from "./IStory.sol";
import {IStory, StoryNotEnabled, TokenDoesNotExist, NotTokenOwner, NotCreator, NotStoryAdmin} from "./IStory.sol";

/*//////////////////////////////////////////////////////////////////////////
Story Contract
Expand All @@ -15,7 +15,7 @@ import {IStory, StoryNotEnabled, TokenDoesNotExist, NotTokenOwner, NotTokenCreat
/// @title Story Contract
/// @dev standalone, inheritable abstract contract implementing the Story Contract interface
/// @author transientlabs.xyz
/// @custom:version 4.0.2
/// @custom:version 5.0.0
abstract contract StoryContract is IStory, ERC165 {
/*//////////////////////////////////////////////////////////////////////////
State Variables
Expand Down Expand Up @@ -53,13 +53,19 @@ abstract contract StoryContract is IStory, ERC165 {
storyEnabled = enabled;
}

/// @inheritdoc IStory
function addCollectionStory(string calldata creatorName, string calldata story) external {
if (!_isCreator(msg.sender)) revert NotCreator();

emit CollectionStory(msg.sender, creatorName, story);
}

/// @inheritdoc IStory
function addCreatorStory(uint256 tokenId, string calldata creatorName, string calldata story)
external
storyMustBeEnabled
{
if (!_tokenExists(tokenId)) revert TokenDoesNotExist();
if (!_isCreator(msg.sender, tokenId)) revert NotTokenCreator();
if (!_isCreator(msg.sender, tokenId)) revert NotCreator();

emit CreatorStory(tokenId, msg.sender, creatorName, story);
}
Expand Down Expand Up @@ -92,6 +98,10 @@ abstract contract StoryContract is IStory, ERC165 {
/// @param tokenId - the token id to check ownership against
function _isTokenOwner(address potentialOwner, uint256 tokenId) internal view virtual returns (bool);

/// @dev function to check creatorship of the collection
/// @param potentialCreator - the address to check creatorship of the collection
function _isCreator(address potentialCreator) internal view virtual returns (bool);

/// @dev function to check creatorship of a token
/// @param potentialCreator - the address to check creatorship of `tokenId`
/// @param tokenId - the token id to check creatorship against
Expand All @@ -102,7 +112,8 @@ abstract contract StoryContract is IStory, ERC165 {
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ERC165
function supportsInterface(bytes4 interfaceId) public view virtual override (ERC165) returns (bool) {
return interfaceId == type(IStory).interfaceId || ERC165.supportsInterface(interfaceId);
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) {
return interfaceId == type(IStory).interfaceId || interfaceId == 0x0d23ecb9 // support interface id for previous IStory interface, since this technically implements it
|| ERC165.supportsInterface(interfaceId);
}
}
25 changes: 17 additions & 8 deletions src/upgradeable/StoryContractUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ pragma solidity ^0.8.17;

import {Initializable} from "openzeppelin-upgradeable/proxy/utils/Initializable.sol";
import {ERC165Upgradeable} from "openzeppelin-upgradeable/utils/introspection/ERC165Upgradeable.sol";
import {
IStory, StoryNotEnabled, TokenDoesNotExist, NotTokenOwner, NotTokenCreator, NotStoryAdmin
} from "../IStory.sol";
import {IStory, StoryNotEnabled, TokenDoesNotExist, NotTokenOwner, NotCreator, NotStoryAdmin} from "../IStory.sol";

/*//////////////////////////////////////////////////////////////////////////
Story Contract
Expand All @@ -14,7 +12,7 @@ import {
/// @title Story Contract
/// @dev upgradeable, inheritable abstract contract implementing the Story Contract interface
/// @author transientlabs.xyz
/// @custom:version 4.0.2
/// @custom:version 5.0.0
abstract contract StoryContractUpgradeable is Initializable, IStory, ERC165Upgradeable {
/*//////////////////////////////////////////////////////////////////////////
State Variables
Expand Down Expand Up @@ -57,13 +55,19 @@ abstract contract StoryContractUpgradeable is Initializable, IStory, ERC165Upgra
storyEnabled = enabled;
}

/// @inheritdoc IStory
function addCollectionStory(string calldata creatorName, string calldata story) external {
if (!_isCreator(msg.sender)) revert NotCreator();

emit CollectionStory(msg.sender, creatorName, story);
}

/// @inheritdoc IStory
function addCreatorStory(uint256 tokenId, string calldata creatorName, string calldata story)
external
storyMustBeEnabled
{
if (!_tokenExists(tokenId)) revert TokenDoesNotExist();
if (!_isCreator(msg.sender, tokenId)) revert NotTokenCreator();
if (!_isCreator(msg.sender, tokenId)) revert NotCreator();

emit CreatorStory(tokenId, msg.sender, creatorName, story);
}
Expand Down Expand Up @@ -96,6 +100,10 @@ abstract contract StoryContractUpgradeable is Initializable, IStory, ERC165Upgra
/// @param tokenId - the token id to check ownership against
function _isTokenOwner(address potentialOwner, uint256 tokenId) internal view virtual returns (bool);

/// @dev function to check creatorship of the collection
/// @param potentialCreator - the address to check creatorship of the collection
function _isCreator(address potentialCreator) internal view virtual returns (bool);

/// @dev function to check creatorship of a token
/// @param potentialCreator - the address to check creatorship of `tokenId`
/// @param tokenId - the token id to check creatorship against
Expand All @@ -106,8 +114,9 @@ abstract contract StoryContractUpgradeable is Initializable, IStory, ERC165Upgra
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ERC165Upgradeable
function supportsInterface(bytes4 interfaceId) public view virtual override (ERC165Upgradeable) returns (bool) {
return interfaceId == type(IStory).interfaceId || ERC165Upgradeable.supportsInterface(interfaceId);
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165Upgradeable) returns (bool) {
return interfaceId == type(IStory).interfaceId || interfaceId == 0x0d23ecb9 // support interface id for previous IStory interface, since this technically implements it
|| ERC165Upgradeable.supportsInterface(interfaceId);
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down
20 changes: 9 additions & 11 deletions test/mocks/Example721.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,37 @@ contract Example721 is ERC721, StoryContract, Ownable {
}
}

function _isStoryAdmin(address potentialAdmin) internal view override (StoryContract) returns (bool) {
function _isStoryAdmin(address potentialAdmin) internal view override(StoryContract) returns (bool) {
return owner() == potentialAdmin;
}

function _tokenExists(uint256 tokenId) internal view override (StoryContract) returns (bool) {
function _tokenExists(uint256 tokenId) internal view override(StoryContract) returns (bool) {
return _exists(tokenId);
}

function _isTokenOwner(address potentialOwner, uint256 tokenId)
internal
view
override (StoryContract)
override(StoryContract)
returns (bool)
{
return ownerOf(tokenId) == potentialOwner;
}

function _isCreator(address potentialCreator) internal view override(StoryContract) returns (bool) {
return owner() == potentialCreator;
}

function _isCreator(address potentialCreator, uint256 /* tokenId */ )
internal
view
override (StoryContract)
override(StoryContract)
returns (bool)
{
return owner() == potentialCreator;
}

function supportsInterface(bytes4 interfaceId)
public
view
virtual
override (ERC721, StoryContract)
returns (bool)
{
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, StoryContract) returns (bool) {
return ERC721.supportsInterface(interfaceId) || StoryContract.supportsInterface(interfaceId);
}
}
14 changes: 9 additions & 5 deletions test/mocks/Example721Upgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,31 @@ contract Example721Upgradeable is ERC721Upgradeable, OwnableUpgradeable, StoryCo
}
}

function _isStoryAdmin(address potentialAdmin) internal view override (StoryContractUpgradeable) returns (bool) {
function _isStoryAdmin(address potentialAdmin) internal view override(StoryContractUpgradeable) returns (bool) {
return owner() == potentialAdmin;
}

function _tokenExists(uint256 tokenId) internal view override (StoryContractUpgradeable) returns (bool) {
function _tokenExists(uint256 tokenId) internal view override(StoryContractUpgradeable) returns (bool) {
return _exists(tokenId);
}

function _isTokenOwner(address potentialOwner, uint256 tokenId)
internal
view
override (StoryContractUpgradeable)
override(StoryContractUpgradeable)
returns (bool)
{
return ownerOf(tokenId) == potentialOwner;
}

function _isCreator(address potentialCreator) internal view override(StoryContractUpgradeable) returns (bool) {
return owner() == potentialCreator;
}

function _isCreator(address potentialCreator, uint256 /* tokenId */ )
internal
view
override (StoryContractUpgradeable)
override(StoryContractUpgradeable)
returns (bool)
{
return owner() == potentialCreator;
Expand All @@ -51,7 +55,7 @@ contract Example721Upgradeable is ERC721Upgradeable, OwnableUpgradeable, StoryCo
public
view
virtual
override (ERC721Upgradeable, StoryContractUpgradeable)
override(ERC721Upgradeable, StoryContractUpgradeable)
returns (bool)
{
return
Expand Down
36 changes: 31 additions & 5 deletions test/testStoryContract.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ pragma solidity ^0.8.17;

import "forge-std/Test.sol";
import {IStory} from "../src/IStory.sol";
import {NotTokenCreator, NotTokenOwner, StoryNotEnabled, TokenDoesNotExist, NotStoryAdmin} from "../src/IStory.sol";
import {NotCreator, NotTokenOwner, StoryNotEnabled, TokenDoesNotExist, NotStoryAdmin} from "../src/IStory.sol";
import {Example721} from "./mocks/Example721.sol";

contract StoryContractTest is Test {
address[] public accounts;
Example721 public contractWithStory;
Example721 public contractNoStory;

event CollectionStory(address indexed creatorAddress, string creatorName, string story);
event CreatorStory(uint256 indexed tokenId, address indexed creatorAddress, string creatorName, string story);
event Story(uint256 indexed tokenId, address indexed collectorAddress, string collectorName, string story);

Expand Down Expand Up @@ -71,11 +72,28 @@ contract StoryContractTest is Test {

function testERC165() public {
assertTrue(contractWithStory.supportsInterface(type(IStory).interfaceId));
assertTrue(contractWithStory.supportsInterface(0x0d23ecb9)); // previous IStory interfaceId
assertTrue(contractNoStory.supportsInterface(type(IStory).interfaceId));
assertTrue(contractNoStory.supportsInterface(0x0d23ecb9)); // previous IStory interfaceId
}

///////////////////// STORY ENABLED TESTS /////////////////////

function testAddCollectionStory() public {
vm.expectEmit(true, false, false, true, address(contractWithStory));
emit CollectionStory(address(this), "XCOPY", "I AM XCOPY");
contractWithStory.addCollectionStory("XCOPY", "I AM XCOPY");
}

function testExpectRevertAddCollectionStory() public {
// revert for not being the token creator
for (uint256 i = 0; i < 3; i++) {
vm.prank(accounts[i], accounts[i]);
vm.expectRevert(NotCreator.selector);
contractWithStory.addCollectionStory("XCOPY", "I AM XCOPY");
}
}

function testAddCreatorStory() public {
for (uint256 i = 0; i < 4; i++) {
uint256 id = i + 1;
Expand All @@ -91,7 +109,7 @@ contract StoryContractTest is Test {
uint256 id = i + 1;

vm.prank(accounts[i], accounts[i]);
vm.expectRevert(NotTokenCreator.selector);
vm.expectRevert(NotCreator.selector);
contractWithStory.addCreatorStory(id, "XCOPY", "I AM XCOPY");
}

Expand Down Expand Up @@ -132,11 +150,18 @@ contract StoryContractTest is Test {

///////////////////// STORY DISABLED TESTS /////////////////////

function testExpectRevertDisabledAddCollectionStory() public {
vm.expectEmit(true, false, false, true, address(contractWithStory));
emit CollectionStory(address(this), "XCOPY", "I AM XCOPY");
contractWithStory.addCollectionStory("XCOPY", "I AM XCOPY");
}

function testExpectRevertDisabledAddCreatorStory() public {
for (uint256 i = 0; i < 4; i++) {
uint256 id = i + 1;
vm.expectRevert(StoryNotEnabled.selector);
contractNoStory.addCreatorStory(id, "XCOPY", "I AM XCOPY");
vm.expectEmit(true, true, false, true, address(contractWithStory));
emit CreatorStory(id, address(this), "XCOPY", "I AM XCOPY");
contractWithStory.addCreatorStory(id, "XCOPY", "I AM XCOPY");
}
}

Expand Down Expand Up @@ -166,7 +191,8 @@ contract StoryContractTest is Test {
// contract no story
vm.prank(accounts[0], accounts[0]);
contractNoStory.transferFrom(accounts[0], accounts[1], 1);
vm.expectRevert(StoryNotEnabled.selector);
vm.expectEmit(true, true, false, true, address(contractNoStory));
emit CreatorStory(1, address(this), "XCOPY", "I AM XCOPY");
contractNoStory.addCreatorStory(1, "XCOPY", "I AM XCOPY");
}

Expand Down
Loading