Skip to content

Instantly share code, notes, and snippets.

@shawnharmsen
Created July 26, 2023 05:12
Show Gist options
  • Save shawnharmsen/a415d5f170516ac68283c5ce12ab2952 to your computer and use it in GitHub Desktop.
Save shawnharmsen/a415d5f170516ac68283c5ce12ab2952 to your computer and use it in GitHub Desktop.
shadowlore
// 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