A Quick Simple Way to Download All the Images on a Page

You don’t need to write a web scraper to do this, just some simple code and standard linux/unix commands.

  1. Open the page in your web browser
  2. Open the Developer Tools
  3. Paste in the following Javascript
var images = document.getElementsByTagName('img'); 
var srcList = [];
for(var i = 0; i < images.length; i++) {
    srcList.push(images[i].src.split('?', 1)[0]);
}
srcList.join('\n');
  1. Create a folder to store the images
  2. Copy the text output from above into a file & save as images.txt in your folder
  3. Inspect images.txt to make sure it looks right
  4. Run the following commands in a terminal, from your folder
cat images.txt | sort | uniq > uniq_images.txt
wget -i uniq_images.txt

Now all the images from that page should be in the folder you created.

woman writing on whiteboard

Developing an Etsy App – Getting Started

I’m working on an Etsy app for some client work (an Etsy listing scheduler) and just getting started is quite a process. So I’m documenting it here for anyone else that may be interested in creating an app for Etsy.

Step 1: Have a Real Etsy Account

To begin, you’ll need a real Etsy account. If you don’t have one already, sign up for an account on the Etsy website.

Step 2: Create a Test Etsy Store

Next, create a test Etsy store that is real enough for testing purposes. You need to create a real listing, even if it’s a digital item that is some throw-away photo. You also need to connect a real bank account to receive payouts. This Etsy store will be your test environment for developing your app.

Step 3: Set Store to Developer Mode

To ensure that your listings are not visible in Etsy’s search, set your store to Developer mode. Only do this if you’re working through your own personal account. Do not do this for a real Etsy shop.

Step 4: Create a Webpage

Create a webpage for your Etsy app. This will serve as the main interface for users to interact with your app. But for now, it’s really for the Etsy app approval team, so they can learn about the purpose of your app.

Step 5: Review Etsy’s Terms and Conditions

Before proceeding further, carefully review Etsy’s terms and conditions. A specific restrictions they have is that you cannot use the term “Etsy” in the name of your app or the title/heading of your website. You should also include the following text on your website: The term ‘Etsy’ is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc.

Step 6: Register for the Etsy API

Register a new Etsy app. This app will be directly tied to your Etsy account, which you created in Step 1. You must also agree to their API Testing Policy.

Step 7: Contact Etsy Developer Support

Reach out to the Etsy Developer Support team by emailing developers@etsy.com. This shouldn’t be necessary since you registered in the previous step, but if you actually want to get a response and your app approved, you need to email them. Someone will review your website and registration to ensure it complies with their terms before approving your app.

Step 8: Start with Personal Token

Initially you will only get a personal API token, which means you can only interact with your own store through the API. This will allow you to test and iterate on your app’s functionality. You’ll need to actually create an initial version of your app before requesting commercial access.

Step 9: Provisional Users

As you progress with your app development, you may want to test your app on other stores. You can add provisional users with a special API.

Step 10: Create Material for Etsy Review

Prepare all the necessary materials, such as documentation and screenshots, for Etsy’s commercial API review process. This might include OAuth permissions required, and API calls that your app makes. These materials will help Etsy understand and evaluate your app.

Step 11: Request Commercial Access

Once your app is ready for wider usage, request commercial access from Etsy. This will allow anyone to authenticate and use your app via OAuth, once you’re approved.

charts on black wooden table

Sentiment Analysis Survey 2023

Do you use a sentiment analysis API or tool? Or have you considered using one but couldn’t find one that fit your needs?

If the answer to either of those questions is yes, then please fill out this Sentiment Analysis Survey. I’d like to learn more and hear directly from users and customers of sentiment analysis tools. Thanks for your time.

shopping business money pay

5 Lesser Known Risk Factors in Payment Fraud

When you’re analyzing payments to determine if they are fraudulent, what should you look for? Stripe Radar is great at blocking the more obvious fraudulent payments, and allowing the payments that are clearly not fraud, but what about the payments that are in between? There are a number of less obvious factors you can look at to determine whether a payment is fraud.

How to Decide if a Payment Under Review is Fraudulent

Here are 5 lesser known factors we’ve identified when working with clients of Streamhacker Technologies. We’ll describe each of these in more detail below

  1. History of adding & removing cards
  2. Specific fraud insights
  3. Fast plan upgrades
  4. Lack of product usage
  5. Multiple IPs and payment attempts

While this article uses examples from Stripe, these factors can apply to almost any payment platform.

History of adding & removing cards

When a customer uses multiples cards to make payments over a relatively short period of time, that’s a big warning sign of card testing. When combined with fast plan upgrades, multiple IPs, and lack of product usage, then you can be confident it’s fraud.

