{
"language": "Solidity",
"sources": {
"@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (access/Ownable2Step.sol)\n\npragma solidity ^0.8.20;\n\nimport {OwnableUpgradeable} from \"./OwnableUpgradeable.sol\";\nimport {Initializable} from \"../proxy/utils/Initializable.sol\";\n\n/**\n * @dev Contract module which provides access control mechanism, where\n * there is an account (an owner) that can be granted exclusive access to\n * specific functions.\n *\n * This extension of the {Ownable} contract includes a two-step mechanism to transfer\n * ownership, where the new owner must call {acceptOwnership} in order to replace the\n * old one. This can help prevent common mistakes, such as transfers of ownership to\n * incorrect accounts, or to contracts that are unable to interact with the\n * permission system.\n *\n * The initial owner is specified at deployment time in the constructor for `Ownable`. This\n * can later be changed with {transferOwnership} and {acceptOwnership}.\n *\n * This module is used through inheritance. It will make available all functions\n * from parent (Ownable).\n */\nabstract contract Ownable2StepUpgradeable is Initializable, OwnableUpgradeable {\n /// @custom:storage-location erc7201:openzeppelin.storage.Ownable2Step\n struct Ownable2StepStorage {\n address _pendingOwner;\n }\n\n // keccak256(abi.encode(uint256(keccak256(\"openzeppelin.storage.Ownable2Step\")) - 1)) & ~bytes32(uint256(0xff))\n bytes32 private constant Ownable2StepStorageLocation = 0x237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00;\n\n function _getOwnable2StepStorage() private pure returns (Ownable2StepStorage storage $) {\n assembly {\n $.slot := Ownable2StepStorageLocation\n }\n }\n\n event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);\n\n function __Ownable2Step_init() internal onlyInitializing {\n }\n\n function __Ownable2Step_init_unchained() internal onlyInitializing {\n }\n /**\n * @dev Returns the address of the pending owner.\n */\n function pendingOwner() public view virtual returns (address) {\n Ownable2StepStorage storage $ = _getOwnable2StepStorage();\n return $._pendingOwner;\n }\n\n /**\n * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one.\n * Can only be called by the current owner.\n *\n * Setting `newOwner` to the zero address is allowed; this can be used to cancel an initiated ownership transfer.\n */\n function transferOwnership(address newOwner) public virtual override onlyOwner {\n Ownable2StepStorage storage $ = _getOwnable2StepStorage();\n $._pendingOwner = newOwner;\n emit OwnershipTransferStarted(owner(), newOwner);\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner.\n * Internal function without access restriction.\n */\n function _transferOwnership(address newOwner) internal virtual override {\n Ownable2StepStorage storage $ = _getOwnable2StepStorage();\n delete $._pendingOwner;\n super._transferOwnership(newOwner);\n }\n\n /**\n * @dev The new owner accepts the ownership transfer.\n */\n function acceptOwnership() public virtual {\n address sender = _msgSender();\n if (pendingOwner() != sender) {\n revert OwnableUnauthorizedAccount(sender);\n }\n _transferOwnership(sender);\n }\n}\n"
},
"@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)\n\npragma solidity ^0.8.20;\n\nimport {ContextUpgradeable} from \"../utils/ContextUpgradeable.sol\";\nimport {Initializable} from \"../proxy/utils/Initializable.sol\";\n\n/**\n * @dev Contract module which provides a basic access control mechanism, where\n * there is an account (an owner) that can be granted exclusive access to\n * specific functions.\n *\n * The initial owner is set to the address provided by the deployer. This can\n * later be changed with {transferOwnership}.\n *\n * This module is used through inheritance. It will make available the modifier\n * `onlyOwner`, which can be applied to your functions to restrict their use to\n * the owner.\n */\nabstract contract OwnableUpgradeable is Initializable, ContextUpgradeable {\n /// @custom:storage-location erc7201:openzeppelin.storage.Ownable\n struct OwnableStorage {\n address _owner;\n }\n\n // keccak256(abi.encode(uint256(keccak256(\"openzeppelin.storage.Ownable\")) - 1)) & ~bytes32(uint256(0xff))\n bytes32 private constant OwnableStorageLocation = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;\n\n function _getOwnableStorage() private pure returns (OwnableStorage storage $) {\n assembly {\n $.slot := OwnableStorageLocation\n }\n }\n\n /**\n * @dev The caller account is not authorized to perform an operation.\n */\n error OwnableUnauthorizedAccount(address account);\n\n /**\n * @dev The owner is not a valid owner account. (eg. `address(0)`)\n */\n error OwnableInvalidOwner(address owner);\n\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /**\n * @dev Initializes the contract setting the address provided by the deployer as the initial owner.\n */\n function __Ownable_init(address initialOwner) internal onlyInitializing {\n __Ownable_init_unchained(initialOwner);\n }\n\n function __Ownable_init_unchained(address initialOwner) internal onlyInitializing {\n if (initialOwner == address(0)) {\n revert OwnableInvalidOwner(address(0));\n }\n _transferOwnership(initialOwner);\n }\n\n /**\n * @dev Throws if called by any account other than the owner.\n */\n modifier onlyOwner() {\n _checkOwner();\n _;\n }\n\n /**\n * @dev Returns the address of the current owner.\n */\n function owner() public view virtual returns (address) {\n OwnableStorage storage $ = _getOwnableStorage();\n return $._owner;\n }\n\n /**\n * @dev Throws if the sender is not the owner.\n */\n function _checkOwner() internal view virtual {\n if (owner() != _msgSender()) {\n revert OwnableUnauthorizedAccount(_msgSender());\n }\n }\n\n /**\n * @dev Leaves the contract without owner. It will not be possible to call\n * `onlyOwner` functions. Can only be called by the current owner.\n *\n * NOTE: Renouncing ownership will leave the contract without an owner,\n * thereby disabling any functionality that is only available to the owner.\n */\n function renounceOwnership() public virtual onlyOwner {\n _transferOwnership(address(0));\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Can only be called by the current owner.\n */\n function transferOwnership(address newOwner) public virtual onlyOwner {\n if (newOwner == address(0)) {\n revert OwnableInvalidOwner(address(0));\n }\n _transferOwnership(newOwner);\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Internal function without access restriction.\n */\n function _transferOwnership(address newOwner) internal virtual {\n OwnableStorage storage $ = _getOwnableStorage();\n address oldOwner = $._owner;\n $._owner = newOwner;\n emit OwnershipTransferred(oldOwner, newOwner);\n }\n}\n"
},
"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (proxy/utils/Initializable.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed\n * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an\n * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer\n * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.\n *\n * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be\n * reused. This mechanism prevents re-execution of each \"step\" but allows the creation of new initialization steps in\n * case an upgrade adds a module that needs to be initialized.\n *\n * For example:\n *\n * [.hljs-theme-light.nopadding]\n * ```solidity\n * contract MyToken is ERC20Upgradeable {\n * function initialize() initializer public {\n * __ERC20_init(\"MyToken\", \"MTK\");\n * }\n * }\n *\n * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {\n * function initializeV2() reinitializer(2) public {\n * __ERC20Permit_init(\"MyToken\");\n * }\n * }\n * ```\n *\n * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as\n * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.\n *\n * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure\n * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.\n *\n * [CAUTION]\n * ====\n * Avoid leaving a contract uninitialized.\n *\n * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation\n * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke\n * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:\n *\n * [.hljs-theme-light.nopadding]\n * ```\n * /// @custom:oz-upgrades-unsafe-allow constructor\n * constructor() {\n * _disableInitializers();\n * }\n * ```\n * ====\n */\nabstract contract Initializable {\n /**\n * @dev Storage of the initializable contract.\n *\n * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions\n * when using with upgradeable contracts.\n *\n * @custom:storage-location erc7201:openzeppelin.storage.Initializable\n */\n struct InitializableStorage {\n /**\n * @dev Indicates that the contract has been initialized.\n */\n uint64 _initialized;\n /**\n * @dev Indicates that the contract is in the process of being initialized.\n */\n bool _initializing;\n }\n\n // keccak256(abi.encode(uint256(keccak256(\"openzeppelin.storage.Initializable\")) - 1)) & ~bytes32(uint256(0xff))\n bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00;\n\n /**\n * @dev The contract is already initialized.\n */\n error InvalidInitialization();\n\n /**\n * @dev The contract is not initializing.\n */\n error NotInitializing();\n\n /**\n * @dev Triggered when the contract has been initialized or reinitialized.\n */\n event Initialized(uint64 version);\n\n /**\n * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,\n * `onlyInitializing` functions can be used to initialize parent contracts.\n *\n * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any\n * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in\n * production.\n *\n * Emits an {Initialized} event.\n */\n modifier initializer() {\n // solhint-disable-next-line var-name-mixedcase\n InitializableStorage storage $ = _getInitializableStorage();\n\n // Cache values to avoid duplicated sloads\n bool isTopLevelCall = !$._initializing;\n uint64 initialized = $._initialized;\n\n // Allowed calls:\n // - initialSetup: the contract is not in the initializing state and no previous version was\n // initialized\n // - construction: the contract is initialized at version 1 (no reinitialization) and the\n // current contract is just being deployed\n bool initialSetup = initialized == 0 && isTopLevelCall;\n bool construction = initialized == 1 && address(this).code.length == 0;\n\n if (!initialSetup && !construction) {\n revert InvalidInitialization();\n }\n $._initialized = 1;\n if (isTopLevelCall) {\n $._initializing = true;\n }\n _;\n if (isTopLevelCall) {\n $._initializing = false;\n emit Initialized(1);\n }\n }\n\n /**\n * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the\n * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be\n * used to initialize parent contracts.\n *\n * A reinitializer may be used after the original initialization step. This is essential to configure modules that\n * are added through upgrades and that require initialization.\n *\n * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`\n * cannot be nested. If one is invoked in the context of another, execution will revert.\n *\n * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in\n * a contract, executing them in the right order is up to the developer or operator.\n *\n * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization.\n *\n * Emits an {Initialized} event.\n */\n modifier reinitializer(uint64 version) {\n // solhint-disable-next-line var-name-mixedcase\n InitializableStorage storage $ = _getInitializableStorage();\n\n if ($._initializing || $._initialized >= version) {\n revert InvalidInitialization();\n }\n $._initialized = version;\n $._initializing = true;\n _;\n $._initializing = false;\n emit Initialized(version);\n }\n\n /**\n * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the\n * {initializer} and {reinitializer} modifiers, directly or indirectly.\n */\n modifier onlyInitializing() {\n _checkInitializing();\n _;\n }\n\n /**\n * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}.\n */\n function _checkInitializing() internal view virtual {\n if (!_isInitializing()) {\n revert NotInitializing();\n }\n }\n\n /**\n * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.\n * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized\n * to any version. It is recommended to use this to lock implementation contracts that are designed to be called\n * through proxies.\n *\n * Emits an {Initialized} event the first time it is successfully executed.\n */\n function _disableInitializers() internal virtual {\n // solhint-disable-next-line var-name-mixedcase\n InitializableStorage storage $ = _getInitializableStorage();\n\n if ($._initializing) {\n revert InvalidInitialization();\n }\n if ($._initialized != type(uint64).max) {\n $._initialized = type(uint64).max;\n emit Initialized(type(uint64).max);\n }\n }\n\n /**\n * @dev Returns the highest version that has been initialized. See {reinitializer}.\n */\n function _getInitializedVersion() internal view returns (uint64) {\n return _getInitializableStorage()._initialized;\n }\n\n /**\n * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.\n */\n function _isInitializing() internal view returns (bool) {\n return _getInitializableStorage()._initializing;\n }\n\n /**\n * @dev Pointer to storage slot. Allows integrators to override it with a custom storage location.\n *\n * NOTE: Consider following the ERC-7201 formula to derive storage locations.\n */\n function _initializableStorageSlot() internal pure virtual returns (bytes32) {\n return INITIALIZABLE_STORAGE;\n }\n\n /**\n * @dev Returns a pointer to the storage namespace.\n */\n // solhint-disable-next-line var-name-mixedcase\n function _getInitializableStorage() private pure returns (InitializableStorage storage $) {\n bytes32 slot = _initializableStorageSlot();\n assembly {\n $.slot := slot\n }\n }\n}\n"
},
"@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)\n\npragma solidity ^0.8.20;\nimport {Initializable} from \"../proxy/utils/Initializable.sol\";\n\n/**\n * @dev Provides information about the current execution context, including the\n * sender of the transaction and its data. While these are generally available\n * via msg.sender and msg.data, they should not be accessed in such a direct\n * manner, since when dealing with meta-transactions the account sending and\n * paying for execution may not be the actual sender (as far as an application\n * is concerned).\n *\n * This contract is only required for intermediate, library-like contracts.\n */\nabstract contract ContextUpgradeable is Initializable {\n function __Context_init() internal onlyInitializing {\n }\n\n function __Context_init_unchained() internal onlyInitializing {\n }\n function _msgSender() internal view virtual returns (address) {\n return msg.sender;\n }\n\n function _msgData() internal view virtual returns (bytes calldata) {\n return msg.data;\n }\n\n function _contextSuffixLength() internal view virtual returns (uint256) {\n return 0;\n }\n}\n"
},
"@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.3.0) (utils/Pausable.sol)\n\npragma solidity ^0.8.20;\n\nimport {ContextUpgradeable} from \"../utils/ContextUpgradeable.sol\";\nimport {Initializable} from \"../proxy/utils/Initializable.sol\";\n\n/**\n * @dev Contract module which allows children to implement an emergency stop\n * mechanism that can be triggered by an authorized account.\n *\n * This module is used through inheritance. It will make available the\n * modifiers `whenNotPaused` and `whenPaused`, which can be applied to\n * the functions of your contract. Note that they will not be pausable by\n * simply including this module, only once the modifiers are put in place.\n */\nabstract contract PausableUpgradeable is Initializable, ContextUpgradeable {\n /// @custom:storage-location erc7201:openzeppelin.storage.Pausable\n struct PausableStorage {\n bool _paused;\n }\n\n // keccak256(abi.encode(uint256(keccak256(\"openzeppelin.storage.Pausable\")) - 1)) & ~bytes32(uint256(0xff))\n bytes32 private constant PausableStorageLocation = 0xcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f03300;\n\n function _getPausableStorage() private pure returns (PausableStorage storage $) {\n assembly {\n $.slot := PausableStorageLocation\n }\n }\n\n /**\n * @dev Emitted when the pause is triggered by `account`.\n */\n event Paused(address account);\n\n /**\n * @dev Emitted when the pause is lifted by `account`.\n */\n event Unpaused(address account);\n\n /**\n * @dev The operation failed because the contract is paused.\n */\n error EnforcedPause();\n\n /**\n * @dev The operation failed because the contract is not paused.\n */\n error ExpectedPause();\n\n /**\n * @dev Modifier to make a function callable only when the contract is not paused.\n *\n * Requirements:\n *\n * - The contract must not be paused.\n */\n modifier whenNotPaused() {\n _requireNotPaused();\n _;\n }\n\n /**\n * @dev Modifier to make a function callable only when the contract is paused.\n *\n * Requirements:\n *\n * - The contract must be paused.\n */\n modifier whenPaused() {\n _requirePaused();\n _;\n }\n\n function __Pausable_init() internal onlyInitializing {\n }\n\n function __Pausable_init_unchained() internal onlyInitializing {\n }\n /**\n * @dev Returns true if the contract is paused, and false otherwise.\n */\n function paused() public view virtual returns (bool) {\n PausableStorage storage $ = _getPausableStorage();\n return $._paused;\n }\n\n /**\n * @dev Throws if the contract is paused.\n */\n function _requireNotPaused() internal view virtual {\n if (paused()) {\n revert EnforcedPause();\n }\n }\n\n /**\n * @dev Throws if the contract is not paused.\n */\n function _requirePaused() internal view virtual {\n if (!paused()) {\n revert ExpectedPause();\n }\n }\n\n /**\n * @dev Triggers stopped state.\n *\n * Requirements:\n *\n * - The contract must not be paused.\n */\n function _pause() internal virtual whenNotPaused {\n PausableStorage storage $ = _getPausableStorage();\n $._paused = true;\n emit Paused(_msgSender());\n }\n\n /**\n * @dev Returns to normal state.\n *\n * Requirements:\n *\n * - The contract must be paused.\n */\n function _unpause() internal virtual whenPaused {\n PausableStorage storage $ = _getPausableStorage();\n $._paused = false;\n emit Unpaused(_msgSender());\n }\n}\n"
},
"@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)\n\npragma solidity ^0.8.20;\nimport {Initializable} from \"../proxy/utils/Initializable.sol\";\n\n/**\n * @dev Contract module that helps prevent reentrant calls to a function.\n *\n * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier\n * available, which can be applied to functions to make sure there are no nested\n * (reentrant) calls to them.\n *\n * Note that because there is a single `nonReentrant` guard, functions marked as\n * `nonReentrant` may not call one another. This can be worked around by making\n * those functions `private`, and then adding `external` `nonReentrant` entry\n * points to them.\n *\n * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,\n * consider using {ReentrancyGuardTransient} instead.\n *\n * TIP: If you would like to learn more about reentrancy and alternative ways\n * to protect against it, check out our blog post\n * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].\n */\nabstract contract ReentrancyGuardUpgradeable is Initializable {\n // Booleans are more expensive than uint256 or any type that takes up a full\n // word because each write operation emits an extra SLOAD to first read the\n // slot's contents, replace the bits taken up by the boolean, and then write\n // back. This is the compiler's defense against contract upgrades and\n // pointer aliasing, and it cannot be disabled.\n\n // The values being non-zero value makes deployment a bit more expensive,\n // but in exchange the refund on every call to nonReentrant will be lower in\n // amount. Since refunds are capped to a percentage of the total\n // transaction's gas, it is best to keep them low in cases like this one, to\n // increase the likelihood of the full refund coming into effect.\n uint256 private constant NOT_ENTERED = 1;\n uint256 private constant ENTERED = 2;\n\n /// @custom:storage-location erc7201:openzeppelin.storage.ReentrancyGuard\n struct ReentrancyGuardStorage {\n uint256 _status;\n }\n\n // keccak256(abi.encode(uint256(keccak256(\"openzeppelin.storage.ReentrancyGuard\")) - 1)) & ~bytes32(uint256(0xff))\n bytes32 private constant ReentrancyGuardStorageLocation = 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;\n\n function _getReentrancyGuardStorage() private pure returns (ReentrancyGuardStorage storage $) {\n assembly {\n $.slot := ReentrancyGuardStorageLocation\n }\n }\n\n /**\n * @dev Unauthorized reentrant call.\n */\n error ReentrancyGuardReentrantCall();\n\n function __ReentrancyGuard_init() internal onlyInitializing {\n __ReentrancyGuard_init_unchained();\n }\n\n function __ReentrancyGuard_init_unchained() internal onlyInitializing {\n ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();\n $._status = NOT_ENTERED;\n }\n\n /**\n * @dev Prevents a contract from calling itself, directly or indirectly.\n * Calling a `nonReentrant` function from another `nonReentrant`\n * function is not supported. It is possible to prevent this from happening\n * by making the `nonReentrant` function external, and making it call a\n * `private` function that does the actual work.\n */\n modifier nonReentrant() {\n _nonReentrantBefore();\n _;\n _nonReentrantAfter();\n }\n\n function _nonReentrantBefore() private {\n ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();\n // On the first call to nonReentrant, _status will be NOT_ENTERED\n if ($._status == ENTERED) {\n revert ReentrancyGuardReentrantCall();\n }\n\n // Any calls to nonReentrant after this point will fail\n $._status = ENTERED;\n }\n\n function _nonReentrantAfter() private {\n ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();\n // By storing the original value once again, a refund is triggered (see\n // https://eips.ethereum.org/EIPS/eip-2200)\n $._status = NOT_ENTERED;\n }\n\n /**\n * @dev Returns true if the reentrancy guard is currently set to \"entered\", which indicates there is a\n * `nonReentrant` function in the call stack.\n */\n function _reentrancyGuardEntered() internal view returns (bool) {\n ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage();\n return $._status == ENTERED;\n }\n}\n"
},
"@openzeppelin/contracts/access/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)\n\npragma solidity ^0.8.20;\n\nimport {Context} from \"../utils/Context.sol\";\n\n/**\n * @dev Contract module which provides a basic access control mechanism, where\n * there is an account (an owner) that can be granted exclusive access to\n * specific functions.\n *\n * The initial owner is set to the address provided by the deployer. This can\n * later be changed with {transferOwnership}.\n *\n * This module is used through inheritance. It will make available the modifier\n * `onlyOwner`, which can be applied to your functions to restrict their use to\n * the owner.\n */\nabstract contract Ownable is Context {\n address private _owner;\n\n /**\n * @dev The caller account is not authorized to perform an operation.\n */\n error OwnableUnauthorizedAccount(address account);\n\n /**\n * @dev The owner is not a valid owner account. (eg. `address(0)`)\n */\n error OwnableInvalidOwner(address owner);\n\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /**\n * @dev Initializes the contract setting the address provided by the deployer as the initial owner.\n */\n constructor(address initialOwner) {\n if (initialOwner == address(0)) {\n revert OwnableInvalidOwner(address(0));\n }\n _transferOwnership(initialOwner);\n }\n\n /**\n * @dev Throws if called by any account other than the owner.\n */\n modifier onlyOwner() {\n _checkOwner();\n _;\n }\n\n /**\n * @dev Returns the address of the current owner.\n */\n function owner() public view virtual returns (address) {\n return _owner;\n }\n\n /**\n * @dev Throws if the sender is not the owner.\n */\n function _checkOwner() internal view virtual {\n if (owner() != _msgSender()) {\n revert OwnableUnauthorizedAccount(_msgSender());\n }\n }\n\n /**\n * @dev Leaves the contract without owner. It will not be possible to call\n * `onlyOwner` functions. Can only be called by the current owner.\n *\n * NOTE: Renouncing ownership will leave the contract without an owner,\n * thereby disabling any functionality that is only available to the owner.\n */\n function renounceOwnership() public virtual onlyOwner {\n _transferOwnership(address(0));\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Can only be called by the current owner.\n */\n function transferOwnership(address newOwner) public virtual onlyOwner {\n if (newOwner == address(0)) {\n revert OwnableInvalidOwner(address(0));\n }\n _transferOwnership(newOwner);\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Internal function without access restriction.\n */\n function _transferOwnership(address newOwner) internal virtual {\n address oldOwner = _owner;\n _owner = newOwner;\n emit OwnershipTransferred(oldOwner, newOwner);\n }\n}\n"
},
"@openzeppelin/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)\n\npragma solidity >=0.4.16;\n\n/**\n * @dev Interface of the ERC-20 standard as defined in the ERC.\n */\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the value of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the value of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\n * allowance mechanism. `value` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 value) external returns (bool);\n}\n"
},
"@openzeppelin/contracts/utils/Context.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)\n\npragma solidity ^0.8.20;\n\n/**\n * @dev Provides information about the current execution context, including the\n * sender of the transaction and its data. While these are generally available\n * via msg.sender and msg.data, they should not be accessed in such a direct\n * manner, since when dealing with meta-transactions the account sending and\n * paying for execution may not be the actual sender (as far as an application\n * is concerned).\n *\n * This contract is only required for intermediate, library-like contracts.\n */\nabstract contract Context {\n function _msgSender() internal view virtual returns (address) {\n return msg.sender;\n }\n\n function _msgData() internal view virtual returns (bytes calldata) {\n return msg.data;\n }\n\n function _contextSuffixLength() internal view virtual returns (uint256) {\n return 0;\n }\n}\n"
},
"contracts/Marketplace.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\nimport {Initializable} from \"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol\";\nimport {Ownable2StepUpgradeable} from \"@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol\";\nimport {ReentrancyGuardUpgradeable} from \"@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol\";\nimport {PausableUpgradeable} from \"@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IMockMobRule} from \"./interfaces/IMockMobRule.sol\";\n\n/**\n * @title Marketplace\n * @notice Decentralized marketplace for digital and physical goods on Asset Hub EVM\n * @dev Upgradeable contract with escrow, encrypted key storage, and multi-asset support\n */\ncontract Marketplace is Initializable, Ownable2StepUpgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable {\n /// @notice Version of the contract\n string public constant VERSION = \"1.0.0\";\n\n /// @notice Dispute period duration (48 hours)\n uint256 public constant DISPUTE_PERIOD = 48 hours;\n\n /// @notice Delivery timeout duration (14 days)\n uint256 public constant DELIVERY_TIMEOUT = 14 days;\n\n /// @notice Post-delivery dispute window (2 days)\n uint256 public constant POST_DELIVERY_TIMEOUT = 2 days;\n\n /// @notice Native asset address (0x0)\n address public constant NATIVE_ASSET = address(0);\n\n /**\n * Note: enums here have corresponding types in packages/types/src/item.ts\n * keep them in sync\n */\n\n enum ItemType {\n DIGITAL,\n PHYSICAL\n }\n\n enum PurchaseStatus {\n PENDING,\n COMPLETED,\n DISPUTED,\n REFUNDED,\n SHIPPED,\n DELIVERED\n }\n\n struct Item {\n uint256 id;\n address seller;\n ItemType itemType;\n string metadataIPFSHash; // JSON metadata with description, images, etc.\n uint256 price;\n address paymentAsset; // address(0) for native token, ERC20 address otherwise\n bool active;\n uint256 createdAt;\n string name; // Item title (max 200 chars)\n string category; // Lowercase category tag (max 50 chars)\n }\n\n struct Purchase {\n uint256 id;\n uint256 itemId;\n address buyer;\n address seller;\n uint256 amount;\n address paymentAsset;\n uint256 purchaseTime;\n uint256 releaseTime;\n PurchaseStatus status;\n // Shipping fields\n bytes32 deliveryCode; // Hash of delivery confirmation code (0x0 if not set)\n uint256 deliveryTimeout; // Block timestamp when delivery should be complete\n uint256 postDeliveryTimeout; // Block timestamp when post-delivery disputes end\n bool deliveryConfirmed; // True if buyer confirmed delivery\n bool disputePaused; // True if dispute pauses seller's ability to claim\n uint256 shippingTimestamp; // Block timestamp when delivery code was set (0 if not shipped)\n uint256 deliveryTimestamp; // Block timestamp when delivery was confirmed (0 if not confirmed)\n // Dispute fields\n uint256 disputeCaseId; // Case ID from MockMobRule (0 = no dispute, >0 = active dispute)\n string disputeMetadataCID; // IPFS CID from Bulletin Chain containing initiator's evidence (empty if no dispute)\n string counterEvidenceCID; // IPFS CID from Bulletin Chain containing counter-evidence from other party (empty if not submitted)\n address disputeInitiator; // Who raised the dispute (address(0) if no dispute, buyer/seller if dispute exists)\n bytes encryptedShippingAddress; // Encrypted shipping address (ECDH with seller's public key)\n string buyerPublicKey; // Buyer's X25519 public key (hex string) for decryption\n bytes encryptedTrackingInfo; // Encrypted tracking info in format \"CARRIER;TrackingNumber\" (ECDH with buyer's public key)\n }\n\n /// @notice Counter for item IDs\n uint256 private _nextItemId;\n\n /// @notice Counter for purchase IDs\n uint256 private _nextPurchaseId;\n\n /// @notice Mapping of item ID to Item\n mapping(uint256 => Item) public items;\n\n /// @notice Mapping of purchase ID to Purchase\n mapping(uint256 => Purchase) public purchases;\n\n /// @notice Mapping of item ID => buyer => purchased\n mapping(uint256 => mapping(address => bool)) public hasPurchased;\n\n /// @notice Mapping of allowed payment assets\n mapping(address => bool) public allowedAssets;\n\n /// @notice Address of MockMobRule contract for dispute resolution\n IMockMobRule public mockMobRule;\n\n // Events\n event ItemCreated(\n uint256 indexed itemId,\n address indexed seller,\n ItemType itemType,\n string name,\n string category,\n uint256 price,\n address paymentAsset\n );\n\n event ItemUpdated(uint256 indexed itemId, string name, string category, string metadataIPFSHash, uint256 price);\n\n event ItemDeactivated(uint256 indexed itemId);\n\n event ItemPurchased(\n uint256 indexed purchaseId,\n uint256 indexed itemId,\n address indexed buyer,\n address seller,\n uint256 amount\n );\n\n event FundsReleased(uint256 indexed purchaseId, address indexed seller, uint256 amount);\n\n event DisputeRaised(\n uint256 indexed purchaseId,\n uint256 indexed caseId,\n address indexed initiator,\n string disputeMetadataCID\n );\n\n event DisputeEvidenceAdded(uint256 indexed purchaseId, address indexed submitter, string counterEvidenceCID);\n\n event DeliveryCodeSet(uint256 indexed purchaseId, address indexed seller);\n\n event TrackingInfoSet(uint256 indexed purchaseId, address indexed seller);\n\n event DeliveryConfirmed(uint256 indexed purchaseId, address indexed buyer);\n\n event PaymentAssetAdded(address indexed asset);\n\n event PaymentAssetRemoved(address indexed asset);\n\n event MockMobRuleUpdated(address indexed newAddress, address indexed oldAddress);\n\n struct Shop {\n address owner;\n string name;\n string metadataCID;\n string publicKey;\n uint256 registeredAt;\n bool active;\n }\n\n mapping(address => Shop) private shops;\n\n event ShopRegistered(address indexed owner, string name, string metadataCID, string publicKey);\n event ShopUpdated(address indexed owner, string name, string metadataCID, string publicKey);\n\n /// @notice Category tracking for autocomplete/discovery\n mapping(string => uint256) public categoryUsageCount;\n string[] private _allCategories;\n mapping(string => bool) private _categoryExists;\n\n /// @custom:oz-upgrades-unsafe-allow constructor\n constructor() {\n _disableInitializers();\n }\n\n /**\n * @notice Initialize the contract\n * @param owner_ Initial owner address\n */\n function initialize(address owner_) public initializer {\n __Ownable_init(owner_);\n __ReentrancyGuard_init();\n __Pausable_init();\n\n // Native asset is always allowed\n allowedAssets[NATIVE_ASSET] = true;\n\n _nextItemId = 1;\n _nextPurchaseId = 1;\n }\n\n /**\n * @dev Normalize category string to lowercase for consistent matching.\n *\n * ASCII Table Reference:\n * - Uppercase letters A-Z have codes 65-90 (0x41-0x5A)\n * - Lowercase letters a-z have codes 97-122 (0x61-0x7A)\n * - The difference between cases is exactly 32 (0x20)\n *\n * Example: 'A' (65) + 32 = 'a' (97)\n *\n * This ensures \"Electronics\", \"ELECTRONICS\", and \"electronics\" all map\n * to \"electronics\" for consistent category filtering and discovery.\n *\n * @param category_ Input category string (any case)\n * @return Lowercase category string\n */\n function _normalizeCategory(string memory category_) private pure returns (string memory) {\n bytes memory categoryBytes = bytes(category_);\n bytes memory lowerBytes = new bytes(categoryBytes.length);\n\n for (uint256 i = 0; i < categoryBytes.length; i++) {\n bytes1 char = categoryBytes[i];\n // Check if character is uppercase A-Z (ASCII 65-90 / 0x41-0x5A)\n // If so, add 32 to convert to lowercase a-z (ASCII 97-122 / 0x61-0x7A)\n if (char >= 0x41 && char <= 0x5A) {\n lowerBytes[i] = bytes1(uint8(char) + 32);\n } else {\n lowerBytes[i] = char;\n }\n }\n\n return string(lowerBytes);\n }\n\n /**\n * @dev Track category usage for discovery\n * @param category_ Normalized category string\n */\n function _trackCategory(string memory category_) private {\n if (!_categoryExists[category_]) {\n _allCategories.push(category_);\n _categoryExists[category_] = true;\n }\n categoryUsageCount[category_]++;\n }\n\n /**\n * @notice Create a new item listing\n * @param itemType_ Type of item (DIGITAL or PHYSICAL)\n * @param name_ Item title (max 200 characters)\n * @param category_ Category tag (will be normalized to lowercase, max 50 chars)\n * @param metadataIPFSHash_ IPFS hash of item metadata JSON\n * @param price_ Price in payment asset\n * @param paymentAsset_ Payment asset address (0x0 for native)\n * @return itemId The ID of the created item\n */\n function createItem(\n ItemType itemType_,\n string memory name_,\n string memory category_,\n string memory metadataIPFSHash_,\n uint256 price_,\n address paymentAsset_\n ) external whenNotPaused returns (uint256) {\n require(bytes(name_).length > 0 && bytes(name_).length <= 200, \"Name must be 1-200 chars\");\n require(bytes(category_).length > 0 && bytes(category_).length <= 50, \"Category must be 1-50 chars\");\n require(this.isShopRegistered(msg.sender), \"Shop not registered\");\n require(bytes(metadataIPFSHash_).length > 0, \"Invalid metadata hash\");\n require(price_ > 0, \"Price must be greater than 0\");\n require(allowedAssets[paymentAsset_], \"Payment asset not allowed\");\n\n // Normalize category to lowercase\n string memory normalizedCategory = _normalizeCategory(category_);\n\n uint256 itemId = _nextItemId++;\n\n items[itemId] = Item({\n id: itemId,\n seller: msg.sender,\n itemType: itemType_,\n name: name_,\n category: normalizedCategory,\n metadataIPFSHash: metadataIPFSHash_,\n price: price_,\n paymentAsset: paymentAsset_,\n active: true,\n createdAt: block.timestamp\n });\n\n // Track category for discovery\n _trackCategory(normalizedCategory);\n\n emit ItemCreated(itemId, msg.sender, itemType_, name_, normalizedCategory, price_, paymentAsset_);\n\n return itemId;\n }\n\n /**\n * @notice Update an existing item\n * @param itemId_ Item ID to update\n * @param name_ New item title\n * @param category_ New category tag\n * @param metadataIPFSHash_ New IPFS hash of metadata\n * @param price_ New price\n */\n function updateItem(\n uint256 itemId_,\n string memory name_,\n string memory category_,\n string memory metadataIPFSHash_,\n uint256 price_\n ) external {\n Item storage item = items[itemId_];\n require(item.seller == msg.sender, \"Not item owner\");\n require(item.active, \"Item not active\");\n require(bytes(name_).length > 0 && bytes(name_).length <= 200, \"Name must be 1-200 chars\");\n require(bytes(category_).length > 0 && bytes(category_).length <= 50, \"Category must be 1-50 chars\");\n require(bytes(metadataIPFSHash_).length > 0, \"Invalid metadata hash\");\n require(price_ > 0, \"Price must be greater than 0\");\n\n // Normalize new category\n string memory normalizedCategory = _normalizeCategory(category_);\n\n // Update category tracking if category changed\n if (keccak256(bytes(item.category)) != keccak256(bytes(normalizedCategory))) {\n // Decrement old category count\n if (categoryUsageCount[item.category] > 0) {\n categoryUsageCount[item.category]--;\n }\n // Track new category\n _trackCategory(normalizedCategory);\n }\n\n item.name = name_;\n item.category = normalizedCategory;\n item.metadataIPFSHash = metadataIPFSHash_;\n item.price = price_;\n\n emit ItemUpdated(itemId_, name_, normalizedCategory, metadataIPFSHash_, price_);\n }\n\n /**\n * @notice Deactivate an item\n * @param itemId_ Item ID to deactivate\n */\n function deactivateItem(uint256 itemId_) external {\n Item storage item = items[itemId_];\n require(item.seller == msg.sender, \"Not item owner\");\n require(item.active, \"Item already inactive\");\n\n item.active = false;\n\n emit ItemDeactivated(itemId_);\n }\n\n /**\n * @notice Purchase an item\n * @dev Digital goods: payment sent directly to seller (instant delivery)\n * @dev Physical goods: payment held in escrow until buyer confirms delivery\n * @param itemId_ Item ID to purchase\n * @param encryptedShippingAddress_ Encrypted shipping address (only for physical items, empty for digital)\n * @param buyerPublicKey_ Buyer's X25519 public key (hex string) for decryption (only for physical items, not implemented yet for digital)\n * @return purchaseId The ID of the purchase\n */\n function purchaseItem(\n uint256 itemId_,\n bytes calldata encryptedShippingAddress_,\n string calldata buyerPublicKey_\n ) external payable nonReentrant whenNotPaused returns (uint256) {\n Item memory item = items[itemId_];\n require(item.active, \"Item not active\");\n require(item.seller != msg.sender, \"Cannot buy own item\");\n\n // Handle payment\n if (item.paymentAsset == NATIVE_ASSET) {\n require(msg.value == item.price, \"Incorrect payment amount\");\n } else {\n require(msg.value == 0, \"No native payment needed\");\n require(\n IERC20(item.paymentAsset).transferFrom(msg.sender, address(this), item.price),\n \"Payment transfer failed\"\n );\n }\n\n uint256 purchaseId = _nextPurchaseId++;\n\n // @TODO update this once digital item encryption is implemented\n // Digital goods: Temporarily instant payment to seller (no escrow needed), immediately completed\n // Physical goods: escrow until buyer confirms delivery\n bool isDigital = item.itemType == ItemType.DIGITAL;\n PurchaseStatus initialStatus = isDigital ? PurchaseStatus.COMPLETED : PurchaseStatus.PENDING;\n\n // Validate shipping address: required for physical items, must be empty for digital items\n if (isDigital) {\n require(encryptedShippingAddress_.length == 0, \"Shipping address not needed for digital items\");\n // for future item decryption (not implemented yet) ?\n // require(bytes(buyerPublicKey_).length > 0, \"Buyer public key required for digital items\");\n } else {\n require(encryptedShippingAddress_.length > 0, \"Shipping address required for physical items\");\n require(bytes(buyerPublicKey_).length > 0, \"Buyer public key required for physical items\");\n }\n\n purchases[purchaseId] = Purchase({\n id: purchaseId,\n itemId: itemId_,\n buyer: msg.sender,\n seller: item.seller,\n amount: item.price,\n paymentAsset: item.paymentAsset,\n purchaseTime: block.timestamp,\n releaseTime: isDigital ? 0 : block.timestamp + DISPUTE_PERIOD,\n status: initialStatus,\n // Initialize shipping fields\n deliveryCode: bytes32(0),\n deliveryTimeout: 0,\n postDeliveryTimeout: 0,\n deliveryConfirmed: isDigital, // Digital goods are immediately delivered\n disputePaused: false,\n shippingTimestamp: 0,\n deliveryTimestamp: 0,\n // Initialize dispute fields\n disputeCaseId: 0,\n disputeMetadataCID: \"\",\n counterEvidenceCID: \"\",\n disputeInitiator: address(0),\n encryptedShippingAddress: encryptedShippingAddress_, // Store encrypted shipping address\n buyerPublicKey: buyerPublicKey_, // Store buyer's public key for decryption\n encryptedTrackingInfo: \"\" // Initialize empty, will be set when seller ships\n });\n\n // Record purchase\n hasPurchased[itemId_][msg.sender] = true;\n\n emit ItemPurchased(purchaseId, itemId_, msg.sender, item.seller, item.price);\n\n // For digital goods, send payment immediately to seller\n if (isDigital) {\n _transferFunds(item.seller, item.price, item.paymentAsset);\n emit FundsReleased(purchaseId, item.seller, item.price);\n }\n\n // For physical goods, the funds are held in escrow until the buyer confirms delivery\n // the escrow logic is implicit\n\n return purchaseId;\n }\n\n /**\n * @notice Release funds to seller after delivery timeout (if buyer didn't confirm)\n * @param purchaseId_ Purchase ID\n * @dev Only for physical goods - digital goods release funds immediately on purchase\n * @dev Can only be called if:\n * - Item has been shipped (status SHIPPED)\n * - Delivery timeout has passed\n * - Buyer hasn't confirmed delivery\n * - No active dispute\n */\n function releaseFunds(uint256 purchaseId_) external nonReentrant {\n Purchase storage purchase = purchases[purchaseId_];\n require(purchase.status == PurchaseStatus.SHIPPED, \"Item not shipped\");\n require(block.timestamp >= purchase.deliveryTimeout, \"Delivery timeout not reached\");\n require(!purchase.deliveryConfirmed, \"Delivery already confirmed\");\n require(!purchase.disputePaused, \"Dispute active - cannot release\");\n\n purchase.status = PurchaseStatus.COMPLETED;\n\n // Transfer funds to seller\n _transferFunds(purchase.seller, purchase.amount, purchase.paymentAsset);\n\n emit FundsReleased(purchaseId_, purchase.seller, purchase.amount);\n }\n\n /**\n * @notice Get item details\n * @param itemId_ Item ID\n * @return Item struct\n */\n function getItem(uint256 itemId_) external view returns (Item memory) {\n return items[itemId_];\n }\n\n /**\n * @notice Get purchase details\n * @param purchaseId_ Purchase ID\n * @return Purchase struct\n */\n function getPurchase(uint256 purchaseId_) external view returns (Purchase memory) {\n return purchases[purchaseId_];\n }\n\n /**\n * @notice Seller sets delivery confirmation code after shipping\n * @param purchaseId_ Purchase ID\n * @param deliveryCode_ Delivery confirmation code (will be hashed on-chain)\n * @dev Only seller can call, only for physical goods, only once\n */\n function setDeliveryCode(uint256 purchaseId_, string memory deliveryCode_) external {\n Purchase storage purchase = purchases[purchaseId_];\n require(purchase.seller == msg.sender, \"Only seller can set code\");\n require(purchase.status == PurchaseStatus.PENDING, \"Purchase not pending\");\n\n Item memory item = items[purchase.itemId];\n require(item.itemType == ItemType.PHYSICAL, \"Only for physical goods\");\n require(purchase.deliveryCode == bytes32(0), \"Code already set\");\n\n // Hash the code (keccak256) for storage\n purchase.deliveryCode = keccak256(abi.encodePacked(deliveryCode_));\n purchase.status = PurchaseStatus.SHIPPED;\n purchase.deliveryTimeout = block.timestamp + DELIVERY_TIMEOUT;\n purchase.shippingTimestamp = block.timestamp;\n\n emit DeliveryCodeSet(purchaseId_, purchase.seller);\n }\n\n /**\n * @notice Seller sets delivery code and tracking info in one transaction\n * @param purchaseId_ Purchase ID\n * @param deliveryCode_ Delivery confirmation code\n * @param encryptedTrackingInfo_ Encrypted tracking info in format \"CARRIER;TrackingNumber\"\n * @dev Only seller can call, only for physical goods\n * @dev This is a convenience function that combines setDeliveryCode and setTrackingInfo\n */\n function setDeliveryCodeAndTrackingInfo(\n uint256 purchaseId_,\n string memory deliveryCode_,\n bytes memory encryptedTrackingInfo_\n ) external {\n Purchase storage purchase = purchases[purchaseId_];\n require(purchase.seller == msg.sender, \"Only seller can set code and tracking info\");\n require(purchase.status == PurchaseStatus.PENDING, \"Purchase not pending\");\n\n Item memory item = items[purchase.itemId];\n require(item.itemType == ItemType.PHYSICAL, \"Only for physical goods\");\n require(purchase.deliveryCode == bytes32(0), \"Code already set\");\n require(encryptedTrackingInfo_.length > 0, \"Tracking info cannot be empty\");\n // not sure if we should allow overwriting the tracking info\n require(purchase.encryptedTrackingInfo.length == 0, \"Tracking info already set\");\n\n // Hash the code (keccak256) for storage\n purchase.deliveryCode = keccak256(abi.encodePacked(deliveryCode_));\n purchase.status = PurchaseStatus.SHIPPED;\n purchase.deliveryTimeout = block.timestamp + DELIVERY_TIMEOUT;\n purchase.shippingTimestamp = block.timestamp;\n\n // Set encrypted tracking info\n purchase.encryptedTrackingInfo = encryptedTrackingInfo_;\n\n emit DeliveryCodeSet(purchaseId_, purchase.seller);\n emit TrackingInfoSet(purchaseId_, purchase.seller);\n }\n\n /**\n * @notice Buyer confirms delivery with confirmation code\n * @param purchaseId_ Purchase ID\n * @param deliveryCode_ Delivery confirmation code\n * @dev Releases funds immediately to seller\n * @dev Only for physical goods - digital goods release funds immediately on purchase\n */\n function confirmDelivery(uint256 purchaseId_, string memory deliveryCode_) external nonReentrant {\n Purchase storage purchase = purchases[purchaseId_];\n require(msg.sender == purchase.buyer, \"Only buyer can confirm\");\n require(purchase.status == PurchaseStatus.SHIPPED, \"Item not shipped\");\n require(!purchase.deliveryConfirmed, \"Already confirmed\");\n // can the buyer confirm delivery after the delivery timeout?\n require(block.timestamp <= purchase.deliveryTimeout, \"Delivery timeout passed\");\n\n // Verify code matches\n bytes32 codeHash = keccak256(abi.encodePacked(deliveryCode_));\n require(purchase.deliveryCode == codeHash, \"Invalid delivery code\");\n\n purchase.deliveryConfirmed = true;\n purchase.status = PurchaseStatus.COMPLETED;\n // Set post-delivery timeout window (2 days after delivery confirmation for disputes)\n purchase.postDeliveryTimeout = block.timestamp + POST_DELIVERY_TIMEOUT;\n purchase.deliveryTimestamp = block.timestamp;\n\n // Release funds\n _transferFunds(purchase.seller, purchase.amount, purchase.paymentAsset);\n\n emit DeliveryConfirmed(purchaseId_, purchase.buyer);\n emit FundsReleased(purchaseId_, purchase.seller, purchase.amount);\n }\n\n /**\n * @notice Set MockMobRule contract address for dispute resolution\n * @param mockMobRule_ Address of MockMobRule contract\n * @dev Validates that address is a contract and implements IMockMobRule interface\n */\n function setMockMobRule(address mockMobRule_) external onlyOwner {\n require(mockMobRule_ != address(0), \"Invalid address\");\n require(mockMobRule_.code.length > 0, \"Not a contract\");\n\n // Verify interface by calling a view function\n // This will revert if interface doesn't match\n IMockMobRule(mockMobRule_).getTotalCases();\n\n address oldAddress = address(mockMobRule);\n mockMobRule = IMockMobRule(mockMobRule_);\n emit MockMobRuleUpdated(mockMobRule_, oldAddress);\n }\n\n /**\n * @notice Raise a dispute for a purchase\n * @param purchaseId_ The purchase ID to dispute\n * @param disputeMetadataCID_ IPFS CID from Bulletin Chain containing full dispute metadata\n * @return caseId The case ID from MockMobRule\n * @dev Only buyer or seller can raise dispute. First one to call wins.\n * @dev Purchase must not be finalized (COMPLETED or REFUNDED)\n * @dev MockMobRule must be set before calling this function\n */\n function raiseDispute(\n uint256 purchaseId_,\n string memory disputeMetadataCID_\n ) external nonReentrant returns (uint256 caseId) {\n // Get purchase\n Purchase storage purchase = purchases[purchaseId_];\n require(purchase.id != 0, \"Purchase does not exist\");\n\n // Check no existing dispute\n require(purchase.disputeCaseId == 0, \"Dispute already exists\");\n\n // Check caller is buyer or seller\n require(msg.sender == purchase.buyer || msg.sender == purchase.seller, \"Not buyer or seller\");\n\n // Check MockMobRule is set\n require(address(mockMobRule) != address(0), \"MockMobRule not set\");\n\n // Verify MockMobRule is still valid (address hasn't changed)\n // This ensures connection is still valid\n try mockMobRule.getTotalCases() returns (uint256) {\n // Connection is valid, continue\n } catch {\n revert(\"MockMobRule connection failed\");\n }\n\n // Check CID not empty\n require(bytes(disputeMetadataCID_).length > 0, \"CID required\");\n\n // Check purchase not finalized\n require(\n purchase.status != PurchaseStatus.COMPLETED && purchase.status != PurchaseStatus.REFUNDED,\n \"Purchase finalized\"\n );\n\n // Call MockMobRule.createDispute()\n caseId = mockMobRule.createDispute(\n purchaseId_,\n purchase.buyer,\n purchase.seller,\n msg.sender, // initiator\n disputeMetadataCID_\n );\n\n // Update purchase\n purchase.disputeCaseId = caseId;\n purchase.disputeMetadataCID = disputeMetadataCID_;\n purchase.disputeInitiator = msg.sender;\n purchase.status = PurchaseStatus.DISPUTED;\n\n // Emit event\n emit DisputeRaised(purchaseId_, caseId, msg.sender, disputeMetadataCID_);\n\n return caseId;\n }\n\n /**\n * @notice Add counter-evidence to an existing dispute (for the other party)\n * @param purchaseId_ The purchase ID with an active dispute\n * @param counterEvidenceCID_ IPFS CID from Bulletin Chain containing counter-evidence metadata\n * @dev Only the non-initiator party (buyer or seller) can add counter-evidence\n * @dev Dispute must exist and not be resolved\n */\n function addDisputeEvidence(uint256 purchaseId_, string memory counterEvidenceCID_) external nonReentrant {\n Purchase storage purchase = purchases[purchaseId_];\n require(purchase.id != 0, \"Purchase does not exist\");\n require(purchase.disputeCaseId != 0, \"No active dispute\");\n require(purchase.status == PurchaseStatus.DISPUTED, \"Dispute not active\");\n require(bytes(counterEvidenceCID_).length > 0, \"CID required\");\n\n // Check caller is buyer or seller\n require(msg.sender == purchase.buyer || msg.sender == purchase.seller, \"Not buyer or seller\");\n\n // Check caller is NOT the initiator (only the other party can add counter-evidence)\n require(msg.sender != purchase.disputeInitiator, \"Initiator cannot add counter-evidence\");\n\n // Check MockMobRule is set\n require(address(mockMobRule) != address(0), \"MockMobRule not set\");\n\n // Call MockMobRule.addCounterEvidence() to store counter-evidence\n mockMobRule.addCounterEvidence(purchase.disputeCaseId, counterEvidenceCID_);\n\n // Also store in Purchase for quick access (optional, can be removed if not needed)\n purchase.counterEvidenceCID = counterEvidenceCID_;\n\n // Emit event\n emit DisputeEvidenceAdded(purchaseId_, msg.sender, counterEvidenceCID_);\n }\n\n /**\n * @notice Get dispute status for a purchase\n * @param purchaseId_ The purchase ID\n * @return hasDispute True if purchase has an active dispute\n * @return caseId The case ID (0 if no dispute)\n * @return verdict The verdict (0 = Pending, 1 = InitiatorWins, 2 = InitiatorLoses)\n * @return isResolved True if dispute has been resolved (verdict != Pending)\n * @return initiator The address that initiated the dispute\n * @return winner The address of the winner (address(0) if pending or no dispute)\n */\n function getDisputeStatus(\n uint256 purchaseId_\n )\n external\n view\n returns (bool hasDispute, uint256 caseId, uint8 verdict, bool isResolved, address initiator, address winner)\n {\n Purchase memory purchase = purchases[purchaseId_];\n require(purchase.id != 0, \"Purchase does not exist\");\n\n hasDispute = purchase.disputeCaseId != 0;\n\n if (!hasDispute) {\n return (false, 0, 0, false, address(0), address(0));\n }\n\n caseId = purchase.disputeCaseId;\n initiator = purchase.disputeInitiator;\n\n // Query MockMobRule for verdict\n require(address(mockMobRule) != address(0), \"MockMobRule not set\");\n\n try mockMobRule.getVerdict(caseId) returns (IMockMobRule.Verdict verdict_) {\n verdict = uint8(verdict_);\n isResolved = verdict_ != IMockMobRule.Verdict.Pending;\n\n // Determine winner based on verdict\n if (isResolved) {\n if (verdict_ == IMockMobRule.Verdict.InitiatorWins) {\n winner = initiator;\n } else {\n // InitiatorLoses\n // Other party wins\n winner = (initiator == purchase.buyer) ? purchase.seller : purchase.buyer;\n }\n } else {\n winner = address(0); // Pending, no winner yet\n }\n } catch {\n // If query fails, assume pending\n verdict = 0; // Pending\n isResolved = false;\n winner = address(0);\n }\n\n return (hasDispute, caseId, verdict, isResolved, initiator, winner);\n }\n\n /**\n * @notice Claim dispute resolution funds (only winner can call)\n * @param purchaseId_ The purchase ID\n * @dev Transfers funds to winner based on verdict:\n * - If buyer wins: refund buyer (status = REFUNDED)\n * - If seller wins: release to seller (status = COMPLETED)\n */\n function claimDisputeResolution(uint256 purchaseId_) external nonReentrant {\n Purchase storage purchase = purchases[purchaseId_];\n require(purchase.id != 0, \"Purchase does not exist\");\n require(purchase.disputeCaseId != 0, \"No dispute\");\n require(purchase.status == PurchaseStatus.DISPUTED, \"Dispute not active\");\n\n (bool hasDispute, , , bool isResolved, , address winner) = this.getDisputeStatus(purchaseId_);\n\n require(hasDispute, \"No dispute\");\n require(isResolved, \"Dispute not resolved\");\n require(winner != address(0), \"No winner determined\");\n require(msg.sender == winner, \"Not winner\");\n\n if (winner == purchase.buyer) {\n purchase.status = PurchaseStatus.REFUNDED;\n } else {\n purchase.status = PurchaseStatus.COMPLETED;\n }\n\n _transferFunds(winner, purchase.amount, purchase.paymentAsset);\n\n emit FundsReleased(purchaseId_, winner, purchase.amount);\n }\n\n /**\n * @notice Get total number of items created\n * @return Total item count\n */\n function getTotalItems() external view returns (uint256) {\n return _nextItemId > 0 ? _nextItemId - 1 : 0;\n }\n\n /**\n * @notice Get all active items (paginated)\n * @param offset_ Starting index\n * @param limit_ Maximum number of items to return\n * @return Array of active items\n */\n function getActiveItems(uint256 offset_, uint256 limit_) external view returns (Item[] memory) {\n require(limit_ > 0 && limit_ <= 100, \"Limit must be between 1 and 100\");\n\n // Count active items\n uint256 activeCount = 0;\n for (uint256 i = 1; i < _nextItemId; i++) {\n if (items[i].active) {\n activeCount++;\n }\n }\n\n // Calculate result size\n uint256 start = offset_ < activeCount ? offset_ : activeCount;\n uint256 end = start + limit_ < activeCount ? start + limit_ : activeCount;\n uint256 resultSize = end > start ? end - start : 0;\n\n Item[] memory result = new Item[](resultSize);\n if (resultSize == 0) return result;\n\n // Collect active items\n uint256 currentIndex = 0;\n uint256 resultIndex = 0;\n\n for (uint256 i = 1; i < _nextItemId && resultIndex < resultSize; i++) {\n if (items[i].active) {\n if (currentIndex >= start) {\n result[resultIndex] = items[i];\n resultIndex++;\n }\n currentIndex++;\n }\n }\n\n return result;\n }\n\n /**\n * @notice Get items by seller (paginated)\n * @param seller_ Seller address\n * @param offset_ Starting index\n * @param limit_ Maximum number of items to return\n * @return Array of items created by seller\n */\n function getItemsBySeller(address seller_, uint256 offset_, uint256 limit_) external view returns (Item[] memory) {\n require(limit_ > 0 && limit_ <= 100, \"Limit must be between 1 and 100\");\n\n // Count seller's items\n uint256 sellerCount = 0;\n for (uint256 i = 1; i < _nextItemId; i++) {\n if (items[i].seller == seller_) {\n sellerCount++;\n }\n }\n\n // Calculate result size\n uint256 start = offset_ < sellerCount ? offset_ : sellerCount;\n uint256 end = start + limit_ < sellerCount ? start + limit_ : sellerCount;\n uint256 resultSize = end > start ? end - start : 0;\n\n Item[] memory result = new Item[](resultSize);\n if (resultSize == 0) return result;\n\n // Collect seller's items\n uint256 currentIndex = 0;\n uint256 resultIndex = 0;\n\n for (uint256 i = 1; i < _nextItemId && resultIndex < resultSize; i++) {\n if (items[i].seller == seller_) {\n if (currentIndex >= start) {\n result[resultIndex] = items[i];\n resultIndex++;\n }\n currentIndex++;\n }\n }\n\n return result;\n }\n\n /**\n * @notice Get items by category (paginated)\n * @param category_ Category to filter by (case-insensitive)\n * @param offset_ Starting index\n * @param limit_ Maximum number of items to return\n * @return Array of items in the specified category\n */\n function getItemsByCategory(\n string memory category_,\n uint256 offset_,\n uint256 limit_\n ) external view returns (Item[] memory) {\n require(limit_ > 0 && limit_ <= 100, \"Limit must be between 1 and 100\");\n\n // Normalize category for comparison\n string memory normalizedCategory = _normalizeCategory(category_);\n\n // Count items in category\n uint256 categoryCount = 0;\n for (uint256 i = 1; i < _nextItemId; i++) {\n if (items[i].active && keccak256(bytes(items[i].category)) == keccak256(bytes(normalizedCategory))) {\n categoryCount++;\n }\n }\n\n // Calculate result size\n uint256 start = offset_ < categoryCount ? offset_ : categoryCount;\n uint256 end = start + limit_ < categoryCount ? start + limit_ : categoryCount;\n uint256 resultSize = end > start ? end - start : 0;\n\n Item[] memory result = new Item[](resultSize);\n if (resultSize == 0) return result;\n\n // Collect items in category\n uint256 currentIndex = 0;\n uint256 resultIndex = 0;\n\n for (uint256 i = 1; i < _nextItemId && resultIndex < resultSize; i++) {\n if (items[i].active && keccak256(bytes(items[i].category)) == keccak256(bytes(normalizedCategory))) {\n if (currentIndex >= start) {\n result[resultIndex] = items[i];\n resultIndex++;\n }\n currentIndex++;\n }\n }\n\n return result;\n }\n\n /**\n * @notice Get all categories that have been used\n * @return Array of all category strings\n */\n function getAllCategories() external view returns (string[] memory) {\n return _allCategories;\n }\n\n /**\n * @notice Get popular categories (with minimum usage threshold)\n * @param minUsage_ Minimum number of items in category\n * @return Array of popular category strings\n */\n function getPopularCategories(uint256 minUsage_) external view returns (string[] memory) {\n // Count categories that meet threshold\n uint256 popularCount = 0;\n for (uint256 i = 0; i < _allCategories.length; i++) {\n if (categoryUsageCount[_allCategories[i]] >= minUsage_) {\n popularCount++;\n }\n }\n\n // Collect popular categories\n string[] memory result = new string[](popularCount);\n uint256 resultIndex = 0;\n\n for (uint256 i = 0; i < _allCategories.length; i++) {\n if (categoryUsageCount[_allCategories[i]] >= minUsage_) {\n result[resultIndex] = _allCategories[i];\n resultIndex++;\n }\n }\n\n return result;\n }\n\n /**\n * @notice Add a payment asset\n * @param asset_ Asset address to add\n */\n function addPaymentAsset(address asset_) external onlyOwner {\n require(!allowedAssets[asset_], \"Asset already allowed\");\n allowedAssets[asset_] = true;\n emit PaymentAssetAdded(asset_);\n }\n\n /**\n * @notice Remove a payment asset\n * @param asset_ Asset address to remove\n */\n function removePaymentAsset(address asset_) external onlyOwner {\n require(asset_ != NATIVE_ASSET, \"Cannot remove native asset\");\n require(allowedAssets[asset_], \"Asset not allowed\");\n allowedAssets[asset_] = false;\n emit PaymentAssetRemoved(asset_);\n }\n\n /**\n * @notice Pause the contract (emergency)\n */\n function pause() external onlyOwner {\n _pause();\n }\n\n /**\n * @notice Unpause the contract\n */\n function unpause() external onlyOwner {\n _unpause();\n }\n\n /**\n * @dev Internal function to transfer funds\n */\n function _transferFunds(address to_, uint256 amount_, address asset_) private {\n if (asset_ == NATIVE_ASSET) {\n (bool success, ) = payable(to_).call{value: amount_}(\"\");\n require(success, \"Transfer failed\");\n } else {\n require(IERC20(asset_).transfer(to_, amount_), \"Transfer failed\");\n }\n }\n\n /**\n * @notice Register or update a shop for the caller\n * @param name_ Human readable shop name\n * @param metadataCID_ CID that points to the shop metadata JSON (logo, description, links, etc.)\n * @param publicKey_ X25519 public encryption key for ECDH (hex string)\n */\n function registerShop(string calldata name_, string calldata metadataCID_, string calldata publicKey_) external {\n require(bytes(name_).length > 0, \"Name required\");\n require(bytes(metadataCID_).length > 0, \"Invalid metadata CID\");\n require(bytes(publicKey_).length > 0, \"Public key required\");\n\n Shop storage shop = shops[msg.sender];\n bool isNewShop = shop.owner == address(0) || !shop.active;\n\n shop.owner = msg.sender;\n shop.name = name_;\n shop.metadataCID = metadataCID_;\n shop.publicKey = publicKey_;\n shop.registeredAt = block.timestamp;\n shop.active = true;\n\n if (isNewShop) {\n emit ShopRegistered(msg.sender, name_, metadataCID_, publicKey_);\n } else {\n emit ShopUpdated(msg.sender, name_, metadataCID_, publicKey_);\n }\n }\n\n /**\n * @notice Returns the shop struct for an owner\n */\n function getShop(address owner_) external view returns (Shop memory) {\n return shops[owner_];\n }\n\n /**\n * @notice Returns true if the owner has an active shop\n */\n function isShopRegistered(address owner_) external view returns (bool) {\n Shop memory shop = shops[owner_];\n return shop.owner != address(0) && shop.active;\n }\n}\n"
},
"contracts/MockMobRule.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\nimport {Ownable} from \"@openzeppelin/contracts/access/Ownable.sol\";\n\n/**\n * @title MockMobRule\n * @notice Mock dispute resolution contract simulating MobRule pallet for Phase 2\n * @dev Admin-resolved disputes - will be replaced with real MobRule in Phase 3\n * @author mark3t Team\n */\ncontract MockMobRule is Ownable {\n /// @notice Verdict types\n /// @dev Pending = not yet resolved, InitiatorWins = initiator's claim is valid, InitiatorLoses = initiator's claim rejected\n enum Verdict {\n Pending,\n InitiatorWins,\n InitiatorLoses\n }\n\n /// @notice Dispute case structure\n struct Case {\n uint256 caseId;\n uint256 purchaseId;\n address buyer;\n address seller;\n address initiator; // Who raised the dispute (buyer or seller)\n string disputeMetadataCID; // IPFS CID from Bulletin Chain containing full dispute metadata (title, description, evidence)\n string counterEvidenceCID; // IPFS CID from Bulletin Chain containing counter-evidence from the other party (empty if not submitted)\n Verdict verdict; // Pending until resolved\n uint256 createdAt;\n }\n\n /// @notice Counter for case IDs\n uint256 private _nextCaseId;\n\n /// @notice Mapping of case ID to Case\n mapping(uint256 => Case) public cases;\n\n /// @notice Mapping of purchase ID to case ID (one dispute per purchase)\n mapping(uint256 => uint256) public purchaseToCase;\n\n /// @notice Event emitted when a new dispute case is created\n /// @param caseId The case ID\n /// @param purchaseId The purchase ID\n /// @param buyer Buyer address\n /// @param seller Seller address\n /// @param initiator Who raised the dispute (buyer or seller)\n /// @param disputeMetadataCID IPFS CID from Bulletin Chain containing full dispute metadata\n event CaseCreated(\n uint256 indexed caseId,\n uint256 indexed purchaseId,\n address indexed buyer,\n address seller,\n address initiator,\n string disputeMetadataCID\n );\n\n /// @notice Event emitted when counter-evidence is added to a case\n /// @param caseId The case ID\n /// @param submitter The address that submitted counter-evidence\n /// @param counterEvidenceCID IPFS CID from Bulletin Chain containing counter-evidence metadata\n event CounterEvidenceAdded(uint256 indexed caseId, address indexed submitter, string counterEvidenceCID);\n\n /// @notice Event emitted when a case is resolved\n /// @param caseId The case ID\n /// @param verdict The verdict (1 = InitiatorWins, 2 = InitiatorLoses)\n /// @dev Admin dashboard can listen to this event for instant updates\n event CaseResolved(uint256 indexed caseId, Verdict indexed verdict);\n\n /**\n * @notice Initialize MockMobRule contract\n * @param initialOwner Address that will own the contract (admin)\n */\n constructor(address initialOwner) Ownable(initialOwner) {\n _nextCaseId = 1;\n }\n\n /**\n * @notice Create a new dispute case\n * @param purchaseId_ The purchase ID from Marketplace\n * @param buyer_ Buyer address\n * @param seller_ Seller address\n * @param initiator_ Who is raising the dispute (must be either buyer_ or seller_)\n * @param disputeMetadataCID_ IPFS CID from Bulletin Chain containing full dispute metadata (title, description, evidence)\n * @return caseId The ID of the created case\n */\n function createDispute(\n uint256 purchaseId_,\n address buyer_,\n address seller_,\n address initiator_,\n string memory disputeMetadataCID_\n ) external returns (uint256) {\n require(buyer_ != address(0), \"Invalid buyer\");\n require(seller_ != address(0), \"Invalid seller\");\n require(initiator_ != address(0), \"Invalid initiator\");\n require(initiator_ == buyer_ || initiator_ == seller_, \"Initiator must be buyer or seller\");\n require(bytes(disputeMetadataCID_).length > 0, \"CID required\");\n require(purchaseToCase[purchaseId_] == 0, \"Dispute exists\");\n\n uint256 caseId = _nextCaseId;\n ++_nextCaseId;\n cases[caseId] = Case({\n caseId: caseId,\n purchaseId: purchaseId_,\n buyer: buyer_,\n seller: seller_,\n initiator: initiator_,\n disputeMetadataCID: disputeMetadataCID_,\n counterEvidenceCID: \"\",\n verdict: Verdict.Pending,\n createdAt: block.timestamp\n });\n\n purchaseToCase[purchaseId_] = caseId;\n\n emit CaseCreated(caseId, purchaseId_, buyer_, seller_, initiator_, disputeMetadataCID_);\n\n return caseId;\n }\n\n /**\n * @notice Add counter-evidence to an existing dispute case\n * @param caseId_ The case ID\n * @param counterEvidenceCID_ IPFS CID from Bulletin Chain containing counter-evidence metadata\n * @dev Only the non-initiator party (buyer or seller) can add counter-evidence\n * @dev Can be called by Marketplace contract on behalf of users\n */\n function addCounterEvidence(uint256 caseId_, string memory counterEvidenceCID_) external {\n Case storage case_ = cases[caseId_];\n require(case_.caseId != 0, \"Case does not exist\");\n require(case_.verdict == Verdict.Pending, \"Case already resolved\");\n require(bytes(counterEvidenceCID_).length > 0, \"CID required\");\n require(bytes(case_.counterEvidenceCID).length == 0, \"Counter-evidence already submitted\");\n\n // Note: Marketplace contract verifies caller is buyer/seller and not initiator\n // We trust Marketplace's verification here\n\n case_.counterEvidenceCID = counterEvidenceCID_;\n\n // Emit event with Marketplace's msg.sender (the actual user)\n // Marketplace will pass the user address in the event\n emit CounterEvidenceAdded(caseId_, msg.sender, counterEvidenceCID_);\n }\n\n /**\n * @notice Resolve a dispute case (admin only)\n * @param caseId_ The case ID to resolve\n * @param verdict_ The verdict (1 = InitiatorWins, 2 = InitiatorLoses)\n * @dev Cannot set verdict back to Pending\n */\n function resolveCase(uint256 caseId_, Verdict verdict_) external onlyOwner {\n Case storage case_ = cases[caseId_];\n require(case_.caseId != 0, \"Case does not exist\");\n require(case_.verdict == Verdict.Pending, \"Case already resolved\");\n require(verdict_ != Verdict.Pending, \"Cannot set Pending\");\n\n case_.verdict = verdict_;\n\n emit CaseResolved(caseId_, verdict_);\n }\n\n /**\n * @notice Get verdict for a case\n * @param caseId_ The case ID\n * @return verdict The verdict (0 = Pending, 1 = InitiatorWins, 2 = InitiatorLoses)\n * @dev Frontend polls this function to check dispute status\n * @dev If verdict == Pending, case is still being resolved\n * @dev If verdict == InitiatorWins, initiator can claim funds\n * @dev If verdict == InitiatorLoses, non-initiator can claim funds\n */\n function getVerdict(uint256 caseId_) external view returns (Verdict verdict) {\n Case memory case_ = cases[caseId_];\n require(case_.caseId != 0, \"Case does not exist\");\n\n return case_.verdict;\n }\n\n /**\n * @notice Get full case information\n * @param caseId_ The case ID\n * @return case_ The full Case struct\n */\n function getCase(uint256 caseId_) external view returns (Case memory) {\n Case memory case_ = cases[caseId_];\n require(case_.caseId != 0, \"Case does not exist\");\n return case_;\n }\n\n /**\n * @notice Get case ID for a purchase\n * @param purchaseId_ The purchase ID\n * @return caseId The case ID (0 if no dispute exists)\n */\n function getCaseIdByPurchase(uint256 purchaseId_) external view returns (uint256) {\n return purchaseToCase[purchaseId_];\n }\n\n /**\n * @notice Get total number of cases\n * @return total The total number of cases created\n */\n function getTotalCases() external view returns (uint256) {\n return _nextCaseId > 0 ? _nextCaseId - 1 : 0;\n }\n}\n"
},
"contracts/interfaces/IMockMobRule.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\n/**\n * @title IMockMobRule\n * @notice Interface for MockMobRule contract\n * @dev Used by Marketplace to interact with MockMobRule without direct dependency\n */\ninterface IMockMobRule {\n /// @notice Verdict types\n enum Verdict {\n Pending,\n InitiatorWins,\n InitiatorLoses\n }\n\n /// @notice Dispute case structure\n struct Case {\n uint256 caseId;\n uint256 purchaseId;\n address buyer;\n address seller;\n address initiator;\n string disputeMetadataCID;\n string counterEvidenceCID;\n Verdict verdict;\n uint256 createdAt;\n }\n\n /**\n * @notice Create a new dispute case\n * @param purchaseId_ The purchase ID from Marketplace\n * @param buyer_ Buyer address\n * @param seller_ Seller address\n * @param initiator_ Who is raising the dispute (must be either buyer_ or seller_)\n * @param disputeMetadataCID_ IPFS CID from Bulletin Chain containing full dispute metadata\n * @return caseId The ID of the created case\n */\n function createDispute(\n uint256 purchaseId_,\n address buyer_,\n address seller_,\n address initiator_,\n string memory disputeMetadataCID_\n ) external returns (uint256);\n\n /**\n * @notice Add counter-evidence to an existing dispute case\n * @param caseId_ The case ID\n * @param counterEvidenceCID_ IPFS CID from Bulletin Chain containing counter-evidence metadata\n */\n function addCounterEvidence(uint256 caseId_, string memory counterEvidenceCID_) external;\n\n /**\n * @notice Resolve a dispute case (admin only)\n * @param caseId_ The case ID to resolve\n * @param verdict_ The verdict (1 = InitiatorWins, 2 = InitiatorLoses)\n */\n function resolveCase(uint256 caseId_, Verdict verdict_) external;\n\n /**\n * @notice Get verdict for a case\n * @param caseId_ The case ID\n * @return verdict The verdict (0 = Pending, 1 = InitiatorWins, 2 = InitiatorLoses)\n */\n function getVerdict(uint256 caseId_) external view returns (Verdict verdict);\n\n /**\n * @notice Get full case information\n * @param caseId_ The case ID\n * @return case_ The full Case struct\n */\n function getCase(uint256 caseId_) external view returns (Case memory);\n\n /**\n * @notice Get case ID for a purchase\n * @param purchaseId_ The purchase ID\n * @return caseId The case ID (0 if no dispute exists)\n */\n function getCaseIdByPurchase(uint256 purchaseId_) external view returns (uint256);\n\n /**\n * @notice Get total number of cases\n * @return total The total number of cases created\n */\n function getTotalCases() external view returns (uint256);\n}\n"
}
},
"settings": {
"outputSelection": {
"*": {
"": [
"ast"
],
"*": [
"evm.bytecode",
"abi",
"evm.methodIdentifiers",
"evm.deployedBytecode",
"storageLayout",
"metadata"
]
}
},
"optimizer": {
"enabled": true,
"details": {}
},
"metadata": {}
}
}