Skip to content

Commit

Permalink
Support EIP-1271 (#9)
Browse files Browse the repository at this point in the history
This PR adds support for EIP-1271 signatures "signed" by contracts. Main use case is for smart contract wallets to be able to use the `permit` and `encumberBySig` functions since they cannot sign off-chain messages like an EOA can.
  • Loading branch information
kevincheng96 authored Jun 30, 2023
1 parent ab7d470 commit 4ee0c74
Show file tree
Hide file tree
Showing 5 changed files with 721 additions and 22 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ It is comparable to the way that Wrapped Ether (WETH) acts as a wrapper around
ETH, providing users with the ability to make use of ETH in any context that
requires an ERC20-compatible token.

The wrapper token comes with [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612)
and [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) support to allow both EOAs
and smart contracts to approve and encumber gaslessly using off-chain signatures.

## Limitations

### Rebasing tokens
Expand Down
83 changes: 65 additions & 18 deletions src/EncumberableToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ contract EncumberableToken is ERC20, IERC20Permit, IERC7246 {
/// @dev The EIP-712 typehash for the contract's domain
bytes32 internal constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

/// @dev The magic value that a contract's `isValidSignature(bytes32 hash, bytes signature)` function should return for a valid signature
/// See https://eips.ethereum.org/EIPS/eip-1271
bytes4 internal constant EIP1271_MAGIC_VALUE = 0x1626ba7e;

/// @notice Number of decimals used for the user represenation of the token
uint8 private immutable _decimals;

Expand Down Expand Up @@ -243,18 +247,16 @@ contract EncumberableToken is ERC20, IERC20Permit, IERC7246 {
bytes32 r,
bytes32 s
) external {
require(uint256(s) <= MAX_VALID_ECDSA_S, "Invalid value s");
// v ∈ {27, 28} (source: https://ethereum.github.io/yellowpaper/paper.pdf #308)
require(v == 27 || v == 28, 'Invalid value v');
require(block.timestamp < expiry, "Signature expired");
uint nonce = nonces[owner];
bytes32 structHash = keccak256(abi.encode(AUTHORIZATION_TYPEHASH, owner, spender, amount, nonce, expiry));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), structHash));
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), 'Bad signatory');
require(owner == signatory, 'Bad signatory');
require(block.timestamp < expiry, 'Signature expired');
nonces[signatory]++;
_approve(owner, spender, amount);
if (isValidSignature(owner, digest, v, r, s)) {
nonces[owner]++;
_approve(owner, spender, amount);
} else {
revert("Bad signatory");
}
}

/**
Expand All @@ -276,18 +278,63 @@ contract EncumberableToken is ERC20, IERC20Permit, IERC7246 {
bytes32 r,
bytes32 s
) external {
require(uint256(s) <= MAX_VALID_ECDSA_S, "Invalid value s");
// v ∈ {27, 28} (source: https://ethereum.github.io/yellowpaper/paper.pdf #308)
require(v == 27 || v == 28, 'Invalid value v');
require(block.timestamp < expiry, "Signature expired");
uint nonce = nonces[owner];
bytes32 structHash = keccak256(abi.encode(ENCUMBER_TYPEHASH, owner, taker, amount, nonce, expiry));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), structHash));
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), 'Bad signatory');
require(owner == signatory, 'Bad signatory');
require(block.timestamp < expiry, 'Signature expired');
nonces[signatory]++;
_encumber(owner, taker, amount);
if (isValidSignature(owner, digest, v, r, s)) {
nonces[owner]++;
_encumber(owner, taker, amount);
} else {
revert("Bad signatory");
}
}

/**
* @notice Checks if a signature is valid
* @dev Supports EIP-1271 signatures for smart contracts
* @param signer The address that signed the signature
* @param digest The hashed message that is signed
* @param v The recovery byte of the signature
* @param r Half of the ECDSA signature pair
* @param s Half of the ECDSA signature pair
* @return bool Whether the signature is valid
*/
function isValidSignature(
address signer,
bytes32 digest,
uint8 v,
bytes32 r,
bytes32 s
) internal view returns (bool) {
if (hasCode(signer)) {
bytes memory signature = abi.encodePacked(r, s, v);
(bool success, bytes memory data) = signer.staticcall(
abi.encodeWithSelector(EIP1271_MAGIC_VALUE, digest, signature)
);
require(success == true, "Call to verify EIP1271 signature failed");
bytes4 returnValue = abi.decode(data, (bytes4));
return returnValue == EIP1271_MAGIC_VALUE;
} else {
require(uint256(s) <= MAX_VALID_ECDSA_S, "Invalid value s");
// v ∈ {27, 28} (source: https://ethereum.github.io/yellowpaper/paper.pdf #308)
require(v == 27 || v == 28, "Invalid value v");
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "Bad signatory");
require(signatory == signer, "Bad signatory");
return true;
}
}

/**
* @notice Checks if an address has code deployed to it
* @param addr The address to check
* @return bool Whether the address contains code
*/
function hasCode(address addr) internal view returns (bool) {
uint256 size;
assembly { size := extcodesize(addr) }
return size > 0;
}

/**
Expand Down
65 changes: 65 additions & 0 deletions src/test/EIP1271Signer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.15;

contract EIP1271Signer {
bytes4 internal constant EIP1271_MAGIC_VALUE = 0x1626ba7e;

address public owner;

constructor(address _owner) {
owner = _owner;
}

function isValidSignature(bytes32 messageHash, bytes memory signature) external view returns (bytes4) {
if (recoverSigner(messageHash, signature) == owner) {
return EIP1271_MAGIC_VALUE;
} else {
return 0xffffffff;
}
}

function recoverSigner(bytes32 messageHash, bytes memory signature) internal pure returns (address) {
require(signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length");

bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := and(mload(add(signature, 65)), 255)
}

// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
//
// Source OpenZeppelin
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol

if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
revert("SignatureValidator#recoverSigner: invalid signature 's' value");
}

if (v != 27 && v != 28) {
revert("SignatureValidator#recoverSigner: invalid signature 'v' value");
}

// Recover ECDSA signer
address signer = ecrecover(messageHash, v, r, s);

// Prevent signer from being 0x0
require(
signer != address(0x0),
"SignatureValidator#recoverSigner: INVALID_SIGNER"
);

return signer;
}
}
Loading

0 comments on commit 4ee0c74

Please sign in to comment.