Much of the time, Stripe will show this behavior in the Related Payments section of a charge. You can see an example here.

Related credit card fraud payments in stripe

However, sometimes you need to go into the customer profile to get the full picture. In the Recent Activity section, you can see if the customer added a new card. Here’s an example of what it looks like when someone changes cards within ~1 day of signing up.

Recent stripe customer activity showing credit card changes

On its own, this is suspect but not necessarily fraud. However, if there’s more than 2 cards, that’s quite suspect. Also very suspect if the cards come from multiple countries. If you click on Show details for any of the cards, you can see the countries.

Credit card change history in stripe showing 2 different countries

Above you can see 2 different cards from 2 different countries. And in this case, the customer’s IP address was in a third country. Very suspicious behavior.

Specific Fraud Insights

On a Stripe charge payment, there’s a Fraud Insights button that shows you various fraud factors. Three that we’ve found to be useful are shown below.

Stripe fraud insights showing authorization rates for email

A low authorization rate and more than 0 declines associated with the customer’s email are significant fraud indicators. The name-email similarity match is a small additional indicator on top. These insights are most useful when combined with the other indicators discussed here.

Fast Plan Upgrades

A “fast plan upgrade” is when someone subscribes to the lowest plan of your service, then upgrades to one of your highest plans within a few minutes. This may be another form of card testing. Maybe your lowest plan is $10 and your highest plan is $100 – those are very different purchase amounts, and a card tester may want to find out if the card that works at a low amount can also be used for larger purchases. If the first upgrade attempt fails, and they switch cards to try again, fraud risk looks a lot more likely. These related payments show an example of this exact behavior.

Related credit card fraud payments showing fast plan upgrades

Here’s what happened:

  1. Attempted to purchase low level plan at $10, but that failed
  2. Switched cards and tried again, Stripe risk score was still 0
  3. One minute later, successfully upgraded to a higher plan, and got a risk score of 47, which Stripe still considers “normal”
  4. 1 day later, tried to upgrade again to an even higher plan, but that failed with a higher risk score

Note: 2 payments are showing as Refunded because they were successful until being refunded as fraud.

Lack of Product Usage

If a new customer doesn’t use your product much right away, that’s ok. But if they also change cards and/or try to upgrade plans without using your product at all, that’s suspicious. In Streamlining Stripe Reviews with Webhooks and Zapier I described how we helped a client highlight product usage metrics as part of their Stripe review process. Getting some product usage metrics into your Stripe charge metadata is very useful for fraud analysis, so you can quickly look at all your risk factors in one place.

Multiple IPs and Payment Attempts

Many people use VPNs and proxy servers for very legitimate reasons. And sometimes people are traveling. Just because the credit card country doesn’t match the IP country, or there’s a low authorization rate for an IP address, that doesn’t necessarily mean a payment is fraud. But when the IP address of a customer changes over a short period of time, and they make multiple payment attempts from multiple IP addresses, that’s unusual. Stripe’s Related Payments section helps to show this kind of behavior.

Conclusion

Deciding whether a payment is fraud can be tricky, and is not always obvious. But there are risk indicators you can look for, and when you see multiple indicators together, you can be more confident in a fraud assessment. Conversely, if you only see one of these indicators, then a payment likely isn’t fraud. Whatever your assessment is, take detailed notes. Stripe’s charge UI has a nice feature where you can leave a note for future reference – be sure to use this so you have a history of why you made a decision, and can revisit these decisions in the future, when you have more information.

Here are some helpful links from Stripe on identifying and preventing payment fraud:

If you think your team or company needs help managing payment fraud, contact Streamhacker Technologies to see what we can do for you.

white and gray round metal frame

Streamlining Stripe Reviews with Webhooks and Zapier

If your company is handling payments through Stripe, you’re likely familiar with their Radar product, which helps protect you from payment fraud. And if you’re using Stripe Radar, then you may have experienced the issue of receiving a lot of Stripe Review emails. Handling all of these reviews can be time-consuming and difficult to manage effectively, and the Stripe review email doesn’t provide any useful information on its own. Getting one email for each review means they can pile up, and sometimes you might go through all the reviews within the Stripe dashboard, but the emails are still in your inbox demanding attention, as if the review is still open. In this case study, we’ll explore how Streamhacker Technologies helped a company tackle this problem using the Stripe API, custom webhooks, and Zapier.

The Problem: Too Many Stripe Review Emails

