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

3078 - Series indexer contract #44

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open

Conversation

lpopo0856
Copy link
Contributor

Implement version 1

Better naming

f
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

solhint

⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(proposedArtistAddr != address(0), "Invalid address");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(!seriesPendingCoArtist[seriesID][proposedArtistID], "Already proposed");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(seriesPendingCoArtist[seriesID][proposedArtistID], "No proposal exists");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(artistID != 0, "Not an artist");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(seriesPendingCoArtist[seriesID][artistID], "No pending proposal");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(artistID != 0, "Not an artist");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(!ownerRightsRevokedForArtistID[artistID], "Already revoked");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(artistID != 0, "Not an artist");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(ownerRightsRevokedForArtistID[artistID], "Not revoked");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(newAddress != address(0), "Invalid new address");


⚠️ [solhint] reported by reviewdog 🐶
Error message for require is too long: 37 counted / 32 allowed

require(addressToArtistID[newAddress] == 0, "Address already assigned to an artist");


⚠️ [solhint] reported by reviewdog 🐶
GC: String exceeds 32 bytes

require(addressToArtistID[newAddress] == 0, "Address already assigned to an artist");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(addressToArtistID[newAddress] == 0, "Address already assigned to an artist");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(oldAddress != address(0), "Invalid artistID");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of require statements

require(isCallerArtist || isOwnerWithRights, "Not authorized to update address");


⚠️ [solhint] reported by reviewdog 🐶
Error message for revert is too long: 39 counted / 32 allowed

revert("One of the artists revoked owner rights");


⚠️ [solhint] reported by reviewdog 🐶
GC: String exceeds 32 bytes

revert("One of the artists revoked owner rights");


⚠️ [solhint] reported by reviewdog 🐶
GC: Use Custom Errors instead of revert statements

revert("One of the artists revoked owner rights");

@lpopo0856 lpopo0856 force-pushed the 3078-series-index-contract branch from a8851ad to abf3af4 Compare December 13, 2024 11:44

