Secure Smart Contract Development (2) — How to Use Digital Signature and Use It Right in NFT (Markets)

BlockSec
6 min readAug 12, 2022

--

1. Introduction

Digital signature is used to ensure the authenticity and integrity. As described in this article, “A valid digital signature, where the prerequisites are satisfied, gives a recipient very high confidence that the message was created by a known sender (authenticity), and that the message was not altered in transit (integrity).”

Digital signature has been widely used in smart contracts, e.g., in allowlist mint and order-book NFT marketplaces. That’s because it helps save transaction costs (off-chain sign and on-chain verification). However, the misuse of the developers also introduces risks in the NFT marketplaces. In this blog, we’d like to talk about the misuse of digital signatures in the NFT ecosystem.

2. Applications

Digital signature has been widely used for allowlist mint (only the users with valid signatures can mint NFTs) in NFT contracts and NFT markets for order verification (only the orders with expected signatures can be executed.) The sign of the data is off-chain to save gas. In the following, we will illustrate these two usage scenarios.

2.1. Allowlist Mint

“NFT minting” is the procedure of creating an NFT on the blockchain. Most NFT projects would like to disseminate their products; they prefer to motivate users by allowlist mint (also called presale, etc.). People who win the spots could mint tokens at a lower price (even free). A digital signature is used to distinguish the allowlist minters and public (ordinary) minters. Below is an example of the implementation of allowlist mint.

function mint_approved(
vData memory info,
uint256 number_of_items_requested,
uint16 _batchNumber
) external {
...
require(verify(info), "Unauthorised access secret");
...
}
function verify(vData memory info) public view returns (bool) {
require(info.from != address(0), "INVALID_SIGNER");
bytes memory cat =
abi.encode(
info.from,
info.start,
info.end,
info.eth_price,
info.dust_price,
info.max_mint,
info.mint_free
);
bytes32 hash = keccak256(cat);
require(info.signature.length == 65, "Invalid signature length");
bytes32 sigR;
bytes32 sigS;
uint8 sigV;
bytes memory signature = info.signature;
assembly {
sigR := mload(add(signature, 0x20))
sigS := mload(add(signature, 0x40))
sigV := byte(0, mload(add(signature, 0x60)))
}
bytes32 data =
keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
address recovered = ecrecover(data, sigV, sigR, sigS);
return signer == recovered;
}

This code snippet is from the Association NFT (Which has a vulnerability — do not copy this code). The function mint_approved() intends to implement the allowlist mint: the project owner signs a mint message (info variable) and sends the message to the permitted minter (who can mint NFTs). Then the minter can invoke approved_mint with the signed variable. The contract will then verify whether the message is signed by the project (signer == recovered). If so, the one who invokes the function is allowed to mint NFTs (which is NOT secure since there is no verification on whether the one who invokes the function is the actual person in the allowlist).

2.2. Order Verification

Order verification is another application of digital signature in the NFT ecosystem. The NFT marketplaces play an essential role in the NFT ecosystem as they provide the trading functionality for the NFTs. As each NFT token is non-fungible, the automated market maker (AMM) trade policy is hard to be used in NFT markets. Thus, most NFT marketplaces, e.g. OpenSea, LooksRare, and X2Y2, take the order-book trade model.

The order-book trade is simple. There is a maker, aka a person who wants to sell an asset for a specific price, and a taker, aka a person who wants to buy the asset at the seller’s price. In this case, the order matches. The process is the same in the order-book NFT marketplaces. The only difference is the process of order offering: the NFT marketplaces use digital signatures for order verification. Figure 1 describes an example of the whole trade process of one of the order-book marketplaces: OpenSea.

Fig 1. OpenSea trading Process

Specifically, the seller signs a sell order and stores it on OpenSea’s server. The buyer could retrieve the signed sell order information from OpenSea’s server and then invokes the NFT market contract with the signed sell order as parameters. The market contract will validate the order to ensure that the seller signs the sell order (since the buyer initiates the transaction.) — to prevent the buyer buys an asset without the seller’s consent.