The billing team found themselves inundated with a high volume of individual review emails from Stripe Radar. Although these emails were meant to highlight potentially fraudulent charges, they often caused annoyance and frustration. The team observed that by the time they opened some reviews, they were already closed due to another team members action, or by automatic fraud controls that got triggered after the review email was sent. In most other cases, the information they needed to make a fraud determination was found elsewhere, in a separate system, with no direct links from Stripe. This back and forth was an inefficient use of their time and attention, and they needed a better solution.

The Solution: Webhooks and Zapier

We devised a solution that utilized Stripe’s API, custom webhooks, and Zapier to streamline the payment review process.

First, we created a custom webhook to retrieve additional customer information associated with each charge. This information helped indicate which reviews were more actionable, by including things like customer age and product usage metrics.

Then, we created a Zap to do the following:

  1. Get all open Stripe reviews
  2. For each Stripe review
    1. Call the custom webhook to get additional information
    2. Add that information as metadata to the Stripe charge under review, including links to systems with additional information about the customer
    3. Append select information to a Digest
  3. Send a single digest email every morning, containing all the open Stripe reviews, with specific indicators to help decide which reviews require attention
  4. Create a mail rule to automatically close the individual Stripe review emails

The Result: Efficient Stripe Review Processing

After implementing this solution, the team noticed a significant reduction in time spent processing fraud reviews. They were able to quickly identify reviews that required action, analyze the payments and customer behavior faster, and they no longer wasted time opening reviews that had already been closed.

Conclusion

Simple custom webhooks + Zapier = more efficient business operations. In this case, we were able to help the team save significant time and attention by improving their existing payment review process, which freed them up to focus on other business problems.

If you think your team or company might benefit from a similar solution, contact Streamhacker Technologies to see what we can do for you.

GPT, LLM, and Langchain Links

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.

Run a Chatbot Locally

Run a Chatbot on PDFs using Langchain

Make a QA Chatbot on your own text

Vault

Delegate.cash Contract Review

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

(note that the code blocks below contain some intermixed code from different parts of DelegationRegistry.sol and IDelegationRegistry.sol for clarity of explanation)

Delegation

There are 3 types of delegation:

  1. ALL – the vault wallet delegates all actions to the delegate wallet
  2. CONTRACT – the vault wallet delegates all actions on a specific contract to the delegate wallet
  3. 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:

  1. Compute a delegationHash
  2. Set DelegationInfo values for that delegationHash
enum DelegationType {
	NONE,
	ALL,
	CONTRACT,
	TOKEN
}

function delegateForAll(address delegate, bool value) external override {
	bytes32 delegationHash = _computeAllDelegationHash(msg.sender, delegate);
	_setDelegationValues(
		delegate, delegationHash, value, IDelegationRegistry.DelegationType.ALL, msg.sender, address(0), 0
	);
	emit IDelegationRegistry.DelegateForAll(msg.sender, delegate, value);
}

function delegateForContract(address delegate, address contract_, bool value) external override {
	bytes32 delegationHash = _computeContractDelegationHash(msg.sender, delegate, contract_);
	_setDelegationValues(
		delegate, delegationHash, value, IDelegationRegistry.DelegationType.CONTRACT, msg.sender, contract_, 0
	);
	emit IDelegationRegistry.DelegateForContract(msg.sender, delegate, contract_, value);
}

function delegateForToken(address delegate, address contract_, uint256 tokenId, bool value) external override {
	bytes32 delegationHash = _computeTokenDelegationHash(msg.sender, delegate, contract_, tokenId);
	_setDelegationValues(
		delegate, delegationHash, value, IDelegationRegistry.DelegationType.TOKEN, msg.sender, contract_, tokenId
	);
	emit IDelegationRegistry.DelegateForToken(msg.sender, delegate, contract_, tokenId, value);
}

Delegation Hash

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.

/// @notice A mapping of wallets to versions (for cheap revocation)
mapping(address => uint256) internal vaultVersion;

/// @notice A mapping of wallets to delegates to versions (for cheap revocation)
mapping(address => mapping(address => uint256)) internal delegateVersion;
	
function delegateForAll(address delegate, bool value) external override {
	bytes32 delegationHash = _computeAllDelegationHash(msg.sender, delegate);
	_setDelegationValues(
		delegate, delegationHash, value, IDelegationRegistry.DelegationType.ALL, msg.sender, address(0), 0
	);
	emit IDelegationRegistry.DelegateForAll(msg.sender, delegate, value);
}

function delegateForContract(address delegate, address contract_, bool value) external override {
	bytes32 delegationHash = _computeContractDelegationHash(msg.sender, delegate, contract_);
	_setDelegationValues(
		delegate, delegationHash, value, IDelegationRegistry.DelegationType.CONTRACT, msg.sender, contract_, 0
	);
	emit IDelegationRegistry.DelegateForContract(msg.sender, delegate, contract_, value);
}

