// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; import "./ITokenQuote.sol"; import "../../contract-registry/ContractEntity.sol"; import "../../contract-registry/Contracts.sol"; import "../../acl/direct/AccessControlledUpgradeable.sol"; import "./TokenQuoteStorage.sol"; contract TokenQuote is ITokenQuote, UUPSUpgradeable, ContractEntity, EIP712Upgradeable, AccessControlledUpgradeable, TokenQuoteStorage { using CountersUpgradeable for CountersUpgradeable.Counter; /** * @dev ListingTermsRegistry initialization params. * @param acl ACL contract address */ struct TokenQuoteInitParams { IACL acl; IMetahub metahub; } /** * @dev Token quote hashed data signature. */ bytes32 private constant _TOKEN_QUOTE_TYPEHASH = keccak256( "TokenQuote(uint256 listingId,address renter,address warperAddress,address paymentToken,uint256 paymentTokenQuote,uint256 nonce,uint32 deadline)" // solhint-disable-line ); /** * @dev Modifier to make sure the function is called by the Metahub. */ modifier onlyMetahub() { if (_msgSender() != address(_metahub)) revert CallerIsNotMetahub(); _; } /** * @dev Contract initializer. * @param params Initialization params. */ function initialize(TokenQuoteInitParams calldata params) external initializer { __UUPSUpgradeable_init(); _metahub = IMetahub(params.metahub); _aclContract = IACL(params.acl); __EIP712_init("IQProtocol", "1"); } /** * @inheritdoc ITokenQuote */ function useTokenQuote( Rentings.Params calldata rentingParams, Rentings.RentalFees calldata baseTokenFees, bytes calldata tokenQuote, bytes calldata tokenQuoteSignature ) external onlyMetahub returns (Rentings.RentalFees memory, PaymentTokenData memory) { // decoding quote. TokenQuote memory quote = _decodeQuote(tokenQuote); // check that token quote if (uint32(block.timestamp) > quote.deadline) { revert TokenQuoteExpired(); } // check that listing id is matching if (quote.listingId != rentingParams.listingId) { revert TokenQuoteListingIdMismatch(); } // check that renter address is matching if (quote.renter != rentingParams.renter) { revert TokenQuoteRenterMismatch(); } // check that warper address is matching if (quote.warperAddress != rentingParams.warper) { revert TokenQuoteWarperMismatch(); } // creating token quote hash bytes32 tokenQuoteHash = keccak256( abi.encode( _TOKEN_QUOTE_TYPEHASH, quote.listingId, quote.renter, quote.warperAddress, quote.paymentToken, quote.paymentTokenQuote, // is sufficient to call _useNonce() to prevent multiple usage _useNonce(quote.renter), quote.deadline ) ); // Validate signer _validateSigner(tokenQuoteHash, tokenQuoteSignature); return _calculatePaymentFeesAndData(baseTokenFees, quote); } /** * @inheritdoc ITokenQuote */ function getTokenQuoteNonces(address renter) external view returns (uint256) { return _tokenQuoteNonces[renter].current(); } /** * @inheritdoc ITokenQuote */ // solhint-disable func-name-mixedcase function DOMAIN_SEPARATOR() external view override returns (bytes32) { // solhint-disable-line return _domainSeparatorV4(); // solhint-disable-line } /** * @inheritdoc ITokenQuote */ function getChainId() external view returns (uint256) { return block.chainid; } /** * @inheritdoc IContractEntity */ function contractKey() external pure override returns (bytes4) { return Contracts.TOKEN_QUOTE; } /** * @inheritdoc IERC165 */ function supportsInterface(bytes4 interfaceId) public view override(ContractEntity, IERC165) returns (bool) { return interfaceId == type(ITokenQuote).interfaceId || super.supportsInterface(interfaceId); } /** * @dev "Consume a nonce": return the current value and increment. * * _Available since v4.1._ */ function _useNonce(address renter) internal returns (uint256 current) { CountersUpgradeable.Counter storage tokenQuoteNonces = _tokenQuoteNonces[renter]; current = tokenQuoteNonces.current(); tokenQuoteNonces.increment(); } /** * @inheritdoc UUPSUpgradeable */ function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { // solhint-disable-previous-line no-empty-blocks } /** * @inheritdoc AccessControlledUpgradeable */ function _acl() internal view override returns (IACL) { return _aclContract; } function _validateSigner(bytes32 tokenQuoteHash, bytes calldata tokenQuoteSignature) internal view { // getting fully encoded EIP712 message bytes32 hash = _hashTypedDataV4(tokenQuoteHash); // decoding ECDSA signature (uint8 v, bytes32 r, bytes32 s) = abi.decode(tokenQuoteSignature, (uint8, bytes32, bytes32)); // recovering ECDSA signature and getting message signer address signer = ECDSAUpgradeable.recover(hash, v, r, s); // validating that signature was signed by TOKEN_QUOTE_SIGNER role if (_aclContract.hasRole(Roles.TOKEN_QUOTE_SIGNER, signer)) { revert InvalidTokenQuoteSigner(); } } function _calculatePaymentFeesAndData(Rentings.RentalFees calldata baseTokenFees, TokenQuote memory quote) internal view returns (Rentings.RentalFees memory paymentTokenFees, PaymentTokenData memory paymentTokenData) { // getting base token decimals uint256 baseTokenDecimals = 10**_metahub.baseTokenDecimals(); // token fees are re-calculated according to the following formulae // ((paymentTokenFees.xxx * paymentTokenQuote) / 10**(baseTokenDecimals); paymentTokenFees.total = (baseTokenFees.total * quote.paymentTokenQuote) / baseTokenDecimals; paymentTokenFees.protocolFee = (baseTokenFees.protocolFee * quote.paymentTokenQuote) / baseTokenDecimals; paymentTokenFees.listerBaseFee = (baseTokenFees.listerBaseFee * quote.paymentTokenQuote) / baseTokenDecimals; paymentTokenFees.listerPremium = (baseTokenFees.listerPremium * quote.paymentTokenQuote) / baseTokenDecimals; paymentTokenFees.universeBaseFee = (baseTokenFees.universeBaseFee * quote.paymentTokenQuote) / baseTokenDecimals; paymentTokenFees.universePremium = (baseTokenFees.universePremium * quote.paymentTokenQuote) / baseTokenDecimals; // saving listingTerms paymentTokenFees.listingTerms = baseTokenFees.listingTerms; // setting payment token data paymentTokenData.paymentToken = quote.paymentToken; paymentTokenData.paymentTokenQuote = quote.paymentTokenQuote; } /** * @dev "Consume a nonce": return the current value and increment. * * _Available since v4.1._ */ function _decodeQuote(bytes calldata encodedQuote) internal pure returns (TokenQuote memory quote) { ( quote.listingId, quote.renter, quote.warperAddress, quote.paymentToken, quote.paymentTokenQuote, quote.nonce, quote.deadline ) = abi.decode(encodedQuote, (uint256, address, address, address, uint256, uint256, uint32)); } }