Skip to content

Commit

Permalink
Merge pull request #788 from bosonprotocol/meta-tx-conditional-commit
Browse files Browse the repository at this point in the history
register commitToConditionalOffer method for meta-transactions
  • Loading branch information
levalleux-ludo committed Aug 31, 2023
2 parents 2b9f60b + cee5625 commit 7c144ef
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 4 deletions.
7 changes: 7 additions & 0 deletions contracts/domain/BosonConstants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ bytes32 constant OFFER_DETAILS_TYPEHASH = keccak256("MetaTxOfferDetails(address
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)"
);
bytes32 constant CONDITIONAL_OFFER_DETAILS_TYPEHASH = keccak256(
"MetaTxConditionalOfferDetails(address buyer,uint256 offerId,uint256 tokenId)"
);
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)"
);
bytes32 constant EXCHANGE_DETAILS_TYPEHASH = keccak256("MetaTxExchangeDetails(uint256 exchangeId)");
bytes32 constant META_TX_EXCHANGE_TYPEHASH = keccak256(
"MetaTxExchange(uint256 nonce,address from,address contractAddress,string functionName,MetaTxExchangeDetails exchangeDetails)MetaTxExchangeDetails(uint256 exchangeId)"
Expand All @@ -235,6 +241,7 @@ bytes32 constant META_TX_DISPUTE_RESOLUTIONS_TYPEHASH = keccak256(

// Function names
string constant COMMIT_TO_OFFER = "commitToOffer(address,uint256)";
string constant COMMIT_TO_CONDITIONAL_OFFER = "commitToConditionalOffer(address,uint256,uint256)";
string constant CANCEL_VOUCHER = "cancelVoucher(uint256)";
string constant REDEEM_VOUCHER = "redeemVoucher(uint256)";
string constant COMPLETE_EXCHANGE = "completeExchange(uint256)";
Expand Down
2 changes: 1 addition & 1 deletion contracts/domain/BosonTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ contract BosonTypes {
CommitToOffer,
Exchange,
Funds,
RaiseDispute,
CommitToConditionalOffer,
ResolveDispute
}

Expand Down
16 changes: 16 additions & 0 deletions contracts/protocol/facets/MetaTransactionsHandlerFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ contract MetaTransactionsHandlerFacet is IBosonMetaTransactionsHandler, Protocol

// Set input type for the function name
pmti.inputType[COMMIT_TO_OFFER] = MetaTxInputType.CommitToOffer;
pmti.inputType[COMMIT_TO_CONDITIONAL_OFFER] = MetaTxInputType.CommitToConditionalOffer;
pmti.inputType[WITHDRAW_FUNDS] = MetaTxInputType.Funds;
pmti.inputType[RESOLVE_DISPUTE] = MetaTxInputType.ResolveDispute;
pmti.inputType[CANCEL_VOUCHER] = MetaTxInputType.Exchange;
Expand All @@ -43,6 +44,10 @@ contract MetaTransactionsHandlerFacet is IBosonMetaTransactionsHandler, Protocol
// Set the hash info to the input type
pmti.hashInfo[MetaTxInputType.Generic] = HashInfo(META_TRANSACTION_TYPEHASH, hashGenericDetails);
pmti.hashInfo[MetaTxInputType.CommitToOffer] = HashInfo(META_TX_COMMIT_TO_OFFER_TYPEHASH, hashOfferDetails);
pmti.hashInfo[MetaTxInputType.CommitToConditionalOffer] = HashInfo(
META_TX_COMMIT_TO_CONDITIONAL_OFFER_TYPEHASH,
hashConditionalOfferDetails
);
pmti.hashInfo[MetaTxInputType.Funds] = HashInfo(META_TX_FUNDS_TYPEHASH, hashFundDetails);
pmti.hashInfo[MetaTxInputType.Exchange] = HashInfo(META_TX_EXCHANGE_TYPEHASH, hashExchangeDetails);
pmti.hashInfo[MetaTxInputType.ResolveDispute] = HashInfo(
Expand Down Expand Up @@ -111,6 +116,17 @@ contract MetaTransactionsHandlerFacet is IBosonMetaTransactionsHandler, Protocol
return keccak256(abi.encode(OFFER_DETAILS_TYPEHASH, buyer, offerId));
}

/**
* @notice Returns hashed representation of the conditional offer details struct.
*
* @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) {
(address buyer, uint256 offerId, uint256 tokenId) = abi.decode(_offerDetails, (address, uint256, uint256));
return keccak256(abi.encode(CONDITIONAL_OFFER_DETAILS_TYPEHASH, buyer, offerId, tokenId));
}

/**
* @notice Returns hashed representation of the exchange details struct.
*
Expand Down
274 changes: 272 additions & 2 deletions test/protocol/MetaTransactionsHandlerTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const {
mockAuthToken,
accountId,
mockExchange,
mockCondition,
} = require("../util/mock");
const { oneMonth } = require("../util/constants");
const {
Expand Down Expand Up @@ -68,7 +69,8 @@ describe("IBosonMetaTransactionsHandler", function () {
bosonToken,
support,
result,
mockMetaTransactionsHandler;
mockMetaTransactionsHandler,
orchestrationHandler;
let metaTransactionsHandler, nonce, functionSignature;
let seller, offerId, buyerId;
let validOfferDetails,
Expand All @@ -79,7 +81,7 @@ describe("IBosonMetaTransactionsHandler", function () {
validExchangeDetails,
exchangeType,
message;
let offer, offerDates, offerDurations;
let offer, offerDates, offerDurations, condition;
let sellerDeposit, price;
let voucherRedeemableFrom;
let exchange;
Expand Down Expand Up @@ -140,6 +142,7 @@ describe("IBosonMetaTransactionsHandler", function () {
disputeHandler: "IBosonDisputeHandler",
metaTransactionsHandler: "IBosonMetaTransactionsHandler",
pauseHandler: "IBosonPauseHandler",
orchestrationHandler: "IBosonOrchestrationHandler",
};

({
Expand All @@ -154,6 +157,7 @@ describe("IBosonMetaTransactionsHandler", function () {
disputeHandler,
metaTransactionsHandler,
pauseHandler,
orchestrationHandler,
},
extraReturnValues: { accessController },
diamondAddress: protocolDiamondAddress,
Expand Down Expand Up @@ -1833,6 +1837,272 @@ describe("IBosonMetaTransactionsHandler", function () {
});
});

context("👉 ExchangeHandlerFacet 👉 commitToConditionalOffer()", async function () {
beforeEach(async function () {
message.functionName = "commitToConditionalOffer(address,uint256,uint256)";

offer.exchangeToken = await mockToken.getAddress();

// Check if domains are valid
expect(offer.isValid()).is.true;
expect(offerDates.isValid()).is.true;
expect(offerDurations.isValid()).is.true;

// top up seller's and buyer's account
await mockToken.mint(await assistant.getAddress(), sellerDeposit);
await mockToken.mint(await buyer.getAddress(), price);

// approve protocol to transfer the tokens
await mockToken.connect(assistant).approve(protocolDiamondAddress, sellerDeposit);
await mockToken.connect(buyer).approve(protocolDiamondAddress, price);

// deposit to seller's pool
await fundsHandler.connect(assistant).depositFunds(seller.id, await mockToken.getAddress(), sellerDeposit);

condition = mockCondition({
tokenAddress: await mockToken.getAddress(),
});
expect(condition.isValid()).to.be.true;

// Create the offer
await orchestrationHandler
.connect(assistant)
.createOfferWithCondition(offer, offerDates, offerDurations, disputeResolver.id, condition, agentId);

// Set the offer Type
offerType = [
{ name: "buyer", type: "address" },
{ name: "offerId", type: "uint256" },
{ name: "tokenId", type: "uint256" },
];

// Set the message Type
metaTransactionType = [
{ name: "nonce", type: "uint256" },
{ name: "from", type: "address" },
{ name: "contractAddress", type: "address" },
{ name: "functionName", type: "string" },
];

metaTransactionType.push({ name: "offerDetails", type: "MetaTxConditionalOfferDetails" });

customTransactionType = {
MetaTxCommitToConditionalOffer: metaTransactionType,
MetaTxConditionalOfferDetails: offerType,
};

// prepare validOfferDetails
validOfferDetails = {
buyer: await buyer.getAddress(),
offerId: offer.id,
tokenId: "0",
};

// Prepare the message
message.offerDetails = validOfferDetails;

// Deposit native currency to the same seller id
await fundsHandler
.connect(rando)
.depositFunds(seller.id, ZeroAddress, sellerDeposit, { value: sellerDeposit });
});

it("Should emit MetaTransactionExecuted event and update state", async () => {
// Collect the signature components
let { r, s, v } = await prepareDataSignatureParameters(
buyer,
customTransactionType,
"MetaTxCommitToConditionalOffer",
message,
await metaTransactionsHandler.getAddress()
);

// Prepare the function signature
functionSignature = exchangeHandler.interface.encodeFunctionData(
"commitToConditionalOffer",
Object.values(validOfferDetails)
);

// Expect that buyer has token balance matching the offer price.
const buyerBalanceBefore = await mockToken.balanceOf(await buyer.getAddress());
assert.equal(buyerBalanceBefore, price, "Buyer initial token balance mismatch");

// send a meta transaction, check for event
await expect(
metaTransactionsHandler.executeMetaTransaction(
await buyer.getAddress(),
message.functionName,
functionSignature,
nonce,
r,
s,
v
)
)
.to.emit(metaTransactionsHandler, "MetaTransactionExecuted")
.withArgs(await buyer.getAddress(), await deployer.getAddress(), message.functionName, nonce);

// Expect that buyer (meta tx signer) has paid the tokens to commit to an offer.
const buyerBalanceAfter = await mockToken.balanceOf(await buyer.getAddress());
assert.equal(buyerBalanceAfter, "0", "Buyer final token balance mismatch");

// Verify that nonce is used. Expect true.
let expectedResult = true;
result = await metaTransactionsHandler.connect(buyer).isUsedNonce(await buyer.getAddress(), nonce);
assert.equal(result, expectedResult, "Nonce is unused");
});

it("does not modify revert reasons - invalid offerId", async function () {
// An invalid offer id
offerId = "666";

// prepare validOfferDetails
validOfferDetails.offerId = offerId;

// Prepare the message
message.offerDetails = validOfferDetails;

// Collect the signature components
let { r, s, v } = await prepareDataSignatureParameters(
buyer,
customTransactionType,
"MetaTxCommitToConditionalOffer",
message,
await metaTransactionsHandler.getAddress()
);

// Prepare the function signature
functionSignature = exchangeHandler.interface.encodeFunctionData(
"commitToConditionalOffer",
Object.values(validOfferDetails)
);

// Execute meta transaction, expecting revert.
await expect(
metaTransactionsHandler.executeMetaTransaction(
await buyer.getAddress(),
message.functionName,
functionSignature,
nonce,
r,
s,
v
)
).to.revertedWith(RevertReasons.NO_SUCH_OFFER);
});

it("does not modify revert reasons - invalid tokenId", async function () {
// An invalid token id
const tokenId = "666";

// prepare validOfferDetails
validOfferDetails.tokenId = tokenId;

// Prepare the message
message.offerDetails = validOfferDetails;

// Collect the signature components
let { r, s, v } = await prepareDataSignatureParameters(
buyer,
customTransactionType,
"MetaTxCommitToConditionalOffer",
message,
await metaTransactionsHandler.getAddress()
);

// Prepare the function signature
functionSignature = exchangeHandler.interface.encodeFunctionData(
"commitToConditionalOffer",
Object.values(validOfferDetails)
);

// Execute meta transaction, expecting revert.
await expect(
metaTransactionsHandler.executeMetaTransaction(
await buyer.getAddress(),
message.functionName,
functionSignature,
nonce,
r,
s,
v
)
).to.revertedWith(RevertReasons.INVALID_TOKEN_ID);
});

context("💔 Revert Reasons", async function () {
beforeEach(async function () {
// Prepare the function signature
functionSignature = exchangeHandler.interface.encodeFunctionData(
"commitToConditionalOffer",
Object.values(validOfferDetails)
);
});

it("Should fail when replay transaction", async function () {
// Collect the signature components
let { r, s, v } = await prepareDataSignatureParameters(
buyer,
customTransactionType,
"MetaTxCommitToConditionalOffer",
message,
await metaTransactionsHandler.getAddress()
);

// Execute the meta transaction.
await metaTransactionsHandler.executeMetaTransaction(
await buyer.getAddress(),
message.functionName,
functionSignature,
nonce,
r,
s,
v
);

// Execute meta transaction again with the same nonce, expecting revert.
await expect(
metaTransactionsHandler.executeMetaTransaction(
await buyer.getAddress(),
message.functionName,
functionSignature,
nonce,
r,
s,
v
)
).to.revertedWith(RevertReasons.NONCE_USED_ALREADY);
});

it("Should fail when Signer and Signature do not match", async function () {
// Prepare the message
message.from = await rando.getAddress();

// Collect the signature components
let { r, s, v } = await prepareDataSignatureParameters(
rando, // Different user, not buyer.
customTransactionType,
"MetaTxCommitToConditionalOffer",
message,
await metaTransactionsHandler.getAddress()
);

// Execute meta transaction, expecting revert.
await expect(
metaTransactionsHandler.executeMetaTransaction(
await buyer.getAddress(),
message.functionName,
functionSignature,
nonce,
r,
s,
v
)
).to.revertedWith(RevertReasons.SIGNER_AND_SIGNATURE_DO_NOT_MATCH);
});
});
});

context("👉 MetaTxExchange", async function () {
beforeEach(async function () {
await offerHandler
Expand Down
3 changes: 2 additions & 1 deletion test/util/upgrade.js
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,7 @@ async function getMetaTxPrivateContractState(protocolDiamondAddress) {
// input type
const inputTypeKeys = [
"commitToOffer(address,uint256)",
"commitToConditionalOffer(address,uint256,uint256)",
"cancelVoucher(uint256)",
"redeemVoucher(uint256)",
"completeExchange(uint256)",
Expand All @@ -1098,7 +1099,7 @@ async function getMetaTxPrivateContractState(protocolDiamondAddress) {
CommitToOffer: 1,
Exchange: 2,
Funds: 3,
RaiseDispute: 4,
CommitToConditionalOffer: 4,
ResolveDispute: 5,
};

Expand Down

0 comments on commit 7c144ef

Please sign in to comment.