diff --git a/contracts/domain/BosonConstants.sol b/contracts/domain/BosonConstants.sol index b918f7e56..ff86ac4d2 100644 --- a/contracts/domain/BosonConstants.sol +++ b/contracts/domain/BosonConstants.sol @@ -212,15 +212,20 @@ bytes32 constant META_TRANSACTION_TYPEHASH = keccak256( "MetaTransaction(uint256 nonce,address from,address contractAddress,string functionName,bytes functionSignature)" ) ); -bytes32 constant OFFER_DETAILS_TYPEHASH = keccak256("MetaTxOfferDetails(address buyer,uint256 offerId)"); +bytes32 constant OFFER_PARAMETERS_TYPEHASH = keccak256( + "MetaTxOfferParameters(uint256 offerId,address exchangeToken,uint256 price,uint256 sellerDeposit,uint256 buyerCancelPenalty,string voucherRedeemableFrom,string disputePeriod,string resolutionPeriod)" +); +bytes32 constant OFFER_DETAILS_TYPEHASH = keccak256( + "MetaTxOfferDetails(address buyer,MetaTxOfferParameters offerParameters)MetaTxOfferParameters(uint256 offerId,address exchangeToken,uint256 price,uint256 sellerDeposit,uint256 buyerCancelPenalty,string voucherRedeemableFrom,string disputePeriod,string resolutionPeriod)" +); bytes32 constant META_TX_COMMIT_TO_OFFER_TYPEHASH = keccak256( - "MetaTxCommitToOffer(uint256 nonce,address from,address contractAddress,string functionName,MetaTxOfferDetails offerDetails)MetaTxOfferDetails(address buyer,uint256 offerId)" + "MetaTxCommitToOffer(uint256 nonce,address from,address contractAddress,string functionName,MetaTxOfferDetails offerDetails)MetaTxOfferDetails(address buyer,MetaTxOfferParameters offerParameters)MetaTxOfferParameters(uint256 offerId,address exchangeToken,uint256 price,uint256 sellerDeposit,uint256 buyerCancelPenalty,string voucherRedeemableFrom,string disputePeriod,string resolutionPeriod)" ); bytes32 constant CONDITIONAL_OFFER_DETAILS_TYPEHASH = keccak256( - "MetaTxConditionalOfferDetails(address buyer,uint256 offerId,uint256 tokenId)" + "MetaTxConditionalOfferDetails(address buyer,MetaTxOfferParameters offerParameters,uint256 tokenId)MetaTxOfferParameters(uint256 offerId,address exchangeToken,uint256 price,uint256 sellerDeposit,uint256 buyerCancelPenalty,string voucherRedeemableFrom,string disputePeriod,string resolutionPeriod)" ); bytes32 constant META_TX_COMMIT_TO_CONDITIONAL_OFFER_TYPEHASH = keccak256( - "MetaTxCommitToConditionalOffer(uint256 nonce,address from,address contractAddress,string functionName,MetaTxConditionalOfferDetails offerDetails)MetaTxConditionalOfferDetails(address buyer,uint256 offerId,uint256 tokenId)" + "MetaTxCommitToConditionalOffer(uint256 nonce,address from,address contractAddress,string functionName,MetaTxConditionalOfferDetails offerDetails)MetaTxConditionalOfferDetails(address buyer,MetaTxOfferParameters offerParameters,uint256 tokenId)MetaTxOfferParameters(uint256 offerId,address exchangeToken,uint256 price,uint256 sellerDeposit,uint256 buyerCancelPenalty,string voucherRedeemableFrom,string disputePeriod,string resolutionPeriod)" ); bytes32 constant EXCHANGE_DETAILS_TYPEHASH = keccak256("MetaTxExchangeDetails(uint256 exchangeId)"); bytes32 constant META_TX_EXCHANGE_TYPEHASH = keccak256( diff --git a/contracts/domain/BosonTypes.sol b/contracts/domain/BosonTypes.sol index c0e222f2d..0b24588cf 100644 --- a/contracts/domain/BosonTypes.sol +++ b/contracts/domain/BosonTypes.sol @@ -282,7 +282,7 @@ contract BosonTypes { struct HashInfo { bytes32 typeHash; - function(bytes memory) internal pure returns (bytes32) hashFunction; + function(bytes memory) internal view returns (bytes32) hashFunction; } struct OfferFees { diff --git a/contracts/protocol/facets/MetaTransactionsHandlerFacet.sol b/contracts/protocol/facets/MetaTransactionsHandlerFacet.sol index 72de932b0..25b521f00 100644 --- a/contracts/protocol/facets/MetaTransactionsHandlerFacet.sol +++ b/contracts/protocol/facets/MetaTransactionsHandlerFacet.sol @@ -10,6 +10,8 @@ import { DiamondLib } from "../../diamond/DiamondLib.sol"; import { ProtocolLib } from "../libs/ProtocolLib.sol"; import { ProtocolBase } from "../bases/ProtocolBase.sol"; import { EIP712Lib } from "../libs/EIP712Lib.sol"; +import { DateTime } from "@quant-finance/solidity-datetime/contracts/DateTime.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; /** * @title MetaTransactionsHandlerFacet @@ -111,9 +113,11 @@ contract MetaTransactionsHandlerFacet is IBosonMetaTransactionsHandler, Protocol * @param _offerDetails - the offer details * @return the hashed representation of the offer details struct */ - function hashOfferDetails(bytes memory _offerDetails) internal pure returns (bytes32) { + function hashOfferDetails(bytes memory _offerDetails) internal view returns (bytes32) { + // The buyer and offerId are part of calldata (address buyer, uint256 offerId) = abi.decode(_offerDetails, (address, uint256)); - return keccak256(abi.encode(OFFER_DETAILS_TYPEHASH, buyer, offerId)); + + return keccak256(abi.encode(OFFER_DETAILS_TYPEHASH, buyer, hashOfferParameters(offerId))); } /** @@ -122,9 +126,68 @@ contract MetaTransactionsHandlerFacet is IBosonMetaTransactionsHandler, Protocol * @param _offerDetails - the conditional offer details * @return the hashed representation of the conditional offer details struct */ - function hashConditionalOfferDetails(bytes memory _offerDetails) internal pure returns (bytes32) { + function hashConditionalOfferDetails(bytes memory _offerDetails) internal view returns (bytes32) { (address buyer, uint256 offerId, uint256 tokenId) = abi.decode(_offerDetails, (address, uint256, uint256)); - return keccak256(abi.encode(CONDITIONAL_OFFER_DETAILS_TYPEHASH, buyer, offerId, tokenId)); + return keccak256(abi.encode(CONDITIONAL_OFFER_DETAILS_TYPEHASH, buyer, hashOfferParameters(offerId), tokenId)); + } + + /** + * @notice Returns hashed representation of the offer parameters struct. + * This is reused for both the offer and conditional offer. + * + * @param _offerId - the offer id + * @return the hashed representation of the offer details struct + */ + function hashOfferParameters(uint256 _offerId) internal view returns (bytes32) { + // Get other offer details from the protocol + Offer storage offer = getValidOffer(_offerId); + bytes memory redeemableFrom; + { + OfferDates storage offerDates = fetchOfferDates(_offerId); + + (uint256 year, uint256 month, uint256 day, uint256 hour, uint256 minute, uint256 second) = DateTime + .timestampToDateTime(offerDates.voucherRedeemableFrom); + redeemableFrom = abi.encodePacked( + Strings.toString(year), + "/", + Strings.toString(month), + "/", + Strings.toString(day), + " ", + Strings.toString(hour), + ":", + Strings.toString(minute), + ":", + Strings.toString(second) + ); + } + + OfferDurations storage offerDurations = fetchOfferDurations(_offerId); + + return + keccak256( + abi.encode( + OFFER_PARAMETERS_TYPEHASH, + _offerId, + offer.exchangeToken, + offer.price, + offer.sellerDeposit, + offer.buyerCancelPenalty, + keccak256(redeemableFrom), + keccak256( + abi.encodePacked( + Strings.toString(offerDurations.disputePeriod / DateTime.SECONDS_PER_DAY), + " days" + ) + ), + keccak256( + abi.encodePacked( + Strings.toString(offerDurations.resolutionPeriod / DateTime.SECONDS_PER_DAY), + " days" + ) + ) + ) + ); } /** diff --git a/package-lock.json b/package-lock.json index e0ac94776..1c2c9b538 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "GPL-3.0-or-later", "dependencies": { "@openzeppelin/contracts": "^4.9.0", - "@openzeppelin/contracts-upgradeable": "4.9.3" + "@openzeppelin/contracts-upgradeable": "4.9.3", + "@quant-finance/solidity-datetime": "^2.2.0" }, "devDependencies": { "@bosonprotocol/solidoc": "3.0.3", @@ -2430,6 +2431,11 @@ "dev": true, "optional": true }, + "node_modules/@quant-finance/solidity-datetime": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@quant-finance/solidity-datetime/-/solidity-datetime-2.2.0.tgz", + "integrity": "sha512-iO0EnqPKTzGCgQOkI9lerpJc0XKUhMNurSjHcA7p7nlP2K2z3U4kk9OC9eQkZUrdBtltft+kIibiDdIOYWuQMg==" + }, "node_modules/@redux-saga/core": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.3.tgz", @@ -21348,6 +21354,11 @@ "dev": true, "optional": true }, + "@quant-finance/solidity-datetime": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@quant-finance/solidity-datetime/-/solidity-datetime-2.2.0.tgz", + "integrity": "sha512-iO0EnqPKTzGCgQOkI9lerpJc0XKUhMNurSjHcA7p7nlP2K2z3U4kk9OC9eQkZUrdBtltft+kIibiDdIOYWuQMg==" + }, "@redux-saga/core": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.3.tgz", diff --git a/package.json b/package.json index 597b510c0..243048c69 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,8 @@ }, "dependencies": { "@openzeppelin/contracts": "^4.9.0", - "@openzeppelin/contracts-upgradeable": "4.9.3" + "@openzeppelin/contracts-upgradeable": "4.9.3", + "@quant-finance/solidity-datetime": "^2.2.0" }, "devDependencies": { "@bosonprotocol/solidoc": "3.0.3", diff --git a/test/protocol/MetaTransactionsHandlerTest.js b/test/protocol/MetaTransactionsHandlerTest.js index db5723d27..6aa782945 100644 --- a/test/protocol/MetaTransactionsHandlerTest.js +++ b/test/protocol/MetaTransactionsHandlerTest.js @@ -30,6 +30,7 @@ const { setupTestEnvironment, getSnapshot, revertToSnapshot, + timestampToDateTime, } = require("../util/utils.js"); const { mockOffer, @@ -42,7 +43,7 @@ const { mockExchange, mockCondition, } = require("../util/mock"); -const { oneMonth } = require("../util/constants"); +const { oneMonth, SECONDS_PER_DAY } = require("../util/constants"); const { getSelectors, FacetCutAction, @@ -74,7 +75,8 @@ describe("IBosonMetaTransactionsHandler", function () { let metaTransactionsHandler, nonce, functionSignature; let seller, offerId, buyerId; let validOfferDetails, - offerType, + offerDetailsType, + offerParametersType, metaTransactionType, metaTxExchangeType, customTransactionType, @@ -1645,9 +1647,20 @@ describe("IBosonMetaTransactionsHandler", function () { .createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId); // Set the offer Type - offerType = [ - { name: "buyer", type: "address" }, + offerParametersType = [ { name: "offerId", type: "uint256" }, + { name: "exchangeToken", type: "address" }, + { name: "price", type: "uint256" }, + { name: "sellerDeposit", type: "uint256" }, + { name: "buyerCancelPenalty", type: "uint256" }, + { name: "voucherRedeemableFrom", type: "string" }, + { name: "disputePeriod", type: "string" }, + { name: "resolutionPeriod", type: "string" }, + ]; + + offerDetailsType = [ + { name: "buyer", type: "address" }, + { name: "offerParameters", type: "MetaTxOfferParameters" }, ]; // Set the message Type @@ -1662,7 +1675,8 @@ describe("IBosonMetaTransactionsHandler", function () { customTransactionType = { MetaTxCommitToOffer: metaTransactionType, - MetaTxOfferDetails: offerType, + MetaTxOfferDetails: offerDetailsType, + MetaTxOfferParameters: offerParametersType, }; // prepare validOfferDetails @@ -1671,8 +1685,27 @@ describe("IBosonMetaTransactionsHandler", function () { offerId: offer.id, }; + let [year, month, day, hour, minute, second] = timestampToDateTime( + Number(offerDates.voucherRedeemableFrom) + ); + let voucherRedeemableFrom = `${year}/${month}/${day} ${hour}:${minute}:${second}`; + + const extendedOfferDetails = { + buyer: validOfferDetails.buyer, + offerParameters: { + offerId: validOfferDetails.offerId, + exchangeToken: offer.exchangeToken, + price: offer.price, + sellerDeposit: offer.sellerDeposit, + buyerCancelPenalty: offer.buyerCancelPenalty, + voucherRedeemableFrom: voucherRedeemableFrom, + disputePeriod: `${BigInt(offerDurations.disputePeriod) / SECONDS_PER_DAY} days`, + resolutionPeriod: `${BigInt(offerDurations.resolutionPeriod) / SECONDS_PER_DAY} days`, + }, + }; + // Prepare the message - message.offerDetails = validOfferDetails; + message.offerDetails = extendedOfferDetails; // Deposit native currency to the same seller id await fundsHandler @@ -1733,7 +1766,7 @@ describe("IBosonMetaTransactionsHandler", function () { validOfferDetails.offerId = offerId; // Prepare the message - message.offerDetails = validOfferDetails; + message.offerDetails.offerParameters.offerId = offerId; // Collect the signature components let { r, s, v } = await prepareDataSignatureParameters( @@ -1870,9 +1903,20 @@ describe("IBosonMetaTransactionsHandler", function () { .createOfferWithCondition(offer, offerDates, offerDurations, disputeResolver.id, condition, agentId); // Set the offer Type - offerType = [ - { name: "buyer", type: "address" }, + offerParametersType = [ { name: "offerId", type: "uint256" }, + { name: "exchangeToken", type: "address" }, + { name: "price", type: "uint256" }, + { name: "sellerDeposit", type: "uint256" }, + { name: "buyerCancelPenalty", type: "uint256" }, + { name: "voucherRedeemableFrom", type: "string" }, + { name: "disputePeriod", type: "string" }, + { name: "resolutionPeriod", type: "string" }, + ]; + + offerDetailsType = [ + { name: "buyer", type: "address" }, + { name: "offerParameters", type: "MetaTxOfferParameters" }, { name: "tokenId", type: "uint256" }, ]; @@ -1888,7 +1932,8 @@ describe("IBosonMetaTransactionsHandler", function () { customTransactionType = { MetaTxCommitToConditionalOffer: metaTransactionType, - MetaTxConditionalOfferDetails: offerType, + MetaTxConditionalOfferDetails: offerDetailsType, + MetaTxOfferParameters: offerParametersType, }; // prepare validOfferDetails @@ -1898,8 +1943,28 @@ describe("IBosonMetaTransactionsHandler", function () { tokenId: "0", }; + let [year, month, day, hour, minute, second] = timestampToDateTime( + Number(offerDates.voucherRedeemableFrom) + ); + let voucherRedeemableFrom = `${year}/${month}/${day} ${hour}:${minute}:${second}`; + + const extendedOfferDetails = { + buyer: validOfferDetails.buyer, + offerParameters: { + offerId: validOfferDetails.offerId, + exchangeToken: offer.exchangeToken, + price: offer.price, + sellerDeposit: offer.sellerDeposit, + buyerCancelPenalty: offer.buyerCancelPenalty, + voucherRedeemableFrom: voucherRedeemableFrom, + disputePeriod: `${BigInt(offerDurations.disputePeriod) / SECONDS_PER_DAY} days`, + resolutionPeriod: `${BigInt(offerDurations.resolutionPeriod) / SECONDS_PER_DAY} days`, + }, + tokenId: validOfferDetails.tokenId, + }; + // Prepare the message - message.offerDetails = validOfferDetails; + message.offerDetails = extendedOfferDetails; // Deposit native currency to the same seller id await fundsHandler @@ -1960,7 +2025,7 @@ describe("IBosonMetaTransactionsHandler", function () { validOfferDetails.offerId = offerId; // Prepare the message - message.offerDetails = validOfferDetails; + message.offerDetails.offerParameters.offerId = offerId; // Collect the signature components let { r, s, v } = await prepareDataSignatureParameters( @@ -1999,7 +2064,7 @@ describe("IBosonMetaTransactionsHandler", function () { validOfferDetails.tokenId = tokenId; // Prepare the message - message.offerDetails = validOfferDetails; + message.offerDetails.tokenId = tokenId; // Collect the signature components let { r, s, v } = await prepareDataSignatureParameters( diff --git a/test/util/constants.js b/test/util/constants.js index 738eb72a1..d7c442e92 100644 --- a/test/util/constants.js +++ b/test/util/constants.js @@ -3,6 +3,9 @@ const oneDay = 86400n; // 1 day in seconds const ninetyDays = oneDay * 90n; // 90 days in seconds const oneWeek = oneDay * 7n; // 7 days in seconds const oneMonth = oneDay * 31n; // 31 days in seconds +const SECONDS_PER_DAY = 24n * 60n * 60n; +const SECONDS_PER_HOUR = 60n * 60n; +const SECONDS_PER_MINUTE = 60n; const VOUCHER_NAME = "Boson Voucher (rNFT)"; const VOUCHER_SYMBOL = "BOSON_VOUCHER_RNFT"; const SEAPORT_ADDRESS = "0x00000000000001ad428e4906aE43D8F9852d0dD6"; // 1.4 @@ -18,3 +21,6 @@ exports.VOUCHER_NAME = VOUCHER_NAME; exports.VOUCHER_SYMBOL = VOUCHER_SYMBOL; exports.maxPriorityFeePerGas = maxPriorityFeePerGas; exports.SEAPORT_ADDRESS = SEAPORT_ADDRESS; +exports.SECONDS_PER_DAY = SECONDS_PER_DAY; +exports.SECONDS_PER_HOUR = SECONDS_PER_HOUR; +exports.SECONDS_PER_MINUTE = SECONDS_PER_MINUTE; diff --git a/test/util/utils.js b/test/util/utils.js index 3c343c7fd..19736944a 100644 --- a/test/util/utils.js +++ b/test/util/utils.js @@ -16,7 +16,14 @@ const { ZeroAddress, } = ethers; const { getFacets } = require("../../scripts/config/facet-deploy.js"); -const { oneWeek, oneMonth, maxPriorityFeePerGas } = require("./constants"); +const { + oneWeek, + oneMonth, + maxPriorityFeePerGas, + SECONDS_PER_DAY, + SECONDS_PER_HOUR, + SECONDS_PER_MINUTE, +} = require("./constants"); const Role = require("../../scripts/domain/Role"); const { toHexString } = require("../../scripts/util/utils.js"); const { expect } = require("chai"); @@ -484,6 +491,40 @@ function deriveTokenId(offerId, exchangeId) { return (BigInt(offerId) << 128n) + BigInt(exchangeId); } +function timestampToDateTime(timestamp) { + timestamp = BigInt(timestamp); + let [year, month, day] = _daysToDate(timestamp / SECONDS_PER_DAY); + let secs = timestamp % SECONDS_PER_DAY; + let hour = secs / SECONDS_PER_HOUR; + secs = secs % SECONDS_PER_HOUR; + let minute = secs / SECONDS_PER_MINUTE; + let second = secs % SECONDS_PER_MINUTE; + + return [year, month, day, hour, minute, second]; +} + +function _daysToDate(_days) { + const OFFSET19700101 = 2440588n; + + let __days = _days; + + let L = __days + 68569n + OFFSET19700101; + let N = (4n * L) / 146097n; + L = L - (146097n * N + 3n) / 4n; + let _year = (4000n * (L + 1n)) / 1461001n; + L = L - (1461n * _year) / 4n + 31n; + let _month = (80n * L) / 2447n; + let _day = L - (2447n * _month) / 80n; + L = _month / 11n; + _month = _month + 2n - 12n * L; + _year = 100n * (N - 49n) + _year + L; + + let year = _year; + let month = _month; + let day = _day; + return [year, month, day]; +} + exports.setNextBlockTimestamp = setNextBlockTimestamp; exports.getEvent = getEvent; exports.eventEmittedWithArgs = eventEmittedWithArgs; @@ -503,3 +544,4 @@ exports.getSnapshot = getSnapshot; exports.revertToSnapshot = revertToSnapshot; exports.deriveTokenId = deriveTokenId; exports.getSellerSalt = getSellerSalt; +exports.timestampToDateTime = timestampToDateTime;