diff --git a/packages/hardhat/contracts/BoxManager.sol b/packages/hardhat/contracts/BoxManager.sol new file mode 100644 index 0000000..4728c4c --- /dev/null +++ b/packages/hardhat/contracts/BoxManager.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract BoxManager is Ownable { + + uint256 public DELIVERY_REPORT_DEADLINE = 60 * 60 * 24 * 4; + uint256 public nextBoxId; + IERC20 public token; + + mapping(uint256 => Box) public boxes; + mapping(uint256 => BoxConfiguration) public boxConfig; + mapping(uint256 => mapping(address => uint256)) public courierStakes; + mapping(address => uint256) public courierRewards; + mapping(address => bool) public courierBlacklist; + + enum BoxStatus { + EMPTY, + FULL, + DELIVERY, + DELIVERED + } + + struct BoxConfiguration { + uint256 stakeMin; + uint256 stakeMax; + uint256 bountyMin; + uint256 bountyMax; + } + + struct Box { + address operator; + address sender; + address receiver; + address courier; + uint256 bounty; + uint256 stake; + uint256 pickupTime; + string location; + BoxStatus status; + } + + constructor(address initialOwner, IERC20 _token) Ownable(initialOwner) { + token = _token; + } + + function registerBox(uint256 stakeMin, uint256 stakeMax, uint256 bountyMin, uint256 bountyMax) public { + require(stakeMin < stakeMax && bountyMin <= bountyMax, "Invalid stake or bounty amount"); + boxes[nextBoxId] = Box(msg.sender, address(0x0), address(0x0), address(0x0), 0, 0, 0, "", BoxStatus.EMPTY); + boxConfig[nextBoxId] = BoxConfiguration(stakeMin, stakeMax, bountyMin, bountyMax); + nextBoxId++; + } + + function sendPackage(uint256 boxId, string memory location, address receiver, uint256 requiredStake, uint256 bountyAmount) public payable { + require(isBoxRegistered(boxId), "Box not registered"); + require(requiredStake >= boxConfig[boxId].stakeMin && requiredStake <= boxConfig[boxId].stakeMax, "Stake amount outside allowed range"); + require(bountyAmount >= boxConfig[boxId].bountyMin && bountyAmount <= boxConfig[boxId].bountyMax, "Bounty amount outside allowed range"); + // courierStakes[boxId][msg.sender] = requiredStake; + boxes[boxId].sender = msg.sender; + boxes[boxId].receiver = receiver; + boxes[boxId].bounty = bountyAmount; + boxes[boxId].stake = requiredStake; + boxes[boxId].location = location; + boxes[boxId].status = BoxStatus.FULL; + + token.transferFrom(msg.sender, address(this), bountyAmount); + + } + + function stake(uint256 boxId) public payable { + require(isBoxRegistered(boxId), "Box not registered"); + // change bs to static state value + require(msg.value >= boxConfig[boxId].stakeMin && msg.value <= boxConfig[boxId].stakeMax, "Stake amount outside allowed range"); + // courierStakes[boxId][msg.sender] = msg.value; + require(courierBlacklist[msg.sender] == false, "You are blacklisted from deliveries"); + // @ToDo do the worldcoin check + boxes[boxId].courier = msg.sender; + boxes[boxId].pickupTime = block.timestamp; + boxes[boxId].status = BoxStatus.DELIVERY; + + token.transferFrom(msg.sender, address(this), boxes[boxId].stake); + } + + function confirmDelivery(uint256 boxId) public { + require(isBoxRegistered(boxId), "Box not registered"); + require(msg.sender == boxes[boxId].receiver, "Only receiver can confirm delivery"); + require(boxes[boxId].status == BoxStatus.DELIVERY, "Incorrect box status"); + + courierStakes[boxId][msg.sender] += boxes[boxId].stake; + courierRewards[boxes[boxId].courier] += boxes[boxId].bounty; + boxes[boxId].status = BoxStatus.EMPTY; + + // @ToDo nullify the box state + } + + function reportDelivery(uint256 boxId) public { + require(isBoxRegistered(boxId), "Box not registered"); + require(boxes[boxId].status == BoxStatus.DELIVERY, "Incorrect box status"); + require(boxes[boxId].pickupTime >= DELIVERY_REPORT_DEADLINE, "Can't report delivery before 4 days have passed"); + require(msg.sender == boxes[boxId].receiver, "Only receiver can report delivery"); + + token.transfer(boxes[boxId].sender, boxes[boxId].stake + boxes[boxId].bounty); + boxes[boxId].status = BoxStatus.EMPTY; + courierBlacklist[boxes[boxId].courier] = true; + + } + + function unstake(uint256 boxId) public { + require(isBoxRegistered(boxId), "Box not registered"); + require(courierStakes[boxId][msg.sender] > 0, "No stake found for courier"); + // require(boxes[boxId].status == BoxStatus.DELIVERY, "Incorrect box status"); + token.transfer(msg.sender, courierStakes[boxId][msg.sender]); + courierStakes[boxId][msg.sender] = 0; + } + + function withdrawRewards() public { + require(courierRewards[msg.sender] > 0, "No rewards found for courier"); + token.transfer(msg.sender, courierRewards[msg.sender]); + courierRewards[msg.sender] = 0; + } + + function isBoxRegistered(uint256 boxId) public view returns (bool) { + return boxes[boxId].operator != address(0); + } +} diff --git a/packages/hardhat/contracts/BoxToken.sol b/packages/hardhat/contracts/BoxToken.sol new file mode 100644 index 0000000..436d113 --- /dev/null +++ b/packages/hardhat/contracts/BoxToken.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +contract BoxToken is ERC20, Ownable, ERC20Permit { + constructor(address initialOwner) + ERC20("BoxToken", "BOX") + Ownable(initialOwner) + ERC20Permit("BoxToken") + {} + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} diff --git a/packages/hardhat/contracts/WorldcoinIdentity.sol b/packages/hardhat/contracts/WorldcoinIdentity.sol new file mode 100644 index 0000000..c24d7c5 --- /dev/null +++ b/packages/hardhat/contracts/WorldcoinIdentity.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import { ByteHasher } from './helpers/ByteHasher.sol'; +import { IWorldID } from './interfaces/IWorldID.sol'; + +contract Contract { + using ByteHasher for bytes; + + /////////////////////////////////////////////////////////////////////////////// + /// ERRORS /// + ////////////////////////////////////////////////////////////////////////////// + + /// @notice Thrown when attempting to reuse a nullifier + error DuplicateNullifier(uint256 nullifierHash); + + /// @dev The World ID instance that will be used for verifying proofs + IWorldID internal immutable worldId; + + /// @dev The contract's external nullifier hash + uint256 internal immutable externalNullifier; + + /// @dev The World ID group ID (always 1) + uint256 internal immutable groupId = 1; + + /// @dev Whether a nullifier hash has been used already. Used to guarantee an action is only performed once by a single person + mapping(uint256 => bool) internal nullifierHashes; + + /// @param nullifierHash The nullifier hash for the verified proof + /// @dev A placeholder event that is emitted when a user successfully verifies with World ID + event Verified(uint256 nullifierHash); + + /// @param _worldId The WorldID router that will verify the proofs + /// @param _appId The World ID app ID + /// @param _actionId The World ID action ID + constructor(IWorldID _worldId, string memory _appId, string memory _actionId) { + worldId = _worldId; + externalNullifier = abi.encodePacked(abi.encodePacked(_appId).hashToField(), _actionId).hashToField(); + } + + /// @param signal An arbitrary input from the user, usually the user's wallet address (check README for further details) + /// @param root The root of the Merkle tree (returned by the JS widget). + /// @param nullifierHash The nullifier hash for this proof, preventing double signaling (returned by the JS widget). + /// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the JS widget). + /// @dev Feel free to rename this method however you want! We've used `claim`, `verify` or `execute` in the past. + function verifyAndExecute(address signal, uint256 root, uint256 nullifierHash, uint256[8] calldata proof) public { + // First, we make sure this person hasn't done this before + if (nullifierHashes[nullifierHash]) revert DuplicateNullifier(nullifierHash); + + // We now verify the provided proof is valid and the user is verified by World ID + worldId.verifyProof( + root, + groupId, + abi.encodePacked(signal).hashToField(), + nullifierHash, + externalNullifier, + proof + ); + + // We now record the user has done this, so they can't do it again (proof of uniqueness) + nullifierHashes[nullifierHash] = true; + + // Finally, execute your logic here, for example issue a token, NFT, etc... + // Make sure to emit some kind of event afterwards! + + emit Verified(nullifierHash); + } +} \ No newline at end of file diff --git a/packages/hardhat/contracts/helpers/ByteHasher.sol b/packages/hardhat/contracts/helpers/ByteHasher.sol new file mode 100644 index 0000000..620004f --- /dev/null +++ b/packages/hardhat/contracts/helpers/ByteHasher.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +library ByteHasher { + /// @dev Creates a keccak256 hash of a bytestring. + /// @param value The bytestring to hash + /// @return The hash of the specified value + /// @dev `>> 8` makes sure that the result is included in our field + function hashToField(bytes memory value) internal pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(value))) >> 8; + } +} \ No newline at end of file diff --git a/packages/hardhat/contracts/interfaces/IWorldID.sol b/packages/hardhat/contracts/interfaces/IWorldID.sol new file mode 100644 index 0000000..c029f6c --- /dev/null +++ b/packages/hardhat/contracts/interfaces/IWorldID.sol @@ -0,0 +1,21 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IWorldID { + /// @notice Reverts if the zero-knowledge proof is invalid. + /// @param root The of the Merkle tree + /// @param groupId The id of the Semaphore group + /// @param signalHash A keccak256 hash of the Semaphore signal + /// @param nullifierHash The nullifier hash + /// @param externalNullifierHash A keccak256 hash of the external nullifier + /// @param proof The zero-knowledge proof + /// @dev Note that a double-signaling check is not included here, and should be carried by the caller. + function verifyProof( + uint256 root, + uint256 groupId, + uint256 signalHash, + uint256 nullifierHash, + uint256 externalNullifierHash, + uint256[8] calldata proof + ) external view; +} \ No newline at end of file diff --git a/packages/hardhat/deploy/00_deploy_your_contract.ts b/packages/hardhat/deploy/00_deploy_your_contract.ts index 716fec7..20b06c1 100644 --- a/packages/hardhat/deploy/00_deploy_your_contract.ts +++ b/packages/hardhat/deploy/00_deploy_your_contract.ts @@ -32,6 +32,20 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn autoMine: true, }); + await deploy("BoxToken", { + from: deployer, + args: [deployer], + log: true, + autoMine: true, + }); + + await deploy("BoxManager", { + from: deployer, + args: [deployer], + log: true, + autoMine: true, + }); + // Get the deployed contract to interact with it after deploying. const yourContract = await hre.ethers.getContract("YourContract", deployer); console.log("👋 Initial greeting:", await yourContract.greeting());