From 08efcf8daac886f9c690356dab702de72d7991f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20B=C3=A9ranger?= Date: Mon, 2 Dec 2024 15:54:44 +0100 Subject: [PATCH] Add delegation proofs (#156) * fix bal script * fix .env * fix script * add auto-delegate * add auto-delegation * add 2 scripts --- .env.template | 14 ++-- README.md | 13 +++- contracts/NFT.sol | 2 + contracts/variants/crosschain/NFT.sol | 102 +++++++++++++++++++++++++- hardhat.config.ts | 26 ++----- package.json | 2 +- scripts/check-my-balance.ts | 54 +++++++++++--- scripts/claim-delegation.ts | 72 ++++++++++++++++++ scripts/gov-burn.ts | 6 +- scripts/gov-manifesto-edit.ts | 6 +- scripts/gov-voting-delay.ts | 2 +- scripts/propose.ts | 8 +- scripts/verify-delegation-proof.ts | 47 ++++++++++++ test/Gov-crosschain.ts | 22 +++++- test/Gov.ts | 28 ++++++- 15 files changed, 350 insertions(+), 54 deletions(-) create mode 100644 scripts/claim-delegation.ts create mode 100644 scripts/verify-delegation-proof.ts diff --git a/.env.template b/.env.template index 5f8beff..518547f 100644 --- a/.env.template +++ b/.env.template @@ -1,22 +1,24 @@ +# Signer Private Key (signer[0]) +SIGNER_PRIVATE_KEY="88888" + # Optimism Mainnet OPTIMISM_MAINNET_RPC_ENDPOINT_URL="https://mainnet.optimism.io" -OPTIMISM_MAINNET_PRIVATE_KEY="88888" OP_ETHERSCAN_API_KEY="88888" # Base Mainnet BASE_MAINNET_RPC_ENDPOINT_URL="https://mainnet.base.org" -BASE_MAINNET_PRIVATE_KEY="88888" BASE_ETHERSCAN_API_KEY="88888" # Sepolia -SEPOLIA_RPC_ENDPOINT_URL="https://ethereum-sepolia.publicnode.com" -SEPOLIA_PRIVATE_KEY="88888" +SEPOLIA_RPC_ENDPOINT_URL="https://sepolia.infura.io/v3/88888" ETHERSCAN_API_KEY="88888" # OP Sepolia OP_SEPOLIA_RPC_ENDPOINT_URL="https://sepolia.optimism.io" -OP_SEPOLIA_PRIVATE_KEY="88888" # Base Sepolia BASE_SEPOLIA_RPC_ENDPOINT_URL="https://sepolia.base.org" -BASE_SEPOLIA_PRIVATE_KEY="88888" \ No newline at end of file + +# Addresses used when running scripts for testing cross-chain scenarios +ALICE="88888" # Alice // 0xD8a394e7d7894bDF2C57139fF17e5CBAa29Dd977 +JUNGLE="88888" # Jungle Fever // 0xBDC0E420aB9ba144213588A95fa1E5e63CEFf1bE \ No newline at end of file diff --git a/README.md b/README.md index be1e40a..9a35ecf 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,13 @@ Then you can add your DAO in [Tally](https://www.tally.xyz/) and/or spin up your ### Crosschain -Make sure the contracts are deployed from the same account. +Make sure that the deployer wallet address is funded on each notwork you want to deploy to: + +``` +pnpm bal +``` + +Then, you can go ahead and deploy: ```bash pnpm crosschain:sepolia @@ -61,7 +67,7 @@ pnpm crosschain:op-sepolia Your DAO will be deployed on every networks at the same address. -Then you can follow these steps to verify that proofs of `safeMint`, `govBurn`, `setMetadata`, `setManifesto`, `setVotingDelay`, and `delegate` can be generated on source chain and claimed on foreign chain: +Then you can follow these steps to verify that proofs can be generated on home chain and claimed on foreign chain: ```bash npx hardhat run scripts/propose.ts --network sepolia @@ -81,6 +87,9 @@ npx hardhat run scripts/claim-manifesto-update.ts --network op-sepolia npx hardhat run scripts/gov-voting-delay.ts --network sepolia npx hardhat run scripts/verify-voting-delay-proof.ts --network sepolia npx hardhat run scripts/claim-voting-delay.ts --network op-sepolia + +npx hardhat run scripts/verify-delegation-proof.ts --network sepolia +npx hardhat run scripts/claim-delegation.ts --network op-sepolia ``` ## Security diff --git a/contracts/NFT.sol b/contracts/NFT.sol index 3d25b5b..b3bfa8c 100644 --- a/contracts/NFT.sol +++ b/contracts/NFT.sol @@ -38,6 +38,7 @@ contract NFT is ) ERC721(_name, _symbol) Ownable(initialOwner) EIP712(_name, "1") { for (uint i; i < _firstMembers.length; i++) { safeMint(_firstMembers[i], _uri); + _delegate(_firstMembers[i], _firstMembers[i]); } } @@ -63,6 +64,7 @@ contract NFT is uint256 tokenId = _nextTokenId++; _safeMint(to, tokenId); _setTokenURI(tokenId, uri); + _delegate(to, to); } /// @notice Updates the NFT ownership diff --git a/contracts/variants/crosschain/NFT.sol b/contracts/variants/crosschain/NFT.sol index c58a811..d3faf76 100644 --- a/contracts/variants/crosschain/NFT.sol +++ b/contracts/variants/crosschain/NFT.sol @@ -14,7 +14,7 @@ import "./ProofHandler.sol"; * @title Cross-chain Membership NFT Contract * @author Web3 Hackers Collective * @notice Non-transferable NFT implementation for DAO membership with cross-chain capabilities - * @dev Extends OpenZeppelin's NFT standards with cross-chain operation support + * @dev Extends OpenZeppelin's NFT standards with cross-chain operation support and delegation * @custom:security-contact julien@strat.cc */ contract NFT is @@ -41,7 +41,8 @@ contract NFT is enum OperationType { MINT, BURN, - SET_METADATA + SET_METADATA, + DELEGATE } /// @notice Emitted when a membership is claimed @@ -62,6 +63,12 @@ contract NFT is /// @param nonce Operation sequence number event MetadataUpdated(uint256 indexed tokenId, string newUri, uint256 nonce); + /// @notice Emitted when delegation is synchronized across chains + /// @param delegator The address delegating their voting power + /// @param delegatee The address receiving the delegation + /// @param nonce Operation sequence number + event DelegationSynced(address indexed delegator, address indexed delegatee, uint256 nonce); + /// @notice Restricts functions to home chain modifier onlyHomeChain() { require(block.chainid == home, "Operation only allowed on home chain"); @@ -88,6 +95,7 @@ contract NFT is home = _home; for (uint i; i < _firstMembers.length; i++) { _govMint(_firstMembers[i], _uri); + _delegate(_firstMembers[i], _firstMembers[i]); } } @@ -127,8 +135,20 @@ contract NFT is emit MetadataUpdated(tokenId, uri, nonce); } + /** + * @notice Delegates voting power to another address on home chain + * @dev Overrides ERC721Votes delegate function to add cross-chain functionality + * @param delegatee Address to delegate voting power to + */ + function delegate(address delegatee) public virtual override onlyHomeChain { + uint256 nonce = _proofStorage.incrementNonce(uint8(OperationType.DELEGATE)); + _delegate(_msgSender(), delegatee); + emit DelegationSynced(_msgSender(), delegatee, nonce); + } + /** * @notice Generates proof for NFT operations + * @dev Only callable on home chain * @param operationType Type of operation * @param params Operation parameters * @return Encoded proof data @@ -142,6 +162,31 @@ contract NFT is return ProofHandler.generateProof(address(this), operationType, params, nextNonce); } + /** + * @notice Generates proof for delegation + * @dev Only callable on home chain + * @param delegator Address delegating voting power + * @param delegatee Address receiving delegation + * @return Encoded proof data + */ + function generateDelegationProof( + address delegator, + address delegatee + ) external view returns (bytes memory) { + require(block.chainid == home, "Proofs only generated on home chain"); + uint256 nextNonce = _proofStorage.getNextNonce(uint8(OperationType.DELEGATE)); + bytes memory params = abi.encode(delegator, delegatee); + return + ProofHandler.generateProof( + address(this), + uint8(OperationType.DELEGATE), + params, + nextNonce + ); + } + + // Claim operations + /** * @notice Claims an NFT operation on a foreign chain * @param proof Proof generated by home chain @@ -163,9 +208,28 @@ contract NFT is (uint256 tokenId, string memory uri) = abi.decode(params, (uint256, string)); _setTokenURI(tokenId, uri); emit MetadataUpdated(tokenId, uri, nonce); + } else if (operationType == uint8(OperationType.DELEGATE)) { + (address delegator, address delegatee) = abi.decode(params, (address, address)); + _delegate(delegator, delegatee); + emit DelegationSynced(delegator, delegatee, nonce); } } + /** + * @notice Claims a delegation operation on a foreign chain + * @param proof Proof generated by home chain + */ + function claimDelegation(bytes memory proof) external { + (uint8 operationType, bytes memory params, uint256 nonce) = ProofHandler + .verifyAndClaimProof(proof, address(this), _proofStorage); + + require(operationType == uint8(OperationType.DELEGATE), "Invalid operation type"); + (address delegator, address delegatee) = abi.decode(params, (address, address)); + + _delegate(delegator, delegatee); + emit DelegationSynced(delegator, delegatee, nonce); + } + /** * @notice Internal function for minting without proof verification * @param to Address to receive token @@ -175,10 +239,19 @@ contract NFT is uint256 tokenId = _nextTokenId++; _safeMint(to, tokenId); _setTokenURI(tokenId, uri); + _delegate(to, to); } // Required overrides + /** + * @notice Updates token data + * @dev Overrides ERC721 _update to make NFTs non-transferable + * @param to Recipient address + * @param tokenId Token ID + * @param auth Address authorized for transfer + * @return Previous owner address + */ function _update( address to, uint256 tokenId, @@ -188,6 +261,12 @@ contract NFT is return super._update(to, tokenId, auth); } + /** + * @notice Increments account balance + * @dev Internal override to maintain compatibility + * @param account Account to update + * @param value Amount to increase by + */ function _increaseBalance( address account, uint128 value @@ -195,22 +274,41 @@ contract NFT is super._increaseBalance(account, value); } + /** + * @notice Gets token URI + * @param tokenId Token ID to query + * @return URI string + */ function tokenURI( uint256 tokenId ) public view override(ERC721, ERC721URIStorage) returns (string memory) { return super.tokenURI(tokenId); } + /** + * @notice Checks interface support + * @param interfaceId Interface identifier to check + * @return bool True if interface is supported + */ function supportsInterface( bytes4 interfaceId ) public view override(ERC721, ERC721Enumerable, ERC721URIStorage) returns (bool) { return super.supportsInterface(interfaceId); } + /** + * @notice Gets current timestamp + * @dev Used for voting snapshots + * @return Current block timestamp + */ function clock() public view override returns (uint48) { return uint48(block.timestamp); } + /** + * @notice Gets clock mode description + * @return String indicating timestamp-based voting + */ function CLOCK_MODE() public pure override returns (string memory) { return "mode=timestamp"; } diff --git a/hardhat.config.ts b/hardhat.config.ts index 4902895..860c1e7 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -6,19 +6,15 @@ import * as dotenv from "dotenv" dotenv.config() const { + SIGNER_PRIVATE_KEY, OPTIMISM_MAINNET_RPC_ENDPOINT_URL, - OPTIMISM_MAINNET_PRIVATE_KEY, OP_ETHERSCAN_API_KEY, BASE_MAINNET_RPC_ENDPOINT_URL, - BASE_MAINNET_PRIVATE_KEY, BASE_ETHERSCAN_API_KEY, SEPOLIA_RPC_ENDPOINT_URL, - SEPOLIA_PRIVATE_KEY, ETHERSCAN_API_KEY, OP_SEPOLIA_RPC_ENDPOINT_URL, - OP_SEPOLIA_PRIVATE_KEY, - BASE_SEPOLIA_RPC_ENDPOINT_URL, - BASE_SEPOLIA_PRIVATE_KEY + BASE_SEPOLIA_RPC_ENDPOINT_URL } = process.env const config: HardhatUserConfig = { @@ -37,7 +33,7 @@ const config: HardhatUserConfig = { SEPOLIA_RPC_ENDPOINT_URL || "https://ethereum-sepolia.publicnode.com", accounts: - SEPOLIA_PRIVATE_KEY !== undefined ? [SEPOLIA_PRIVATE_KEY] : [] + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, optimism: { chainId: 10, @@ -45,17 +41,13 @@ const config: HardhatUserConfig = { OPTIMISM_MAINNET_RPC_ENDPOINT_URL || "https://mainnet.optimism.io", accounts: - OPTIMISM_MAINNET_PRIVATE_KEY !== undefined - ? [OPTIMISM_MAINNET_PRIVATE_KEY] - : [] + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, base: { chainId: 8453, url: BASE_MAINNET_RPC_ENDPOINT_URL || "https://mainnet.base.org", accounts: - BASE_MAINNET_PRIVATE_KEY !== undefined - ? [BASE_MAINNET_PRIVATE_KEY] - : [] + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, "op-sepolia": { chainId: 11155420, @@ -63,17 +55,13 @@ const config: HardhatUserConfig = { OP_SEPOLIA_RPC_ENDPOINT_URL || "https://ethereum-sepolia.publicnode.com", accounts: - OP_SEPOLIA_PRIVATE_KEY !== undefined - ? [OP_SEPOLIA_PRIVATE_KEY] - : [] + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, "base-sepolia": { chainId: 84532, url: BASE_SEPOLIA_RPC_ENDPOINT_URL || "https://sepolia.base.org", accounts: - BASE_SEPOLIA_PRIVATE_KEY !== undefined - ? [BASE_SEPOLIA_PRIVATE_KEY] - : [] + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] } }, solidity: { diff --git a/package.json b/package.json index 8af041a..18fab1a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "deploy:base-sepolia": "hardhat deploy --network base-sepolia --reset", "crosschain:sepolia": "hardhat deploy --network sepolia --tags CrosschainGov --reset", "crosschain:op-sepolia": "hardhat deploy --network op-sepolia --tags CrosschainGov --reset", - "bal": "npx hardhat run scripts/check-my-balance.ts --network", + "bal": "npx hardhat run scripts/check-my-balance.ts", "prettier": "prettier --write \"**/*.ts\"", "prettier-check": "prettier --check \"**/*.ts\"" }, diff --git a/scripts/check-my-balance.ts b/scripts/check-my-balance.ts index f6036d8..e4cc948 100644 --- a/scripts/check-my-balance.ts +++ b/scripts/check-my-balance.ts @@ -1,21 +1,55 @@ const color = require("cli-color") var msg = color.xterm(39).bgXterm(128) -import hre, { ethers, network } from "hardhat" -import fs from "fs" +import hre from "hardhat" +import { ethers } from "ethers" async function main() { - const [signer] = await ethers.getSigners() + const networks = Object.entries(hre.config.networks) console.log( - "\nCurrent signer wallet (" + signer.address + ") has", + color.cyanBright( + "\nFetching signer balances for all supported networks...\n" + ) + ) + + for (const [networkName, networkConfig] of networks) { + try { + console.log(color.magenta(`\nSwitching to network: ${networkName}`)) + + // Ensure network has an RPC URL and accounts + const { url, accounts } = networkConfig as any + + if (!url || accounts.length === 0) { + console.error( + color.yellow( + `Skipping network "${networkName}" due to missing RPC URL or accounts.` + ) + ) + continue + } + + // Create provider and signer + const provider = new ethers.JsonRpcProvider(url) + const signer = new ethers.Wallet(accounts[0], provider) - msg( - ethers.formatEther( - String(await ethers.provider.getBalance(signer.address)) + // Get balance + const balance = await provider.getBalance(signer.address) + + console.log( + `Signer (${signer.address}) on network "${networkName}" has`, + msg(ethers.formatEther(balance)), + "ETH." ) - ), - "ETH." - ) + } catch (error: any) { + console.error( + color.red( + `Failed to process network ${networkName}: ${error.message}` + ) + ) + } + } + + console.log(color.cyanBright("\nDone fetching balances for all networks.")) } main().catch(error => { diff --git a/scripts/claim-delegation.ts b/scripts/claim-delegation.ts new file mode 100644 index 0000000..0bbcefa --- /dev/null +++ b/scripts/claim-delegation.ts @@ -0,0 +1,72 @@ +import { ethers } from "hardhat" +import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" + +async function main() { + const ALICE_PRIVATE_KEY = process.env.ALICE + if (!ALICE_PRIVATE_KEY) { + throw new Error("Please set ALICE private key in your .env file") + } + + const NFT_ADDRESS = "0xe74bC6A3Ee4ED824708DD88465BD2CdD6b869620" + const provider = new ethers.JsonRpcProvider( + process.env.OP_SEPOLIA_RPC_ENDPOINT_URL + ) + const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) + const nft = NFT__factory.connect(NFT_ADDRESS, aliceSigner) + + // Replace with actual proof from verify script + const proof = + "0x000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001105ba21c724f00f5f34580504d14bfc47bd93927bcf076081ff7652e1a1b77d80000000000000000000000000000000000000000000000000000000000000040000000000000000000000000d8a394e7d7894bdf2c57139ff17e5cbaa29dd977000000000000000000000000d8a394e7d7894bdf2c57139ff17e5cbaa29dd977" + + try { + console.log("Simulating delegation claim...") + await nft.claimOperation.staticCall(proof) + console.log("✅ Simulation successful") + + console.log("Submitting delegation claim...") + const tx = await nft.claimOperation(proof, { + gasLimit: 500000 + }) + + console.log("Transaction submitted:", tx.hash) + const receipt = await tx.wait() + + if (receipt?.status === 1) { + console.log("Delegation claimed successfully! 🎉") + + const delegationEvent = receipt?.logs.find(log => { + try { + return ( + nft.interface.parseLog(log)?.name === "DelegationSynced" + ) + } catch { + return false + } + }) + + if (delegationEvent) { + const parsedEvent = nft.interface.parseLog(delegationEvent) + console.log("\nDelegation details:") + console.log("Delegator:", parsedEvent?.args?.delegator) + console.log("Delegatee:", parsedEvent?.args?.delegatee) + console.log("Nonce:", parsedEvent?.args?.nonce.toString()) + } + } + } catch (error: any) { + console.error("\nError details:", error) + if (error.data) { + try { + const decodedError = nft.interface.parseError(error.data) + console.error("Decoded error:", decodedError) + } catch (e) { + console.error("Raw error data:", error.data) + } + } + throw error + } +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/gov-burn.ts b/scripts/gov-burn.ts index 92a5835..9ae30fa 100644 --- a/scripts/gov-burn.ts +++ b/scripts/gov-burn.ts @@ -18,8 +18,8 @@ function getProposalState(state: number): string { async function main() { const ALICE_PRIVATE_KEY = process.env.ALICE - const SEPOLIA_PRIVATE_KEY = process.env.SEPOLIA_PRIVATE_KEY - if (!ALICE_PRIVATE_KEY || !SEPOLIA_PRIVATE_KEY) { + const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY + if (!ALICE_PRIVATE_KEY || !SIGNER_PRIVATE_KEY) { throw new Error("Please set required private keys in your .env file") } @@ -31,7 +31,7 @@ async function main() { process.env.SEPOLIA_RPC_ENDPOINT_URL ) const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) - const sepoliaSigner = new ethers.Wallet(SEPOLIA_PRIVATE_KEY, provider) + const sepoliaSigner = new ethers.Wallet(SIGNER_PRIVATE_KEY, provider) console.log("Connected with address:", aliceSigner.address) diff --git a/scripts/gov-manifesto-edit.ts b/scripts/gov-manifesto-edit.ts index 53b2bb2..dbacbd2 100644 --- a/scripts/gov-manifesto-edit.ts +++ b/scripts/gov-manifesto-edit.ts @@ -18,8 +18,8 @@ function getProposalState(state: number): string { async function main() { const ALICE_PRIVATE_KEY = process.env.ALICE - const SEPOLIA_PRIVATE_KEY = process.env.SEPOLIA_PRIVATE_KEY - if (!ALICE_PRIVATE_KEY || !SEPOLIA_PRIVATE_KEY) { + const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY + if (!ALICE_PRIVATE_KEY || !SIGNER_PRIVATE_KEY) { throw new Error("Please set required private keys in your .env file") } @@ -31,7 +31,7 @@ async function main() { process.env.SEPOLIA_RPC_ENDPOINT_URL ) const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) - const sepoliaSigner = new ethers.Wallet(SEPOLIA_PRIVATE_KEY, provider) + const sepoliaSigner = new ethers.Wallet(SIGNER_PRIVATE_KEY, provider) console.log("Connected with address:", aliceSigner.address) diff --git a/scripts/gov-voting-delay.ts b/scripts/gov-voting-delay.ts index 639ee59..be99151 100644 --- a/scripts/gov-voting-delay.ts +++ b/scripts/gov-voting-delay.ts @@ -64,7 +64,7 @@ async function main() { newVotingDelay: 250n, rpcUrl: process.env.SEPOLIA_RPC_ENDPOINT_URL, aliceKey: process.env.ALICE, - sepoliaKey: process.env.SEPOLIA_PRIVATE_KEY + sepoliaKey: process.env.SIGNER_PRIVATE_KEY } // Validate environment diff --git a/scripts/propose.ts b/scripts/propose.ts index 3dca557..249c201 100644 --- a/scripts/propose.ts +++ b/scripts/propose.ts @@ -8,12 +8,12 @@ function sleep(ms: number) { async function main() { const ALICE_PRIVATE_KEY = process.env.ALICE - const SEPOLIA_PRIVATE_KEY = process.env.SEPOLIA_PRIVATE_KEY + const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY if (!ALICE_PRIVATE_KEY) { throw new Error("Please set ALICE private key in your .env file") } - if (!SEPOLIA_PRIVATE_KEY) { - throw new Error("Please set SEPOLIA_PRIVATE_KEY in your .env file") + if (!SIGNER_PRIVATE_KEY) { + throw new Error("Please set SIGNER_PRIVATE_KEY in your .env file") } const JUNGLE_ADDRESS = "0xBDC0E420aB9ba144213588A95fa1E5e63CEFf1bE" @@ -25,7 +25,7 @@ async function main() { process.env.SEPOLIA_RPC_ENDPOINT_URL ) const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) - const sepoliaSigner = new ethers.Wallet(SEPOLIA_PRIVATE_KEY, provider) + const sepoliaSigner = new ethers.Wallet(SIGNER_PRIVATE_KEY, provider) console.log("Using address for proposals:", aliceSigner.address) console.log("Using address for execution:", sepoliaSigner.address) diff --git a/scripts/verify-delegation-proof.ts b/scripts/verify-delegation-proof.ts new file mode 100644 index 0000000..c7b5470 --- /dev/null +++ b/scripts/verify-delegation-proof.ts @@ -0,0 +1,47 @@ +import { ethers } from "hardhat" +import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" +import { NFT } from "../typechain-types/contracts/variants/crosschain/NFT" + +async function main() { + const ALICE_PRIVATE_KEY = process.env.ALICE + if (!ALICE_PRIVATE_KEY) { + throw new Error("Please set ALICE private key in your .env file") + } + + const NFT_ADDRESS = "0xe74bC6A3Ee4ED824708DD88465BD2CdD6b869620" + const DELEGATOR = "0xD8a394e7d7894bDF2C57139fF17e5CBAa29Dd977" + const DELEGATEE = "0xD8a394e7d7894bDF2C57139fF17e5CBAa29Dd977" + const PROOF_HANDLER_ADDRESS = "0x0152ee45780385dACCCCB128D816031CfFe5F36B" + + const provider = new ethers.JsonRpcProvider( + process.env.SEPOLIA_RPC_ENDPOINT_URL + ) + const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) + + const NFTFactory = await ethers.getContractFactory( + "contracts/variants/crosschain/NFT.sol:NFT", + { + libraries: { + ProofHandler: PROOF_HANDLER_ADDRESS + } + } + ) + const nft = NFT__factory.connect(NFT_ADDRESS, aliceSigner) + + console.log("Generating delegation proof...") + console.log("Delegator:", DELEGATOR) + console.log("Delegatee:", DELEGATEE) + + const encodedParams = ethers.AbiCoder.defaultAbiCoder().encode( + ["address", "address"], + [DELEGATOR, DELEGATEE] + ) + + const proof = await nft.generateOperationProof(3, encodedParams) // 3 is DELEGATE operation type + console.log("\nProof:", proof) +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/test/Gov-crosschain.ts b/test/Gov-crosschain.ts index 93482a0..cb3903f 100644 --- a/test/Gov-crosschain.ts +++ b/test/Gov-crosschain.ts @@ -1,4 +1,4 @@ -import { loadFixture } from "@nomicfoundation/hardhat-network-helpers" +import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers" import { expect } from "chai" import { ethers } from "hardhat" import type { NFT, Gov, ProofHandler } from "../typechain-types" @@ -433,6 +433,7 @@ describe("Crosschain Gov", function () { "Manifesto proof digest verification failed" ) }) + describe("Governance Parameter Proofs", () => { it("should verify voting delay proof correctly", async function () { const newVotingDelay = 48n @@ -650,6 +651,25 @@ describe("Crosschain Gov", function () { }) }) + it("should auto-delegate voting power to initial members on deployment", async function () { + const { nft } = await loadFixture(deployContracts) + + // Get current timestamp + const startTime = await time.latest() + + // Increase time by only 1 sec in on this network + await time.increase(1) + + // Check voting power at the starting timestamp + const alicePower = await nft.getPastVotes(alice.address, startTime) + const bobPower = await nft.getPastVotes(bob.address, startTime) + + expect(alicePower).to.equal(1) + expect(bobPower).to.equal(1) + expect(await nft.delegates(alice.address)).to.equal(alice.address) + expect(await nft.delegates(bob.address)).to.equal(bob.address) + }) + describe("Delegation Transfers", function () { it("should properly transfer voting power when changing delegates", async function () { // Initial state - Alice and Bob each have 1 vote diff --git a/test/Gov.ts b/test/Gov.ts index fd515aa..ea8ff4c 100644 --- a/test/Gov.ts +++ b/test/Gov.ts @@ -149,7 +149,7 @@ describe("Gov", function () { // Change delegation await nft.connect(alice).delegate(bob.address) expect(await nft.getVotes(david.address)).to.equal(0) - expect(await nft.getVotes(bob.address)).to.equal(1) + expect(await nft.getVotes(bob.address)).to.equal(2) }) it("should maintain zero voting power for non-holders across multiple delegations", async function () { @@ -162,12 +162,36 @@ describe("Gov", function () { await nft.connect(charlie).delegate(bob.address) expect(await nft.getVotes(david.address)).to.equal(0) - expect(await nft.getVotes(alice.address)).to.equal(0) + expect(await nft.getVotes(alice.address)).to.equal(1) + // Bob should maintain only his original voting power if any expect(await nft.getVotes(bob.address)).to.equal( initialBobVotes ) }) + it("should auto-delegate voting power to initial members on deployment", async function () { + const { nft } = await loadFixture(deployContracts) + + // Get current timestamp + const startTime = await time.latest() + + // Increase time by only 1 sec in on this network + await time.increase(1) + + // Check voting power at the starting timestamp + const alicePower = await nft.getPastVotes( + alice.address, + startTime + ) + const bobPower = await nft.getPastVotes(bob.address, startTime) + + expect(alicePower).to.equal(1) + expect(bobPower).to.equal(1) + expect(await nft.delegates(alice.address)).to.equal( + alice.address + ) + expect(await nft.delegates(bob.address)).to.equal(bob.address) + }) }) describe("Historical Delegation Checks", function () {