From 3fd6c16da4f93ac33e92c1beb0dfd2a06ac309f7 Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Tue, 20 Feb 2024 12:32:14 +0000 Subject: [PATCH 1/7] Add passport contract --- contracts/passport/PassportRegistry.sol | 143 ++++++++++++++ hardhat.config.ts | 23 ++- scripts/passport/deployPassportRegistry.ts | 25 +++ scripts/shared.ts | 10 + test/contracts/passport/passportRegistry.ts | 195 ++++++++++++++++++++ test/shared/artifacts.ts | 2 + 6 files changed, 394 insertions(+), 4 deletions(-) create mode 100644 contracts/passport/PassportRegistry.sol create mode 100644 scripts/passport/deployPassportRegistry.ts create mode 100644 test/contracts/passport/passportRegistry.ts diff --git a/contracts/passport/PassportRegistry.sol b/contracts/passport/PassportRegistry.sol new file mode 100644 index 0000000..1b81045 --- /dev/null +++ b/contracts/passport/PassportRegistry.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +contract PassportRegistry is Ownable { + // 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 + uint256 public totalCreates; + + // Total number of passports created by admin + uint256 public totalAdminCreates; + + // Total number of passports publicly created + uint256 public totalPublicCreates; + + // flag to enable or disable the contract + bool public enabled = true; + + // Initial id of passport creations + uint256 public initialPassportId = 1000; + + // A new passport has been created + event Create(address indexed wallet, uint256 passportId, bool admin, string source); + + // A passport has been deactivated + event Deactivate(address indexed wallet, uint256 passportId); + + // A passport has been activated + event Activate(address indexed wallet, uint256 passportId); + + constructor(address contractOwner) { + transferOwnership(contractOwner); + } + + modifier onlyWhileEnabled() { + require(enabled, "The contract is disabled."); + _; + } + + function create(string memory source) public onlyWhileEnabled { + require(passportId[msg.sender] == 0, "Passport already exists"); + + uint256 newPassportId = SafeMath.add(initialPassportId, SafeMath.add(totalPublicCreates, 1)); + totalPublicCreates = SafeMath.add(totalPublicCreates, 1); + + _create(msg.sender, newPassportId, false, source); + } + + // Admin + + function adminCreate(address wallet, string memory source) public onlyWhileEnabled onlyOwner { + require(passportId[wallet] == 0, "Passport already exists"); + + uint256 newPassportId = SafeMath.add(initialPassportId, SafeMath.add(totalPublicCreates, 1)); + + totalAdminCreates = SafeMath.add(totalAdminCreates, 1); + _create(wallet, newPassportId, true, source); + } + + function adminCreateWithId(address wallet, uint256 id, string memory source) public onlyWhileEnabled onlyOwner { + require(passportId[wallet] == 0, "Passport already exists"); + require(idPassport[id] == address(0), "Passport id already assigned"); + + totalAdminCreates = SafeMath.add(totalAdminCreates, 1); + _create(wallet, id, true, source); + } + + function activate(address wallet) public onlyWhileEnabled 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); + } + + function deactivate(address wallet) public onlyWhileEnabled 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 Disables the contract, disabling future creations. + * @dev Can only be called by the owner. + */ + function disable() public onlyWhileEnabled onlyOwner { + enabled = false; + } + + /** + * @notice Enables the contract, enabling new creations. + * @dev Can only be called by the owner. + */ + function enable() public onlyOwner { + enabled = true; + } + + // private + + function _create(address wallet, uint256 id, bool admin, string memory source) private { + totalCreates = SafeMath.add(totalCreates, 1); + + 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, admin, source); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 2135cd3..178ac22 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -13,11 +13,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 +59,12 @@ const config: HardhatUserConfig = { chainId: 137, gasMultiplier: 1.5, }, + baseSepolia: { + url: "https://sepolia.base.org", + accounts: deployer, + chainId: 84532, + gasMultiplier: 1.5, + }, }, gasReporter: { currency: "ETH", @@ -70,6 +76,7 @@ const config: HardhatUserConfig = { alfajores: process.env.CELO_API_KEY || "", polygon: process.env.POLYGON_API_KEY || "", polygonMumbai: process.env.POLYGON_API_KEY || "", + baseSepolia: process.env.BASE_SEPOLIA_API_KEY || "", }, // Custom chains that are not supported by default customChains: [ @@ -89,6 +96,14 @@ const config: HardhatUserConfig = { browserURL: "https://celoscan.io/", }, }, + { + network: "baseSepolia", + chainId: 84532, + urls: { + apiURL: "https://api-sepolia.basescan.org/api", + browserURL: "https://sepolia.basescan.org", + }, + }, ], }, }; diff --git a/scripts/passport/deployPassportRegistry.ts b/scripts/passport/deployPassportRegistry.ts new file mode 100644 index 0000000..ac8c75e --- /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 buy vTal package ${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/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..95b70f8 --- /dev/null +++ b/test/contracts/passport/passportRegistry.ts @@ -0,0 +1,195 @@ +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("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.totalAdminCreates()).to.eq(0); + expect(await contract.totalPublicCreates()).to.eq(0); + expect(await contract.enabled()).to.eq(true); + expect(await contract.initialPassportId()).to.eq(1000); + }); + + it("emits a create event everytime a create 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(1001); + expect(event?.args?.admin).to.eq(false); + + tx = await contract.connect(admin).adminCreate(holderTwo.address, "farcaster"); + + 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?.admin).to.eq(true); + expect(event?.args?.source).to.eq("farcaster"); + }); + + it("stores the contract state correctly", async () => { + await contract.connect(holderOne).create("farcaster"); + + await contract.connect(admin).adminCreate(holderTwo.address, "farcaster"); + + await contract.connect(admin).adminCreateWithId(holderThree.address, 5, "farcaster"); + + const adminCreates = await contract.totalAdminCreates(); + const publicCreates = await contract.totalPublicCreates(); + + 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); + + expect(adminCreates).to.eq(2); + expect(publicCreates).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); + }); + + it("prevents other accounts to use admin Create", async () => { + const action = contract.connect(holderOne).adminCreate(holderOne.address, "farcaster"); + + await expect(action).to.be.reverted; + }); + + it("prevents duplicated passports", async () => { + await contract.connect(holderOne).create("farcaster"); + const action = contract.connect(holderOne).create("farcaster"); + + await expect(action).to.be.reverted; + }); + }); + + describe("testing passport activate and deactivate", () => { + beforeEach(async () => { + contract = (await builder()) as PassportRegistry; + }); + + it("emits events", async () => { + await contract.connect(holderOne).create("farcaster"); + + 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.enabled()).to.be.equal(true); + + await contract.connect(admin).disable(); + + expect(await contract.enabled()).to.be.equal(false); + + await contract.connect(admin).enable(); + + expect(await contract.enabled()).to.be.equal(true); + }); + + it("prevents other accounts to disable the contract", async () => { + expect(await contract.enabled()).to.be.equal(true); + + const action = contract.connect(holderOne).disable(); + + await expect(action).to.be.reverted; + + expect(await contract.enabled()).to.be.equal(true); + }); + + it("prevents other accounts to enable the contract", async () => { + const action = contract.connect(holderOne).enable(); + + await expect(action).to.be.reverted; + }); + + it("prevents disable when the contract is already disabled", async () => { + expect(await contract.enabled()).to.be.equal(true); + + await contract.connect(admin).disable(); + + const action = contract.connect(admin).disable(); + + await expect(action).to.be.reverted; + }); + + it("prevents new creates when the contract is disabled", async () => { + expect(await contract.enabled()).to.be.equal(true); + + await contract.connect(admin).disable(); + + expect(await contract.enabled()).to.be.equal(false); + + const action = contract.connect(holderOne).create("farcaster"); + + await expect(action).to.be.revertedWith("The contract is disabled."); + }); + }); +}); 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, }; From 8139a27e4052c98e94bedce48faa8a5bbe99216e Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Fri, 8 Mar 2024 11:40:02 +0000 Subject: [PATCH 2/7] Add openzeppelin counters and paused libs --- contracts/passport/PassportRegistry.sol | 52 ++++++++++----------- hardhat.config.ts | 6 +++ test/contracts/passport/passportRegistry.ts | 34 +++++++------- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/contracts/passport/PassportRegistry.sol b/contracts/passport/PassportRegistry.sol index 1b81045..effc7bb 100644 --- a/contracts/passport/PassportRegistry.sol +++ b/contracts/passport/PassportRegistry.sol @@ -2,9 +2,13 @@ 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 { +contract PassportRegistry is Ownable, Pausable { + using Counters for Counters.Counter; + // wallet => passport id mapping(address => uint256) public passportId; @@ -24,16 +28,13 @@ contract PassportRegistry is Ownable { mapping(string => uint256) public sourcePassports; // Total number of passports created - uint256 public totalCreates; + Counters.Counter public totalCreates; // Total number of passports created by admin - uint256 public totalAdminCreates; + Counters.Counter public totalAdminCreates; // Total number of passports publicly created - uint256 public totalPublicCreates; - - // flag to enable or disable the contract - bool public enabled = true; + Counters.Counter public totalPublicCreates; // Initial id of passport creations uint256 public initialPassportId = 1000; @@ -51,40 +52,35 @@ contract PassportRegistry is Ownable { transferOwnership(contractOwner); } - modifier onlyWhileEnabled() { - require(enabled, "The contract is disabled."); - _; - } - - function create(string memory source) public onlyWhileEnabled { + function create(string memory source) public whenNotPaused { require(passportId[msg.sender] == 0, "Passport already exists"); - uint256 newPassportId = SafeMath.add(initialPassportId, SafeMath.add(totalPublicCreates, 1)); - totalPublicCreates = SafeMath.add(totalPublicCreates, 1); + uint256 newPassportId = SafeMath.add(initialPassportId, SafeMath.add(totalPublicCreates.current(), 1)); + totalPublicCreates.increment(); _create(msg.sender, newPassportId, false, source); } // Admin - function adminCreate(address wallet, string memory source) public onlyWhileEnabled onlyOwner { + function adminCreate(address wallet, string memory source) public whenNotPaused onlyOwner { require(passportId[wallet] == 0, "Passport already exists"); - uint256 newPassportId = SafeMath.add(initialPassportId, SafeMath.add(totalPublicCreates, 1)); + uint256 newPassportId = SafeMath.add(initialPassportId, SafeMath.add(totalPublicCreates.current(), 1)); - totalAdminCreates = SafeMath.add(totalAdminCreates, 1); + totalAdminCreates.increment(); _create(wallet, newPassportId, true, source); } - function adminCreateWithId(address wallet, uint256 id, string memory source) public onlyWhileEnabled onlyOwner { + function adminCreateWithId(address wallet, uint256 id, string memory source) public whenNotPaused onlyOwner { require(passportId[wallet] == 0, "Passport already exists"); require(idPassport[id] == address(0), "Passport id already assigned"); - totalAdminCreates = SafeMath.add(totalAdminCreates, 1); + totalAdminCreates.increment(); _create(wallet, id, true, source); } - function activate(address wallet) public onlyWhileEnabled onlyOwner { + function activate(address wallet) public whenNotPaused onlyOwner { require(passportId[wallet] != 0, "Passport must exist"); require(walletActive[wallet] == false, "Passport must be inactive"); @@ -97,7 +93,7 @@ contract PassportRegistry is Ownable { emit Activate(wallet, id); } - function deactivate(address wallet) public onlyWhileEnabled onlyOwner { + function deactivate(address wallet) public whenNotPaused onlyOwner { require(passportId[wallet] != 0, "Passport must exist"); require(walletActive[wallet] == true, "Passport must be active"); @@ -111,25 +107,25 @@ contract PassportRegistry is Ownable { } /** - * @notice Disables the contract, disabling future creations. + * @notice Pauses the contract, disabling future creations. * @dev Can only be called by the owner. */ - function disable() public onlyWhileEnabled onlyOwner { - enabled = false; + function pause() public whenNotPaused onlyOwner { + _pause(); } /** * @notice Enables the contract, enabling new creations. * @dev Can only be called by the owner. */ - function enable() public onlyOwner { - enabled = true; + function unpause() public whenPaused onlyOwner { + _unpause(); } // private function _create(address wallet, uint256 id, bool admin, string memory source) private { - totalCreates = SafeMath.add(totalCreates, 1); + totalCreates.increment(); idPassport[id] = wallet; passportId[wallet] = id; diff --git a/hardhat.config.ts b/hardhat.config.ts index 178ac22..0facd9c 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -65,6 +65,11 @@ const config: HardhatUserConfig = { chainId: 84532, gasMultiplier: 1.5, }, + base: { + url: "https://api.developer.coinbase.com/rpc/v1/base/w6ubd9S5jJzUzPlMn0yYmuP9UWbjKvrH", + accounts: deployer, + chainId: 8453, + }, }, gasReporter: { currency: "ETH", @@ -77,6 +82,7 @@ const config: HardhatUserConfig = { polygon: process.env.POLYGON_API_KEY || "", polygonMumbai: process.env.POLYGON_API_KEY || "", baseSepolia: process.env.BASE_SEPOLIA_API_KEY || "", + base: process.env.BASE_SEPOLIA_API_KEY || "", }, // Custom chains that are not supported by default customChains: [ diff --git a/test/contracts/passport/passportRegistry.ts b/test/contracts/passport/passportRegistry.ts index 95b70f8..a3466c3 100644 --- a/test/contracts/passport/passportRegistry.ts +++ b/test/contracts/passport/passportRegistry.ts @@ -39,7 +39,7 @@ describe("Passport", () => { expect(await contract.totalCreates()).to.eq(0); expect(await contract.totalAdminCreates()).to.eq(0); expect(await contract.totalPublicCreates()).to.eq(0); - expect(await contract.enabled()).to.eq(true); + expect(await contract.paused()).to.eq(false); expect(await contract.initialPassportId()).to.eq(1000); }); @@ -143,53 +143,53 @@ describe("Passport", () => { }); it("allows the contract owner to disable and enable the contract", async () => { - expect(await contract.enabled()).to.be.equal(true); + expect(await contract.paused()).to.be.equal(false); - await contract.connect(admin).disable(); + await contract.connect(admin).pause(); - expect(await contract.enabled()).to.be.equal(false); + expect(await contract.paused()).to.be.equal(true); - await contract.connect(admin).enable(); + await contract.connect(admin).unpause(); - expect(await contract.enabled()).to.be.equal(true); + expect(await contract.paused()).to.be.equal(false); }); it("prevents other accounts to disable the contract", async () => { - expect(await contract.enabled()).to.be.equal(true); + expect(await contract.paused()).to.be.equal(false); - const action = contract.connect(holderOne).disable(); + const action = contract.connect(holderOne).pause(); await expect(action).to.be.reverted; - expect(await contract.enabled()).to.be.equal(true); + expect(await contract.paused()).to.be.equal(false); }); it("prevents other accounts to enable the contract", async () => { - const action = contract.connect(holderOne).enable(); + 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.enabled()).to.be.equal(true); + expect(await contract.paused()).to.be.equal(false); - await contract.connect(admin).disable(); + await contract.connect(admin).pause(); - const action = contract.connect(admin).disable(); + 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.enabled()).to.be.equal(true); + expect(await contract.paused()).to.be.equal(false); - await contract.connect(admin).disable(); + await contract.connect(admin).pause(); - expect(await contract.enabled()).to.be.equal(false); + expect(await contract.paused()).to.be.equal(true); const action = contract.connect(holderOne).create("farcaster"); - await expect(action).to.be.revertedWith("The contract is disabled."); + await expect(action).to.be.revertedWith("Pausable: paused"); }); }); }); From 24341ed1cf488e098412b352ba24c59123e372ba Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Fri, 8 Mar 2024 11:58:29 +0000 Subject: [PATCH 3/7] Add ability to transfer passport --- contracts/passport/PassportRegistry.sol | 20 +++++++++++++++++ test/contracts/passport/passportRegistry.ts | 25 +++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/contracts/passport/PassportRegistry.sol b/contracts/passport/PassportRegistry.sol index effc7bb..d6a2448 100644 --- a/contracts/passport/PassportRegistry.sol +++ b/contracts/passport/PassportRegistry.sol @@ -36,12 +36,18 @@ contract PassportRegistry is Ownable, Pausable { // Total number of passports publicly created Counters.Counter public totalPublicCreates; + // Total number of passport transfers + Counters.Counter public totalPassportTransfers; + // Initial id of passport creations uint256 public initialPassportId = 1000; // A new passport has been created event Create(address indexed wallet, uint256 passportId, bool admin, string source); + // A passport has been tranfered + event Transfer(uint256 passportId, address indexed oldWallet, address indexed newWallet); + // A passport has been deactivated event Deactivate(address indexed wallet, uint256 passportId); @@ -61,6 +67,19 @@ contract PassportRegistry is Ownable, Pausable { _create(msg.sender, newPassportId, false, source); } + function transfer(address newWallet) public whenNotPaused { + uint256 id = passportId[msg.sender]; + require(id != 0, "Passport does not exist"); + + passportId[msg.sender] = 0; + passportId[newWallet] = id; + idPassport[id] = newWallet; + walletActive[msg.sender] = false; + totalPassportTransfers.increment(); + + emit Transfer(id, msg.sender, newWallet); + } + // Admin function adminCreate(address wallet, string memory source) public whenNotPaused onlyOwner { @@ -75,6 +94,7 @@ contract PassportRegistry is Ownable, Pausable { function adminCreateWithId(address wallet, uint256 id, string memory source) public whenNotPaused onlyOwner { require(passportId[wallet] == 0, "Passport already exists"); require(idPassport[id] == address(0), "Passport id already assigned"); + require(id <= initialPassportId, "Passport id must be less or equal to 1000"); totalAdminCreates.increment(); _create(wallet, id, true, source); diff --git a/test/contracts/passport/passportRegistry.ts b/test/contracts/passport/passportRegistry.ts index a3466c3..0f9552a 100644 --- a/test/contracts/passport/passportRegistry.ts +++ b/test/contracts/passport/passportRegistry.ts @@ -89,6 +89,31 @@ describe("Passport", () => { expect(holderThreeActivePassportId).to.eq(true); }); + it("emits a tranfer event everytime a passport is tranfered", async () => { + let tx = await contract.connect(holderOne).create("farcaster"); + + tx = await contract.connect(holderOne).transfer(holderTwo.address); + + const event = await findEvent(tx, "Transfer"); + + expect(event).to.exist; + expect(event?.args?.passportId).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("prevents admin Creates with a wrong passportId", async () => { + const action = contract.connect(admin).adminCreateWithId(holderOne.address, 1001, "farcaster"); + + await expect(action).to.be.reverted; + }); + it("prevents other accounts to use admin Create", async () => { const action = contract.connect(holderOne).adminCreate(holderOne.address, "farcaster"); From 3ab619835e063343a838c229ee90f126d6c05aab Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Fri, 15 Mar 2024 12:02:44 +0000 Subject: [PATCH 4/7] Add admin transfer function --- contracts/passport/PassportRegistry.sol | 59 ++++++++++++-------- hardhat.config.ts | 3 +- test/contracts/passport/passportRegistry.ts | 61 +++++++++++++++------ 3 files changed, 82 insertions(+), 41 deletions(-) diff --git a/contracts/passport/PassportRegistry.sol b/contracts/passport/PassportRegistry.sol index d6a2448..879f34f 100644 --- a/contracts/passport/PassportRegistry.sol +++ b/contracts/passport/PassportRegistry.sol @@ -30,9 +30,6 @@ contract PassportRegistry is Ownable, Pausable { // Total number of passports created Counters.Counter public totalCreates; - // Total number of passports created by admin - Counters.Counter public totalAdminCreates; - // Total number of passports publicly created Counters.Counter public totalPublicCreates; @@ -43,10 +40,10 @@ contract PassportRegistry is Ownable, Pausable { uint256 public initialPassportId = 1000; // A new passport has been created - event Create(address indexed wallet, uint256 passportId, bool admin, string source); + event Create(address indexed wallet, uint256 passportId, string source); // A passport has been tranfered - event Transfer(uint256 passportId, address indexed oldWallet, address indexed newWallet); + event Transfer(uint256 oldPassportId, uint256 newPassportId, address indexed oldWallet, address indexed newWallet); // A passport has been deactivated event Deactivate(address indexed wallet, uint256 passportId); @@ -64,9 +61,13 @@ contract PassportRegistry is Ownable, Pausable { uint256 newPassportId = SafeMath.add(initialPassportId, SafeMath.add(totalPublicCreates.current(), 1)); totalPublicCreates.increment(); - _create(msg.sender, newPassportId, false, source); + _create(msg.sender, newPassportId, 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]; require(id != 0, "Passport does not exist"); @@ -77,29 +78,39 @@ contract PassportRegistry is Ownable, Pausable { walletActive[msg.sender] = false; totalPassportTransfers.increment(); - emit Transfer(id, msg.sender, newWallet); + emit Transfer(id, id, msg.sender, newWallet); } // Admin - function adminCreate(address wallet, string memory source) public whenNotPaused onlyOwner { - require(passportId[wallet] == 0, "Passport already exists"); - - uint256 newPassportId = SafeMath.add(initialPassportId, SafeMath.add(totalPublicCreates.current(), 1)); + /** + * @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 whenNotPaused 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"); - totalAdminCreates.increment(); - _create(wallet, newPassportId, true, source); - } + 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; - function adminCreateWithId(address wallet, uint256 id, string memory source) public whenNotPaused onlyOwner { - require(passportId[wallet] == 0, "Passport already exists"); - require(idPassport[id] == address(0), "Passport id already assigned"); - require(id <= initialPassportId, "Passport id must be less or equal to 1000"); + totalPassportTransfers.increment(); - totalAdminCreates.increment(); - _create(wallet, id, true, source); + 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"); @@ -113,6 +124,10 @@ contract PassportRegistry is Ownable, Pausable { 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"); @@ -144,7 +159,7 @@ contract PassportRegistry is Ownable, Pausable { // private - function _create(address wallet, uint256 id, bool admin, string memory source) private { + function _create(address wallet, uint256 id, string memory source) private { totalCreates.increment(); idPassport[id] = wallet; @@ -154,6 +169,6 @@ contract PassportRegistry is Ownable, Pausable { idSource[id] = source; sourcePassports[source] = SafeMath.add(sourcePassports[source], 1); // emit event - emit Create(wallet, id, admin, source); + emit Create(wallet, id, source); } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 0facd9c..d723349 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -66,9 +66,10 @@ const config: HardhatUserConfig = { gasMultiplier: 1.5, }, base: { - url: "https://api.developer.coinbase.com/rpc/v1/base/w6ubd9S5jJzUzPlMn0yYmuP9UWbjKvrH", + url: "https://mainnet.base.org", accounts: deployer, chainId: 8453, + gasMultiplier: 1.5, }, }, gasReporter: { diff --git a/test/contracts/passport/passportRegistry.ts b/test/contracts/passport/passportRegistry.ts index 0f9552a..d03bbaa 100644 --- a/test/contracts/passport/passportRegistry.ts +++ b/test/contracts/passport/passportRegistry.ts @@ -37,13 +37,12 @@ describe("Passport", () => { it("is created with the correct state", async () => { expect(await contract.totalCreates()).to.eq(0); - expect(await contract.totalAdminCreates()).to.eq(0); expect(await contract.totalPublicCreates()).to.eq(0); expect(await contract.paused()).to.eq(false); expect(await contract.initialPassportId()).to.eq(1000); }); - it("emits a create event everytime a create is created", async () => { + 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"); @@ -51,42 +50,44 @@ describe("Passport", () => { expect(event).to.exist; expect(event?.args?.wallet).to.eq(holderOne.address); expect(event?.args?.passportId).to.eq(1001); - expect(event?.args?.admin).to.eq(false); - tx = await contract.connect(admin).adminCreate(holderTwo.address, "farcaster"); + 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(1002); - expect(event?.args?.admin).to.eq(true); expect(event?.args?.source).to.eq("farcaster"); }); it("stores the contract state correctly", async () => { await contract.connect(holderOne).create("farcaster"); - await contract.connect(admin).adminCreate(holderTwo.address, "farcaster"); + await contract.connect(holderTwo).create("passport"); - await contract.connect(admin).adminCreateWithId(holderThree.address, 5, "farcaster"); + await contract.connect(holderThree).create("passport"); + + await contract.connect(admin).adminTransfer(holderThree.address, 5); - const adminCreates = await contract.totalAdminCreates(); const publicCreates = await contract.totalPublicCreates(); + 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(2); - expect(publicCreates).to.eq(1); + expect(publicCreates).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 () => { @@ -97,7 +98,8 @@ describe("Passport", () => { const event = await findEvent(tx, "Transfer"); expect(event).to.exist; - expect(event?.args?.passportId).to.eq(1001); + 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); @@ -108,16 +110,25 @@ describe("Passport", () => { expect(holderTwoPassportId).to.eq(1001); }); - it("prevents admin Creates with a wrong passportId", async () => { - const action = contract.connect(admin).adminCreateWithId(holderOne.address, 1001, "farcaster"); + it("emits a tranfer event everytime a passport is tranfered by an admin", async () => { + let tx = await contract.connect(holderOne).create("farcaster"); - await expect(action).to.be.reverted; - }); + let holderOnePassportId = await contract.passportId(holderOne.address); + expect(holderOnePassportId).to.eq(1001); - it("prevents other accounts to use admin Create", async () => { - const action = contract.connect(holderOne).adminCreate(holderOne.address, "farcaster"); + tx = await contract.connect(admin).adminTransfer(holderOne.address, 1); - await expect(action).to.be.reverted; + 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 duplicated passports", async () => { @@ -126,6 +137,20 @@ describe("Passport", () => { await expect(action).to.be.reverted; }); + + it("prevents admin transfers of existing passports", async () => { + await contract.connect(holderOne).create("farcaster"); + + 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("testing passport activate and deactivate", () => { From 2c819d9c21edf5b78d2746f6d6e16d7725a105a5 Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Mon, 18 Mar 2024 11:32:58 +0000 Subject: [PATCH 5/7] Allow transfer on paused contract --- contracts/passport/PassportRegistry.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/passport/PassportRegistry.sol b/contracts/passport/PassportRegistry.sol index 879f34f..f75e82b 100644 --- a/contracts/passport/PassportRegistry.sol +++ b/contracts/passport/PassportRegistry.sol @@ -87,7 +87,7 @@ contract PassportRegistry is Ownable, Pausable { * @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 whenNotPaused onlyOwner { + 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"); From ab58cbb21d683587d63f626131c619775a0445eb Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Tue, 16 Apr 2024 16:36:25 +0100 Subject: [PATCH 6/7] Add sequencial and admin generation modes --- contracts/passport/PassportRegistry.sol | 88 ++++++++++- hardhat.config.ts | 17 +- package.json | 10 +- scripts/passport/deployPassportRegistry.ts | 2 +- scripts/passport/mintTestPassport.ts | 96 ++++++++++++ scripts/passport/transferPassportOwnership.ts | 55 +++++++ test/contracts/passport/passportRegistry.ts | 147 ++++++++++++++++-- yarn.lock | 96 ++++++++++++ 8 files changed, 478 insertions(+), 33 deletions(-) create mode 100644 scripts/passport/mintTestPassport.ts create mode 100644 scripts/passport/transferPassportOwnership.ts diff --git a/contracts/passport/PassportRegistry.sol b/contracts/passport/PassportRegistry.sol index f75e82b..1f27223 100644 --- a/contracts/passport/PassportRegistry.sol +++ b/contracts/passport/PassportRegistry.sol @@ -30,14 +30,20 @@ contract PassportRegistry is Ownable, Pausable { // Total number of passports created Counters.Counter public totalCreates; - // Total number of passports publicly created - Counters.Counter public totalPublicCreates; + // 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; - // Initial id of passport creations - uint256 public initialPassportId = 1000; + // 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); @@ -51,17 +57,58 @@ contract PassportRegistry is Ownable, Pausable { // 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 { + function create(string memory source) public whenNotPaused whenSequencialGeneration { require(passportId[msg.sender] == 0, "Passport already exists"); - uint256 newPassportId = SafeMath.add(initialPassportId, SafeMath.add(totalPublicCreates.current(), 1)); - totalPublicCreates.increment(); + 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"); + require(idPassport[id] == address(0), "Passport id already issued"); + + totalAdminsCreates.increment(); - _create(msg.sender, newPassportId, source); + _create(wallet, id, source); } /** @@ -157,6 +204,31 @@ contract PassportRegistry is Ownable, Pausable { _unpause(); } + /** + * @notice Changes the contract generation mode. + * @dev Can only be called by the owner. + */ + function setGenerationMode(bool sequencialFlag, uint256 nextSequencialPassportId) public whenNotPaused 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 { diff --git a/hardhat.config.ts b/hardhat.config.ts index d723349..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"; @@ -60,13 +61,13 @@ const config: HardhatUserConfig = { gasMultiplier: 1.5, }, baseSepolia: { - url: "https://sepolia.base.org", + url: "https://api.developer.coinbase.com/rpc/v1/base-sepolia/Ip9cOQPtBOm81rN2I9_1rBiMXOfKBxii", accounts: deployer, chainId: 84532, gasMultiplier: 1.5, }, base: { - url: "https://mainnet.base.org", + url: "https://api.developer.coinbase.com/rpc/v1/base/Ip9cOQPtBOm81rN2I9_1rBiMXOfKBxii", accounts: deployer, chainId: 8453, gasMultiplier: 1.5, @@ -82,8 +83,8 @@ const config: HardhatUserConfig = { alfajores: process.env.CELO_API_KEY || "", polygon: process.env.POLYGON_API_KEY || "", polygonMumbai: process.env.POLYGON_API_KEY || "", - baseSepolia: process.env.BASE_SEPOLIA_API_KEY || "", - base: process.env.BASE_SEPOLIA_API_KEY || "", + base: "", + baseSepolia: "", }, // Custom chains that are not supported by default customChains: [ @@ -111,6 +112,14 @@ const config: HardhatUserConfig = { 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 index ac8c75e..a5af295 100644 --- a/scripts/passport/deployPassportRegistry.ts +++ b/scripts/passport/deployPassportRegistry.ts @@ -3,7 +3,7 @@ import { ethers, network } from "hardhat"; import { deployPassport } from "../shared"; async function main() { - console.log(`Deploying buy vTal package ${network.name}`); + console.log(`Deploying passport registry at ${network.name}`); const [admin] = await ethers.getSigners(); 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/test/contracts/passport/passportRegistry.ts b/test/contracts/passport/passportRegistry.ts index d03bbaa..d5ac830 100644 --- a/test/contracts/passport/passportRegistry.ts +++ b/test/contracts/passport/passportRegistry.ts @@ -30,20 +30,54 @@ describe("Passport", () => { return deployContract(admin, Artifacts.PassportRegistry, [admin.address]); } - describe("behaviour", () => { + 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.totalPublicCreates()).to.eq(0); + expect(await contract.totalSequencialCreates()).to.eq(0); expect(await contract.paused()).to.eq(false); - expect(await contract.initialPassportId()).to.eq(1000); + 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(holderOne).create("farcaster"); + let tx = await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); let event = await findEvent(tx, "Create"); @@ -51,7 +85,7 @@ describe("Passport", () => { expect(event?.args?.wallet).to.eq(holderOne.address); expect(event?.args?.passportId).to.eq(1001); - tx = await contract.connect(holderTwo).create("farcaster"); + tx = await contract.connect(admin).adminCreate("farcaster", holderTwo.address, 1002); event = await findEvent(tx, "Create"); @@ -62,15 +96,15 @@ describe("Passport", () => { }); it("stores the contract state correctly", async () => { - await contract.connect(holderOne).create("farcaster"); + await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); - await contract.connect(holderTwo).create("passport"); + await contract.connect(admin).adminCreate("passport", holderTwo.address, 1002); - await contract.connect(holderThree).create("passport"); + await contract.connect(admin).adminCreate("passport", holderThree.address, 1003); await contract.connect(admin).adminTransfer(holderThree.address, 5); - const publicCreates = await contract.totalPublicCreates(); + const adminCreates = await contract.totalAdminsCreates(); const totalPassportTransfers = await contract.totalPassportTransfers(); const holderOnePassportId = await contract.passportId(holderOne.address); @@ -80,7 +114,7 @@ describe("Passport", () => { const holderThreeActivePassportId = await contract.idActive(5); const holderThreePreviousPassportId = await contract.idActive(1003); - expect(publicCreates).to.eq(3); + expect(adminCreates).to.eq(3); expect(totalPassportTransfers).to.eq(1); expect(holderOnePassportId).to.eq(1001); expect(holderTwoPassportId).to.eq(1002); @@ -91,7 +125,7 @@ describe("Passport", () => { }); it("emits a tranfer event everytime a passport is tranfered", async () => { - let tx = await contract.connect(holderOne).create("farcaster"); + let tx = await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); tx = await contract.connect(holderOne).transfer(holderTwo.address); @@ -111,7 +145,7 @@ describe("Passport", () => { }); it("emits a tranfer event everytime a passport is tranfered by an admin", async () => { - let tx = await contract.connect(holderOne).create("farcaster"); + let tx = await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); let holderOnePassportId = await contract.passportId(holderOne.address); expect(holderOnePassportId).to.eq(1001); @@ -131,15 +165,27 @@ describe("Passport", () => { expect(holderOnePassportId).to.eq(1); }); - it("prevents duplicated passports", async () => { - await contract.connect(holderOne).create("farcaster"); + 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(holderOne).create("farcaster"); + await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); const action = contract.connect(admin).adminTransfer(holderOne.address, 1001); @@ -153,13 +199,82 @@ describe("Passport", () => { }); }); + 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("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(holderOne).create("farcaster"); + await contract.connect(admin).adminCreate("farcaster", holderOne.address, 1001); let holderActivePassport = await contract.walletActive(holderOne.address); expect(holderActivePassport).to.eq(true); 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" From b1b67b851438e3c061fe3fd64f3260043e65c0e9 Mon Sep 17 00:00:00 2001 From: Ruben Dinis Date: Thu, 18 Apr 2024 11:36:16 +0100 Subject: [PATCH 7/7] Add some safety checks --- contracts/passport/PassportRegistry.sol | 8 ++++++-- test/contracts/passport/passportRegistry.ts | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/contracts/passport/PassportRegistry.sol b/contracts/passport/PassportRegistry.sol index 1f27223..f27bd04 100644 --- a/contracts/passport/PassportRegistry.sol +++ b/contracts/passport/PassportRegistry.sol @@ -104,7 +104,6 @@ contract PassportRegistry is Ownable, Pausable { uint256 id ) public onlyOwner whenNotPaused whenAdminGeneration { require(passportId[wallet] == 0, "Passport already exists"); - require(idPassport[id] == address(0), "Passport id already issued"); totalAdminsCreates.increment(); @@ -117,12 +116,15 @@ contract PassportRegistry is Ownable, Pausable { */ 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); @@ -208,7 +210,7 @@ contract PassportRegistry is Ownable, Pausable { * @notice Changes the contract generation mode. * @dev Can only be called by the owner. */ - function setGenerationMode(bool sequencialFlag, uint256 nextSequencialPassportId) public whenNotPaused onlyOwner { + function setGenerationMode(bool sequencialFlag, uint256 nextSequencialPassportId) public onlyOwner { _sequencial = sequencialFlag; _nextSequencialPassportId = nextSequencialPassportId; @@ -232,6 +234,8 @@ contract PassportRegistry is Ownable, Pausable { // 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; diff --git a/test/contracts/passport/passportRegistry.ts b/test/contracts/passport/passportRegistry.ts index d5ac830..621f474 100644 --- a/test/contracts/passport/passportRegistry.ts +++ b/test/contracts/passport/passportRegistry.ts @@ -261,6 +261,27 @@ describe("Passport", () => { 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);