diff --git a/packages/zevm-app-contracts/contracts/proof-of-liveness/ProofOfLiveness.sol b/packages/zevm-app-contracts/contracts/proof-of-liveness/ProofOfLiveness.sol new file mode 100644 index 0000000..231bb30 --- /dev/null +++ b/packages/zevm-app-contracts/contracts/proof-of-liveness/ProofOfLiveness.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "hardhat/console.sol"; + +contract ProofOfLiveness { + uint256 constant PROOF_PERIOD = 24 hours; + uint256 constant LAST_PERIODS_LENGTH = 5; + + // Mapping to track the proof history for each user (last 5 proof timestamps) + mapping(address => uint256[LAST_PERIODS_LENGTH]) public proofHistory; + + // Custom error for when a user has already proved liveness within the last PROOF_PERIOD + error ProofWithinLast24Hours(uint256 lastProofTime); + + // Event to log when liveness is proved + event LivenessProved(address indexed user, uint256 proofTimestamp); + + // The function to prove liveness, can only be called once every PROOF_PERIOD + function proveLiveness() external { + uint256 currentTime = block.timestamp; + uint256 lastProofTime = proofHistory[msg.sender][0]; // The most recent proof timestamp is always stored in the first position + + // Check if the user has proved liveness within the last PROOF_PERIOD + if (currentTime < lastProofTime + PROOF_PERIOD) { + revert ProofWithinLast24Hours(lastProofTime); + } + + // Shift the proof history and add the new timestamp + _updateProofHistory(msg.sender, currentTime); + + // Emit an event to track the liveness proof + emit LivenessProved(msg.sender, currentTime); + } + + // Helper function to check if a user can prove liveness (returns true if PROOF_PERIOD has passed) + function canProveLiveness(address user) external view returns (bool) { + uint256 currentTime = block.timestamp; + return currentTime >= proofHistory[user][0] + PROOF_PERIOD; + } + + // View function to return the liveness proof status for the last LAST_PERIODS_LENGTH periods (each PROOF_PERIOD long) + function getLastPeriodsStatus(address user) external view returns (bool[LAST_PERIODS_LENGTH] memory) { + uint256 currentTime = block.timestamp; + bool[LAST_PERIODS_LENGTH] memory proofStatus; + + for (uint256 i = 0; i < LAST_PERIODS_LENGTH; i++) { + // Calculate the end of the period (going back i * PROOF_PERIOD) + uint256 periodEnd = currentTime - (i * PROOF_PERIOD); + uint256 periodStart = periodEnd - PROOF_PERIOD - 1; + // If the proof timestamp falls within this period, mark it as true + proofStatus[i] = hasProofedAt(user, periodStart, periodEnd); + } + + return proofStatus; + } + + function hasProofedAt(address user, uint256 periodStart, uint256 periodEnd) public view returns (bool) { + for (uint256 i = 0; i < LAST_PERIODS_LENGTH; i++) { + if (proofHistory[user][i] >= periodStart && proofHistory[user][i] < periodEnd) { + return true; + } + } + return false; + } + + function getProofHistory(address user) external view returns (uint256[LAST_PERIODS_LENGTH] memory) { + return proofHistory[user]; + } + + // Internal function to update the user's proof history by shifting timestamps and adding the new proof + function _updateProofHistory(address user, uint256 newProofTimestamp) internal { + // Shift the history to the right + for (uint256 i = LAST_PERIODS_LENGTH - 1; i > 0; i--) { + proofHistory[user][i] = proofHistory[user][i - 1]; + } + + // Add the new timestamp in the first position + proofHistory[user][0] = newProofTimestamp; + } +} diff --git a/packages/zevm-app-contracts/data/addresses.json b/packages/zevm-app-contracts/data/addresses.json index 94022ab..0e8d707 100644 --- a/packages/zevm-app-contracts/data/addresses.json +++ b/packages/zevm-app-contracts/data/addresses.json @@ -8,7 +8,8 @@ "invitationManager": "0x3649C03C472B698213926543456E9c21081e529d", "withdrawERC20": "0xa349B9367cc54b47CAb8D09A95836AE8b4D1d84E", "ZetaXP": "0x5c25b6f4D2b7a550a80561d3Bf274C953aC8be7d", - "InstantRewards": "0x10DfEd4ba9b8F6a1c998E829FfC0325D533c80E3" + "InstantRewards": "0x10DfEd4ba9b8F6a1c998E829FfC0325D533c80E3", + "ProofOfLiveness": "0x981EB6fD19717Faf293Fba0cBD05C6Ac97b8C808" }, "zeta_mainnet": { "disperse": "0x23ce409Ea60c3d75827d04D9db3d52F3af62e44d", @@ -18,7 +19,8 @@ "invitationManager": "0x3649C03C472B698213926543456E9c21081e529d", "withdrawERC20": "0xa349B9367cc54b47CAb8D09A95836AE8b4D1d84E", "ZetaXP": "0x9A4e8bB5FFD8088ecF1DdE823e97Be8080BD38cb", - "InstantRewards": "0x018412ec1D5bBb864eAe0A4BECaa683052890238" + "InstantRewards": "0x018412ec1D5bBb864eAe0A4BECaa683052890238", + "ProofOfLiveness": "0x327c9837B183e69C522a30E6f91A42c86e057432" } } } \ No newline at end of file diff --git a/packages/zevm-app-contracts/scripts/proof-of-liveness/deploy.ts b/packages/zevm-app-contracts/scripts/proof-of-liveness/deploy.ts new file mode 100644 index 0000000..5a35f89 --- /dev/null +++ b/packages/zevm-app-contracts/scripts/proof-of-liveness/deploy.ts @@ -0,0 +1,33 @@ +import { isProtocolNetworkName } from "@zetachain/protocol-contracts"; +import { ethers, network } from "hardhat"; + +import { ProofOfLiveness__factory } from "../../typechain-types"; +import { saveAddress } from "../address.helpers"; +import { verifyContract } from "../explorer.helpers"; + +const networkName = network.name; + +const deployProofOfLiveness = async () => { + if (!isProtocolNetworkName(networkName)) throw new Error("Invalid network name"); + + const ProofOfLivenessFactory = (await ethers.getContractFactory("ProofOfLiveness")) as ProofOfLiveness__factory; + const ProofOfLiveness = await ProofOfLivenessFactory.deploy(); + + await ProofOfLiveness.deployed(); + + console.log("ProofOfLiveness deployed to:", ProofOfLiveness.address); + + saveAddress("ProofOfLiveness", ProofOfLiveness.address, networkName); + + await verifyContract(ProofOfLiveness.address, []); +}; + +const main = async () => { + if (!isProtocolNetworkName(networkName)) throw new Error("Invalid network name"); + await deployProofOfLiveness(); +}; + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/zevm-app-contracts/test/proof-of-liveness/proof-of-liveness.ts b/packages/zevm-app-contracts/test/proof-of-liveness/proof-of-liveness.ts new file mode 100644 index 0000000..5476c77 --- /dev/null +++ b/packages/zevm-app-contracts/test/proof-of-liveness/proof-of-liveness.ts @@ -0,0 +1,93 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { ProofOfLiveness } from "../../typechain-types"; + +const PROOF_PERIOD = 24 * 60 * 60; // 24 hours in seconds + +describe("Proof Of Liveness Contract test", () => { + let proofOfLiveness: ProofOfLiveness, + owner: SignerWithAddress, + signer: SignerWithAddress, + user: SignerWithAddress, + addrs: SignerWithAddress[]; + + beforeEach(async () => { + [owner, signer, user, ...addrs] = await ethers.getSigners(); + const ProofOfLivenessFactory = await ethers.getContractFactory("ProofOfLiveness"); + + proofOfLiveness = await ProofOfLivenessFactory.deploy(); + + await proofOfLiveness.deployed(); + }); + + it("Should proof", async () => { + const tx = await proofOfLiveness.proveLiveness(); + + const receipt = await tx.wait(); + const blockTimestamp = (await ethers.provider.getBlock(receipt.blockNumber)).timestamp; + + await expect(tx).to.emit(proofOfLiveness, "LivenessProved").withArgs(owner.address, blockTimestamp); + }); + + it("Should proof 5 times every 24 hours and return correct view values", async () => { + // Prove liveness 5 times + for (let i = 0; i < 5; i++) { + // Call the proveLiveness function + const tx = await proofOfLiveness.proveLiveness(); + await tx.wait(); + + // Increase the time by 24 hours in the EVM + await ethers.provider.send("evm_increaseTime", [PROOF_PERIOD]); + await ethers.provider.send("evm_mine", []); // Mine a new block to apply the time change + } + + // Now check the getLastPeriodsStatus for the owner + const periodsStatus = await proofOfLiveness.getLastPeriodsStatus(owner.address); + + // We expect that all 5 periods should return true + expect(periodsStatus).to.deep.equal([true, true, true, true, true]); + }); + + it("Should proof 5 times every 24 hours and return correct view values if one day is missing", async () => { + // Prove liveness 5 times + for (let i = 0; i < 5; i++) { + // Call the proveLiveness function if day is not 3 + if (i !== 3) { + const tx = await proofOfLiveness.proveLiveness(); + await tx.wait(); + } + + // Increase the time by 24 hours in the EVM + await ethers.provider.send("evm_increaseTime", [PROOF_PERIOD]); + await ethers.provider.send("evm_mine", []); // Mine a new block to apply the time change + } + + // Now check the getLastPeriodsStatus for the owner + const periodsStatus = await proofOfLiveness.getLastPeriodsStatus(owner.address); + + // We expect that all 5 periods should return true but 3 + expect(periodsStatus).to.deep.equal([true, false, true, true, true]); + }); + + it("Should proof view return if only one day was proof", async () => { + const tx = await proofOfLiveness.proveLiveness(); + await tx.wait(); + await ethers.provider.send("evm_mine", []); // Mine a new block to apply the time change + + // Now check the getLastPeriodsStatus for the owner + const periodsStatus = await proofOfLiveness.getLastPeriodsStatus(owner.address); + + expect(periodsStatus).to.deep.equal([true, false, false, false, false]); + }); + + it("Should revert if trying to prove twice in less than 24 hours", async () => { + // Prove liveness for the first time + await proofOfLiveness.proveLiveness(); + + const tx = proofOfLiveness.proveLiveness(); + + await expect(tx).to.be.revertedWith("ProofWithinLast24Hours"); + }); +});