function delegateForToken(address delegate, address contract_, uint256 tokenId, bool value) external override {
	bytes32 delegationHash = _computeTokenDelegationHash(msg.sender, delegate, contract_, tokenId);
	_setDelegationValues(
		delegate, delegationHash, value, IDelegationRegistry.DelegationType.TOKEN, msg.sender, contract_, tokenId
	);
	emit IDelegationRegistry.DelegateForToken(msg.sender, delegate, contract_, tokenId, value);
}

Delegation Info

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:

  1. Confirm the vault wallet owns tokenId
  2. Call checkDelegateForToken(delegate, vault, contract_, tokenId)
  3. Proceed with the claim if checkDelegateForToken returns true
  4. 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:

  1. git clone --recurse-submodules https://github.com/0xfoobar/delegation-registry.git
  2. cd delegation-registry/lib
  3. slither ../src/DelegationRegistry.sol --print human-summary
  4. slither ../src/DelegationRegistry.sol
Human summary of slither analysis

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.

Picaroons Contract Review

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:

  1. Sale isn’t closed
  2. The hash argument was signed (signature) by a known signing address
  3. The hash argument can be verified with the other function arguments
  4. There are tokens available to mint
  5. You have not minted too much
  6. 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.

Match Address Signer

    address private _signerAddress = 0xF5F6C1B8F13F2A41Ce8474AAD3Dd5050364eef1f;

    ...
        
    function matchAddresSigner(bytes32 hash, bytes memory signature) private view returns(bool) {
        return _signerAddress == hash.recover(signature);
    }

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:

  1. Encode function arguments into a message
  2. Hash that message
  3. 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.

$ slither 0x545f0a45Ba06C7C5b1a5Fb0b29008462ceEA07b7 --print human-summary

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 Contract Review

Dead mfers is a collection by sartoodles, who is an active member of the unofficial mfers community, and has created a few mfers derivative collections. 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.

Split Payment

    function _splitPayment() internal
    {
        if(msg.value != 0){
            uint256 splitValue = msg.value / 10;
            uint256 remainingValue = msg.value - splitValue;
            
            payable(shareAddress).transfer(splitValue);
            payable(owner()).transfer(remainingValue);
        }
    }

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):

    function create(string memory name_,
        string memory symbol_,
        string memory baseURI_,
        string memory appId_,
        uint256 mintFee_,
        uint256 size_,
        bool mintSelectionEnabled_,
        bool mintRandomEnabled_,
        address whiteListSignerAddress_,
        uint256 mintLimit_,
        uint256 royaltyBasis_,
        string memory placeholderImage_) payable public
    {
        require(msg.value >= fee, 'Must pass the correct fee to the contract');
        
        address payable clone = payable(ClonesUpgradeable.clone(erc721Implementation));

        AutoMinterERC721(clone).initialize(name_,
            symbol_,
            baseURI_,
            msg.sender,
            mintFee_,
            size_,
            mintSelectionEnabled_,
            mintRandomEnabled_,
            whiteListSignerAddress_,
            mintLimit_,
            royaltyBasis_,
            placeholderImage_
        );
        
        emit ContractDeployed(appId_, appId_, clone, msg.sender);
    }

The erc721Implementation address points to the current deployed version of AutoMinterERC721, so create clones this contract then initializes it.

To recap, the contract deployment worked like this:

  1. The create function is called on a proxy contract that forwards to the AutoMinterFactory implementation contract (a proxy is used so that the factory contract can be upgraded in the future)
  2. The AutoMinterFactory implementation contract clones and initializes a minimal proxy contract for the current version of the AutoMinterERC721 implementation contract
  3. The minimal proxy contract is initialized with the parameters from the create function in step 1
  4. 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.

$ slither 0x50092539a224953d82995c9d950e042da4556283 --print human-summary

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:

$ slither 0x50092539a224953d82995c9d950e042da4556283
AutoMinterERC721._drawRandomIndex() (contracts/AutoMinterERC721.sol#114-129) uses a weak PRNG: "i = uint256(keccak256(bytes)(abi.encodePacked(block.timestamp))) % remaining (contracts/AutoMinterERC721.sol#116)"

OwnableUpgradeable.__gap (@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol#82) shadows:
	- ContextUpgradeable.__gap (@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol#31)
ERC721Upgradeable.__gap (@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol#431) shadows:
	- ERC165Upgradeable.__gap (@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol#36)
	- ContextUpgradeable.__gap (@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol#31)

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.

%d bloggers like this: