Skip to content

Commit

Permalink
Add permit and encumberBySig (#18)
Browse files Browse the repository at this point in the history
Implements `permit` and `encumberBySig`, both of which support EIP-1271 signatures.
  • Loading branch information
kevincheng96 authored Oct 20, 2023
1 parent c615df7 commit ac5a639
Show file tree
Hide file tree
Showing 10 changed files with 1,366 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
path = lib/solmate
url = https://github.com/transmissions11/solmate
branch = v7
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at fd81a9
7 changes: 5 additions & 2 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
solmate/=lib/solmate/src/
ds-test/=lib/forge-std/lib/ds-test/src/
solmate/=lib/solmate/src/
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
openzeppelin-contracts/=lib/openzeppelin-contracts/
openzeppelin/=lib/openzeppelin-contracts/contracts/
140 changes: 140 additions & 0 deletions src/CometWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CometInterface, TotalsBasic } from "./vendor/CometInterface.sol";
import { CometHelpers } from "./CometHelpers.sol";
import { ICometRewards } from "./vendor/ICometRewards.sol";
import { IERC7246 } from "./vendor/IERC7246.sol";
import { ECDSA } from "openzeppelin/utils/cryptography/ECDSA.sol";

/**
* @title Comet Wrapper
Expand All @@ -22,6 +23,23 @@ contract CometWrapper is ERC4626, IERC7246, CometHelpers {
uint64 baseTrackingIndex;
}

/// @notice The major version of this contract
string public constant VERSION = "1";

/// @dev The EIP-712 typehash for authorization via permit
bytes32 internal constant AUTHORIZATION_TYPEHASH = keccak256("Authorization(address owner,address spender,uint256 amount,uint256 nonce,uint256 expiry)");

/// @dev The EIP-712 typehash for encumber via encumberBySig
bytes32 internal constant ENCUMBER_TYPEHASH = keccak256("Encumber(address owner,address taker,uint256 amount,uint256 nonce,uint256 expiry)");

/// @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 Mapping of users to basic data
mapping(address => UserBasic) public userBasic;

Expand All @@ -48,9 +66,13 @@ contract CometWrapper is ERC4626, IERC7246, CometHelpers {

/** Custom errors **/

error BadSignatory();
error EIP1271VerificationFailed();
error InsufficientAllowance();
error InsufficientAvailableBalance();
error InsufficientEncumbrance();
error InvalidSignatureS();
error SignatureExpired();
error TimestampTooLarge();
error UninitializedReward();
error ZeroShares();
Expand Down Expand Up @@ -590,4 +612,122 @@ contract CometWrapper is ERC4626, IERC7246, CometHelpers {
releaseEncumbranceInternal(owner, spender, amount);
}
}

/**
* @notice Returns the domain separator used in the encoding of the signature for permit
* @return bytes32 The domain separator
*/
function DOMAIN_SEPARATOR() public view override returns (bytes32) {
return keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), keccak256(bytes(VERSION)), block.chainid, address(this)));
}

/**
* @notice Sets approval amount for a spender via signature from signatory
* @param owner The address that signed the signature
* @param spender The address to authorize (or rescind authorization from)
* @param amount Amount that `owner` is approving for `spender`
* @param expiry Expiration time for the signature
* @param v The recovery byte of the signature
* @param r Half of the ECDSA signature pair
* @param s Half of the ECDSA signature pair
*/
function permit(
address owner,
address spender,
uint256 amount,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) public override {
if (block.timestamp >= expiry) revert SignatureExpired();

uint256 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));
if (isValidSignature(owner, digest, v, r, s)) {
nonces[owner]++;
allowance[owner][spender] = amount;
emit Approval(owner, spender, amount);
} else {
revert BadSignatory();
}
}

/**
* @notice Sets an encumbrance from owner to taker via signature from signatory
* @param owner The address that signed the signature
* @param taker The address to create an encumbrance to
* @param amount Amount that owner is encumbering to taker
* @param expiry Expiration time for the signature
* @param v The recovery byte of the signature
* @param r Half of the ECDSA signature pair
* @param s Half of the ECDSA signature pair
*/
function encumberBySig(
address owner,
address taker,
uint256 amount,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external {
if (block.timestamp >= expiry) revert SignatureExpired();

uint256 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));
if (isValidSignature(owner, digest, v, r, s)) {
nonces[owner]++;
encumberInternal(owner, taker, amount);
} else {
revert BadSignatory();
}
}

/**
* @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)
);
if (success == false) revert EIP1271VerificationFailed();
bytes4 returnValue = abi.decode(data, (bytes4));
return returnValue == EIP1271_MAGIC_VALUE;
} else {
(address recoveredSigner, ECDSA.RecoverError recoverError) = ECDSA.tryRecover(digest, v, r, s);
if (recoverError == ECDSA.RecoverError.InvalidSignatureS) revert InvalidSignatureS();
if (recoverError == ECDSA.RecoverError.InvalidSignature) revert BadSignatory();
if (recoveredSigner != signer) revert BadSignatory();
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;
}
}
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: MIT
pragma solidity 0.8.21;

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;
}
}
3 changes: 2 additions & 1 deletion test/BaseUSDbCTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ pragma solidity 0.8.21;

import { Test } from "forge-std/Test.sol";
import { CometWrapper, CometInterface, ICometRewards, CometHelpers, ERC20 } from "../src/CometWrapper.sol";
import { BySigTest } from "./BySig.t.sol";
import { CometWrapperTest } from "./CometWrapper.t.sol";
import { CometWrapperInvariantTest } from "./CometWrapperInvariant.t.sol";
import { EncumberTest } from "./Encumber.t.sol";
import { RewardsTest } from "./Rewards.t.sol";

contract BaseUSDbCTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest {
contract BaseUSDbCTest is CometWrapperTest, CometWrapperInvariantTest, EncumberTest, RewardsTest, BySigTest {
string public override NETWORK = "base";
uint256 public override FORK_BLOCK_NUMBER = 4791144;

Expand Down
Loading

0 comments on commit ac5a639

Please sign in to comment.