Blend contract
Signatures.sol
// SPDX-License-Identifier: BSL 1.1 - Blend (c) Non Fungible Trading Ltd.
pragma solidity 0.8.17;
 
import "./Structs.sol";
import "./Errors.sol";
import "../interfaces/ISignatures.sol";
 
abstract contract Signatures is ISignatures {
    bytes32 private immutable _LOAN_OFFER_TYPEHASH;
    bytes32 private immutable _FEE_TYPEHASH;
    bytes32 private immutable _SELL_OFFER_TYPEHASH;
    bytes32 private immutable _ORACLE_OFFER_TYPEHASH;
    bytes32 private immutable _EIP_712_DOMAIN_TYPEHASH;
 
    string private constant _NAME = "Blend";
    string private constant _VERSION = "1.0";
 
    mapping(address => uint256) public nonces;
    mapping(address => uint256) public oracles;
    uint256 public blockRange;
 
    uint256[50] private _gap;
 
    constructor() {
        (
            _LOAN_OFFER_TYPEHASH,
            _SELL_OFFER_TYPEHASH,
            _FEE_TYPEHASH,
            _ORACLE_OFFER_TYPEHASH,
            _EIP_712_DOMAIN_TYPEHASH
        ) = _createTypehashes();
    }
 
    function information() external view returns (string memory version, bytes32 domainSeparator) {
        version = _VERSION;
        domainSeparator = _hashDomain(
            _EIP_712_DOMAIN_TYPEHASH,
            keccak256(bytes(_NAME)),
            keccak256(bytes(_VERSION))
        );
    }
 
    function getSellOfferHash(SellOffer calldata offer) external view returns (bytes32) {
        return _hashSellOffer(offer);
    }
 
    function getOfferHash(LoanOffer calldata offer) external view returns (bytes32) {
        return _hashOffer(offer);
    }
 
    function getOracleOfferHash(bytes32 hash, uint256 blockNumber) external view returns (bytes32) {
        return _hashOracleOffer(hash, blockNumber);
    }
 
    /**
     * @notice Generate all EIP712 Typehashes
     */
    function _createTypehashes()
        internal
        view
        returns (
            bytes32 loanOfferTypehash,
            bytes32 sellOfferTypehash,
            bytes32 feeTypehash,
            bytes32 oracleOfferTypehash,
            bytes32 eip712DomainTypehash
        )
    {
        eip712DomainTypehash = keccak256(
            bytes.concat(
                "EIP712Domain(",
                "string name,",
                "string version,",
                "uint256 chainId,",
                "address verifyingContract",
                ")"
            )
        );
 
        oracleOfferTypehash = keccak256(
            bytes.concat("OracleOffer(", "bytes32 hash,", "uint256 blockNumber", ")")
        );
 
        loanOfferTypehash = keccak256(
            bytes.concat(
                "LoanOffer(",
                "address lender,",
                "address collection,",
                "uint256 totalAmount,",
                "uint256 minAmount,",
                "uint256 maxAmount,",
                "uint256 auctionDuration,",
                "uint256 salt,",
                "uint256 expirationTime,",
                "uint256 rate,",
                "address oracle,",
                "uint256 nonce",
                ")"
            )
        );
 
        bytes memory feeTypestring = bytes.concat("Fee(", "uint16 rate,", "address recipient", ")");
 
        feeTypehash = keccak256(feeTypestring);
        sellOfferTypehash = keccak256(
            bytes.concat(
                "SellOffer(",
                "address borrower,",
                "uint256 lienId,",
                "uint256 price,",
                "uint256 expirationTime,",
                "uint256 salt,",
                "address oracle,",
                "Fee[] fees,",
                "uint256 nonce",
                ")",
                feeTypestring
            )
        );
    }
 
    function _hashDomain(
        bytes32 eip712DomainTypehash,
        bytes32 nameHash,
        bytes32 versionHash
    ) internal view returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    eip712DomainTypehash,
                    nameHash,
                    versionHash,
                    block.chainid,
                    address(this)
                )
            );
    }
 
    function _hashFee(Fee calldata fee) internal view returns (bytes32) {
        return keccak256(abi.encode(_FEE_TYPEHASH, fee.rate, fee.recipient));
    }
 
    function _packFees(Fee[] calldata fees) internal view returns (bytes32) {
        bytes32[] memory feeHashes = new bytes32[](fees.length);
        uint256 feesLength = fees.length;
        for (uint256 i; i < feesLength; ) {
            feeHashes[i] = _hashFee(fees[i]);
            unchecked {
                ++i;
            }
        }
        return keccak256(abi.encodePacked(feeHashes));
    }
 
    function _hashSellOffer(SellOffer calldata offer) internal view returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    _SELL_OFFER_TYPEHASH,
                    offer.borrower,
                    offer.lienId,
                    offer.price,
                    offer.expirationTime,
                    offer.salt,
                    offer.oracle,
                    _packFees(offer.fees),
                    nonces[offer.borrower]
                )
            );
    }
 
    function _hashOffer(LoanOffer calldata offer) internal view returns (bytes32) {
        return
            keccak256(
                abi.encode(
                    _LOAN_OFFER_TYPEHASH,
                    offer.lender,
                    offer.collection,
                    offer.totalAmount,
                    offer.minAmount,
                    offer.maxAmount,
                    offer.auctionDuration,
                    offer.salt,
                    offer.expirationTime,
                    offer.rate,
                    offer.oracle,
                    nonces[offer.lender]
                )
            );
    }
 
    function _hashOracleOffer(bytes32 hash, uint256 blockNumber) internal view returns (bytes32) {
        return keccak256(abi.encode(_ORACLE_OFFER_TYPEHASH, hash, blockNumber));
    }
 
    function _hashToSign(bytes32 hash) internal view returns (bytes32) {
        return keccak256(
            bytes.concat(
                bytes2(0x1901),
                _hashDomain(
                    _EIP_712_DOMAIN_TYPEHASH,
                    keccak256(bytes(_NAME)),
                    keccak256(bytes(_VERSION))
                ),
                hash
            )
        );
    }
 
    function _hashToSignOracle(bytes32 hash, uint256 blockNumber) internal view returns (bytes32) {
        return
            keccak256(
                bytes.concat(
                    bytes2(0x1901),
                    _hashDomain(
                        _EIP_712_DOMAIN_TYPEHASH,
                        keccak256(bytes(_NAME)),
                        keccak256(bytes(_VERSION))
                    ),
                    _hashOracleOffer(hash, blockNumber)
                )
            );
    }
 
    /**
     * @notice Verify authorization of offer
     * @param offerHash Hash of offer struct
     * @param lender Lender address
     * @param oracle Oracle address
     * @param signature Packed offer signature (with oracle signature if necessary)
     */
    function _verifyOfferAuthorization(
        bytes32 offerHash,
        address lender,
        address oracle,
        bytes calldata signature
    ) internal view {
        bytes32 hashToSign = _hashToSign(offerHash);
        bytes32 r;
        bytes32 s;
        uint8 v;
        assembly {
            r := calldataload(signature.offset)
            s := calldataload(add(signature.offset, 0x20))
            v := shr(248, calldataload(add(signature.offset, 0x40)))
        }
        _verify(lender, hashToSign, v, r, s);
 
        /* Verify oracle signature if required. */
        if (oracle != address(0)) {
            uint256 blockNumber;
            assembly {
                r := calldataload(add(signature.offset, 0x41))
                s := calldataload(add(signature.offset, 0x61))
                v := shr(248, calldataload(add(signature.offset, 0x81)))
                blockNumber := calldataload(add(signature.offset, 0x82))
            }
            if (oracles[oracle] == 0) {
                revert UnauthorizedOracle();
            }
            if (blockNumber + blockRange < block.number) {
                revert SignatureExpired();
            }
 
            hashToSign = _hashToSignOracle(offerHash, blockNumber);
            _verify(oracle, hashToSign, v, r, s);
        }
    }
 
    /**
     * @notice Verify signature of digest
     * @param signer Address of expected signer
     * @param digest Signature digest
     * @param v v parameter
     * @param r r parameter
     * @param s s parameter
     */
    function _verify(address signer, bytes32 digest, uint8 v, bytes32 r, bytes32 s) internal pure {
        if (v != 27 && v != 28) {
            revert InvalidVParameter();
        }
        address recoveredSigner = ecrecover(digest, v, r, s);
        if (recoveredSigner == address(0) || signer != recoveredSigner) {
            revert InvalidSignature();
        }
    }
}