Clarifying ERC-1271: Smart Contract Signature Verification

EOAs sign messages, and then verify signatures with isValidSignature() on ERC-1271 compliant smart contracts!

田少谷 Shao
Taipei Ethereum Meetup
6 min readMay 22, 2024

--

Bolivia

Outline

0. Intro
1. ERC-1271
2. The Return Value
3. Terminologies Clarification
- It's ERC-1271, not EIP-1271.
- To sign or not to sign, that's the question.
4. Use Cases
5. Summary

0. Intro

Recently, I came across a talk by Cow DAO on ERC-1271, and I couldn’t help but wonder: what is this seemingly magical ERC-1271 that I’ve never heard of?

It turns out that it’s a very simple ERC, yet crucial in setting the standard for verifying signatures with smart contracts (wallets), which has been a popular topic these days.

As there are already many articles on the examples and implementation approaches on the topic, this article is merely a (hopefully) brief note on the confusion of terminologies and concepts I encountered when going through the materials.

Before we dive in, sharing my favorite song these days; never fails to set a calm mood 😇

1. ERC-1271

The official documentation is always the most helpful resource: click here.

Some articles explain the motive of ERC-1271 as imitating EOA “signing messages”, for smart contracts don’t have private keys and thus couldn’t “sign” messages.

However, IMO it’s easier to understand ERC-1271 as a standard for performing signature verification only, and to set aside the notion of contract “signing” for now.

I’ll explain why in section 3. Terminologies Clarification!

Before ERC-1271, there wasn’t a standard for how contracts should verify signatures, and the lack of a standard could lead to various unstandardised implementations.

contract ERC1271 {

// bytes4(keccak256("isValidSignature(bytes32,bytes)")
bytes4 constant internal MAGICVALUE = 0x1626ba7e;

function isValidSignature(
bytes32 _hash,
bytes memory _signature)
public
view
returns (bytes4 magicValue);
}

The interface of ERC-1271 is shown above.

We can observe that as long as a contract implements the function isValidSignature() and returns the function selector 0x1626ba7e on successful verification, it’s qualified as an ERC-1271 compliant contract. The logic inside isValidSignature() is customisable.

To verify signatures, simply call isValidSignature() on the contract. This is what ERC-1271 is for!

2. The Return Value

As anyone can see, ERC-1271 is fairly simple and easy-to-understand. The only question might be: why is the return value a bytes4 instead of a boolean? Isn’t it a convention to return a boolean whenever a function name has the prefix is?

For those curious, we can refer to the original discussion: click here.

TL;DR: Returning a specific value, 0x1626ba7e, can ensure that the value is always returned intentionally, preventing functions from unintentionally returning true.

For a more detailed explanation, let’s consider fallback functions, as mentioned in the original thread.

If we call isValidSignature() on a contract that isn’t ERC-1271 compliant, meaning there’s no isValidSignature(), the fallback function will be triggered instead. Since the fallback function is customised, it can return anything, including a boolean true. But, it’s unlikely that a fallback function, without implementing ERC-1271, would unintentionally return the specific value 0x1626ba7e.

Below is a simplified code snippet demonstrating a case where the return value of isValidSignature() is a boolean instead:

contract Callee {
// fallback() can only return (bytes memory),
// so we have to wrap the boolean
fallback(bytes calldata) external returns (bytes memory) {
bool result = true;
bytes memory toReturn = abi.encode(result);
return toReturn;
}
}

// to get the return value as a boolean from the fallback function,
// we can wrap Callee with an interface,
// so that we don't need to use Callee.call() and decode (bytes memory)
interface JustAnInterface {
function isValidSignature() external returns (bool);
}

contract Caller {
function triggerFallbackOnCallee(address callee) public returns (bool) {
JustAnInterface justAnInterface = JustAnInterface(callee);
bool result = justAnInterface.isValidSignature();
return result;
}
}

Here, we can see that Callee isn’t an ERC-1271 contract, yet its fallback function always return a bool (wrapped in bytes memory). Thus, while no signature verification is actually happening in Callee, it returns true to Caller.triggerFallbackOnCallee() anyway, indicating a successful verification. This can lead to catastrophic results, since signature verifications often involve real funds.

IMO, the concern for fallback functions is indeed valid, as demonstrated above. However, the choice of the function name isValidSignature() could have been improved, e.g. getSelectorForValidSignature()(although too long and thus probably unideal). But,

3. Terminologies Clarification

It’s ERC-1271, not EIP-1271.

First off, by convention, EIP is usually for Ethereum protocol-level changes, while ERC is for smart contract development standardization.

Although most people use EIP and ERC interchangeably, don’t let the common usage of EIP-1271 misguide you.

There’s no protocol upgrade involved. Instead, developers follow the standard interface of ERC-1271 to develop contract wallets accordingly.

Funny enough, I also didn’t pay attention to this nuance when writing about EIP-4626 Inflation Attack (should be ERC-4626) 🙈

To sign or not to sign, that is the question.

Secondly, as mentioned above, some articles describe ERC-1271 as enabling contracts to also “sign” messages.

However, if we define “signing” strictly as “signing messages with private keys”, obviously, contracts will never be able to “sign” messages, for they don’t and will never have private keys.

The reason is simple: blockchains are transparent; where can contracts store “private” keys in a public space?

Thus, I believe that whenever other articles mention “contract signing messages”, what they actually mean is “sending a tx by calling isValidSignature() on an ERC-1271 compliant contract with inputs that include a message signed by an EOA”.

That’s a long-arse clarification 😵‍💫

Let’s look at an explanation, still from the official doc.

This function should be implemented by contracts which desire to sign messages (e.g. smart contract wallets, DAOs, multisignature wallets, etc.) Applications wanting to support contract signatures should call this method if the signer is a contract.

Since smart contract wallets are controlled by EOAs, by smart contract “signing”, it’s still EOAs signing on behalf of the smart contract wallet.

The other two explanations I can think of why those article writers worded this way are:

  • I guess “sending a signed message” is semantically equivalent to “signing” 🤔
  • Before ERC-1271, signatures by EOAs were mostly done off-chain. Thus, it’s true that ERC-1271 did commence on-chain signing, or more accurately speaking, submitting signatures on-chain.

Any other ideas you can come up with? Pls share with me 💡

4. Use Cases

Since ERC-1271 is about smart contracts and signature verification, it’s no surprise that all use cases involve smart contract wallets! Well, if there are signatures, ownership is involved, and then the contracts in use can always be called wallets.

I strongly recommend reading the article below, as it elaborates on the two implementation approaches of how signature and verification are done by Safe:

  • On-chain: A user submits a tx including the signature and registers it on an ERC-1271 contract wallet; later, calls isValidSignature() to look up the signature already in the registry.
  • Off-chain: Rather than separating signature registration and verification, the signature can also be included in the same tx as the verification behaviour. In this case, we might need a function wrapping isValidSignature() to handle verification, customized logic, and likely some states changes to complete both tasks in one go.

5. Summary

Let’s state this again: smart contracts don’t have private keys, so they can never sign messages like EOAs do. With ERC-1271, there’s a standard for users (EOAs) to sign messages, and then verify signatures with isValidSignature() on ERC-1271 compliant smart contract wallets!

Honestly, in the beginning, I was very confused by the combination of many articles:

  1. Referring to ERC-1271 as EIP-1271, which suggests an Ethereum protocol change.
  2. Using the word “sign” casually, while strictly speaking, smart contracts could never sign messages “with private keys”.

So I thought: was there a hard-fork that somehow magically enabled private keys on smart contracts? 🤯🤯🤯 That’s impossible!

It turned out, it was all a misunderstanding 😇

That’s all for clarifications on ERC-1271! I hope this article can help anyone who gets confused going through the many great but somewhat puzzling materials on this tiny little topic!

Leave any comments down below if you’d like to discuss or find any errors! Until next time!

A huge thanks to NIC Lin for helping with the code snippet and editing!

--

--