contract SeriesIndexer is Ownable.Ownable {
// Counter
uint256 private nextSeriesID = 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the length of existing series map so we don't need to maintain this counter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this's not possible since the mapping in solidity is a simple hash table without key management.

contract SeriesIndexer is Ownable.Ownable {
// Counter
uint256 private nextSeriesID = 1;
uint256 private nextArtistID = 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same has above comment but for the artist map.

uint256 private nextArtistID = 1;

struct Series {
string metadata;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this field is for series metadata source, so it should store the uri so the name should be metadataURI


struct Series {
string metadata;
string contractTokenData;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above comment, it should be tokenDataURI

// Artist Management
mapping(uint256 => address) private artistIDToAddress;
mapping(address => uint256) private addressToArtistID;
mapping(uint256 => bool) private ownerRightsRevokedForArtistID;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be on series level instead of artist level. We could also provide a feature for revoking all series from an artist.

// Internal Helper Functions
// ------------------------

function _checkOwnerOrArtist(uint256 seriesID) internal view {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should pass the address instead of assume the msg.sender. The caller should determine which one should be passed, that will be more extensible and readable. If we just want to stick with the msg.sender, we might need a better name like _checkMsgSenderIsOwnerOrArtist

}
}

function _checkArtist(uint256 seriesID) internal view {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same here.

uint256 indexed seriesID,
uint256[] artistIDs,
string metadata,
string seriesContractTokenCID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer using something like URI instead of CID that we need to stick to ipfs. Using URI could be better for extensibility later on.

Comment on lines 211 to 212
require(length <= 50, "Batch size too large"); // Prevent DOS attacks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean for prevent DOS attacks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that mean if someone try to put really large number of list, the contract will deny to service. It's not quite of an attack I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the words is removed anyway

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that could be out of gas since it could reach the block gas limit so i don't consider it as "denial of service"

seriesExists(seriesID)
onlyOwnerOrArtist(seriesID)
{
Series storage series = seriesDetails[seriesID];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use memory

Series storage series = seriesDetails[seriesID];
for (uint256 i = 0; i < series.artistIDs.length; i++) {
uint256 artistID = series.artistIDs[i];
isArtistIDInSeries[seriesID][artistID] = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be redundant since we did it in the _removeSeriesFromArtist

for (uint256 i = 0; i < series.artistIDs.length; i++) {
uint256 artistID = series.artistIDs[i];
isArtistIDInSeries[seriesID][artistID] = false;
_removeSeriesFromArtist(artistID, seriesID);
Copy link
Member

@jollyjoker992 jollyjoker992 Dec 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we call the function _removeArtistFromSeries as well if we tend to remove the artist from series?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, you did it when deleting the storage below.


_ensureArtistHasID(proposedArtistAddr);
uint256 proposedArtistID = addressToArtistID[proposedArtistAddr];

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you don't check the proposed address has been already artist for this series.

}

Series storage series = seriesDetails[seriesID];
series.artistIDs.push(artistID);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic could be presented already in _addArtistsToSeries, could it be reused?

if (artistID == 0) {
revert NotAnArtistError(msg.sender);
}
if (!ownerRightsRevokedForArtistID[artistID]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant parentheses

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol" as Ownable;
import "@openzeppelin/contracts/utils/structs/BitMaps.sol";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [solhint] reported by reviewdog 🐶
global import of path @openzeppelin/contracts/utils/structs/BitMaps.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)

* @dev Creates a new series
*/
function addSeries(
address[] calldata artistAddrs,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make clear variable name instead of abbreviation? This is the external function that would be seen publicly so I would suggest using clear name.

* @dev Batch creation of series
*/
function batchAddSeries(
address[][] calldata artistsArray,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name could be confusing because of the word array, would just artists or seriesArtists be better?

) external returns (uint256[] memory) {
_validateBatchParameters(artistsArray.length, metadataURIs, tokenIDsMapURIs);

uint256[] memory createdSeriesIDs = new uint256[](artistsArray.length);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just seriesIDs is good enough.

if (artistAddrs.length == 0) {
revert NoArtistsForSeriesError();
}
if (msg.sender == owner()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could write a private function _isCallerOwner for removing duplicated checks.

/**
* @dev Ensures all provided artist addresses have not revoked owner rights
*/
function _validateArtistsNotRevoked(address[] memory artistAddrs) internal view {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be _ensureNoArtistsRevokedOwnerRights, that will be more precise.

/**
* @dev Validates that artists can only add series that only have themselves as artist
*/
function _validateOnlySelfAsArtist(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just _ensureCallerIsOnlyArtist is good enough.

_validateOnlySelfAsArtist(artistAddrs);
}
uint256 seriesID = nextSeriesID++;
_validateMetadataAndTokenURI(metadataURI, tokenIDsMapURI);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should call this before the seriesID increment.

mapping(uint256 => BitMaps.BitMap) private artistOwnerRightsRevokedBitMap;

// Series Management
mapping(uint256 => Series) private seriesDetails;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering why isn't it just series according to the artists declaration above?


// Artist Management
mapping(uint256 => Artist) private artists;
mapping(address => uint256) private addressToArtistID;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be artistAddressToID

uint256 seriesID,
string memory metadataURI,
string memory tokenIDsMapURI
) internal seriesExists(seriesID) onlyOwnerOrArtist(seriesID) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd preferring using the modifiers in public/external function to leverage the visibility of them, we also could prevent potential gas waste when calling internal function multiple times.

Copy link
Contributor Author

@lpopo0856 lpopo0856 Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this into internal function since the check unit is per series so we can reuse the modifier. If we want the modifier only at external function level we'll need to create both single and plural unit modifiers. It's ok to me, just confirm that's what we want to do.

*/
function _deleteSeries(uint256 seriesID)
internal
seriesExists(seriesID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

/**
* @dev Validates batch parameters for series creation
*/
function _validateBatchParameters(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite ambiguous to me, maybe _validateSeriesWriteBatch could be better. It describes this is for series write operation in batch.

tokenIDsMapURIs.length
);
}
if (seriesCount == 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should check these count first since it's cheaper.
And using in-memory variables to avoid access array length multiple times, it could be better in readability and gas (not so sure)

/**
* @dev Allows an artist to remove themselves from a series
*/
function removeSelfFromSeries(uint256 seriesID) external seriesExists(seriesID) onlyArtist(seriesID) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatGPT suggests a name resignFromSeries that's pretty good to me, It emphasizes the voluntary, you could consider.

}

// Remove series from artist's list
_removeSeriesFromArtist(artistID, seriesID);
Copy link
Member

@jollyjoker992 jollyjoker992 Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we just create a function like _removeArtistFromSeries to grab above logic and make this function more structured?
BTW, this name _unlinkSeriesFromArtist suggested by chatGPT is pretty good to emphasize the function purpose.

*/
function ownerUpdateSeriesArtists(
uint256 seriesID,
address[] calldata artistAddrs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use clear name instead of abbreviation.

// Add new artists
_addArtistsToSeries(seriesID, artistAddrs);

emit SeriesUpdated(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This event could be unclear. We just update the series artists so we don't need to fire the event with metadata URI or token data URI, I'd suggest to create new event if needed.


// Remove current artists
Series storage series = seriesDetails[seriesID];
for (uint256 i = 0; i < series.artistIDs.length; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block of code is for removing artists from series, I'd suggest write a private/internal function to do that. The ownerUpdateSeriesArtists is basically call 2 internal functions to remove and add artists again.

// Remove series from artist's list
_removeSeriesFromArtist(artistID, seriesID);

emit SeriesUpdated(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. If you just want to fire a general event for series update, you might just want to pass the seriesID and the client should use the seriesID to query the details from the contract. Adding more unrelated parameters in the event could cost more gas and also make it more confusing.

*/
function proposeCoArtist(
uint256 seriesID,
address proposedArtistAddr
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't use the abbreviation.

/**
* @dev Adds a pending co-artist request
*/
function _addPendingCoArtistRequest(uint256 artistID, uint256 seriesID) internal {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could name it better like _addCoArtistProposal

/**
* @dev Removes a pending co-artist request
*/
function _removePendingCoArtistRequest(uint256 artistID, uint256 seriesID) internal {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be named as _removeCoArtistProposal

string metadataURI; // IPFS hash or similar identifier for series metadata
string contractTokenDataURI; // Token-related data for the series
uint256[] artistIDs; // List of artists associated with this series
uint256[] pendingCoArtists; // List of pending co-artists
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintaining a pending artist list within a series is not a good choice to me. The reason is the pending artist is just a temporary data acts as an intermediate stage for artist proposal. In other words, it doesn't really relate to the series. The series should have artist, metadata and token data, that's what we need for a series. When query the series from contract, we don't care about the co-artist since it's not official one.
Instead, I'd suggest maintain a pending co-artist as separate data, out of the series.

struct Artist {
address artistAddress; // Artist's wallet address
uint256[] seriesIDs; // List of series associated with this artist
uint256[] pendingCoArtistSeries; // List of pending co-artist series
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to this comment.

/**
* @dev Revokes owner rights for the calling artist
*/
function revokeOwnerRightsForArtist() external {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using the name revokeContractOwnerRights

/**
* @dev Approves owner rights for the calling artist
*/
function approveOwnerRightsForArtist() external {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using the name approveContractOwnerRights

/**
* @notice Checks if the given `artistAddr` has revoked owner rights
*/
function ownerRightsRevoked(address artistAddr) external view returns (bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be hasArtistRevokedOwnerRights for more clear.
Also shouldn't use abbreviation for parameter.

/**
* @notice Returns all artist IDs for a given series
*/
function getSeriesArtistIDs(uint256 seriesID) external view returns (uint256[] memory) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we consider to expose a function to getSeries instead of expose multiple one that just easily achieved the same thing by just one function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer returning the basic data type instead of struct so it could be used easier by client.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants