This post is a compilation of links I’ve seen recently related to running GPT or other language models locally and/or with customizations. The ecosystem around GPT & Large Language Models is changing quickly but these are good places to get started.
Delegate.cash is a new protocol intended to make airdrops and token claims safer. If you have vaulted NFTs, you strongly want to avoid using that vault wallet to interact with any contracts. And if you have to move NFTs back & forth between your vault wallet and hot wallet to make claims, then you probably won’t. foobar, one of the authors of delegate.cash, has a great write up on why this kind of protocol needs to exist. Let’s take a look at the code…
ALL – the vault wallet delegates all actions to the delegate wallet
CONTRACT – the vault wallet delegates all actions on a specific contract to the delegate wallet
TOKEN – the vault wallet delegates all actions for a specific token (on a specific contract) to the delegate wallet
There are delegation functions for each of these delegation types that allow you to enable or disable the delegation (based on value). These functions must be called from the vault wallet (vault = msg.sender), but they are quite straightforward and safe:
The delegationHash is where the authors got clever. Not only does the hash make delegation lookups faster and more efficient, it makes revoking delegations very cheap for gas. Every vault wallet has a numeric vaultVersion, which is initially 0. If this gets changed, then the delegationHash will change. There is a similar numeric delegateVersion for (vault, delegate) pairs. Revoking (covered more below) is just a matter of changing those version numbers, therefore changing the hash.
When delegating, a DelegationInfo object is created and stored by delegateHash. This DelegationInfo contains the following data:
vault: the vault wallet
delegate: the delegate or hot wallet
type_: the DelegationType, which can be ALL, CONTRACT, or TOKEN
contract_: the contract address for CONTRACT & TOKEN types, or the 0 address for ALL
tokenId: the token number for the TOKEN type, or 0
If value is false, then the delegateHash and corresponding DelegationInfo are deleted, thereby revoking a specific delegation. However, there are cheaper ways to revoke delegation, covered next.
/// @notice Info about a single delegation, used for onchain enumeration
struct DelegationInfo {
DelegationType type_;
address vault;
address delegate;
address contract_;
uint256 tokenId;
}
function _setDelegationValues(
address delegate,
bytes32 delegateHash,
bool value,
IDelegationRegistry.DelegationType type_,
address vault,
address contract_,
uint256 tokenId
) internal {
if (value) {
delegations[vault][vaultVersion[vault]].add(delegateHash);
delegationHashes[delegate].add(delegateHash);
delegationInfo[delegateHash] =
DelegationInfo({vault: vault, delegate: delegate, type_: type_, contract_: contract_, tokenId: tokenId});
} else {
delegations[vault][vaultVersion[vault]].remove(delegateHash);
delegationHashes[delegate].remove(delegateHash);
delete delegationInfo[delegateHash];
}
}
Revoking
The contract provides very cheap ways to revoke delegation, by changing the vaultVersion or delegateVersion.
revokeAllDelegates would be called from the vault wallet, and invalidates all delegations for that wallet
revokeDelegate would also be called from the vault wallet, to invalidate a specific delegate wallet
revokeSelf would be called from a delegate wallet, to invalidate all delegations from a specific vault wallet
As mentioned above, instead of a complicated and gas expensive deletion of DelegationInfo, by incrementing a version number the delegation hash computation is changed. Technically this means the old DelegationInfo remains in the contract storage, but it is no longer accessible and cannot be used by any delegation lookups. There is also a theoretical limit of 2^256-1 for the number of times you can revoke and change either of the version numbers, but you’d have to try extremely hard and spend a lot of gas to do this.
function revokeAllDelegates() external override {
++vaultVersion[msg.sender];
emit IDelegationRegistry.RevokeAllDelegates(msg.sender);
}
function revokeDelegate(address delegate) external override {
_revokeDelegate(delegate, msg.sender);
}
function revokeSelf(address vault) external override {
_revokeDelegate(msg.sender, vault);
}
function _revokeDelegate(address delegate, address vault) internal {
++delegateVersion[vault][delegate];
// For enumerations, filter in the view functions
emit IDelegationRegistry.RevokeDelegate(vault, msg.sender);
}
Delegation Lookups
There are a number of different ways to lookup delegations. I’ve included the function signatures below, but not the complete functions, as they are complicated, highly optimized loops iterating over the delegations created by the functions covered above. They are view only functions and therefore safe to call.
function getDelegatesForAll(address vault)
external view returns (address[] memory delegates)
function getDelegatesForContract(address vault, address contract_)
external view override returns (address[] memory delegates)
function getDelegatesForToken(address vault, address contract_, uint256 tokenId)
external view override returns (address[] memory delegates)
function getContractLevelDelegations(address vault)
external view returns (IDelegationRegistry.ContractDelegation[] memory contractDelegations)
function getTokenLevelDelegations(address vault)
external view returns (IDelegationRegistry.TokenDelegation[] memory tokenDelegations)
function checkDelegateForAll(address delegate, address vault)
public view override returns (bool)
function checkDelegateForContract(address delegate, address vault, address contract_)
public view override returns (bool)
function checkDelegateForToken(address delegate, address vault, address contract_, uint256 tokenId)
public view override returns (bool)
These functions are designed to be called from other contracts or dapps, to enable the delegation protocol. For example, if a delegate wallet wants to make a claim for a NFT, it could provide a vault wallet and tokenId to the claim contract. The claim contract would then do the following:
Proceed with the claim if checkDelegateForToken returns true
Deny the claim if checkDelegateForToken returns false
For the best UX, you’d probably want to call these functions initially from the dapp, but for security, you definitely want to verify everything within the claim contract.
Slither Analysis
Unfortunately, slither 0x00000000000076A84feF008CDAbe6409d2FE638B doesn’t work because the openzepplin contract dependency paths are different than how etherscan structures things. To make slither work, I did the following:
As you can see, it’s in pretty good shape. The 4 medium issues are all unused returns in _setDelegationValues when adding or removing delegation hashes from a set. The add and remove functions just return a bool if the set was changed, which doesn’t really matter for this contract, so ignoring the return value is totally fine.
The 1 optimization issue is that checkDelegateForToken should be declared external. It is actually external in the IDelegationRegistry interface, so I’m not sure why it’s public in DelegationRegistry. Maybe it’s just a simple oversight, but not really a big deal either.
Some of the informational issues are about inline assembly in some of the delegation lookups. Assembly should definitely be used with care. In this case, the assembly is in view only functions, in a way that helps reduce the computation cost.
Conclusion
Delegate.cash does exactly what it claims to, in a very straightforward way. However, it’s useless on its own. For this protocol to work, developers have to start including it in their own contracts and dapps, then recommending it to their users. The team behind delegate.cash are attempting to make an official standard with EIP-5639. But the best thing they can do is to provide easy to use libraries and sample code for integrating the protocol into other contracts and dapps. Ideally this would go as far as re-usable React components and a sample claim contract to demonstrate an entire user & wallet flow. If developers can essentially copy & paste to give their users more secure options, then adoption will be much easier.
The contract was written by West Coast NFT, who also wrote the mfers contract. Let’s take a look at the code, which begins with a giant ascii creyzie.
Airdrop
struct SendData {
address receiver;
uint256 amount;
}
...
function airdrop(SendData[] calldata sendData) external onlyRole(SUPPORT_ROLE) nonReentrant {
uint256 ts = baseContractAddress.totalSupply();
// loop through all addresses
for (uint256 index = 0; index < sendData.length; index++) {
require(totalSupply() + sendData[index].amount <= ts, 'Exceeds original supply');
_safeMint(sendData[index].receiver, sendData[index].amount);
}
}
The airdrop function receives an array of SendData structs, each of which specifies a receiver address and an amount of tokens to airdrop. Before this function was called, the devs did a snapshot of all mfers owner addresses, and counted how many mfers were owned by each address, to create this SendData array. Then they called this function 14 times to perform the airdrop, for a total gas cost of ~15 ETH. Sartoshi deposited 20 ETH to fund the airdrop, then withdrew 5 ETH when the airdrop was complete.
The baseContractAddress is the mfers contract address, which you can see in the Constructor Arguments below the code. So the first line of the function gets the total number of mfers from the mfers contract. Then, looping through the array of SendData, there’s a requirement that the current supply + the amount to be minted is less than or equal to the total mfers supply. The amounts should have been calculated correctly during the snapshot, but this is a good way to enforce the supply limit. Finally, the amount of tokens is minted to the receiver address. The Creyzies contract uses ERC721A, so minting multiple tokens at a time is very efficient.
There’s no token ownership checks at mint time, so this does all assume that the SendData was calculated correctly from a snapshot. Some addresses were explicitly left out of the airdrop – these were known smart contract addresses such as a Gnosis Safe or NiftyGateway. The devs knew the airdrop may not work for them, so they setup a claim site and implemented a redeem function.
Redeem
function setUnclaimedTokenIds(uint256[] calldata tokenIds) external onlyRole(SUPPORT_ROLE) {
for (uint256 index = 0; index < tokenIds.length; index++) {
unclaimedTokenIds[tokenIds[index]] = true;
}
}
function redeem(uint256[] calldata tokenIds) external nonReentrant {
uint256 numberOfTokens = tokenIds.length;
for (uint256 index = 0; index < numberOfTokens; index++) {
require(unclaimedTokenIds[tokenIds[index]], 'Token has already been claimed');
try baseContractAddress.ownerOf(tokenIds[index]) returns (address ownerOfAddress) {
require(ownerOfAddress == msg.sender, 'Caller must own NFTs');
} catch (bytes memory) {
revert('Bad token contract');
}
unclaimedTokenIds[tokenIds[index]] = false;
}
uint256 ts = baseContractAddress.totalSupply();
require(totalSupply() + numberOfTokens <= ts, 'Exceeds original supply');
_safeMint(msg.sender, numberOfTokens);
}
Sartoshi posted claim instructions to redeem unclaimed tokens that were not airdropped. setUnclaimedTokenIds was called immediately after the airdrop, and sets a flag to true in unclaimedTokenIds. Then, when an owner goes to creyzies.art to claim their token(s), the redeem function is called. For each token attempting to be redeem, the function checks that
The token hasn’t been claimed already
The redeemer (msg.sender) owns the corresponding mfers token.
If those checks pass, the token is marked as claimed. But before it is minted, there is one final check that minting these tokens won’t exceed the total mfers supply. The devs definitely put some extra effort into the contract to double check their work, to ensure that Creyzies tokens went to the right addresses and never exceeded mfers supply.
Royalties
Often, royalties from sales are handled by secondary marketplaces, using info from royaltyregistry.xyz. However, this contract implements ERC2981, which defines a royalty standard specified in the contract. Like with mfers, the default royalties are set fairly low, to 2.5%. The royalties are sent to 0x750314177EF0a319DCdC959599C76D63964729f1, which appears to be another contract that splits the royalties automatically.
Provenance
Like with The Picaroons, a provenance hash was set before minting, to 3307bc0bd06029bba4a826856f2e19db62d6df5b7f70d447fa5262117256c46c. They also published a page proving the fairness of the drop.
Token URI
While some NFTs exist fully on chain, most are token numbers in a contract that point to a URI. This URI points to metadata, which is where to find the NFT image and properties. For mfers, the token URI is on ipfs, a distributed filesystem, where the JSON metadata for each mfers token is stored. Creyzies, though, currently points to a web app on heroku.
The token URI for Creyzies #1 is https://minting-pipeline-10.herokuapp.com/1
This is not great, because it means that the Creyzies metadata depends on a Heroku app running correctly and being available. ipfs, being distributed, is a more decentralized and resilient location, and any files stored there can’t be changed without changing the URI. Metadata being served from a web app can be changed very easily. Hopefully there’s a plan to put all the metadata into ipfs as well; the images are there already. If that is done correctly, then the devs can call setBaseURI with the new ipfs URI, just like they did to set the heroku base URI. However, if there is some other surprise planned that requires changing the metadata, then keeping the token URIs pointing to a web app make sense.
function setBaseURI(string memory baseURI_) external onlyRole(SUPPORT_ROLE) {
_baseURIextended = baseURI_;
}
/**
* @dev See {ERC721-_baseURI}.
*/
function _baseURI() internal view virtual override returns (string memory) {
return _baseURIextended;
}
Slither Analysis
In order to make slither work this time, I had to tweak the code for one of the dependencies, crytic-compile. Using my branch, you can run slither to analyze the contract.
The issues can all be seen by running slither 0x19bb64b80cbf61e61965b0e5c2560cc7364c6546. I think the way this contract calls baseContractAddress.ownerOf within a try-catch block might be a little unusual, since slither says the return value is unused, when that’s not really the case. The other medium issues are within ERC721A._mint, and look ignorable. However there is an issue about costly operations inside a loop within ERC721A._mint, for _currentIndex = updatedIndex. This is within an unchecked block, which is the same thing that the Counters library does. The Azuki gas optimization analysis was pretty extensive, so there may not be any more that can be done to optimize gas in this case.
Conclusion
This is a very nice & unexpected gift to all mfers holders, and it looks like the devs did excellent work to ensure accuracy and minimize airdrop costs. The Crezies art is cc0 just like mfers, and it’s definitely worth checking out more of Rey’s work.
The Picaroons is a new NFT collection by Alex Lucas, with support from Pranksy. Holders of certain NFTs by Alex were able to mint up to 2 tokens, then during public mint anyone could mint 2 tokens (and previous minters could mint 2 more). The 10k collection quickly sold out during public mint, and is now only available on secondary marketplaces.
An interesting thing about this contract is the extra effort into preventing bots from minting directly from the contract. They maintained an allow list of addresses in their web app instead of putting the snapshot into the contract, and minting required a signature generated by their web app. Let’s dig into the contract code…
Mint
function mint(bytes32 hash, bytes memory signature, uint256 tokenQuantity, uint256 maxAmountAllowed) external payable {
require(saleLive, "SALE_CLOSED");
// Is the signature valid
require(matchAddresSigner(hash, signature), "DIRECT_MINT_DISALLOWED");
// Verify if the keccak256 hash matches the hash generated by the hashTransaction function
require(hashTransaction(msg.sender, tokenQuantity, maxAmountAllowed) == hash, "HASH_FAIL");
// Out of stock check
require(totalSupply() + tokenQuantity <= TOTAL_SUPPLY, "OUT_OF_STOCK");
// Is the address allow more mint more tokens?
require(amountMintedList[msg.sender] + tokenQuantity <= maxAmountAllowed, "EXCEED_ALLOC");
require(PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
for(uint256 i = 0; i < tokenQuantity; i++) {
amountMintedList[msg.sender]++;
_safeMint(msg.sender, totalSupply() + 1);
}
}
The mint function requires the following:
Sale isn’t closed
The hash argument was signed (signature) by a known signing address
The hash argument can be verified with the other function arguments
There are tokens available to mint
You have not minted too much
You sent enough ETH
Once those checks pass, it’s a simple mint loop, incrementing a counter for how many you have minted. The interesting parts are requirements 2 & 3.
The _signerAddress is defined as a static variable. In order to generate a verifiable signature for the hash, the private key associated with this signing address must exist in their web app. Because this address is static, with no way to change it, they hopefully had some good app and server security to prevent any leaking of the private key. Assuming no one else has access to the private key, the signature can only be generated by their web app, and therefore no one, including bots, could mint directly from the contract. All minting actions had to go through their web app, which can be protected from bots using more traditional methods.
How did they secure their web app from bots?
They required a captcha before a signature was generated, which prevents most web bots.
Before the public mint, only addresses on an allow list based on NFT ownership were able to mint.
This allow list was generated from a snapshot ~2 weeks before minting was enabled. But unlike many allow list snapshots, it wasn’t put into the contract. Instead they must have made a database for their web app to check against, which was only used during the pre-sale.
How did they make a signature in a web app that could be verified within the contract?
Libraries like web3.js provide a way to sign data in javascript using a private key. The library also provides message hashing. So by using a library like web3.js in a server-side web app, you can do the following:
Encode function arguments into a message
Hash that message
Sign the hash
The hash and signature can then be returned from the web app to the browser, then passed to the contract, thereby preventing direct contract minting.
Hash Transaction
This function reconstructs the hashed message that was signed. It does this by encoding the arguments, then hashing them with solidity’s standard hashing algorithm, keccak256.
function hashTransaction(address sender, uint256 qty, uint256 maxAmountAllowed) private pure returns(bytes32) {
bytes32 hash = keccak256(abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
keccak256(abi.encodePacked(sender, qty, maxAmountAllowed)))
);
return hash;
}
That’s about it for the minting. Let’s see what Slither says…
Slither Analysis
Slither is a python tool for static analysis of Solidity contracts. You can use it to get a quick summary of the contract code, and then look for any deeper issues.
No significant issues found. Nearly all the issues are in the dependency contracts, which are pretty standard, and the only issues Slither finds for ThePicaroons contract are cosmetic.
Gas Efficiency
The contract uses ERC721Enumerable. While it’s a somewhat common extension to ERC721, and it adds some useful functionality, it definitely increases gas costs for minting and transfers. Azuki did a deep analysis of this and created ERC721A to solve this gas issue without losing key functionality. Using ERC721A would have been a nice improvement to the Picaroons contract, but the contract is otherwise very simple and efficient.
Provenance Hash
One final detail in the contract is the use of a NFT Provenance Hash. This can be used to verify that the reveal after mint was defined before minting began, and not manipulated after mint but before reveal. The fourth transaction on the contract, right after public mint was enabled, was to set the provenance hash to e4b58ca66f8bf42902acff422cb634ff8c6d012627a71f195071c725558670b9. While they don’t give a nice breakdown on their website for verifying the metadata of the tokens, like BAYC does, it’s still a good thing to do and provides that verification capability to anyone that wants to do the work.
/// Set the smart-contract proof as an hash of all NFT metadata hashes
/// @param hash of all NFTs metadata hashes
function setProvenanceHash(string calldata hash) external onlyOwner notLocked {
proof = hash;
}
Dead mfers is a collection by sartoodles, who is an active member of the unofficial mfers community, and has created a few mfers derivativecollections. Owners of Dead mfers receive free airdrops regularly on the polygon network, and more benefits will be unlocked when the collection is 90% minted. There is also a video game in the works. Let’s look at the contract code…
Mint Random
The mint function used for Dead mfers is mintRandom:
function mintRandom() payable public
{
require(mintRandomEnabled == true, 'Random minting is not enabled for this contract');
require(msg.value == mintFee, 'Eth sent does not match the mint fee');
_splitPayment();
uint256 tokenID = _drawRandomIndex();
_safeMint(msg.sender, tokenID);
}
This requires that random minting is enabled, and the fee passed in is correct, in this case 0.0069 ETH. _safeMint is a standard ERC721 function from OpenZeppelin, so let’s look at the middle 2 functions: _splitPayment and _drawRandomIndex.
This function sends 10% of the mint fee to shareAddress, and the remaining 90% to the contract owner. The shareAddress is set when this contract is initialized by the factory contract, which is covered in more detail below. If you look at any of the Mint Random transactions, you can see that 10% of every mint fee is sent to 0xe28564784a0f57554d8beec807e8609b40a97241, aka autominter.eth. This is how AutoMinter makes money by providing tools to NFT creators.
Sample transaction showing 10% of mint fee going to autominter.eth
Draw Random Index
function _drawRandomIndex() internal returns (uint256 index) {
//RNG
uint256 i = uint(keccak256(abi.encodePacked(block.timestamp))) % remaining;
// if there's a cache at cache[i] then use it
// otherwise use i itself
index = cache[i] == 0 ? i : cache[i];
// grab a number from the tail
cache[i] = cache[remaining - 1] == 0 ? remaining - 1 : cache[remaining - 1];
// store the position of moved token in cache to be looked up (add 1 to avoid 0, remove when recovering)
cachePosition[cache[i]] = i + 1;
remaining = remaining - 1;
}
This function gets a pseudo random number between 0 and remaining, then returns that number (index). The rest is mostly to coordinate between other minting functions like _drawIndex that are not used for Dead mfers, so that the same token isn’t minted twice.
That’s about it – the code is relatively minimal without much going on. One downside to this contract is that you can only mint a single token at a time. I suspect that if it allowed batch minting of multiple tokens in a single transaction, like in Backgroundmfers or 3DMutantMfers, then a lot more Dead mfers would be minted.
I could conclude the review here, but there’s some interesting additional details to cover. If you’re looking at the code on etherscan, you may notice it says Minimal Proxy Contract for 0x72668b08926a69ae9c926aeb572559efc7f42cd6. What does this mean?
AutoMinter Proxy
The Dead mfers contract is not a custom contract. Instead, it is a minimal proxy for a contract by AutoMinter. This means that the Dead mfers contract forwards all the contract interactions to the autominter contract code, while the state data that records your actions and NFT ownership is maintained within the Dead mfers contract. By using this minimal proxy pattern, sartoodles was able to re-use someone else’s code, and save ETH with a cheaper contract deployment.
When deploying a minimal proxy contract, the main thing is to provide initialization parameters. This is what customizes the contract for your use vs someone else’s. In other words, many other contracts could be minimal proxies for the same autominter contract, but each minimal proxy has been initialized differently. For Dead mfers, here are the first 5 initialization parameters:
To find these, start on the contract page. There is a More Info section in the upper right that looks like this:
In the middle row, it shows “… at txn” and then a transaction hash. Click on that transaction hash to see the transaction details page, which looks like this:
You can see when the transaction happened, the Contract that was interacted with (the AutoMinterFactory covered below), the transaction Value of 0.025 ETH, and lots of other details. At the bottom, click on the Click to see More link. That displays the Input Data, which shows that a create function was called.
What you see is the binary data passed in to the create function. At the bottom, click Decode Input Data to see a human readable table of parameters, such as the name_ being Dead mfers. This create function actually belongs to a different contract, AutoMinterFactory. Below is a short video showing the contract navigation to find all this information, as well as the AutoMinterFactory contract described next.
Auto Minter Factory
The AutoMinterFactory contract does the actual deployment and initialization of the minimal proxy contract used by Dead mfers. As part of that initialization, it sets some of its own parameters on the contract, such as the shareAddress mentioned above.
The create function on the factory contract is relatively clear: (note that the actual create function used by the latest AutoMinterFactory might be different than below, because it’s an upgradeable contract behind a proxy contract):
The AutoMinterFactory implementation contract clones and initializes a minimal proxy contract for the current version of the AutoMinterERC721 implementation contract
The minimal proxy contract is initialized with the parameters from the create function in step 1
All calls to the minimal proxy contract (for Dead mfers) are forwarded to the AutoMinterERC721 implementation contract
None of the above is bad or dangerous; it’s necessary complexity to provide re-usable & customizable contract code. The result is a simple & cheap contract.
Upgradeable
You may also notice that one of the first lines of code says the contract is upgradeable:
contract AutoMinterERC721 is Initializable, ERC721Upgradeable, OwnableUpgradeable
If the Dead mfers contract was a regular proxy contract, then this line above would make it possible to deploy a new version of the AutoMinterERC721 contract. Upgradeable NFT contracts are not necessarily bad, but it is a warning sign, because the implementation contract could be changed at any time, potentially introducing insecure code. In the case of Dead mfers, because it’s only a minimal proxy, the implementation contract cannot be upgraded.
Slither Analysis
Slither is a python tool for static analysis of Solidity contracts. You can use it to get a quick summary of the contract code, and then look for any deeper issues.
In this case, some of library contracts are marked as Complex, while the main contract AutoMinterERC721 is not, which is good. But Slither also says there are 3 high severity issues. Let’s see what those are:
The first issue says that _drawRandomIndex, called by the mintRandom function, uses a weak PRNG, which stands for pseudorandom number generator. The random index for a token is generated by doing a modulo on the current block timestamp, which apparently could be manipulated by an ethereum blockchain miner, to reorder the block to get a different random index. This may be a problem in other situations, but in this case, it doesn’t really matter which random token you get when you call mintRandom.
The second and third issues are about state variable shadowing in some of the OpenZeppelin upgradeable contract dependencies. This means the __gap variable is used in multiple contracts but is not explicitly assigned. This appears to be a design choice in upgradeable contracts, to explicitly leave room for new state variables in future versions of a contract. So this high severity issue isn’t actually a problem here either.
All the other issues Slither detects either don’t really matter or don’t apply to how the Dead mfers contract is used.
Conclusion
Overall this is a simple contract. It is unfortunately limited to minting one at a time, and it doesn’t look like AutoMinter supports batch minting. This is one reason why many other projects will deploy their own custom contracts, in addition to custom code for different minting options. So if you’re ok with single minting, AutoMinter might be a great option for creating your own NFT collection. And be sure to mint your own Dead mfer to receive some free airdrops, or if you want to play the upcoming video game, recently teased on @Sartoodles.
3DMutantMfers is a new NFT collection by @scott_visuals. He is also the artist and creator of 3DMfers and Cosmo Creatures. For this mint, the team wanted to reward holders of 3DMfers and Cosmo Creatures with free mints, while still keeping public mints very affordable at 0.029 ETH. The idea with the free mint was that you would be able to get 1 3DMutantMfer for each 3DMfer or Cosmo Creature you held. Unfortunately, there was a bug in the contract function that caused the free claim to fail for most people. Let’s dig in to the contract code.
Public Mint
Let’s start with the public mint function, which works great and is very simple.
function mint( uint numberOfTokens ) external payable mintCompliance( numberOfTokens ) {
require( msg.value >= price * numberOfTokens, "Ether value sent is not correct" );
_mintLoop( msg.sender, numberOfTokens );
}
The mint function takes in the number of tokens you want to mint, ensures that you’ve sent the correct amount of ETH (0.029 * numberOfTokens), then calls _mintLoop with msg.sender (your wallet address).
Mint Compliance
Before you can mint, there’s a number of conditions that are checked by mintCompliance.
modifier mintCompliance( uint numberOfTokens ) {
require( isActive, "Sale must be active to mint 3DMutantMfers" );
require( numberOfTokens <= maxOrder, "Can only mint 20 tokens at a time" );
require( numberOfTokens > 0, "Token Mint Count must be > 0" );
uint256 supply = _owners.length;
require( supply + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max supply of 3DMutantMfers" );
uint256 mintedCount = addressMintedBalance[msg.sender];
require(mintedCount + numberOfTokens <= MAX_PER_WALLET, "Max NFT per address exceeded");
_;
}
The mintCompliance modifier function is also used by the freeClaimMint function covered below. It checks for the following conditions:
Minting is active
You are minting at least one token but not more than 20
You can’t exceed the total supply of 4444 tokens
You can’t mint more than 1000 tokens into 1 wallet
All of these numbers can be changed by the contract owner, with the restriction that total supply cannot be set lower than the number of tokens. The functions that allow these modifications are below:
function setActive(bool isActive_) external onlyOwner {
if( isActive != isActive_ )
isActive = isActive_;
}
function setMaxOrder(uint maxOrder_) external onlyOwner {
if( maxOrder != maxOrder_ )
maxOrder = maxOrder_;
}
function setPrice(uint price_ ) external onlyOwner {
if( price != price_ )
price = price_;
}
function setMaxSupply(uint maxSupply_ ) external onlyOwner {
if( MAX_SUPPLY != maxSupply_ ){
require(maxSupply_ >= _owners.length, "Specified supply is lower than current balance" );
MAX_SUPPLY = maxSupply_;
}
}
// Update Max Tokens A Wallet can mint
function setMaxPerWallet(uint maxPerWallet_ ) external onlyOwner {
if( MAX_PER_WALLET != maxPerWallet_ ){
MAX_PER_WALLET = maxPerWallet_;
}
}
Mint Loop
function _mintLoop(address _receiver, uint256 numberOfTokens) internal {
uint256 supply = _owners.length;
for (uint256 i = 0; i < numberOfTokens; i++) {
addressMintedBalance[_receiver]++;
_safeMint( _receiver, supply++, "" );
}
}
This function simply iterates through the number of tokens to mint, increments that number of tokens for your wallet (_receiver) then mints each token. This contract uses ERC721B, which is an implementation of ERC721 optimized to reduce gas when minting multiple tokens. Because this is relatively new code, and not from OpenZeppelin (the defacto standard library), it’s possible there’s some issues that haven’t been identified yet. However, the code in each function is relatively simple and very similar to standard ERC721 code.
Free Claim Mint
Let’s look at the freeClaimMint, which had an issue where most transactions attempts failed with “Warning! Error encountered during contract execution [Out of gas]“.
function freeClaimMint( uint numberOfTokens, bytes memory signature, string[] memory contract1TokenIds, string[] memory contract2TokenIds ) external mintCompliance( numberOfTokens ) {
require(verifySender(signature, contract1TokenIds, contract2TokenIds), "Invalid Access");
// Check to make sure there are token ids
require(contract1TokenIds.length > 0 || contract2TokenIds.length > 0, "Empty Token IDs");
uint totalTokenIds = contract1TokenIds.length + contract2TokenIds.length;
require(totalTokenIds == numberOfTokens, "Token IDs and Mint Count mismatch");
// Lets make sure we are not claiming for already claimed tokens of contract 1
bool isValidTokenIds = true;
for (uint i = 0; isValidTokenIds && i < contract1TokenIds.length; i++) {
for (uint j = 0; isValidTokenIds && j < contract1ClaimedTokensCount; j++) {
string memory contractClaimedToken = contract1ClaimedTokens[j];
string memory tokenToClaim = contract1TokenIds[i];
if (keccak256(bytes(tokenToClaim)) == keccak256(bytes(contractClaimedToken))) {
isValidTokenIds = false;
}
}
}
require(isValidTokenIds, "Cosmo Creatures Token ID passed is already claimed");
// Lets make sure we are not claiming for already claimed tokens of contract 2
for (uint i = 0; isValidTokenIds && i < contract2TokenIds.length; i++) {
for (uint j = 0; isValidTokenIds && j < contract2ClaimedTokensCount; j++) {
string memory contractClaimedToken = contract2ClaimedTokens[j];
string memory tokenToClaim = contract2TokenIds[i];
if (keccak256(bytes(tokenToClaim)) == keccak256(bytes(contractClaimedToken))) {
isValidTokenIds = false;
}
}
}
require(isValidTokenIds, "3D Mfrs Token ID passed is already claimed");
for (uint i = 0; i < contract1TokenIds.length; i++) {
contract1ClaimedTokensCount++;
contract1ClaimedTokens.push(contract1TokenIds[i]);
}
for (uint i = 0; i < contract2TokenIds.length; i++) {
contract2ClaimedTokensCount++;
contract2ClaimedTokens.push(contract2TokenIds[i]);
}
_mintLoop( msg.sender, numberOfTokens );
}
As you can see, this is quite a complicated function. You can compare it to the BackgroundMfers minting functions to see how much simpler their implementation is for each free claim function. In this case, the intention was to provide a single function that would mint 1 or more tokens depending on how many you owned from 3DMfers and Cosmo Creatures. But for most people that tried this, the transaction failed, losing them gas. The team is very supportive of their community, though, and they very quickly responded by:
announcing the problem
removing the free claim button from their website
refunding gas fees to everyone that had a failed transaction
creating a claim form and then airdropping 3DMutantMfers, at their own expense
Over 1.3 ETH is a somewhat expensive bug to pay for, but it builds a lot of good will, and makes it clear this is a quality team that’s not out for a quick cash grab. So what went wrong? I’m reminded of the Zen of Python: explicit is better than implicit. In the BackgroundMfers minting functions, they made everything very explicit:
each contract has its own minting function (mfers, dadmfers1, dadmfers2)
you have to select precisely which tokens from the external contract to mint with
For 3DMutantMfers, the team tried to make a very simple user experience, requiring a lot of implicit/hidden complexity that didn’t quite work. Let’s look at the code more.
Verify Signature
The very first line, after mintCompliance, is verifySignature, which checks that the function arguments have been signed by a known signerAddress.
function verifySender(bytes memory signature, string[] memory contract1TokenIds, string[] memory contract2TokenIds) internal view returns (bool) {
string memory contract1TokensString = "";
string memory contract2TokensString = "";
for (uint i = 0; i < contract1TokenIds.length; i++) {
contract1TokensString = string(abi.encodePacked(contract1TokensString, contract1TokenIds[i], i < contract1TokenIds.length - 1 ? "," : ""));
}
for (uint i = 0; i < contract2TokenIds.length; i++) {
contract2TokensString = string(abi.encodePacked(contract2TokensString, contract2TokenIds[i], i < contract2TokenIds.length - 1 ? "," : ""));
}
bytes32 hash = ECDSA.toEthSignedMessageHash(keccak256(abi.encodePacked(msg.sender, contract1TokensString, contract2TokensString)));
return ECDSA.recover(hash, signature) == signerAddress;
}
This means the website must be generating these signatures, hopefully on a backend server where the private key for the signerAddress is kept secure. There is a function to change the signer address, which could be used in case the key is compromised, or just as good practice key rotation.
function setSignerAddress(address _newSignerAddress) external onlyOwner {
signerAddress = _newSignerAddress;
}
Token Validation
Next the function tries to validate the following:
There is at least 1 external contract token to check (3DMfers or Cosmo Creatures)
The number of tokens to mint matches the number of tokens that you own
The tokens to mint haven’t been claimed already
For the last point, this is done for each contract, and I’ve copied the first block below.
// Lets make sure we are not claiming for already claimed tokens of contract 1
bool isValidTokenIds = true;
for (uint i = 0; isValidTokenIds && i < contract1TokenIds.length; i++) {
for (uint j = 0; isValidTokenIds && j < contract1ClaimedTokensCount; j++) {
string memory contractClaimedToken = contract1ClaimedTokens[j];
string memory tokenToClaim = contract1TokenIds[i];
if (keccak256(bytes(tokenToClaim)) == keccak256(bytes(contractClaimedToken))) {
isValidTokenIds = false;
}
}
}
require(isValidTokenIds, "Cosmo Creatures Token ID passed is already claimed");
Minting
Finally, there’s 2 loops, 1 for each contract, to increment a counter and record which contract tokens have been claimed. I only included 1 loop below for brevity. Once the loops are completed, there’s a call to _mintLoop to finally mint the tokens.
for (uint i = 0; i < contract2TokenIds.length; i++) {
contract2ClaimedTokensCount++;
contract2ClaimedTokens.push(contract2TokenIds[i]);
}
_mintLoop( msg.sender, numberOfTokens );
This final part seems simple enough, but it turns out this where the bug is. Let’s see what we can learn from slither.
Slither Analysis
Slither is a python tool for static analysis of Solidity contracts. You can use it to get a quick summary of the contract code, and then look for any deeper issues.
As we’ve seen above, there’s definitely complex code. One less important issue identified is that there’s no check for a zero address when setting the signerAddress. However, signerAddress is only used for the free claim, which shouldn’t be used anyway due to the bug. Almost all other issues are for the 11 dependency contracts, and none are significant. Except when you look at all the analysis, slither does identify the likely cause for the out of gas error:
MutantMfers.freeClaimMint(uint256,bytes,string[],string[]) (MutantMfers.sol#1401-1450) has costly operations inside a loop:
- contract1ClaimedTokensCount ++ (MutantMfers.sol#1440)
MutantMfers.freeClaimMint(uint256,bytes,string[],string[]) (MutantMfers.sol#1401-1450) has costly operations inside a loop:
- contract2ClaimedTokensCount ++ (MutantMfers.sol#1445)
Magmar, the creator of the BackgroundMfers contract, explained the issue in the Discord:
The issue here is out of gas. The problem is the free mint contract has two variable-gas for loops and is screwing with Metamask’s infura gas estimation fees. These functions were likely tested with low quantities but the gas usage will increase as minting increases, costing more and more gas, with a higher likelihood of failing. They will likely work if you gas up your transaction heavily, but you will spend 0.02+ for a free mint.
In other words, for users that had many 3DMfers and/or Cosmo Creatures, their wallet underestimated the gas fee required, causing the transaction to fail. Some people in the discord said they successfully got free claims by increasing the gas fee, which is a more advanced user behavior. Perhaps if a Counter was used instead, everything would have worked great – lower gas fees and better wallet gas estimation. This is an excellent example of how different Solidity and the EVM are different from most other programming languages and environments, where incrementing a counter is simple and very cheap.
Conclusion
So what could have been done differently? As stated above, if contract1ClaimedTokensCount and contract2ClaimedTokensCount were Counters, then maybe everything would have worked as intended. Also, it’s possible the counters were not even necessary. In all the instances that one of the counters is used, the corresponding array length (such as contract2ClaimedTokens.length) could have been used instead. Alternatively (or in addition to) they could have done the following:
separate claiming functions for 3DMfers and Cosmo Creatures
let users choose how many free tokens to mint, and which tokens to claim with
address any user experience changes on the website, such as an option to mint with all tokens from a contract (some people own quite a few Cosmo Creatures, so a “claim with all” button could be helpful)
The user experience would be a little different, but I doubt free minters would complain. And two more things that every contract creator should do:
dadmfers is the first mfers derivative project, and they just recently released BackgroundMfers, a series of dadmfers inspired banner images. This required a new contract, with some additional complexity because of the number of mint options. The contract provides 5 separate minting functions for getting your background mfers NFTs:
Public mint for anyone
Mfer mint, for anyone that has mfers, at a reduced price
Whitelist mint, free anyone on their list
Dadmfers v1 mint for free
Dadmfers v2 mint for free
Why some many minting options? It does add complexity, but there are good reasons, such as more favorable pricing for mfers holders, or free mints for dadmfers holders, while still allowing anyone to participate with the public option. There are two dadmfers options, v1 & v2, because the original v1 contract for dadmfers had very high gas fees. This turned a lot of people away, but the team quickly responded with a much more gas optimized contract. However, since the original contract had already been deployed, a new one was needed, because smart contracts cannot be upgraded once deployed (unless you use a proxy contract). So a v2 gas optimized contract was deployed, and everyone that minted on v1 was given free airdrops for v2. Ok, enough backstory, lets look at the background mfers contract code, available on etherscan.
function mintPublic(uint256 _mintAmount) public payable mintCompliance(_mintAmount) {
require(msg.value >= publicCost * _mintAmount, "Not enough eth sent!");
require(_mintAmount < maxMintAmountPlusOne, "trying to mint too many!");
_mintLoop(msg.sender, _mintAmount);
}
Public Mint
This mintPublic function is very simple at first glance, but has a lot of dependencies to go into. It takes a _mintAmount, checks it with mintCompliance, then does 2 more checks before minting. Let’s look at mintCompliance since it’s also used by the other mint functions.
The max supply is defined by uint256 public maxSupplyPlusOne = 10_001 at the top of the contract. However, there’s also the following function at the bottom of the contract, which allows the contract owner to lower the supply.
function lowerSupply(uint256 newSupply) public onlyOwner {
if (newSupply < maxSupplyPlusOne) {
maxSupplyPlusOne = newSupply;
}
}
Supply lowering can be a good thing to do if the collection doesn’t mint out in a certain period of time. By lowering the supply, you can preserve the current rarities and NFT values, potentially making the existing NFTs more valuable. Dadmfers v2 also lowered the supply after some time passed and it hadn’t sold out.
The contract owner can also disable or enable minting with the setSale function, which sets the salesIsActive variable.
function setSale(bool newState) public onlyOwner {
saleIsActive = newState;
}
_mintLoop
function _mintLoop(address _receiver, uint256 _mintAmount) internal {
for (uint256 i = 0; i < _mintAmount; i++) {
supply.increment();
_safeMint(_receiver, supply.current());
}
}
This function is called with msg.sender (i.e. your wallet address) and the amount you want to mint. It does a simple for loop to increment the used supply, then mints a token. _safeMint is a standard function in OpenZeppelin’s ERC721, so we won’t go into that here.
Here’s a sample transaction for mintPublic, which transfers 1 token for 0.0169 Ether.
Mfers Mint
function mintWithMfers(uint256 [] memory nftIds) public payable mintCompliance(nftIds.length) {
require(msg.value >= mferCost * nftIds.length, "Not enough eth sent!");
for (uint256 i = 0; i < nftIds.length; i++) {
require(mfersContract.ownerOf(nftIds[i]) == msg.sender, "You must own all the mfers!");
require(usedMferIds[nftIds[i]] == false, "One of the mfer IDs has already been used!");
supply.increment();
_safeMint(msg.sender, supply.current());
usedMferIds[nftIds[i]] = true;
}
}
For this minting function, it expects a list of mfers tokens. These come from the selectors on the website. After checking mintCompliance and amount of eth sent, it loops through the tokens. If any token is not owned by you, or has already been used to mint, this function will fail. But if you are the owner of all the mfers tokens, it will do a mint for each one, and record the mfers token as used.
If you’re wondering why you have to check token ownership in the contract, when it’s already done on the website, that’s because smart contracts can be called directly, without going through the website. For example, you could go to the Write Contract section on etherscan, find the mintWithMfers function, and enter values directly there. This is something that all smart contract developers need to be aware of – you can’t assume people will only interact with the contract through your website.
Let’s dig into require(mfersContract.ownerOf(nftIds[i]) == msg.sender a bit more. mfersContract is defined at the top of the contract as nftInterface mfersContract = nftInterface(0x79FCDEF22feeD20eDDacbB2587640e45491b757f);.
So what’s nftInterface?
interface nftInterface {
function ownerOf(uint256 tokenId) external view returns (address owner);
function balanceOf(address owner) external view returns (uint256);
function totalSupply() external view returns (uint256);
}
This is defined outside the contract, and provides a way to interact with another contract, by defining some functions that the contract should support. So mfersContract is an interface to the smart contract behind mfers (which you can see at this address on etherscan: 0x79FCDEF22feeD20eDDacbB2587640e45491b757f), and the background mfers contract is calling the mfers contract to check token ownership. These 3 functions (ownerOf, balanceOf, and totalSupply) are all standard ERC721 functions.
Here’s a sample transaction for mintWithMfers, which transfers 1 token for 0.0069 Ether.
Dadmfer v1 & v2 Mint
function mintWithDadmfersV1(uint256 [] memory nftIds) public mintCompliance(nftIds.length) {
for (uint256 i = 0; i < nftIds.length; i++) {
require(dadmfersV1Contract.ownerOf(nftIds[i]) == msg.sender, "You must own all the dadmfer V1s!");
require(usedV1Ids[nftIds[i]] == false, "One of the dadmfer IDs has already been used!");
supply.increment();
_safeMint(msg.sender, supply.current());
usedV1Ids[nftIds[i]] = true;
}
}
function mintWithDadmfersV2(uint256 [] memory nftIds) public mintCompliance(nftIds.length) {
for (uint256 i = 0; i < nftIds.length; i++) {
require(dadmfersV2Contract.ownerOf(nftIds[i]) == msg.sender, "You must own all the dadmfer V2s!");
require(usedV2Ids[nftIds[i]] == false, "One of the dadmfer IDs has already been used!");
supply.increment();
_safeMint(msg.sender, supply.current());
usedV2Ids[nftIds[i]] = true;
}
}
Both of these functions are very similar to each other and the mintWithMfers function. The main difference is dadmfersV1Contract vs dadmfersV2Contract. Just like with mfersContract, these are interfaces to the dadmfers contracts. The difference with mintWithMfers is that the initial requirement check for eth sent is gone, because these are free mints.
Here’s an example transaction for mintWithDadmfersV2, getting 3 tokens for 0 Ether.
Slither Analysis
Slither is a python tool for static analysis of Solidity contracts. You can use it to get a quick summary of the contract code, and then look for any deeper issues.
This fits with what we’ve seen above, the BackgroundMfers contract is complex code for minting ERC721 NFTs. The 11 medium issues are very technical to describe, but reduce down to “not a problem”. The contract does not implement onERC721Received, so there’s no real reentrency concerns, and the other issues look more like syntax & style choices in foreignNftsForWallet, which is a read-only function that is not used for minting.
Conclusion
While somewhat complicated, the background mfers contract looks quite safe for minting, and appears to be gas optimized. It allows batch minting to receive multiple tokens, which usually saves in gas fees. And there’s nothing in the minting functions that seems unnecessary. If you have any questions about the project, you can ask in the dadmfers discord; everyone is very friendly. As of publish time, they have not revealed the images yet, but when they do, you’ll be able to see the background mfers on looksrare or opensea.
I didn’t cover the whitelistMint because most people won’t be using that one. There’s also a complicated looking function foreignNftsForWallet that returns all the token IDs owned by a wallet, for a given contract. You can test this for yourself in the Read Contract section on etherscan, if you know a wallet address that owns one or more of mfers, dadmfers v1 or v2. For example, the address 0x4873f1768e1833fa6fb720b183715c7f57ecf953 is the wallet of the contract creator, so if you enter that and input 1, you can see it owns token 908 for dadmfers v2. Use input 0 for dadmfers v1 or input 2 for mfers.
I’ve recently pushed some updates to nltk-trainer, so that it now supports Python 3.7 and NLTK 3.4.5 or greater. NLTK also just released version 3.5.
One significant change is that the default part of speech tagger is now the PerceptronTagger, originally written by Matthew Honnibal (author of spacy) before it was ported to NLTK.
Ansible is a framework that helps with automating deployments, among other things. It has a feature called Ansible Vault that enables you to encrypt secrets in your ansible files. These vault encrypted secrets can only be decrypted if you provide the correct password. This means you can store things like database passwords and other sensitive settings in your repository, in a secure manner. For password access to your secrets, you are given 3 options:
Ansible asks you to enter a password every time the secrets are needed
You provide a file that has the password in it
You leave everything decrypted until you’re ready to commit your changes, then you encrypt them using option 1 or 2 (and later decrypt when you want to make changes).
Entering a password all the time gets annoying real quick, but having a password file laying around does not seem all that secure. Plus it’s hard to share securely if you’re collaborating with others. Option 3 requires you to not make a mistake and accidentally commit decrypted secrets. What if there was a better way?
Lastpass is a great place to store your passwords, and generate secure ones, but it is annoying to lookup, copy, then paste the password back in ansible, and you need to add —ask-vault-pass to every ansible command. However, Lastpass has a neat command line utility that you can use to get a password saved in Lastpass. With some minor scripting, you can integrate this with the ansible password file, so that you don’t have a plaintext password file laying around. I learned a lot about how to do this from How to use Ansible Vault with LastPass but decided that simple scripting worked better for me than install a ruby gem.
Now you can run ansible with vault encrypted secrets, and at worst you’ll be prompted for your lastpass master password.
This isn’t only more convenient for an individual, it can also be great for teams: you can check vault encrypted secrets into a shared repository, then share the password in Lastpass. Now nothing is exposed in the repository, and the only people that can access the secrets are those with the Lasspass password.