3. Security Incidents

The Horton Principle is a maxim for cryptographic systems and can be expressed as “Authenticate what is being meant, not what is being said” or “mean what you sign and sign what you mean”, it requires signing the action totally and precisely. If the signature is partially or non-accurate, the result will be disastrous.

3.1 Association NFT

Recalling the NBA NFT contract in section 2.1. The function verify does a standard signature verification, but it's missing one CRITICAL component. The signature verification only ensures that the message is signed by the project. However, there is no enforcement that the person who provides the signature to the contract is consistent with the allowlist minter in the signed message. As a result, anyone can use the same signature to pass the verification and mint NFTs.

3.2 OpenSea

Another security issue is about OpenSea. In early 2022, researchers disclosed a potential vulnerability of the OpenSea marketplace contract (version: wyvern 2.2), which implements the core functionality of NFTs trading.

In Wyvern protocol, users author listings (sell offers) or offers (buy offers) off-chain, and signatures of offers are verified on-chain. Wyvern offers contain many parameters and the parameters are aggregated together into a single bytes string to calculate the digest of the offer. Then the contract will validate the signature of the digest. The parameters aggregation method simply packs the parameters into a bytes string with the following methods.

index = ArrayUtils.unsafeWriteAddress(index, order.target);
index = ArrayUtils.unsafeWriteUint8(index, uint8(order.howToCall));
index = ArrayUtils.unsafeWriteBytes(index, order.calldata);
index = ArrayUtils.unsafeWriteBytes(index, order.replacementPattern);
index = ArrayUtils.unsafeWriteAddress(index, order.staticTarget);
index = ArrayUtils.unsafeWriteBytes(index, order.staticExtradata);
index = ArrayUtils.unsafeWriteAddress(index, order.paymentToken);

For example, if the parameters compose of 2 components: (address, bytes), and the parameters are (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xc098"), the aggregated bytes would be 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fc098, just address + bytes. Seems to be easy and clear, right?

Now, consider a more complex example, the structure of parameters is (address, bytes, bytes).

parameter 1 is (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xab", "0xcdef").

parameter 2 is (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xabcd", "0xef").

The aggregated bytes are:

parameter 1: 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef.

parameter 2: 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef.

Wow! two different parameters have the same aggregated result, which means their digests are SAME, resulting that one signature could verify the two different parameters.

This is because there are many variable-length components in the parameters. An attacker can truncate part of the variables and attach the truncated parts to their previous or later components. Unfortunately, Wyvern contracts have many variable-length parameters as the below shows.

......
address target;
/* HowToCall. */
AuthenticatedProxy.HowToCall howToCall;
/* Calldata. */
bytes calldata;
/* Calldata replacement pattern, or an empty byte array for no replacement. */
bytes replacementPattern;
/* Static call target, zero-address for no static call. */
address staticTarget;
/* Static call extra data. */
bytes staticExtradata;
......

The impact of the vulnerability is that attacker could (if possible) control the victim’s accounts to execute some malicious behaviours. A detailed analysis of the vulnerability is here.

Both two security incidents mentioned in this section all violate the Horton Principle. Specifically, the NBA contract does not include the minter in the signed message (or does not check the consistency with the information contained in the signed message with the actual invocator), and the Wyvern contract signs structureless parameters so that the meaning of the action could be modified while the presentation (saying) of the parameters remains.

Suggestions

Follow the Horton Principle, sign what you mean, not what you say. The signature should contain all-around and accurate information needed.

  • Put all information that to be verified in the signature. Check the consistency of the data in the signed message with the runtime value (e.g., the intended user in the signed message and the actual user.)
  • The message to be signed needs to be deterministically encoded, e.g., there do not exist messages that are with different structures but have the same encoding result.

--

--

BlockSec
BlockSec

Written by BlockSec

The BlockSec focuses on the security of the blockchain ecosystem and the research of DeFi attack monitoring and blocking. https://blocksec.com

No responses yet