Skip to content

Commit

Permalink
updated interface and EIP 712 message needed
Browse files Browse the repository at this point in the history
  • Loading branch information
mpeyfuss committed Jan 2, 2024
1 parent 2a3f144 commit bf675a1
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 64 deletions.
17 changes: 13 additions & 4 deletions src/erc-721/TRACE/ITRACE.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ interface ITRACE {
/// @param baseUri The base uri for the batch, expecting json to be in order, starting at file name 0, and SHOULD NOT have a trailing `/`
function airdrop(address[] calldata addresses, string calldata baseUri) external;

/// @notice Function to allow an approved mint contract to mint
/// @dev Requires the caller to be an approved mint contract
/// @param recipient The recipient of the token - assumed as able to receive 721 tokens
/// @param uri The token uri to mint
function externalMint(address recipient, string calldata uri) external;

/// @notice Function to transfer token to another wallet
/// @dev Callable only by owner or admin
/// @dev Useful if a chip fails or an alteration damages a chip in some way
Expand All @@ -55,10 +61,13 @@ interface ITRACE {
/// @param signature The signtature from the chip to verify physical presence
function addVerifiedStory(uint256 tokenId, string calldata story, bytes calldata signature) external;

/// @notice Function to return the nonce for a token
/// @param tokenId The token to query
/// @return uint256 The token nonce
function getTokenNonce(uint256 tokenId) external view returns (uint256);
/// @notice Function to write a batch of stories for tokens
/// @dev Requires that the passed signature is signed by the token owner, which is the ARX Halo Chip (physical)
/// @dev Uses EIP-712 for the signature
/// @param tokenIds The tokens to add a stories to
/// @param stories The story text
/// @param signatures The signtatures from the chip to verify physical presence
function addVerifiedStoryBatch(uint256[] calldata tokenIds, string[] calldata stories, bytes[] calldata signatures) external;

/// @notice Function to update a token uri for a specific token
/// @dev Requires owner or admin
Expand Down
149 changes: 89 additions & 60 deletions src/erc-721/TRACE/TRACE.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "openzeppelin-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {OwnableAccessControlUpgradeable} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol";
import {EIP2981TLUpgradeable} from "tl-sol-tools/upgradeable/royalties/EIP2981TLUpgradeable.sol";
import {ITRACE} from "src/trace/ITRACE.sol";
import {ITRACE} from "src/erc-721/trace/ITRACE.sol";
import {IBlockListRegistry} from "src/interfaces/IBlockListRegistry.sol";
import {ICreatorBase} from "src/interfaces/ICreatorBase.sol";
import {IStory} from "src/interfaces/IStory.sol";
Expand Down Expand Up @@ -39,25 +39,24 @@ contract TRACE is
Custom Types
//////////////////////////////////////////////////////////////////////////*/

/// @dev struct defining a batch mint - used for airdrops
/// @dev Struct defining a batch mint - used for airdrops
struct BatchMint {
uint256 fromTokenId;
uint256 toTokenId;
string baseUri;
}

/// @dev struct for verified story & signed EIP-712 message
/// @dev Struct for verified story & signed EIP-712 message
struct VerifiedStory {
uint256 nonce;
address nftContract;
uint256 tokenId;
address sender;
string story;
}

/// @dev string representation of uint256
/// @dev String representation of uint256
using Strings for uint256;

/// @dev string representation for address
/// @dev String representation for address
using Strings for address;

/*//////////////////////////////////////////////////////////////////////////
Expand All @@ -66,47 +65,51 @@ contract TRACE is

string public constant VERSION = "3.0.0";
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant APPROVED_MINT_CONTRACT = keccak256("APPROVED_MINT_CONTRACT");
ITRACERSRegistry public tracersRegistry;
uint256 private _counter; // token ids
mapping(uint256 => string) private _tokenUris; // established token uris
mapping(uint256 => uint256) private _tokenNonces; // token nonces to prevent replay attacks
mapping(bytes32 => bool) private _verifiedStoryHashUsed; // prevent replay attacks
BatchMint[] private _batchMints; // dynamic array for batch mints

/*//////////////////////////////////////////////////////////////////////////
Custom Errors
//////////////////////////////////////////////////////////////////////////*/

/// @dev token uri is an empty string
/// @dev Token uri is an empty string
error EmptyTokenURI();

/// @dev batch mint to zero address
/// @dev Batch mint to zero address
error MintToZeroAddress();

/// @dev batch size too small
/// @dev Batch size too small
error BatchSizeTooSmall();

/// @dev airdrop to too few addresses
/// @dev Airdrop to too few addresses
error AirdropTooFewAddresses();

/// @dev token not owned by the owner of the contract
/// @dev Token not owned by the owner of the contract
error TokenNotOwnedByOwner();

/// @dev caller is not the owner of the specific token
/// @dev Caller is not the owner of the specific token
error CallerNotTokenOwner();

/// @dev caller is not approved or owner
/// @dev Caller is not approved or owner
error CallerNotApprovedOrOwner();

/// @dev token does not exist
/// @dev Token does not exist
error TokenDoesntExist();

/// @dev no proposed token uri to change to
error NoTokenUriUpdateAvailable();
/// @dev Verified story already written for token
error VerifiedStoryAlreadyWritten();

/// @dev invalid signature
/// @dev Array length mismatch
error ArrayLengthMismatch();

/// @dev Invalid signature
error InvalidSignature();

/// @dev unauthorized to add a verified story
/// @dev Unauthorized to add a verified story
error Unauthorized();

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -165,8 +168,8 @@ contract TRACE is
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ICreatorBase
function setApprovedMintContracts(address[] calldata /*minters*/, bool /*status*/) external {
revert("N/A");
function setApprovedMintContracts(address[] calldata minters, bool status) external onlyRoleOrOwner(ADMIN_ROLE) {
_setRole(APPROVED_MINT_CONTRACT, minters, status);
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -210,6 +213,14 @@ contract TRACE is
}
}

/// @inheritdoc ITRACE
function externalMint(address recipient, string calldata uri) external onlyRole(APPROVED_MINT_CONTRACT) {
if (bytes(uri).length == 0) revert EmptyTokenURI();
_counter++;
_tokenUris[_counter] = uri;
_mint(recipient, _counter);
}

/*//////////////////////////////////////////////////////////////////////////
T.R.A.C.E. Functions
//////////////////////////////////////////////////////////////////////////*/
Expand All @@ -227,47 +238,23 @@ contract TRACE is

/// @inheritdoc ITRACE
function addVerifiedStory(uint256 tokenId, string calldata story, bytes calldata signature) external {
// default name to hex address
string memory registeredAgentName = msg.sender.toHexString();

// only check registered agent if the registry is not the zero address
if (address(tracersRegistry) != address(0)) {
if (address(tracersRegistry).code.length == 0) revert Unauthorized();
bool isRegisteredAgent;
(isRegisteredAgent, registeredAgentName) = tracersRegistry.isRegisteredAgent(msg.sender);
if (!isRegisteredAgent) revert Unauthorized();
}

// verify signature
address tokenOwner = ownerOf(tokenId);
bytes32 digest = _hashTypedDataV4(_hashVerifiedStory(tokenId, _tokenNonces[tokenId]++, msg.sender, story));
if (tokenOwner != ECDSA.recover(digest, signature)) revert InvalidSignature();

// emit story
emit Story(tokenId, msg.sender, registeredAgentName, story);
_addVerifiedStory(tokenId, story, signature);
}

/// @inheritdoc ITRACE
function getTokenNonce(uint256 tokenId) external view returns (uint256) {
return _tokenNonces[tokenId];
}

/// @notice function to hash the typed data
function _hashVerifiedStory(uint256 tokenId, uint256 nonce, address sender, string memory story)
internal
pure
returns (bytes32)
{
return keccak256(
abi.encode(
// keccak256("VerifiedStory(uint256 nonce,uint256 tokenId,address sender,string story)"),
0x3ea278f3e0e25a71281e489b82695f448ae01ef3fc312598f1e61ac9956ab954,
nonce,
tokenId,
sender,
keccak256(bytes(story))
)
);
function addVerifiedStoryBatch(uint256[] calldata tokenIds, string[] calldata stories, bytes[] calldata signatures) external {
if (tokenIds.length != stories.length && stories.length != signatures.length) {
revert ArrayLengthMismatch();
}
for (uint256 i = 0; i < tokenIds.length; i++) {
// get variables
uint256 tokenId = tokenIds[i];
string memory story = stories[i];
bytes memory signature = signatures[i];

// add verified story
_addVerifiedStory(tokenId, story, signature);
}
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -364,4 +351,46 @@ contract TRACE is
function _exists(uint256 tokenId) internal view returns (bool) {
return _ownerOf(tokenId) != address(0);
}

/// @notice Function to add a verified story in a reusable way
function _addVerifiedStory(uint256 tokenId, string memory story, bytes memory signature) internal {
// default name to hex address
string memory registeredAgentName = msg.sender.toHexString();

// only check registered agent if the registry is not the zero address
if (address(tracersRegistry) != address(0)) {
if (address(tracersRegistry).code.length == 0) revert Unauthorized();
bool isRegisteredAgent;
(isRegisteredAgent, registeredAgentName) = tracersRegistry.isRegisteredAgent(msg.sender);
if (!isRegisteredAgent) revert Unauthorized();
}

// verify signature
bytes32 verifiedStoryHash = _hashVerifiedStory(address(this), tokenId, story);
if (_verifiedStoryHashUsed[verifiedStoryHash]) revert VerifiedStoryAlreadyWritten();
_verifiedStoryHashUsed[verifiedStoryHash] = true;
address tokenOwner = ownerOf(tokenId);
bytes32 digest = _hashTypedDataV4(verifiedStoryHash);
if (tokenOwner != ECDSA.recover(digest, signature)) revert InvalidSignature();

// emit story
emit Story(tokenId, msg.sender, registeredAgentName, story);
}

/// @notice Function to hash the typed data
function _hashVerifiedStory(address nftContract, uint256 tokenId, string memory story)
internal
pure
returns (bytes32)
{
return keccak256(
abi.encode(
// keccak256("VerifiedStory(address nftContract,uint256 tokenId,string story)"),
0x76b12200216600191228eb643bc7cba6e319d03951a863e3306595415759682b,
nftContract,
tokenId,
keccak256(bytes(story))
)
);
}
}

0 comments on commit bf675a1

Please sign in to comment.