diff --git a/contracts/passport/PassportRegistry.sol b/contracts/passport/PassportRegistry.sol new file mode 100644 index 0000000..f27bd04 --- /dev/null +++ b/contracts/passport/PassportRegistry.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; +import "@openzeppelin/contracts/security/Pausable.sol"; +import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +contract PassportRegistry is Ownable, Pausable { + using Counters for Counters.Counter; + + // wallet => passport id + mapping(address => uint256) public passportId; + + // passport id => wallet + mapping(uint256 => address) public idPassport; + + // wallet => bool + mapping(address => bool) public walletActive; + + // id => bool + mapping(uint256 => bool) public idActive; + + // id => source + mapping(uint256 => string) public idSource; + + // source => # passports + mapping(string => uint256) public sourcePassports; + + // Total number of passports created + Counters.Counter public totalCreates; + + // Total number of passports sequencially created + Counters.Counter public totalSequencialCreates; + + // Total number of passports created by admins + Counters.Counter public totalAdminsCreates; + + // Total number of passport transfers + Counters.Counter public totalPassportTransfers; + + // The next id to be issued + uint256 private _nextSequencialPassportId; + + // Smart contract id in sequencial mode + bool private _sequencial; + + // A new passport has been created + event Create(address indexed wallet, uint256 passportId, string source); + + // A passport has been tranfered + event Transfer(uint256 oldPassportId, uint256 newPassportId, address indexed oldWallet, address indexed newWallet); + + // A passport has been deactivated + event Deactivate(address indexed wallet, uint256 passportId); + + // A passport has been activated + event Activate(address indexed wallet, uint256 passportId); + + // Passport generation mode changed + event PassportGenerationChanged(bool sequencial, uint256 nextSequencialPassportId); + + /** + * @dev Modifier to make a function callable only when the contract is in sequencial mode. + * + * Requirements: + * + * - The contract must be in sequencial mode. + */ + modifier whenSequencialGeneration() { + require(sequencial(), "Admin generation mode"); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is in admin generation mode. + * + * Requirements: + * + * - The contract must be in admin generation mode. + */ + modifier whenAdminGeneration() { + require(!sequencial(), "Sequencial generation mode"); + _; + } + + constructor(address contractOwner) { + transferOwnership(contractOwner); + _sequencial = false; + } + + function create(string memory source) public whenNotPaused whenSequencialGeneration { + require(passportId[msg.sender] == 0, "Passport already exists"); + + totalSequencialCreates.increment(); + + _create(msg.sender, _nextSequencialPassportId, source); + _nextSequencialPassportId += 1; + } + + function adminCreate( + string memory source, + address wallet, + uint256 id + ) public onlyOwner whenNotPaused whenAdminGeneration { + require(passportId[wallet] == 0, "Passport already exists"); + + totalAdminsCreates.increment(); + + _create(wallet, id, source); + } + + /** + * @notice Transfer the passport id of the msg.sender to the newWallet. + * @dev Can only be called by the passport owner. + */ + function transfer(address newWallet) public whenNotPaused { + uint256 id = passportId[msg.sender]; + uint256 newWalletId = passportId[newWallet]; + require(id != 0, "Passport does not exist"); + require(newWalletId == 0, "Wallet passed already has a passport"); + + passportId[msg.sender] = 0; + passportId[newWallet] = id; + idPassport[id] = newWallet; + walletActive[msg.sender] = false; + walletActive[newWallet] = true; + totalPassportTransfers.increment(); + + emit Transfer(id, id, msg.sender, newWallet); + } + + // Admin + + /** + * @notice Change the wallet passport id to a new one. + * @dev Can only be called by the owner. + */ + function adminTransfer(address wallet, uint256 id) public onlyOwner { + uint256 oldId = passportId[wallet]; + address idOwner = idPassport[id]; + require(oldId != 0, "Wallet does not have a passport to transfer from"); + require(idOwner == address(0), "New passport id already has a owner"); + + string memory source = idSource[oldId]; + idSource[id] = source; + idSource[oldId] = ""; + passportId[wallet] = id; + idPassport[oldId] = address(0); + walletActive[wallet] = true; + idActive[id] = true; + idActive[oldId] = false; + + totalPassportTransfers.increment(); + + emit Transfer(oldId, id, wallet, wallet); + } + + /** + * @notice Activates the passport of a given walley. + * @dev Can only be called by the owner. + */ + function activate(address wallet) public whenNotPaused onlyOwner { + require(passportId[wallet] != 0, "Passport must exist"); + require(walletActive[wallet] == false, "Passport must be inactive"); + + uint256 id = passportId[wallet]; + + walletActive[wallet] = true; + idActive[id] = true; + + // emit event + emit Activate(wallet, id); + } + + /** + * @notice Deactivates the passport of a given walley. + * @dev Can only be called by the owner. + */ + function deactivate(address wallet) public whenNotPaused onlyOwner { + require(passportId[wallet] != 0, "Passport must exist"); + require(walletActive[wallet] == true, "Passport must be active"); + + uint256 id = passportId[wallet]; + + walletActive[wallet] = false; + idActive[id] = false; + + // emit event + emit Deactivate(wallet, id); + } + + /** + * @notice Pauses the contract, disabling future creations. + * @dev Can only be called by the owner. + */ + function pause() public whenNotPaused onlyOwner { + _pause(); + } + + /** + * @notice Enables the contract, enabling new creations. + * @dev Can only be called by the owner. + */ + function unpause() public whenPaused onlyOwner { + _unpause(); + } + + /** + * @notice Changes the contract generation mode. + * @dev Can only be called by the owner. + */ + function setGenerationMode(bool sequencialFlag, uint256 nextSequencialPassportId) public onlyOwner { + _sequencial = sequencialFlag; + _nextSequencialPassportId = nextSequencialPassportId; + + emit PassportGenerationChanged(sequencialFlag, nextSequencialPassportId); + } + + /** + * @dev Returns true if the contract is in sequencial mode, and false otherwise. + */ + function sequencial() public view virtual returns (bool) { + return _sequencial; + } + + /** + * @dev Returns the next id to be generated. + */ + function nextId() public view virtual returns (uint256) { + return _nextSequencialPassportId; + } + + // private + + function _create(address wallet, uint256 id, string memory source) private { + require(idPassport[id] == address(0), "Passport id already issued"); + + totalCreates.increment(); + + idPassport[id] = wallet; + passportId[wallet] = id; + walletActive[wallet] = true; + idActive[id] = true; + idSource[id] = source; + sourcePassports[source] = SafeMath.add(sourcePassports[source], 1); + // emit event + emit Create(wallet, id, source); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 2135cd3..c9bd97e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -2,6 +2,7 @@ import { task } from "hardhat/config"; import "@typechain/hardhat"; import "@nomiclabs/hardhat-ethers"; +import "@nomicfoundation/hardhat-viem"; import "@nomiclabs/hardhat-etherscan"; import "@nomiclabs/hardhat-waffle"; import "@openzeppelin/hardhat-upgrades"; @@ -13,11 +14,11 @@ dotenv.config(); import type { HardhatUserConfig } from "hardhat/config"; -// const deployer = { -// mnemonic: process.env.MNEMONIC || "test test test test test test test test test test test junk", -// }; +const deployer = { + mnemonic: process.env.MNEMONIC || "test test test test test test test test test test test junk", +}; -const deployer = [process.env.PK_1 || "0x1111111111111111111111111111111111111111111111111111111111111111"]; +// const deployer = [""]; task("accounts", "Prints the list of accounts", async (args, hre) => { const accounts = await hre.ethers.getSigners(); @@ -59,6 +60,18 @@ const config: HardhatUserConfig = { chainId: 137, gasMultiplier: 1.5, }, + baseSepolia: { + url: "https://api.developer.coinbase.com/rpc/v1/base-sepolia/Ip9cOQPtBOm81rN2I9_1rBiMXOfKBxii", + accounts: deployer, + chainId: 84532, + gasMultiplier: 1.5, + }, + base: { + url: "https://api.developer.coinbase.com/rpc/v1/base/Ip9cOQPtBOm81rN2I9_1rBiMXOfKBxii", + accounts: deployer, + chainId: 8453, + gasMultiplier: 1.5, + }, }, gasReporter: { currency: "ETH", @@ -70,6 +83,8 @@ const config: HardhatUserConfig = { alfajores: process.env.CELO_API_KEY || "", polygon: process.env.POLYGON_API_KEY || "", polygonMumbai: process.env.POLYGON_API_KEY || "", + base: "", + baseSepolia: "", }, // Custom chains that are not supported by default customChains: [ @@ -89,6 +104,22 @@ const config: HardhatUserConfig = { browserURL: "https://celoscan.io/", }, }, + { + network: "baseSepolia", + chainId: 84532, + urls: { + apiURL: "https://api-sepolia.basescan.org/api", + browserURL: "https://sepolia.basescan.org", + }, + }, + { + network: "base", + chainId: 8453, + urls: { + apiURL: "https://api.basescan.org/api", + browserURL: "https://basescan.org", + }, + }, ], }, }; diff --git a/package.json b/package.json index 7d2f0bf..161fc08 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@ethersproject/abi": "^5.0.0", "@ethersproject/bytes": "^5.0.0", "@ethersproject/providers": "^5.0.0", + "@nomicfoundation/hardhat-viem": "^2.0.0", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-ganache": "^2.0.0", "@nomiclabs/hardhat-waffle": "^2.0.1", @@ -25,14 +26,18 @@ "nft.storage": "^7.0.0", "node-fetch": "3.3.1", "openzeppelin-solidity": "^4.2.0", + "permissionless": "^0.1.4", "solhint": "^3.3.6", "solidity-coverage": "^0.8.2", "solidity-docgen": "^0.5.16", "ts-node": "^10.8.2", "typechain": "^8.1.1", - "typescript": "^4.4.2" + "typescript": "^4.4.2", + "viem": "2.7.16" }, "devDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", + "@nomicfoundation/hardhat-network-helpers": "^1.0.8", "@nomiclabs/hardhat-etherscan": "^3.1.7", "@types/chai": "^4.3.4", "@types/chai-as-promised": "^7.1.4", @@ -46,10 +51,7 @@ "dotenv": "^16.0.3", "eslint": "^8.19.0", "ethereum-waffle": "^3.4.0", - "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", - "@nomicfoundation/hardhat-network-helpers": "^1.0.8", "hardhat-celo": "^0.0.4", - "@nomiclabs/hardhat-etherscan": "^3.1.7", "prettier": "^2.4.1", "prettier-plugin-solidity": "^1.0.0-beta.18", "solc": "^0.8.17" diff --git a/scripts/passport/deployPassportRegistry.ts b/scripts/passport/deployPassportRegistry.ts new file mode 100644 index 0000000..a5af295 --- /dev/null +++ b/scripts/passport/deployPassportRegistry.ts @@ -0,0 +1,25 @@ +import { ethers, network } from "hardhat"; + +import { deployPassport } from "../shared"; + +async function main() { + console.log(`Deploying passport registry at ${network.name}`); + + const [admin] = await ethers.getSigners(); + + console.log(`Admin will be ${admin.address}`); + + const passport = await deployPassport(admin.address); + + console.log(`Passport Address: ${passport.address}`); + console.log(`Passport owner: ${await passport.owner()}`); + + console.log("Done"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/passport/mintTestPassport.ts b/scripts/passport/mintTestPassport.ts new file mode 100644 index 0000000..3bc631a --- /dev/null +++ b/scripts/passport/mintTestPassport.ts @@ -0,0 +1,96 @@ +import hre from "hardhat"; + +import { http, getContract } from "viem"; + +import { createSmartAccountClient, ENTRYPOINT_ADDRESS_V06 } from "permissionless"; +import { baseSepolia, base } from "viem/chains"; +import { privateKeyToSimpleSmartAccount } from "permissionless/accounts"; +import { createPimlicoPaymasterClient } from "permissionless/clients/pimlico"; + +import * as PassportRegistry from "../../artifacts/contracts/passport/PassportRegistry.sol/PassportRegistry.json"; + +async function main() { + const [admin] = await hre.viem.getWalletClients(); + const publicClient = await hre.viem.getPublicClient(); + const chain = baseSepolia; + + console.log(`Changing owner on chain ${chain.name}`); + + // https://api.developer.coinbase.com/rpc/v1/base/w6ubd9S5jJzUzPlMn0yYmuP9UWbjKvrH + const rpcUrl = "https://api.developer.coinbase.com/rpc/v1/base-sepolia/w6ubd9S5jJzUzPlMn0yYmuP9UWbjKvrH"; + const contractAddress = "0x0fDD539a38B5ee3f077238e20d65177F3A5688Df"; + const privateKey = "0x"; + const entryPoint = ENTRYPOINT_ADDRESS_V06; + + console.log("privateKey", privateKey); + const smartAccount = await privateKeyToSimpleSmartAccount(publicClient, { + privateKey, + entryPoint, // global entrypoint + factoryAddress: "0x9406Cc6185a346906296840746125a0E44976454", + }); + + console.log(`Owner SCA ${smartAccount.address}`); + + const paymasterClient = createPimlicoPaymasterClient({ + transport: http(rpcUrl), + entryPoint, + chain, + }); + + console.log("paymasterClient"); + + const smartAccountClient = createSmartAccountClient({ + account: smartAccount, + entryPoint, + chain, + bundlerTransport: http(rpcUrl), + // IMPORTANT: Set up the Cloud Paymaster to sponsor your transaction + middleware: { + sponsorUserOperation: paymasterClient.sponsorUserOperation, + }, + }); + + const passportRegistry = getContract({ + address: contractAddress, + abi: PassportRegistry.abi, + client: { + public: publicClient, + wallet: smartAccountClient, + }, + }); + + const owner = await passportRegistry.read.owner(); + + console.log(`Registry owner: ${owner}`); + + const sequencial = await passportRegistry.read.sequencial(); + + console.log(`Registry sequencial: ${sequencial}`); + + const nextId = await passportRegistry.read.nextId(); + + console.log(`Registry nextId: ${nextId}`); + + // const txHash = await passportRegistry.write.setGenerationMode([false, 0]); + + const txHash = await passportRegistry.write.adminCreate([ + "fasrcaster", + "0x436cA2299e7fDF36C4b1164cA3e80081E68c318A", + 2, + ]); + + console.log(`UserOperation included: https://sepolia.basescan.org/tx/${txHash}`); + + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const passportId = await passportRegistry.read.passportId([admin.account.address]); + + console.log(`New passportId: ${passportId}`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/passport/transferPassportOwnership.ts b/scripts/passport/transferPassportOwnership.ts new file mode 100644 index 0000000..0d020bb --- /dev/null +++ b/scripts/passport/transferPassportOwnership.ts @@ -0,0 +1,55 @@ +import hre from "hardhat"; + +import { getContract } from "viem"; +import { baseSepolia, base } from "viem/chains"; +import { privateKeyToSimpleSmartAccount } from "permissionless/accounts"; + +import * as PassportRegistry from "../../artifacts/contracts/passport/PassportRegistry.sol/PassportRegistry.json"; + +const ENTRYPOINT = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; + +async function main() { + const [admin] = await hre.viem.getWalletClients(); + const publicClient = await hre.viem.getPublicClient(); + const chain = baseSepolia; + + console.log(`Changing owner on chain ${chain.name}`); + + // https://api.developer.coinbase.com/rpc/v1/base/w6ubd9S5jJzUzPlMn0yYmuP9UWbjKvrH + const rpcUrl = "https://api.developer.coinbase.com/rpc/v1/base-sepolia/Ip9cOQPtBOm81rN2I9_1rBiMXOfKBxii"; + const contractAddress = "0x0fDD539a38B5ee3f077238e20d65177F3A5688Df"; + const privateKey = "0x"; + + console.log("privateKey", privateKey); + const smartAccount = await privateKeyToSimpleSmartAccount(publicClient, { + privateKey, + entryPoint: ENTRYPOINT, // global entrypoint + factoryAddress: "0x9406Cc6185a346906296840746125a0E44976454", + }); + + console.log(`Owner SCA ${smartAccount.address}`); + + const passportRegistry = getContract({ + address: contractAddress, + abi: PassportRegistry.abi, + client: { + public: publicClient, + wallet: admin, + }, + }); + + const tx = await passportRegistry.write.transferOwnership([smartAccount.address]); + + await publicClient.waitForTransactionReceipt({ hash: tx }); + + const owner = await passportRegistry.read.passportId([admin.account.address]); + + console.log(`New owner: ${owner}`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/shared.ts b/scripts/shared.ts index 3d2621c..0e04ded 100644 --- a/scripts/shared.ts +++ b/scripts/shared.ts @@ -9,6 +9,7 @@ import type { TalentNFT, TalentSponsorship, VirtualTALBuy, + PassportRegistry, } from "../typechain-types"; export async function deployToken(): Promise { @@ -78,6 +79,15 @@ export async function deployVirtualTalBuy( return deployedVirtualTalBuy as VirtualTALBuy; } +export async function deployPassport(owner: string): Promise { + const passportRegistryContract = await ethers.getContractFactory("PassportRegistry"); + + const deployedPassport = await passportRegistryContract.deploy(owner); + await deployedPassport.deployed(); + + return deployedPassport as PassportRegistry; +} + export async function deployStaking( start: number, end: number, diff --git a/test/contracts/passport/passportRegistry.ts b/test/contracts/passport/passportRegistry.ts new file mode 100644 index 0000000..621f474 --- /dev/null +++ b/test/contracts/passport/passportRegistry.ts @@ -0,0 +1,381 @@ +import chai from "chai"; +import { ethers, waffle } from "hardhat"; +import { solidity } from "ethereum-waffle"; + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; + +import { PassportRegistry } from "../../../typechain-types"; +import { Artifacts } from "../../shared"; + +import { findEvent } from "../../shared/utils"; + +chai.use(solidity); + +const { expect } = chai; +const { deployContract } = waffle; + +describe("Passport", () => { + let admin: SignerWithAddress; + let holderOne: SignerWithAddress; + let holderTwo: SignerWithAddress; + let holderThree: SignerWithAddress; + + let contract: PassportRegistry; + + beforeEach(async () => { + [admin, holderOne, holderTwo, holderThree] = await ethers.getSigners(); + }); + + async function builder() { + return deployContract(admin, Artifacts.PassportRegistry, [admin.address]); + } + + describe("admin and sequencial generation behaviour", () => { + beforeEach(async () => { + contract = (await builder()) as PassportRegistry; + }); + + it("stores the contract state correctly", async () => { + await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); + + let tx = await contract.connect(admin).setGenerationMode(true, 1050); + let event = await findEvent(tx, "PassportGenerationChanged"); + + expect(event).to.exist; + expect(event?.args?.sequencial).to.eq(true); + expect(event?.args?.nextSequencialPassportId).to.eq(1050); + + tx = await contract.connect(holderTwo).create("farcaster"); + + event = await findEvent(tx, "Create"); + + expect(event).to.exist; + expect(event?.args?.wallet).to.eq(holderTwo.address); + expect(event?.args?.passportId).to.eq(1050); + + tx = await contract.connect(holderThree).create("farcaster"); + + event = await findEvent(tx, "Create"); + + expect(event).to.exist; + expect(event?.args?.wallet).to.eq(holderThree.address); + expect(event?.args?.passportId).to.eq(1051); + }); + }); + + describe("admin generation behaviour", () => { + beforeEach(async () => { + contract = (await builder()) as PassportRegistry; + }); + + it("is created with the correct state", async () => { + expect(await contract.totalCreates()).to.eq(0); + expect(await contract.totalSequencialCreates()).to.eq(0); + expect(await contract.paused()).to.eq(false); + expect(await contract.sequencial()).to.eq(false); + expect(await contract.nextId()).to.eq(0); + }); + + it("emits a create event everytime a passport is created", async () => { + let tx = await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); + + let event = await findEvent(tx, "Create"); + + expect(event).to.exist; + expect(event?.args?.wallet).to.eq(holderOne.address); + expect(event?.args?.passportId).to.eq(1001); + + tx = await contract.connect(admin).adminCreate("farcaster", holderTwo.address, 1002); + + event = await findEvent(tx, "Create"); + + expect(event).to.exist; + expect(event?.args?.wallet).to.eq(holderTwo.address); + expect(event?.args?.passportId).to.eq(1002); + expect(event?.args?.source).to.eq("farcaster"); + }); + + it("stores the contract state correctly", async () => { + await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); + + await contract.connect(admin).adminCreate("passport", holderTwo.address, 1002); + + await contract.connect(admin).adminCreate("passport", holderThree.address, 1003); + + await contract.connect(admin).adminTransfer(holderThree.address, 5); + + const adminCreates = await contract.totalAdminsCreates(); + const totalPassportTransfers = await contract.totalPassportTransfers(); + + const holderOnePassportId = await contract.passportId(holderOne.address); + const holderTwoPassportId = await contract.passportId(holderTwo.address); + const holderThreePassportId = await contract.passportId(holderThree.address); + const holderThreeActivePassport = await contract.walletActive(holderThree.address); + const holderThreeActivePassportId = await contract.idActive(5); + const holderThreePreviousPassportId = await contract.idActive(1003); + + expect(adminCreates).to.eq(3); + expect(totalPassportTransfers).to.eq(1); + expect(holderOnePassportId).to.eq(1001); + expect(holderTwoPassportId).to.eq(1002); + expect(holderThreePassportId).to.eq(5); + expect(holderThreeActivePassport).to.eq(true); + expect(holderThreeActivePassportId).to.eq(true); + expect(holderThreePreviousPassportId).to.eq(false); + }); + + it("emits a tranfer event everytime a passport is tranfered", async () => { + let tx = await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); + + tx = await contract.connect(holderOne).transfer(holderTwo.address); + + const event = await findEvent(tx, "Transfer"); + + expect(event).to.exist; + expect(event?.args?.oldPassportId).to.eq(1001); + expect(event?.args?.newPassportId).to.eq(1001); + expect(event?.args?.oldWallet).to.eq(holderOne.address); + expect(event?.args?.newWallet).to.eq(holderTwo.address); + + const holderOnePassportId = await contract.passportId(holderOne.address); + const holderTwoPassportId = await contract.passportId(holderTwo.address); + + expect(holderOnePassportId).to.eq(0); + expect(holderTwoPassportId).to.eq(1001); + }); + + it("emits a tranfer event everytime a passport is tranfered by an admin", async () => { + let tx = await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); + + let holderOnePassportId = await contract.passportId(holderOne.address); + expect(holderOnePassportId).to.eq(1001); + + tx = await contract.connect(admin).adminTransfer(holderOne.address, 1); + + const event = await findEvent(tx, "Transfer"); + + expect(event).to.exist; + expect(event?.args?.oldPassportId).to.eq(1001); + expect(event?.args?.newPassportId).to.eq(1); + expect(event?.args?.oldWallet).to.eq(holderOne.address); + expect(event?.args?.newWallet).to.eq(holderOne.address); + + holderOnePassportId = await contract.passportId(holderOne.address); + + expect(holderOnePassportId).to.eq(1); + }); + + it("prevents generation for non admins", async () => { + const action = contract.connect(holderOne).adminCreate("farcaster", holderOne.address, 1); + + await expect(action).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("prevents sequencial generation", async () => { + const action = contract.connect(holderOne).create("farcaster"); + + await expect(action).to.be.revertedWith("Admin generation mode"); + }); + + it("prevents duplicated passports", async () => { + await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); + const action = contract.connect(admin).adminCreate("farcaster", holderOne.address, 1002); + + await expect(action).to.be.reverted; + }); + + it("prevents admin transfers of existing passports", async () => { + await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); + + const action = contract.connect(admin).adminTransfer(holderOne.address, 1001); + + await expect(action).to.be.revertedWith("New passport id already has a owner"); + }); + + it("prevents admin transfers of wallets without passport", async () => { + const action = contract.connect(admin).adminTransfer(holderTwo.address, 2); + + await expect(action).to.be.revertedWith("Wallet does not have a passport to transfer from"); + }); + }); + + describe("sequencial generation behaviour", () => { + beforeEach(async () => { + contract = (await builder()) as PassportRegistry; + await contract.connect(admin).setGenerationMode(true, 1); + }); + + it("is created with the correct state", async () => { + expect(await contract.totalCreates()).to.eq(0); + expect(await contract.totalSequencialCreates()).to.eq(0); + expect(await contract.paused()).to.eq(false); + expect(await contract.sequencial()).to.eq(true); + expect(await contract.nextId()).to.eq(1); + }); + + it("emits a create event everytime a passport is created", async () => { + let tx = await contract.connect(holderOne).create("farcaster"); + + let event = await findEvent(tx, "Create"); + + expect(event).to.exist; + expect(event?.args?.wallet).to.eq(holderOne.address); + expect(event?.args?.passportId).to.eq(1); + + tx = await contract.connect(holderTwo).create("farcaster"); + + event = await findEvent(tx, "Create"); + + expect(event).to.exist; + expect(event?.args?.wallet).to.eq(holderTwo.address); + expect(event?.args?.passportId).to.eq(2); + expect(event?.args?.source).to.eq("farcaster"); + }); + + it("stores the contract state correctly", async () => { + await contract.connect(holderOne).create("farcaster"); + + await contract.connect(holderTwo).create("passport"); + + await contract.connect(holderThree).create("passport"); + + await contract.connect(admin).adminTransfer(holderThree.address, 5); + + const sequencialCreates = await contract.totalSequencialCreates(); + const totalPassportTransfers = await contract.totalPassportTransfers(); + + const holderOnePassportId = await contract.passportId(holderOne.address); + const holderTwoPassportId = await contract.passportId(holderTwo.address); + const holderThreePassportId = await contract.passportId(holderThree.address); + const holderThreeActivePassport = await contract.walletActive(holderThree.address); + const holderThreeActivePassportId = await contract.idActive(5); + const holderThreePreviousPassportId = await contract.idActive(1003); + + expect(sequencialCreates).to.eq(3); + expect(totalPassportTransfers).to.eq(1); + expect(holderOnePassportId).to.eq(1); + expect(holderTwoPassportId).to.eq(2); + expect(holderThreePassportId).to.eq(5); + expect(holderThreeActivePassport).to.eq(true); + expect(holderThreeActivePassportId).to.eq(true); + expect(holderThreePreviousPassportId).to.eq(false); + }); + + it("allows the passport owner to transfer the passport", async () => { + await contract.connect(holderOne).create("farcaster"); + + await contract.connect(holderOne).transfer(holderThree.address); + + const holderOnePassportId = await contract.passportId(holderOne.address); + const holderThreePassportId = await contract.passportId(holderThree.address); + + expect(holderOnePassportId).to.eq(0); + expect(holderThreePassportId).to.eq(1); + }); + + it("prevents the passport owner to transfer the passport to an existing owner wallet", async () => { + await contract.connect(holderOne).create("farcaster"); + await contract.connect(holderThree).create("lens"); + + const action = contract.connect(holderOne).transfer(holderThree.address); + + await expect(action).to.be.revertedWith("Wallet passed already has a passport"); + }); + + it("admin generation", async () => { + const action = contract.connect(admin).adminCreate("farcaster", holderOne.address, 1010); + + await expect(action).to.be.revertedWith("Sequencial generation mode"); + }); + }); + + describe("testing passport activate and deactivate", () => { + beforeEach(async () => { + contract = (await builder()) as PassportRegistry; + }); + + it("emits events", async () => { + await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); + + let holderActivePassport = await contract.walletActive(holderOne.address); + expect(holderActivePassport).to.eq(true); + + let tx = await contract.connect(admin).deactivate(holderOne.address); + let event = await findEvent(tx, "Deactivate"); + + expect(event).to.exist; + expect(event?.args?.wallet).to.eq(holderOne.address); + expect(event?.args?.passportId).to.eq(1001); + + holderActivePassport = await contract.walletActive(holderOne.address); + expect(holderActivePassport).to.eq(false); + + tx = await contract.connect(admin).activate(holderOne.address); + + event = await findEvent(tx, "Activate"); + + expect(event).to.exist; + expect(event?.args?.wallet).to.eq(holderOne.address); + expect(event?.args?.passportId).to.eq(1001); + + holderActivePassport = await contract.walletActive(holderOne.address); + expect(holderActivePassport).to.eq(true); + }); + }); + + describe("testing contract enable and disable", () => { + beforeEach(async () => { + contract = (await builder()) as PassportRegistry; + }); + + it("allows the contract owner to disable and enable the contract", async () => { + expect(await contract.paused()).to.be.equal(false); + + await contract.connect(admin).pause(); + + expect(await contract.paused()).to.be.equal(true); + + await contract.connect(admin).unpause(); + + expect(await contract.paused()).to.be.equal(false); + }); + + it("prevents other accounts to disable the contract", async () => { + expect(await contract.paused()).to.be.equal(false); + + const action = contract.connect(holderOne).pause(); + + await expect(action).to.be.reverted; + + expect(await contract.paused()).to.be.equal(false); + }); + + it("prevents other accounts to enable the contract", async () => { + const action = contract.connect(holderOne).unpause(); + + await expect(action).to.be.reverted; + }); + + it("prevents disable when the contract is already disabled", async () => { + expect(await contract.paused()).to.be.equal(false); + + await contract.connect(admin).pause(); + + const action = contract.connect(admin).pause(); + + await expect(action).to.be.reverted; + }); + + it("prevents new creates when the contract is disabled", async () => { + expect(await contract.paused()).to.be.equal(false); + + await contract.connect(admin).pause(); + + expect(await contract.paused()).to.be.equal(true); + + const action = contract.connect(holderOne).create("farcaster"); + + await expect(action).to.be.revertedWith("Pausable: paused"); + }); + }); +}); diff --git a/test/shared/artifacts.ts b/test/shared/artifacts.ts index a453945..8984e37 100644 --- a/test/shared/artifacts.ts +++ b/test/shared/artifacts.ts @@ -9,6 +9,7 @@ import TalentNFT from "../../artifacts/contracts/talent-nft/TalentNFT.sol/Talent import StakingMigration from "../../artifacts/contracts/StakingMigration.sol/StakingMigration.json"; import TalentSponsorship from "../../artifacts/contracts/season3/TalentSponsorship.sol/TalentSponsorship.json"; import VirtualTALBuy from "../../artifacts/contracts/season3/VirtualTalBuy.sol/VirtualTALBuy.json"; +import PassportRegistry from "../../artifacts/contracts/passport/PassportRegistry.sol/PassportRegistry.json"; // test-only contracts import USDTMock from "../../artifacts/contracts/test/ERC20Mock.sol/USDTMock.json"; @@ -36,4 +37,5 @@ export { TalentNFT, TalentSponsorship, VirtualTALBuy, + PassportRegistry, }; diff --git a/yarn.lock b/yarn.lock index 26c0890..f183046 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adraffy/ens-normalize@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" + integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== + "@assemblyscript/loader@^0.9.4": version "0.9.4" resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.9.4.tgz#a483c54c1253656bb33babd464e3154a173e1577" @@ -594,11 +599,28 @@ multiformats "^9.5.4" murmurhash3js-revisited "^3.0.0" +"@noble/curves@1.2.0", "@noble/curves@~1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" + integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== + "@noble/secp256k1@1.7.1", "@noble/secp256k1@~1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -780,6 +802,14 @@ resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-1.0.2.tgz#342b79e19c456a56d8e76bc2e9cc8474cbcfc774" integrity sha512-8CEgWSKUK2aMit+76Sez8n7UB0Ze1lwT+LcWxj4EFP30lQWOwOws048t6MTPfThH0BlSWjC6hJRr0LncIkc1Sw== +"@nomicfoundation/hardhat-viem@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-viem/-/hardhat-viem-2.0.0.tgz#4f5de792028a5607984ea9fd1e17727a71e3cdb8" + integrity sha512-ilXQKTc1jWHqJ66fAN6TIyCRyormoChOn1yQTCGoBQ+G6QcVCu5FTaGL2r0KUOY4IkTohtphK+UXQrKcxQX5Yw== + dependencies: + abitype "^0.9.8" + lodash.memoize "^4.1.2" + "@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.1.tgz#4c858096b1c17fe58a474fe81b46815f93645c15" @@ -1183,6 +1213,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== +"@scure/base@~1.1.2": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" + integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== + "@scure/bip32@1.1.5": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" @@ -1192,6 +1227,15 @@ "@noble/secp256k1" "~1.7.0" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" + integrity sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA== + dependencies: + "@noble/curves" "~1.2.0" + "@noble/hashes" "~1.3.2" + "@scure/base" "~1.1.2" + "@scure/bip39@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" @@ -1200,6 +1244,14 @@ "@noble/hashes" "~1.2.0" "@scure/base" "~1.1.0" +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -1708,6 +1760,16 @@ abbrev@1.0.x: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" integrity sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q== +abitype@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" + integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== + +abitype@^0.9.8: + version "0.9.10" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.10.tgz#fa6fa30a6465da98736f98b6c601a02ed49f6eec" + integrity sha512-FIS7U4n7qwAT58KibwYig5iFG4K61rbhAqaQh/UWj8v1Y8mjX3F8TC9gd8cz9yT1TYel9f8nS5NO5kZp2RW0jQ== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -6991,6 +7053,11 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +isows@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" + integrity sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -7594,6 +7661,11 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -8967,6 +9039,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +permissionless@^0.1.4: + version "0.1.17" + resolved "https://registry.yarnpkg.com/permissionless/-/permissionless-0.1.17.tgz#d50d0475287b70cbd0d76e7f946449be1844f9c5" + integrity sha512-xesPWvTvLb5yWB7CzH3W1e989gzqx6EKKo2/jIELLpA5si11yEFnD1rbWxKKwvREip0mJzzIzUbQepQBIhk3Kg== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -11425,6 +11502,20 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +viem@2.7.16: + version "2.7.16" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.7.16.tgz#99e66bbec661b2284bc32061474f20a90381bdcb" + integrity sha512-yOPa9yaoJUm44m0Qe3ugHnkHol3QQlFxN3jT+bq+lQip7X7cWdPfmguyfLWX2viCXcmYZUDiQdeFbkPW9lw11Q== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@scure/bip32" "1.3.2" + "@scure/bip39" "1.2.1" + abitype "1.0.0" + isows "1.0.3" + ws "8.13.0" + web-encoding@1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/web-encoding/-/web-encoding-1.1.5.tgz#fc810cf7667364a6335c939913f5051d3e0c4864" @@ -12141,6 +12232,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"