Created
July 26, 2023 05:12
-
-
Save shawnharmsen/a415d5f170516ac68283c5ce12ab2952 to your computer and use it in GitHub Desktop.
shadowlore
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0 | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@///@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@//////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@//////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@///////////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@////////////////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@////////////////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@////////....../////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@////////....../////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@///////////....../////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@//////..... /////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@//////..... /////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@****** ....../////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@****** ....../////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@////// //////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@//// //////@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#### ######@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@/////(((((((((((@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@...........@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@...........@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@............................*****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@............................*****@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@................***********@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@................***********@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@................***********......@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@................***********......@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@......................*****......@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@......................*****......@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@......................*****......@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@................***********@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@................***********@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@......................*****@@@@@@@@@@@@(((((((((((@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@......................*****@@@@@@@@(((((((((((((((((@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@......................*****@@@@@@(((((((((@@@@@((((((((@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@......................*****@@@@@@(((((((((@@@@@@@@(((((@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@...........,,,,,,,,,,,*****%%%%%%((((((@@@@@@@@@@@(((((@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@...........****************((((((((((((@@@@@@@@@(((((((@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@############,,,,,,,,,,,***********/////((((((%%%%%%@@@@@%%%%(((((%%@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@**********************************(((((((((((@@@@@@@@@@@((((((@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@**********************************(((((((((((@@@@@@@@@@@((((((@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@((((((****************(((((((((((((((((@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@((((((****************(((((((((((((((((@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@(((((((((((((((((((((((((((@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@(((((((((((((((((((((((((((@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | |
pragma solidity ^0.8.0; | |
import '@openzeppelin/contracts/access/Ownable.sol'; | |
import '@openzeppelin/contracts/token/ERC721/IERC721.sol'; | |
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; | |
import '@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol'; | |
/** | |
* @title The Book of Lore | |
*/ | |
contract BookOfLore is Ownable, EIP712 { | |
struct Lore { | |
address creator; | |
uint256 parentLoreId; | |
bool nsfw; | |
bool struck; | |
string loreMetadataURI; | |
} | |
// tokenContract tokenId loreId | |
mapping(address => mapping(uint256 => Lore[])) public tokenLore; | |
mapping(address => bool) public loreTokenContractAllowlist; | |
mapping(address => bool) public scribeAllowlist; | |
event LoreAdded(address tokenContract, uint256 tokenId, uint256 loreIdx); | |
event LoreUpdated(address tokenContract, uint256 tokenId, uint256 loreIdx); | |
event LoreStruck(address tokenContract, uint256 tokenId, uint256 loreIdx); | |
event ShadowLore( | |
address tokenContract, | |
uint256 tokenId, | |
uint256 loreIdx, | |
address creator, | |
uint256 parentLoreId, | |
bool nsfw, | |
bool struck, | |
string loreMetadataURI | |
); | |
modifier onlyAllowedTokenContract(address tokenContract) { | |
require( | |
loreTokenContractAllowlist[tokenContract], | |
'tokenContract is not on the allowlist' | |
); | |
_; | |
} | |
constructor() EIP712('BookOfLore', '1') {} | |
function numLore(address tokenContract, uint256 tokenId) | |
public | |
view | |
returns (uint256) | |
{ | |
return tokenLore[tokenContract][tokenId].length; | |
} | |
function loreFor(address tokenContract, uint256 tokenId) | |
public | |
view | |
returns (Lore[] memory) | |
{ | |
return tokenLore[tokenContract][tokenId]; | |
} | |
function loreAt( | |
address tokenContract, | |
uint256 tokenId, | |
uint256 startIdx, | |
uint256 endIdx | |
) public view returns (Lore[] memory) { | |
Lore[] memory l = new Lore[](endIdx - startIdx + 1); | |
uint256 length = endIdx - startIdx + 1; | |
for (uint256 i = 0; i < length; i++) { | |
l[i] = tokenLore[tokenContract][tokenId][startIdx + i]; | |
} | |
return l; | |
} | |
function setLoreTokenAllowlist(address tokenContract, bool isListed) | |
public | |
onlyOwner | |
{ | |
loreTokenContractAllowlist[tokenContract] = isListed; | |
} | |
function setScribeAllowlist(address scribeAddress, bool isScribe) | |
public | |
onlyOwner | |
{ | |
scribeAllowlist[scribeAddress] = isScribe; | |
} | |
function addLore( | |
address tokenContract, | |
uint256 tokenId, | |
uint256 parentLoreId, | |
bool nsfw, | |
string memory loreMetadataURI | |
) public onlyAllowedTokenContract(tokenContract) { | |
address tokenOwner = IERC721(tokenContract).ownerOf(tokenId); | |
require( | |
tokenOwner == _msgSender(), | |
'Owner: caller is not the token owner' | |
); | |
tokenLore[tokenContract][tokenId].push( | |
Lore(_msgSender(), parentLoreId, nsfw, false, loreMetadataURI) | |
); | |
emit LoreAdded( | |
tokenContract, | |
tokenId, | |
tokenLore[tokenContract][tokenId].length - 1 | |
); | |
uint256 newLoreIdx = tokenLore[tokenContract][tokenId].length - 1; | |
Lore memory newLore = tokenLore[tokenContract][tokenId][newLoreIdx]; | |
emit ShadowLore( | |
tokenContract, | |
tokenId, | |
newLoreIdx, | |
newLore.creator, | |
newLore.parentLoreId, | |
newLore.nsfw, | |
newLore.struck, | |
newLore.loreMetadataURI | |
); | |
} | |
function addLoreWithSignature( | |
bytes memory signature, | |
address tokenContract, | |
uint256 tokenId, | |
uint256 loreId, | |
uint256 parentLoreId, | |
bool nsfw, | |
string memory loreMetadataURI | |
) public onlyAllowedTokenContract(tokenContract) { | |
// construct an expected hash, given the parameters | |
bytes32 digest = _hashTypedDataV4( | |
keccak256( | |
abi.encode( | |
keccak256( | |
'AddLore(address tokenContract,uint256 tokenId,uint256 loreId,uint256 parentLoreId,bool nsfw,string loreMetadataURI)' | |
), | |
tokenContract, | |
tokenId, | |
loreId, // acts as nonce | |
parentLoreId, | |
nsfw, | |
keccak256(bytes(loreMetadataURI)) // tricky! | |
) | |
) | |
); | |
// now recover the signer from the provided signature | |
address signer = ECDSA.recover(digest, signature); | |
// make sure the recover extracted a signer, but beware, because this | |
// can return non-zero for some invalid cases (apparently?) | |
require(signer != address(0), 'ECDSA: invalid signature'); | |
// get the owner of this wizard | |
address tokenOwner = IERC721(tokenContract).ownerOf(tokenId); | |
require( | |
signer == tokenOwner, | |
'addLoreWithSignature: signature is not the current token owner' | |
); | |
require( | |
numLore(tokenContract, tokenId) == loreId, | |
'addLoreWithSignature: loreId is stale' | |
); | |
tokenLore[tokenContract][tokenId].push( | |
Lore(signer, parentLoreId, nsfw, false, loreMetadataURI) | |
); | |
emit LoreAdded( | |
tokenContract, | |
tokenId, | |
tokenLore[tokenContract][tokenId].length - 1 | |
); | |
uint256 newLoreIdx = tokenLore[tokenContract][tokenId].length - 1; | |
Lore memory newLore = tokenLore[tokenContract][tokenId][newLoreIdx]; | |
emit ShadowLore( | |
tokenContract, | |
tokenId, | |
newLoreIdx, | |
newLore.creator, | |
newLore.parentLoreId, | |
newLore.nsfw, | |
newLore.struck, | |
newLore.loreMetadataURI | |
); | |
} | |
/** | |
* A Scribe is a contract that is allowed to write Lore *if* the transaction | |
* originated from the token owner. For example, The Great Burning may write | |
* the death of a Wizard and the inception of their Soul | |
*/ | |
function addLoreWithScribe( | |
address tokenContract, | |
uint256 tokenId, | |
uint256 parentLoreId, | |
bool nsfw, | |
string memory loreMetadataURI | |
) public onlyAllowedTokenContract(tokenContract) { | |
require(scribeAllowlist[_msgSender()], 'sender is not a Scribe'); | |
address tokenOwner = IERC721(tokenContract).ownerOf(tokenId); | |
require( | |
tokenOwner == tx.origin, // ! - note that msg.sender must be a Scribe for this to work | |
'Owner: tx.origin is not the token owner' | |
); | |
tokenLore[tokenContract][tokenId].push( | |
// we credit this lore to the Scribe, not the token owner | |
Lore(_msgSender(), parentLoreId, nsfw, false, loreMetadataURI) | |
); | |
emit LoreAdded( | |
tokenContract, | |
tokenId, | |
tokenLore[tokenContract][tokenId].length - 1 | |
); | |
} | |
function updateLoreMetadataURI( | |
address tokenContract, | |
uint256 tokenId, | |
uint256 loreIdx, | |
string memory newLoreMetadataURI | |
) public onlyAllowedTokenContract(tokenContract) { | |
// is lore creator | |
require( | |
tokenLore[tokenContract][tokenId][loreIdx].creator == _msgSender(), | |
'Owner: caller is not the Lore creator' | |
); | |
// holds wizard currently | |
address tokenOwner = IERC721(tokenContract).ownerOf(tokenId); | |
require( | |
tokenOwner == _msgSender(), | |
'Owner: caller is not the token owner' | |
); | |
tokenLore[tokenContract][tokenId][loreIdx] | |
.loreMetadataURI = newLoreMetadataURI; | |
emit LoreUpdated(tokenContract, tokenId, loreIdx); | |
} | |
function updateLoreNSFW( | |
address tokenContract, | |
uint256 tokenId, | |
uint256 loreIdx, | |
bool newNSFW | |
) public onlyAllowedTokenContract(tokenContract) { | |
address tokenOwner = IERC721(tokenContract).ownerOf(tokenId); | |
require( | |
(tokenLore[tokenContract][tokenId][loreIdx].creator == | |
_msgSender() && | |
tokenOwner == _msgSender()) || (owner() == _msgSender()), | |
'Owner: caller is neither the Lore creator nor the Lore Master' | |
); | |
tokenLore[tokenContract][tokenId][loreIdx].nsfw = newNSFW; | |
emit LoreUpdated(tokenContract, tokenId, loreIdx); | |
} | |
function strikeLore( | |
address tokenContract, | |
uint256 tokenId, | |
uint256 loreIdx, | |
bool newStruck | |
) public onlyAllowedTokenContract(tokenContract) { | |
address tokenOwner = IERC721(tokenContract).ownerOf(tokenId); | |
require( | |
(tokenLore[tokenContract][tokenId][loreIdx].creator == | |
_msgSender() && | |
tokenOwner == _msgSender()) || (owner() == _msgSender()), | |
'Owner: caller is neither the Lore creator nor the Lore Master' | |
); | |
tokenLore[tokenContract][tokenId][loreIdx].struck = newStruck; | |
emit LoreStruck(tokenContract, tokenId, loreIdx); | |
} | |
function domainSeparator() external view returns (bytes32) { | |
return _domainSeparatorV4(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment