Skip to content

Other small inefficiencies#

Informational

Inefficiency 1#

In CanvasCreator.sol there is _IMPLEMENTATION_SLOT constant. In its definition it is given a value of 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc. In constructor it is then validated that it equals to keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1. This validation is unnecessary, because it will always be right, unless you change the code.

Solution#

Remove line 12 assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1));. If you want to be sure that the value is right, and you are not sure in the hardcoded hex value, then just replace it with bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1). It will give the same result.

bytes32 internal constant _IMPLEMENTATION_SLOT = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);

Inefficiency 2#

In Core.sol, in the setVrfResult function tokenCounter is subtracted by randomizedTraitCounter to get the remainder. If the remainder is higher than 0, then randomizedTraitCounter is added up with remainder and saved. There is no point to making this calculation since randomizedTraitCounter + remainder will always equal tokenCounter.

uint256 tokenCounter = ds.canvasSystem[canvasId].tokenIdCounter;
uint256 remainder = tokenCounter.sub(ds.canvasSystem[canvasId].randomizedTraitsCounter);

if (ds.canvasSystem[canvasId].vrfPending && remainder > 0) {
  ds.randomizedTraits[canvasId].push(Schema.RandomTraits({
    randomizedAtTokenId: tokenCounter,
    randomWord: randomWord
  }));

  ds.canvasSystem[canvasId].randomizedTraitsCounter = ds.canvasSystem[canvasId].randomizedTraitsCounter.add(remainder);
  ds.canvasSystem[canvasId].vrfPending = false;
  ds.canvasSystem[canvasId].vrfLastRunTimestamp = block.timestamp;
  emit VRFResult(canvasId, tokenCounter, randomWord);
}

Solution#

Remove the calculation (ds.canvasSystem[canvasId].randomizedTraitsCounter.add(remainder)) and replace it with tokenCounter.

uint256 tokenCounter = ds.canvasSystem[canvasId].tokenIdCounter;

if (ds.canvasSystem[canvasId].vrfPending && tokenCounter.sub(ds.canvasSystem[canvasId].randomizedTraitsCounter) > 0) {
  ds.randomizedTraits[canvasId].push(Schema.RandomTraits({
    randomizedAtTokenId: tokenCounter,
    randomWord: randomWord
  }));

  ds.canvasSystem[canvasId].randomizedTraitsCounter = tokenCounter;
  ds.canvasSystem[canvasId].vrfPending = false;
  ds.canvasSystem[canvasId].vrfLastRunTimestamp = block.timestamp;
  emit VRFResult(canvasId, tokenCounter, randomWord);
}

Inefficiency 3#

In Create.sol there is createCanvas function that on line 114 stores a value to storage and then reads it multiple times. Reading from storage is more expensive than reading from memory. So caching the value to memory and reading it from there would be more optimal.

ds.contractInfo.canvasCounter = ds.contractInfo.canvasCounter.add(1);
ds.canvas[ds.contractInfo.canvasCounter] = canvas;
ds.canvas[ds.contractInfo.canvasCounter].admin = msgSender;
ds.creators[ds.contractInfo.canvasCounter] = new address[](1);
ds.creators[ds.contractInfo.canvasCounter][0] = msgSender;
if (canvas.feeRecipient == address(0)) ds.canvas[ds.contractInfo.canvasCounter].feeRecipient = payable(msgSender);

return ds.contractInfo.canvasCounter;

Solution#

Cache the value to memory and read it from there.

uint256 canvasId = ++ds.contractInfo.canvasCounter;
ds.canvas[canvasId] = canvas;
ds.canvas[canvasId].admin = msgSender;
ds.creators[canvasId] = new address[](1);
ds.creators[canvasId][0] = msgSender;
if (canvas.feeRecipient == address(0)) ds.canvas[canvasId].feeRecipient = payable(msgSender);

return canvasId;
In above example, feeRecipient and admin are changed when canvas is already saved to storage. You can also make those changes in memory first and store the canvas after.
uint256 canvasId = ++ds.contractInfo.canvasCounter;
canvas.admin = msgSender;
if (canvas.feeRecipient == address(0)) canvas.feeRecipient = payable(msgSender);
ds.canvas[canvasId] = canvas;
ds.creators[canvasId] = new address[](1);
ds.creators[canvasId][0] = msgSender;

return canvasId;

Inefficiency 4#

In Create.sol on line 359, a new struct is created and saved to storage. The struct includes mintCounter variable, which must stay the same as it was before. This means that the value must be read from storage and then included in the new struct being saved. This is unnecessarily complex, when you can just update the values that are being changed.

ds.partners[canvasId][partners[i]] = Schema.Partnership({
  discountBps: discounts[i],
  allocation: allocation[i],
  mintCounter: ds.partners[canvasId][partners[i]].mintCounter
});

Solution#

Only update the discountBps and allocation.

ds.partners[canvasId][partners[i]].discountBps = discounts[i];
ds.partners[canvasId][partners[i]].allocation = allocation[i];

Inefficiency 5#

In Minting.sol the mint function receives data and then decodes it using getMintRequest to get MintRequest struct. However, the function could receive MintRequest by default. Also, in that case the mintRequest would stay as calldata and wouldn't need to use memory.

function mint(
    address msgSender,
    uint256 ethValue,
    uint256 canvasId,
    bytes calldata data
  ) external nonReentrant {
    Schema.Storage storage ds = canvasStorage();
    Schema.Canvas storage canvas = ds.canvas[canvasId];

    Schema.MintRequest memory mintRequest = getMintRequest(data);

    if (!canvas.reserveAuction) defaultMint(msgSender, canvasId, ethValue, mintRequest);
    else {
      mintReserveAuction(msgSender, canvasId, mintRequest.selectedTraits);
      emit OneMinted(msgSender, canvasId, mintRequest);
    }
}

Solution#

Remove conversion of data to MintRequest and accept MintRequest as argument.

function mint(
    uint256 canvasId,
    Schema.MintRequest calldata mintRequest
  ) external nonReentrant {
    Schema.Canvas storage canvas = canvasStorage().canvas[canvasId];

    if (!canvas.reserveAuction) defaultMint(msg.sender, canvasId, msg.value, mintRequest);
    else {
      mintReserveAuction(msg.sender, canvasId, mintRequest.selectedTraits);
      emit OneMinted(msg.sender, canvasId, mintRequest);
    }
}
*Notice: in this solution msgSender and ethValue were also removed, as stated in Redundant use of msg.sender and msg.value.

Inefficiency 6#

In Minting.sol the checkCanMint function gets storage of a canvas Schema.Canvas storage canvas = ds.canvas[canvasId], but then proceeds to access it multiple times using ds.canvas[canvasId] instead of canvas.

Schema.Canvas storage canvas = ds.canvas[canvasId];

if (canvas.bulkMax > 0 && mintRequest.quantity > canvas.bulkMax)
  revert OverMintLimit(canvas.bulkMax);
if (ds.partners[canvasId][msgSender].discountBps > 0) {
  if (ds.partners[canvasId][msgSender].mintCounter + mintRequest.quantity > ds.partners[canvasId][msgSender].allocation) 
    revert OverAllowance(ds.partners[canvasId][msgSender].allocation);
  ds.partners[canvasId][msgSender].mintCounter += mintRequest.quantity;
} else if (canvas.walletMax > 0 && ds.purchaseTracker[canvasId][msgSender].quantity + mintRequest.quantity > canvas.walletMax)
  revert OverMintLimit(canvas.walletMax - ds.purchaseTracker[canvasId][msgSender].quantity);


if (ds.canvas[canvasId].presaleActive && !ds.canvas[canvasId].saleActive) {
  if (ds.canvas[canvasId].mintPassPerQuantity > 0 &&
      ds.mintPassTokens[canvasId].length > 0)
    processMintPass(msgSender, canvasId, mintRequest);
  else checkAllowList(msgSender, canvasId, mintRequest);
} else {
  if (canvas.saleStart > 0 &&
      (canvas.saleStart > block.timestamp || canvas.saleEnd < block.timestamp) ||
      !ds.canvas[canvasId].saleActive)
    revert SaleInactive();

  if (ds.canvas[canvasId].mintPassPerQuantity > 0 &&
      ds.mintPassTokens[canvasId].length > 0)
    processMintPass(msgSender, canvasId, mintRequest);
  else if (ds.canvas[canvasId].restrictToAllowList)
    checkAllowList(msgSender, canvasId, mintRequest);
}

Solution#

Replace ds.canvas[canvasId] with canvas.

Schema.Canvas storage canvas = ds.canvas[canvasId];

if (canvas.bulkMax > 0 && mintRequest.quantity > canvas.bulkMax)
  revert OverMintLimit(canvas.bulkMax);
if (ds.partners[canvasId][msgSender].discountBps > 0) {
  if (ds.partners[canvasId][msgSender].mintCounter + mintRequest.quantity > ds.partners[canvasId][msgSender].allocation) 
    revert OverAllowance(ds.partners[canvasId][msgSender].allocation);
  ds.partners[canvasId][msgSender].mintCounter += mintRequest.quantity;
} else if (canvas.walletMax > 0 && ds.purchaseTracker[canvasId][msgSender].quantity + mintRequest.quantity > canvas.walletMax)
  revert OverMintLimit(canvas.walletMax - ds.purchaseTracker[canvasId][msgSender].quantity);


if (canvas.presaleActive && !canvas.saleActive) {
  if (canvas.mintPassPerQuantity > 0 &&
      ds.mintPassTokens[canvasId].length > 0)
    processMintPass(msgSender, canvasId, mintRequest);
  else checkAllowList(msgSender, canvasId, mintRequest);
} else {
  if (canvas.saleStart > 0 &&
      (canvas.saleStart > block.timestamp || canvas.saleEnd < block.timestamp) ||
      !canvas.saleActive)
    revert SaleInactive();

  if (canvas.mintPassPerQuantity > 0 &&
      ds.mintPassTokens[canvasId].length > 0)
    processMintPass(msgSender, canvasId, mintRequest);
  else if (canvas.restrictToAllowList)
    checkAllowList(msgSender, canvasId, mintRequest);
}

Inefficiency 7#

In URI.sol the getSelectedTraits function gets canvasStorage() two times unnecessarily. The first time it is saved to ds variable, meaning there is no need to get it the second time, because it can be accessed using ds.

Schema.Storage storage ds = canvasStorage();
uint256 canvasId = ds.collectionToCanvas[collectionAddress];
Schema.CanvasTraits[] storage canvasTraits = canvasStorage().canvasTraits[canvasId];

Solution#

Replace the second canvasStorage with ds.

Schema.Storage storage ds = canvasStorage();
uint256 canvasId = ds.collectionToCanvas[collectionAddress];
Schema.CanvasTraits[] storage canvasTraits = ds.canvasTraits[canvasId];

Inefficiency 8#

In URI.sol the getSelectedTraits function overwrites encodedTraits with its own value in case hasTrait is false.

encodedTraits = hasTrait ? 
  string(abi.encodePacked(
    keccak256(bytes(encodedTraits)) != keccak256(bytes('')) ?
    selectedTraits
    : Parser.substring(selectedTraits, 0, bytes(selectedTraits).length.sub(1)),
    encodedTraits
  )) : encodedTraits;

Solution#

Change the code so you only update encodedTraits if hasTrait is true.

if(hasTrait) {
  encodedTraits = string(
    abi.encodePacked(
      keccak256(bytes(encodedTraits)) != keccak256(bytes('')) ?
      selectedTraits
      : Parser.substring(selectedTraits, 0, bytes(selectedTraits).length.sub(1)),
      encodedTraits
    ));
}

Inefficiency 9#

In URI.sol the tokenURI function is making same hashing and comparison two times (L489 and L505), same conversion of uint to string three times (L484, L490 and L499) and there is also a redundant encodePacked operation on line 496. All of these increase gas cost.

string memory animationUrl = string(abi.encodePacked(
  ds.canvasAssets[canvasId][versionIndex].baseUri,
  getRefUri(collectionAddress, tokenId),
  specialParams,
  getCustomTitles(collectionAddress, tokenId),
  '&id=',
  Parser.uint2str(tokenId)
));

string memory tokenAddressString = string(abi.encodePacked(
  Parser.addressToString(collectionAddress),
  keccak256(bytes(canvas.externalUrl)) != keccak256(bytes('')) && canvas.externalUrlSlash ? "/" : ":",
  Parser.uint2str(tokenId)
));

return string(abi.encodePacked(
  "data:application/json;base64,",
  Base64.encode(bytes(abi.encodePacked(
    '{"name":"', string(abi.encodePacked(
      canvas.name,
      canvas.editioned ? ' #' : '',
      canvas.editioned ? Parser.uint2str(tokenId) : '')),
    '", "description":"', usageDescription(canvasId, collectionAddress, tokenId),
    '","image":"',ds.canvasAssets[canvasId][versionIndex].thumbnailUri,
    '","animation_url":"',animationUrl,
    '","collection_url":"', canvas.collectionUri,
    '","external_url":"',
      keccak256(bytes(canvas.externalUrl)) != keccak256(bytes('')) ?
        canvas.externalUrl :
        ds.externalBaseUrl
      ,tokenAddressString,
    '","attributes": [', encodedTraits,'] }'
  ))))
);

Solution#

Cache the result of conversion (Parser.uint2str(tokenId)) and comparison (keccak256(bytes(canvas.externalUrl)) != keccak256(bytes(''))) to memory. Remove the redundant encodePacked operation.

string tokenIdStr = Parser.uint2str(tokenId);
bool hasExternalUrl = keccak256(bytes(canvas.externalUrl)) != keccak256(bytes(''));

string memory animationUrl = string(abi.encodePacked(
  ds.canvasAssets[canvasId][versionIndex].baseUri,
  getRefUri(collectionAddress, tokenId),
  specialParams,
  getCustomTitles(collectionAddress, tokenId),
  '&id=',
  tokenIdStr
));

string memory tokenAddressString = string(abi.encodePacked(
  Parser.addressToString(collectionAddress),
  hasExternalUrl && canvas.externalUrlSlash ? "/" : ":",
  tokenIdStr
));

return string(abi.encodePacked(
  "data:application/json;base64,",
  Base64.encode(bytes(abi.encodePacked(
    '{"name":"', 
      canvas.name,
      canvas.editioned ? ' #' : '',
      canvas.editioned ? tokenIdStr : '',
    '", "description":"', usageDescription(canvasId, collectionAddress, tokenId),
    '","image":"',ds.canvasAssets[canvasId][versionIndex].thumbnailUri,
    '","animation_url":"',animationUrl,
    '","collection_url":"', canvas.collectionUri,
    '","external_url":"',
      hasExternalUrl ?
        canvas.externalUrl :
        ds.externalBaseUrl
      ,tokenAddressString,
    '","attributes": [', encodedTraits,'] }'
  ))))
);