From 8e994e64c44a390113ce3fd5e00c3fb02355503f Mon Sep 17 00:00:00 2001 From: Ekrem Seren Date: Fri, 18 Oct 2024 03:37:23 +0300 Subject: [PATCH] update cm account and bt operator with cancellation funcs and add 1 test --- .../account/CMAccount.sol/CMAccount.json | 72 ++++++++++++ .../BookingTokenOperator.json | 39 +++++++ .../IBookingToken.sol/IBookingToken.json | 106 ++++++++++++++++++ contracts/account/CMAccount.sol | 35 ++++++ .../booking-token/BookingTokenOperator.sol | 33 ++++++ contracts/booking-token/IBookingToken.sol | 45 ++++++++ test/BookingToken.test.js | 71 ++++++++++++ test/utils/fixtures.js | 2 +- 8 files changed, 402 insertions(+), 1 deletion(-) diff --git a/abi/contracts/account/CMAccount.sol/CMAccount.json b/abi/contracts/account/CMAccount.sol/CMAccount.json index 15c02f1..eb7327e 100644 --- a/abi/contracts/account/CMAccount.sol/CMAccount.json +++ b/abi/contracts/account/CMAccount.sol/CMAccount.json @@ -1050,6 +1050,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "acceptCancellation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1171,6 +1184,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "cancelCancellationProposal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1219,6 +1245,29 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "refundCurrency", + "type": "address" + } + ], + "name": "counterCancellationProposal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "getAllServiceHashes", @@ -1869,6 +1918,29 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "refundCurrency", + "type": "address" + } + ], + "name": "initiateCancellation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/abi/contracts/booking-token/BookingTokenOperator.sol/BookingTokenOperator.json b/abi/contracts/booking-token/BookingTokenOperator.sol/BookingTokenOperator.json index 9e991bc..61015db 100644 --- a/abi/contracts/booking-token/BookingTokenOperator.sol/BookingTokenOperator.json +++ b/abi/contracts/booking-token/BookingTokenOperator.sol/BookingTokenOperator.json @@ -19,5 +19,44 @@ ], "name": "TokenApprovalFailed", "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "bookingToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getCancellationProposalStatus", + "outputs": [ + { + "internalType": "uint256", + "name": "refundAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "refundCurrency", + "type": "address" + }, + { + "internalType": "address", + "name": "initiatedBy", + "type": "address" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" } ] diff --git a/abi/contracts/booking-token/IBookingToken.sol/IBookingToken.json b/abi/contracts/booking-token/IBookingToken.sol/IBookingToken.json index f769ad4..be88947 100644 --- a/abi/contracts/booking-token/IBookingToken.sol/IBookingToken.json +++ b/abi/contracts/booking-token/IBookingToken.sol/IBookingToken.json @@ -1,4 +1,17 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "acceptCancellation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -12,6 +25,76 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "cancelCancellationProposal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newRefundAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "newRefundCurrency", + "type": "address" + } + ], + "name": "counterCancellationProposal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getCancellationProposalStatus", + "outputs": [ + { + "internalType": "uint256", + "name": "refundAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "refundCurrency", + "type": "address" + }, + { + "internalType": "address", + "name": "initiatedBy", + "type": "address" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -36,6 +119,29 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "refundCurrency", + "type": "address" + } + ], + "name": "initiateCancellation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/account/CMAccount.sol b/contracts/account/CMAccount.sol index b4444b6..06ef756 100644 --- a/contracts/account/CMAccount.sol +++ b/contracts/account/CMAccount.sol @@ -761,4 +761,39 @@ contract CMAccount is function setGasMoneyWithdrawal(uint256 limit, uint256 period) public onlyRole(BOT_ADMIN_ROLE) { _setGasMoneyWithdrawal(limit, period); } + + /*************************************************** + * CANCELLATION * + ***************************************************/ + + // FIXME: Create a specific role for those + + function initiateCancellation( + uint256 tokenId, + uint256 refundAmount, + address refundCurrency + ) public onlyRole(SERVICE_ADMIN_ROLE) { + BookingTokenOperator.initiateCancellation(getBookingTokenAddress(), tokenId, refundAmount, refundCurrency); + } + + function acceptCancellation(uint256 tokenId) public onlyRole(SERVICE_ADMIN_ROLE) { + BookingTokenOperator.acceptCancellation(getBookingTokenAddress(), tokenId); + } + + function counterCancellationProposal( + uint256 tokenId, + uint256 refundAmount, + address refundCurrency + ) public onlyRole(SERVICE_ADMIN_ROLE) { + BookingTokenOperator.counterCancellationProposal( + getBookingTokenAddress(), + tokenId, + refundAmount, + refundCurrency + ); + } + + function cancelCancellationProposal(uint256 tokenId) public onlyRole(SERVICE_ADMIN_ROLE) { + BookingTokenOperator.cancelCancellationProposal(getBookingTokenAddress(), tokenId); + } } diff --git a/contracts/booking-token/BookingTokenOperator.sol b/contracts/booking-token/BookingTokenOperator.sol index 24e21ae..a988a66 100644 --- a/contracts/booking-token/BookingTokenOperator.sol +++ b/contracts/booking-token/BookingTokenOperator.sol @@ -78,4 +78,37 @@ library BookingTokenOperator { IBookingToken(bookingToken).buyReservedToken{ value: price }(tokenId); } } + + function initiateCancellation( + address bookingToken, + uint256 tokenId, + uint256 refundAmount, + address refundCurrency + ) public { + IBookingToken(bookingToken).initiateCancellation(tokenId, refundAmount, refundCurrency); + } + + function acceptCancellation(address bookingToken, uint256 tokenId) public { + IBookingToken(bookingToken).acceptCancellation(tokenId); + } + + function counterCancellationProposal( + address bookingToken, + uint256 tokenId, + uint256 refundAmount, + address refundCurrency + ) public { + IBookingToken(bookingToken).counterCancellationProposal(tokenId, refundAmount, refundCurrency); + } + + function cancelCancellationProposal(address bookingToken, uint256 tokenId) public { + IBookingToken(bookingToken).cancelCancellationProposal(tokenId); + } + + function getCancellationProposalStatus( + address bookingToken, + uint256 tokenId + ) public view returns (uint256 refundAmount, address refundCurrency, address initiatedBy, bool isActive) { + return IBookingToken(bookingToken).getCancellationProposalStatus(tokenId); + } } diff --git a/contracts/booking-token/IBookingToken.sol b/contracts/booking-token/IBookingToken.sol index 41ee40e..dc96b4a 100644 --- a/contracts/booking-token/IBookingToken.sol +++ b/contracts/booking-token/IBookingToken.sol @@ -15,4 +15,49 @@ interface IBookingToken { function buyReservedToken(uint256 tokenId) external payable; function getReservationPrice(uint256 tokenId) external view returns (uint256 price, IERC20 paymentToken); + + /** + * @notice Initiates a cancellation for a bought token. + * + * @param tokenId The token id to initiate the cancellation for + * @param refundAmount The proposed refund amount in wei + * @param refundCurrency The ERC20 token address for the refund + */ + function initiateCancellation(uint256 tokenId, uint256 refundAmount, address refundCurrency) external; + + /** + * @notice Accepts a cancellation proposal for a bought token. + * + * @param tokenId The token id to accept the cancellation for + */ + function acceptCancellation(uint256 tokenId) external; + + /** + * @notice Counters a cancellation proposal with a new proposal. + * + * @param tokenId The token id to counter the cancellation for + * @param newRefundAmount The new proposed refund amount in wei + * @param newRefundCurrency The new ERC20 token address for the refund + */ + function counterCancellationProposal(uint256 tokenId, uint256 newRefundAmount, address newRefundCurrency) external; + + /** + * @notice Cancels an active cancellation proposal. Only the initiator can cancel. + * + * @param tokenId The token id for which to cancel the proposal + */ + function cancelCancellationProposal(uint256 tokenId) external; + + /** + * @notice Retrieves the current cancellation proposal status for a given token. + * + * @param tokenId The token id to check the proposal status for + * @return refundAmount The proposed refund amount + * @return refundCurrency The address of the proposed refund currency + * @return initiatedBy The address that initiated the cancellation + * @return isActive The status of the cancellation proposal + */ + function getCancellationProposalStatus( + uint256 tokenId + ) external view returns (uint256 refundAmount, address refundCurrency, address initiatedBy, bool isActive); } diff --git a/test/BookingToken.test.js b/test/BookingToken.test.js index 6650bdb..feb6429 100644 --- a/test/BookingToken.test.js +++ b/test/BookingToken.test.js @@ -594,4 +594,75 @@ describe("BookingToken", function () { .withArgs(0n, await distributorCMAccount.getAddress()); }); }); + describe("Cancellation", function () { + it("supplier: should initiate cancellation of a booking token correctly", async function () { + const { cmAccountManager, supplierCMAccount, distributorCMAccount, bookingToken } = + await loadFixture(deployBookingTokenFixture); + + const tokenURI = + "data:application/json;base64,eyJuYW1lIjoiQ2FtaW5vIE1lc3NlbmdlciBCb29raW5nVG9rZW4gVGVzdCJ9Cg=="; + + const expirationTimestamp = Math.floor(Date.now() / 1000) + 120; + + const price = ethers.parseEther("0.05"); + + /*************************************************** + * SUPPLIER * + ***************************************************/ + + // Grant BOOKING_OPERATOR_ROLE + const BOOKING_OPERATOR_ROLE = await supplierCMAccount.BOOKING_OPERATOR_ROLE(); + await expect( + supplierCMAccount + .connect(signers.cmAccountAdmin) + .grantRole(BOOKING_OPERATOR_ROLE, signers.btAdmin.address), + ).to.not.reverted; + + await expect( + await supplierCMAccount.connect(signers.btAdmin).mintBookingToken( + distributorCMAccount.getAddress(), // set reservedFor address to distributor CMAccount + tokenURI, // tokenURI + expirationTimestamp, // expiration + price, // price + ethers.ZeroAddress, // zero address + ), + ) + .to.be.emit(bookingToken, "TokenReserved") + .withArgs( + 0n, + distributorCMAccount.getAddress(), + supplierCMAccount.getAddress(), + expirationTimestamp, + price, + ethers.ZeroAddress, // zero address + ); + + // Check token ownership + expect(await bookingToken.ownerOf(0n)).to.equal(await supplierCMAccount.getAddress()); + + // Try to cancel the token + + const token_id = 0n; + const initiator = await supplierCMAccount.getAddress(); + const refundAmount = ethers.parseEther("0.045"); + const refundCurrency = ethers.ZeroAddress; + + await expect( + supplierCMAccount + .connect(signers.cmAccountAdmin) + .initiateCancellation(0n, refundAmount, refundCurrency), + ) + .to.emit(bookingToken, "CancellationInitiated") + .withArgs(token_id, initiator, refundAmount, refundCurrency); + + // Sanity check + expect(await bookingToken.getCancellationProposalStatus(token_id)).to.be.deep.equal([ + refundAmount, + refundCurrency, + initiator, + true, + ]); + }); + // FIXME: add tests for other cases + }); }); diff --git a/test/utils/fixtures.js b/test/utils/fixtures.js index af2a357..ec97a01 100644 --- a/test/utils/fixtures.js +++ b/test/utils/fixtures.js @@ -115,7 +115,7 @@ async function deployAndConfigureAllFixture() { // Deploy BookingToken - const BookingToken = await ethers.getContractFactory("BookingToken"); + const BookingToken = await ethers.getContractFactory("BookingTokenV2"); const bookingToken = await upgrades.deployProxy( BookingToken, [await cmAccountManager.getAddress(), signers.btAdmin.address, signers.btUpgrader.address],