diff --git a/contracts/mechs/native/MechFactoryFixedPriceNative.sol b/contracts/mechs/native/MechFactoryFixedPriceNative.sol index d6182df..ff6a675 100644 --- a/contracts/mechs/native/MechFactoryFixedPriceNative.sol +++ b/contracts/mechs/native/MechFactoryFixedPriceNative.sol @@ -16,7 +16,7 @@ error MarketplaceOnly(address sender, address marketplace); /// @dev Provided zero address. error ZeroAddress(); -/// @title Mech Factory Basic - Periphery smart contract for managing basic mech creation +/// @title MechFactoryFixedPriceNative - Periphery smart contract for managing fixed price native token mech creation contract MechFactoryFixedPriceNative { event CreateFixedPriceMech(address indexed mech, uint256 indexed serviceId, uint256 maxDeliveryRate); diff --git a/contracts/mechs/token/MechFactoryFixedPriceToken.sol b/contracts/mechs/token/MechFactoryFixedPriceToken.sol index 8a4c48d..0ff7e11 100644 --- a/contracts/mechs/token/MechFactoryFixedPriceToken.sol +++ b/contracts/mechs/token/MechFactoryFixedPriceToken.sol @@ -16,7 +16,7 @@ error MarketplaceOnly(address sender, address marketplace); /// @dev Provided zero address. error ZeroAddress(); -/// @title Mech Factory Basic - Periphery smart contract for managing basic mech creation +/// @title MechFactoryFixedPriceToken - Periphery smart contract for managing fixed price token mech creation contract MechFactoryFixedPriceToken { event CreateFixedPriceMech(address indexed mech, uint256 indexed serviceId, uint256 maxDeliveryRate); diff --git a/contracts/test/MockMech.sol b/contracts/test/MockMech.sol new file mode 100644 index 0000000..b52dbd7 --- /dev/null +++ b/contracts/test/MockMech.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +interface IMechMarketplace { + function deliverMarketplace(uint256 requestId, bytes memory requestData) external; + function request(bytes memory data, uint256 priorityMechServiceId, uint256 requesterServiceId, + uint256 responseTimeout, bytes memory paymentData) external payable returns (uint256); +} + +contract MockMech { + address public immutable mechMarketplace; + + uint256 public serviceId = 99; + bool public isNotSelf; + + constructor(address _mechMarketplace) { + mechMarketplace = _mechMarketplace; + } + + function setServiceId(uint256 _serviceId) external { + serviceId = _serviceId; + } + + function setNotSelf(bool _isNotSelf) external { + isNotSelf = _isNotSelf; + } + + function deliverMarketplace(uint256 requestId, bytes memory requestData) external { + IMechMarketplace(mechMarketplace).deliverMarketplace(requestId, requestData); + } + + function request( + bytes memory data, + uint256 priorityMechServiceId, + uint256 requesterServiceId, + uint256 responseTimeout, + bytes memory paymentData + ) external payable returns (uint256) { + return IMechMarketplace(mechMarketplace).request{value: msg.value}(data, priorityMechServiceId, + requesterServiceId, responseTimeout, paymentData); + } + + function tokenId() external view returns (uint256) { + return serviceId; + } + + function getOperator() external view returns (address) { + if (isNotSelf) { + return address(1); + } + + return address(this); + } + + function getFinalizedDeliveryRate(uint256) external returns (uint256) { + return 1; + } +} \ No newline at end of file diff --git a/contracts/test/MockMechFactory.sol b/contracts/test/MockMechFactory.sol new file mode 100644 index 0000000..49b77f6 --- /dev/null +++ b/contracts/test/MockMechFactory.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {MockMech} from "./MockMech.sol"; + +/// @dev Incorrect data length. +/// @param provided Provided data length. +/// @param expected Expected data length. +error IncorrectDataLength(uint256 provided, uint256 expected); + +/// @dev Only `marketplace` has a privilege, but the `sender` was provided. +/// @param sender Sender address. +/// @param marketplace Required marketplace address. +error MarketplaceOnly(address sender, address marketplace); + +/// @dev Provided zero address. +error ZeroAddress(); + +/// @title MockMechFactory - Periphery smart contract for managing mock mech creation +contract MockMechFactory { + event CreateMockMech(address indexed mech, uint256 maxDeliveryRate); + + // Agent factory version number + string public constant VERSION = "0.1.0"; + // Mech marketplace address + address public immutable mechMarketplace; + + // Decode max delivery rate + uint256 public maxDeliveryRate = 1; + + // Nonce + uint256 internal _nonce; + + /// @dev MechFactoryFixedPriceNative constructor. + /// @param _mechMarketplace Mech marketplace address. + constructor(address _mechMarketplace) { + mechMarketplace = _mechMarketplace; + } + + /// @dev Registers service as a mech. + /// @return mech The created mech instance address. + function createMech( + address, + uint256, + bytes memory + ) external returns (address mech) { + // Check for marketplace access + if (msg.sender != mechMarketplace) { + revert MarketplaceOnly(msg.sender, mechMarketplace); + } + + uint256 localNonce = _nonce; + // Get salt + bytes32 salt = keccak256(abi.encode(block.timestamp, msg.sender, localNonce)); + _nonce = localNonce + 1; + + // Service multisig is isOperator() for the mech + mech = address((new MockMech){salt: salt}(mechMarketplace)); + + // Check for zero address + if (mech == address(0)) { + revert ZeroAddress(); + } + + emit CreateMockMech(mech, maxDeliveryRate); + } +} diff --git a/test/MechMarketplace.js b/test/MechMarketplace.js index a1cc90c..a13c741 100644 --- a/test/MechMarketplace.js +++ b/test/MechMarketplace.js @@ -2,6 +2,7 @@ const { expect } = require("chai"); const { ethers } = require("hardhat"); +const helpers = require("@nomicfoundation/hardhat-network-helpers"); describe("MechMarketplace", function () { let MechMarketplace; @@ -12,11 +13,15 @@ describe("MechMarketplace", function () { let karma; let mechFactoryFixedPrice; let balanceTrackerFixedPriceNative; + let mockMech; + let mockMechFactory; let signers; let deployer; const AddressZero = ethers.constants.AddressZero; const maxDeliveryRate = 1000; const fee = 10; + const data = "0x00"; + const defaultRequestId = 1; const minResponseTimeout = 10; const maxResponseTimeout = 20; const mechServiceId = 1; @@ -97,6 +102,19 @@ describe("MechMarketplace", function () { // Whitelist balance tracker paymentTypeHash = await priorityMech.paymentType(); await mechMarketplace.setPaymentTypeBalanceTrackers([paymentTypeHash], [balanceTrackerFixedPriceNative.address]); + + // Deploy mock mech + const MockMech = await ethers.getContractFactory("MockMech"); + mockMech = await MockMech.deploy(mechMarketplace.address); + await mockMech.deployed(); + + // Deploy mock mech factory + const MockMechFactory = await ethers.getContractFactory("MockMechFactory"); + mockMechFactory = await MockMechFactory.deploy(mechMarketplace.address); + await mockMechFactory.deployed(); + + // Whitelist mock mech factory + await mechMarketplace.setMechFactoryStatuses([mockMechFactory.address], [true]); }); context("Initialization", async function () { @@ -198,7 +216,7 @@ describe("MechMarketplace", function () { mechMarketplace.create(mechServiceId, deployer.address, mechCreationData) ).to.be.revertedWithCustomError(mechMarketplace, "UnauthorizedAccount"); - // Trying to set mech factory statuses and balance trackersnot by the owner + // Trying to set mech factory statuses and balance trackers not by the owner await expect( mechMarketplace.connect(signers[1]).setMechFactoryStatuses([AddressZero], [true]) ).to.be.revertedWithCustomError(mechMarketplace, "OwnerOnly"); @@ -226,6 +244,72 @@ describe("MechMarketplace", function () { mechMarketplace.setPaymentTypeBalanceTrackers([ethers.constants.HashZero], [deployer.address]) ).to.be.revertedWithCustomError(mechMarketplace, "ZeroValue"); }); + + it("Try to deliver by a mock mech", async function () { + // Take a snapshot of the current state of the blockchain + const snapshot = await helpers.takeSnapshot(); + + // Trying to deliver by a random mech + await expect( + mockMech.deliverMarketplace(defaultRequestId, data) + ).to.be.revertedWithCustomError(mechMarketplace, "UnauthorizedAccount"); + + // Create mock mech via the factory + let mockServiceId = await mockMech.tokenId(); + let tx = await mechMarketplace.create(mockServiceId, mockMechFactory.address, data); + let res = await tx.wait(); + // Get mech contract address from the event + const mechMockAddress = "0x" + res.logs[0].topics[1].slice(26); + // Get mech contract instance + const mechMock = await ethers.getContractAt("MockMech", mechMockAddress); + + // Try to deliver a non-existent request + await expect( + mechMock.deliverMarketplace(defaultRequestId, data) + ).to.be.revertedWithCustomError(mechMarketplace, "ZeroAddress"); + + // Request in priority mech + // Get request Id + const requestId = await mechMarketplace.getRequestId(mechMock.address, data, 0); + + // Change mock service Id not to be within deployed ones (id-s 100+ in MockServiceRegistry) + mockServiceId = mockServiceId.add(1); + await mechMock.setServiceId(mockServiceId); + + // Try to post a request from a requester service that is not deployed + await expect( + mechMock.request(data, mechServiceId, mockServiceId, minResponseTimeout, "0x", {value: maxDeliveryRate}) + ).to.be.revertedWithCustomError(mechMarketplace, "WrongServiceState"); + + // Change mock service Id to be back within deployed ones (0 to 99 in MockServiceRegistry) + mockServiceId = mockServiceId.sub(1); + await mechMock.setServiceId(mockServiceId); + + // Try to post a request not by a correct requester multisig + await expect( + mechMock.request(data, mechServiceId, mockServiceId, minResponseTimeout, "0x", {value: maxDeliveryRate}) + ).to.be.revertedWithCustomError(mechMarketplace, "OwnerOnly"); + + // Pseudo-create and deploy requester service + await serviceRegistry.setServiceOwner(mockServiceId, mechMock.address); + + // Post a request + await mechMock.request(data, mechServiceId, mockServiceId, minResponseTimeout, "0x", {value: maxDeliveryRate}); + + // Increase the time such that the request expires for a priority mech + await helpers.time.increase(maxResponseTimeout); + + // Try to deliver directly via a marketplace + await mechMock.deliverMarketplace(requestId, data); + + // Try to deliver the same request once again + await expect( + mechMock.deliverMarketplace(requestId, data) + ).to.be.revertedWithCustomError(mechMarketplace, "AlreadyDelivered"); + + // Restore a previous state of blockchain + snapshot.restore(); + }); }); context("Request checks", async function () { @@ -240,6 +324,8 @@ describe("MechMarketplace", function () { mechMarketplace.checkRequester(deployer.address, requesterServiceId) ).to.be.revertedWithCustomError(mechMarketplace, "OwnerOnly"); + // Check requester + await mechMarketplace.checkRequester(signers[1].address, requesterServiceId); }); }); });