diff --git a/.env.template b/.env.template index 518547f..2bd1e64 100644 --- a/.env.template +++ b/.env.template @@ -9,16 +9,23 @@ OP_ETHERSCAN_API_KEY="88888" BASE_MAINNET_RPC_ENDPOINT_URL="https://mainnet.base.org" BASE_ETHERSCAN_API_KEY="88888" +# Arbitrum One Mainnet +ARBITRUM_MAINNET_RPC_ENDPOINT_URL="88888" +ARBITRUM_ETHERSCAN_API_KEY="88888" + # Sepolia -SEPOLIA_RPC_ENDPOINT_URL="https://sepolia.infura.io/v3/88888" +SEPOLIA_RPC_ENDPOINT_URL="88888" ETHERSCAN_API_KEY="88888" # OP Sepolia -OP_SEPOLIA_RPC_ENDPOINT_URL="https://sepolia.optimism.io" +OP_SEPOLIA_RPC_ENDPOINT_URL="88888" # Base Sepolia BASE_SEPOLIA_RPC_ENDPOINT_URL="https://sepolia.base.org" +# Arbitrum Sepolia +ARBITRUM_SEPOLIA_RPC_ENDPOINT_URL="88888" + # 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 +ALICE="88888" +JUNGLE="88888" \ No newline at end of file diff --git a/.gitignore b/.gitignore index ae62128..06e8463 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ artifacts .env* !.env.template NOTES.md -/deployments \ No newline at end of file +deployments +data.json \ No newline at end of file diff --git a/README.md b/README.md index d564462..bbe7a01 100644 --- a/README.md +++ b/README.md @@ -52,51 +52,19 @@ Then you can add your DAO in [Tally](https://www.tally.xyz/) and/or spin up your ### Crosschain -Make sure that the deployer wallet address is funded on each notwork you want to deploy to: +Run the `scenario1.sh` bash script: ``` -pnpm bal +./scenario1.sh ``` -Then, you can go ahead and deploy: +It will: -```bash -pnpm crosschain:sepolia -pnpm crosschain:opSepolia -pnpm crosschain:baseSepolia -pnpm crosschain:arbitrumSepolia -``` - -Your DAO will be deployed on every networks at the same address (watch the [Asciinema video](https://asciinema.org/a/rc8bTqbBiW7e0xevewxCwCP7C)). - -Then you can follow these steps to verify that proofs can be generated on home chain and claimed on foreign chain: - -```bash - -# Watch the [Asciinema video](https://asciinema.org/a/1iZZQVKU51U86hzYYLfjSVtw6) -npx hardhat run scripts/propose.ts --network sepolia -npx hardhat run scripts/verify-proof.ts --network sepolia -npx hardhat run scripts/claim-membership.ts --network opSepolia -npx hardhat run scripts/claim-membership.ts --network baseSepolia -npx hardhat run scripts/claim-membership.ts --network arbitrumSepolia - -npx hardhat run scripts/gov-burn.ts --network sepolia -npx hardhat run scripts/verify-gov-burn-proof.ts --network sepolia -npx hardhat run scripts/claim-gov-burn.ts --network opSepolia - -npx hardhat run scripts/verify-metadata-proof.ts --network sepolia -npx hardhat run scripts/claim-metadata-update.ts --network opSepolia - -npx hardhat run scripts/verify-manifesto-proof.ts --network sepolia -npx hardhat run scripts/claim-manifesto-update.ts --network opSepolia - -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 opSepolia - -npx hardhat run scripts/verify-delegation-proof.ts --network sepolia -npx hardhat run scripts/claim-delegation.ts --network opSepolia -``` +- Deploy to OP Sepolia +- Deploy to Arbitrum Sepolia +- Submit a proposal and add a member +- Generate a membership proof on OP Sepolia +- Claim that proof on Arbitrum Sepolia ## Security @@ -122,9 +90,23 @@ The following functions are `onlyOwner`, and since the NFT contract ownership is |---------|----------|---------------| | Optimism Mainnet | 10 | [Documentation](https://docs.optimism.io/chain/networks#op-mainnet) | | Base Mainnet | 8453 | [Documentation](https://docs.base.org/docs/network-information#base-mainnet) | +| Arbitrum One | 42161 | [Documentation](https://docs.arbitrum.io/welcome/get-started) | | Sepolia Testnet | 11155111 | [Documentation](https://ethereum.org/nb/developers/docs/networks/#sepolia) | -| OP Sepolia Testnet | 11155420 | [Documentation](https://docs.optimism.io/chain/networks#opSepolia) | +| OP Sepolia Testnet | 11155420 | [Documentation](https://docs.optimism.io/chain/networks#op-sepolia) | | Base Sepolia Testnet | 84532 | [Documentation](https://docs.base.org/docs/network-information/#base-testnet-sepolia) | +| Arbitrum Sepolia | 421614 | [Documentation](https://docs.arbitrum.io/welcome/get-started) | + +## Contract Verification + +| Network | Explorer URL | API URL | API Key Variable | +|---------|--------------|---------|-----------------| +| Optimism | https://optimistic.etherscan.io | https://api-optimistic.etherscan.io/api | OP_ETHERSCAN_API_KEY | +| Base | https://basescan.org | https://api.basescan.org/api | BASE_ETHERSCAN_API_KEY | +| Arbitrum One | https://arbiscan.io | https://api.arbiscan.io/api | ARBITRUM_ETHERSCAN_API_KEY | +| Sepolia | https://sepolia.etherscan.io | https://api-sepolia.etherscan.io/api | ETHERSCAN_API_KEY | +| OP Sepolia | https://sepolia-optimism.etherscan.io | https://api-sepolia-optimistic.etherscan.io/api | OP_ETHERSCAN_API_KEY | +| Base Sepolia | https://sepolia.basescan.org | https://api-sepolia.basescan.org/api | BASE_ETHERSCAN_API_KEY | +| Arbitrum Sepolia | https://sepolia.arbiscan.io | https://api-sepolia.arbiscan.io/api | ARBITRUM_ETHERSCAN_API_KEY | ## Core Dependencies @@ -136,4 +118,4 @@ The following functions are `onlyOwner`, and since the NFT contract ownership is ## Support -Feel free to reach out to [Julien](https://github.com/julienbrg): [Farcaster](https://warpcast.com/julien-), [Element](https://matrix.to/#/@julienbrg:matrix.org), [Status](https://status.app/u/iwSACggKBkp1bGllbgM=#zQ3shmh1sbvE6qrGotuyNQB22XU5jTrZ2HFC8bA56d5kTS2fy), [Telegram](https://t.me/julienbrg), [Twitter](https://twitter.com/julienbrg), [Discord](https://discordapp.com/users/julienbrg), or [LinkedIn](https://www.linkedin.com/in/julienberanger/). \ No newline at end of file +Feel free to reach out to [Julien](https://github.com/julienbrg) on [Farcaster](https://warpcast.com/julien-), [Element](https://matrix.to/#/@julienbrg:matrix.org), [Status](https://status.app/u/iwSACggKBkp1bGllbgM=#zQ3shmh1sbvE6qrGotuyNQB22XU5jTrZ2HFC8bA56d5kTS2fy), [Telegram](https://t.me/julienbrg), [Twitter](https://twitter.com/julienbrg), [Discord](https://discordapp.com/users/julienbrg), or [LinkedIn](https://www.linkedin.com/in/julienberanger/). \ No newline at end of file diff --git a/contracts/variants/crosschain/Gov.sol b/contracts/variants/crosschain/Gov.sol index acbe722..04a7c38 100644 --- a/contracts/variants/crosschain/Gov.sol +++ b/contracts/variants/crosschain/Gov.sol @@ -6,7 +6,6 @@ import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; -import "./ProofHandler.sol"; /** * @title Cross-chain Governance Contract @@ -28,8 +27,10 @@ contract Gov is /// @notice IPFS CID of the DAO's manifesto string public manifesto; - /// @notice Storage for proof handling - ProofHandler.ProofStorage private _proofStorage; + /// @notice Emitted when the manifesto is updated + /// @param oldManifesto Previous manifesto CID + /// @param newManifesto New manifesto CID + event ManifestoUpdated(string oldManifesto, string newManifesto); /// @notice Types of operations that can be synchronized across chains enum OperationType { @@ -40,22 +41,14 @@ contract Gov is UPDATE_QUORUM } - /// @notice Emitted when the manifesto is updated - /// @param oldManifesto Previous manifesto CID - /// @param newManifesto New manifesto CID - /// @param nonce Update sequence number - event ManifestoUpdated(string oldManifesto, string newManifesto, uint256 nonce); - /// @notice Emitted when a governance parameter is updated - /// @param operationType Type of parameter updated - /// @param oldValue Previous value - /// @param newValue New value - /// @param nonce Update sequence number + /// @param operationType Type of parameter that was updated + /// @param oldValue Previous value of the parameter + /// @param newValue New value of the parameter event GovernanceParameterUpdated( OperationType indexed operationType, uint256 oldValue, - uint256 newValue, - uint256 nonce + uint256 newValue ); /// @notice Restricts functions to be called only on the home chain @@ -96,188 +89,208 @@ contract Gov is } /** - * @notice Updates the DAO's manifesto on the home chain + * @notice Updates the DAO's manifesto + * @dev Can only be called through governance on home chain * @param newManifesto New manifesto CID */ function setManifesto(string memory newManifesto) public onlyGovernance onlyHomeChain { - uint256 nonce = ProofHandler.incrementNonce( - _proofStorage, - uint8(OperationType.SET_MANIFESTO) + string memory oldManifesto = manifesto; + manifesto = newManifesto; + emit ManifestoUpdated(oldManifesto, newManifesto); + } + + /** + * @notice Generates proof for cross-chain manifesto update + * @dev Can only be called on home chain + * @param newManifesto New manifesto CID to generate proof for + * @return Encoded proof data for manifesto update + */ + function generateManifestoProof( + string memory newManifesto + ) external view returns (bytes memory) { + require(block.chainid == home, "Proofs can only be generated on home chain"); + bytes32 message = keccak256( + abi.encodePacked(address(this), uint8(OperationType.SET_MANIFESTO), newManifesto) + ); + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message)); + return abi.encode(newManifesto, digest); + } + + /** + * @notice Claims a manifesto update on a foreign chain + * @dev Verifies and applies manifesto updates from home chain + * @param proof Proof generated by home chain + */ + function claimManifestoUpdate(bytes memory proof) external { + (string memory newManifesto, bytes32 digest) = abi.decode(proof, (string, bytes32)); + + bytes32 message = keccak256( + abi.encodePacked(address(this), uint8(OperationType.SET_MANIFESTO), newManifesto) ); + bytes32 expectedDigest = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", message) + ); + require(digest == expectedDigest, "Invalid manifesto proof"); + string memory oldManifesto = manifesto; manifesto = newManifesto; - emit ManifestoUpdated(oldManifesto, newManifesto, nonce); + emit ManifestoUpdated(oldManifesto, newManifesto); } /** - * @notice Updates the voting delay parameter on the home chain - * @param newVotingDelay New voting delay value + * @notice Updates the voting delay parameter + * @dev Can only be called through governance on home chain + * @param newVotingDelay New voting delay value (in blocks) */ function setVotingDelay( uint48 newVotingDelay ) public virtual override onlyGovernance onlyHomeChain { - uint256 nonce = ProofHandler.incrementNonce( - _proofStorage, - uint8(OperationType.UPDATE_VOTING_DELAY) - ); uint256 oldValue = votingDelay(); _setVotingDelay(newVotingDelay); emit GovernanceParameterUpdated( OperationType.UPDATE_VOTING_DELAY, oldValue, - newVotingDelay, - nonce + newVotingDelay ); } /** - * @notice Updates the voting period parameter on the home chain - * @param newVotingPeriod New voting period value + * @notice Updates the voting period parameter + * @dev Can only be called through governance on home chain + * @param newVotingPeriod New voting period value (in blocks) */ function setVotingPeriod( uint32 newVotingPeriod ) public virtual override onlyGovernance onlyHomeChain { - uint256 nonce = ProofHandler.incrementNonce( - _proofStorage, - uint8(OperationType.UPDATE_VOTING_PERIOD) - ); uint256 oldValue = votingPeriod(); _setVotingPeriod(newVotingPeriod); emit GovernanceParameterUpdated( OperationType.UPDATE_VOTING_PERIOD, oldValue, - newVotingPeriod, - nonce + newVotingPeriod ); } /** - * @notice Updates the proposal threshold parameter on the home chain + * @notice Updates the proposal threshold parameter + * @dev Can only be called through governance on home chain * @param newProposalThreshold New proposal threshold value */ function setProposalThreshold( uint256 newProposalThreshold ) public virtual override onlyGovernance onlyHomeChain { - uint256 nonce = ProofHandler.incrementNonce( - _proofStorage, - uint8(OperationType.UPDATE_PROPOSAL_THRESHOLD) - ); uint256 oldValue = proposalThreshold(); _setProposalThreshold(newProposalThreshold); emit GovernanceParameterUpdated( OperationType.UPDATE_PROPOSAL_THRESHOLD, oldValue, - newProposalThreshold, - nonce + newProposalThreshold ); } /** - * @notice Updates the quorum numerator on the home chain - * @param newQuorumNumerator New quorum numerator value + * @notice Updates the quorum numerator + * @dev Can only be called through governance on home chain + * @param newQuorumNumerator New quorum numerator value (percentage * 100) */ function updateQuorumNumerator( uint256 newQuorumNumerator - ) public virtual override onlyGovernance onlyHomeChain { - uint256 nonce = ProofHandler.incrementNonce( - _proofStorage, - uint8(OperationType.UPDATE_QUORUM) - ); + ) public virtual override(GovernorVotesQuorumFraction) onlyGovernance onlyHomeChain { uint256 oldValue = quorumNumerator(); _updateQuorumNumerator(newQuorumNumerator); - emit GovernanceParameterUpdated( - OperationType.UPDATE_QUORUM, - oldValue, - newQuorumNumerator, - nonce - ); + emit GovernanceParameterUpdated(OperationType.UPDATE_QUORUM, oldValue, newQuorumNumerator); } /** - * @notice Generates proof for parameter updates + * @notice Generates proof for cross-chain parameter updates + * @dev Can only be called on home chain * @param operationType Type of parameter being updated - * @param value Encoded parameter value - * @return proof Encoded proof data + * @param value Encoded value for the parameter update + * @return Encoded proof data for parameter update */ function generateParameterProof( - uint8 operationType, + OperationType operationType, bytes memory value - ) external view returns (bytes memory proof) { - require(block.chainid == home, "Proofs only generated on home chain"); - uint256 nextNonce = ProofHandler.getNextNonce(_proofStorage, operationType); - return ProofHandler.generateProof(address(this), operationType, value, nextNonce); + ) external view returns (bytes memory) { + require(block.chainid == home, "Proofs can only be generated on home chain"); + bytes32 message = keccak256(abi.encodePacked(address(this), uint8(operationType), value)); + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message)); + return abi.encode(operationType, value, digest); } /** * @notice Claims a parameter update on a foreign chain + * @dev Verifies and applies parameter updates from home chain * @param proof Proof generated by home chain */ function claimParameterUpdate(bytes memory proof) external { - (uint8 operationType, bytes memory value, uint256 nonce) = ProofHandler.verifyAndClaimProof( + (OperationType operationType, bytes memory value, bytes32 digest) = abi.decode( proof, - address(this), - _proofStorage + (OperationType, bytes, bytes32) ); - if (operationType == uint8(OperationType.SET_MANIFESTO)) { - string memory newManifesto = abi.decode(value, (string)); - string memory oldManifesto = manifesto; - manifesto = newManifesto; - emit ManifestoUpdated(oldManifesto, newManifesto, nonce); - } else if (operationType == uint8(OperationType.UPDATE_VOTING_DELAY)) { + bytes32 message = keccak256(abi.encodePacked(address(this), uint8(operationType), value)); + bytes32 expectedDigest = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", message) + ); + require(digest == expectedDigest, "Invalid parameter update proof"); + + if (operationType == OperationType.UPDATE_VOTING_DELAY) { uint48 newValue = uint48(bytes6(value)); uint256 oldValue = votingDelay(); _setVotingDelay(newValue); - emit GovernanceParameterUpdated( - OperationType.UPDATE_VOTING_DELAY, - oldValue, - newValue, - nonce - ); - } else if (operationType == uint8(OperationType.UPDATE_VOTING_PERIOD)) { + emit GovernanceParameterUpdated(operationType, oldValue, newValue); + } else if (operationType == OperationType.UPDATE_VOTING_PERIOD) { uint32 newValue = uint32(bytes4(value)); uint256 oldValue = votingPeriod(); _setVotingPeriod(newValue); - emit GovernanceParameterUpdated( - OperationType.UPDATE_VOTING_PERIOD, - oldValue, - newValue, - nonce - ); - } else if (operationType == uint8(OperationType.UPDATE_PROPOSAL_THRESHOLD)) { + emit GovernanceParameterUpdated(operationType, oldValue, newValue); + } else if (operationType == OperationType.UPDATE_PROPOSAL_THRESHOLD) { uint256 newValue = abi.decode(value, (uint256)); uint256 oldValue = proposalThreshold(); _setProposalThreshold(newValue); - emit GovernanceParameterUpdated( - OperationType.UPDATE_PROPOSAL_THRESHOLD, - oldValue, - newValue, - nonce - ); - } else if (operationType == uint8(OperationType.UPDATE_QUORUM)) { + emit GovernanceParameterUpdated(operationType, oldValue, newValue); + } else if (operationType == OperationType.UPDATE_QUORUM) { uint256 newValue = abi.decode(value, (uint256)); uint256 oldValue = quorumNumerator(); _updateQuorumNumerator(newValue); - emit GovernanceParameterUpdated(OperationType.UPDATE_QUORUM, oldValue, newValue, nonce); + emit GovernanceParameterUpdated(operationType, oldValue, newValue); } } // Required overrides + /** + * @notice Gets the current voting delay + * @return Current voting delay in blocks + */ function votingDelay() public view override(Governor, GovernorSettings) returns (uint256) { return super.votingDelay(); } + /** + * @notice Gets the current voting period + * @return Current voting period in blocks + */ function votingPeriod() public view override(Governor, GovernorSettings) returns (uint256) { return super.votingPeriod(); } + /** + * @notice Gets the quorum required for a specific block + * @param blockNumber Block number to check quorum for + * @return Minimum number of votes required for quorum + */ function quorum( uint256 blockNumber ) public view override(Governor, GovernorVotesQuorumFraction) returns (uint256) { return super.quorum(blockNumber); } + /** + * @notice Gets the current proposal threshold + * @return Minimum number of votes required to create a proposal + */ function proposalThreshold() public view diff --git a/contracts/variants/crosschain/NFT.sol b/contracts/variants/crosschain/NFT.sol index 245ed2c..a84bb3d 100644 --- a/contracts/variants/crosschain/NFT.sol +++ b/contracts/variants/crosschain/NFT.sol @@ -8,13 +8,12 @@ import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Votes.sol"; -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 and delegation + * @notice A non-transferable NFT implementation for DAO membership with cross-chain capabilities + * @dev Extends OpenZeppelin's NFT standards with cross-chain operation support * @custom:security-contact julien@strat.cc */ contract NFT is @@ -26,59 +25,63 @@ contract NFT is EIP712, ERC721Votes { - using ProofHandler for ProofHandler.ProofStorage; - - /// @notice Chain ID where contract was originally deployed + /// @notice The chain ID where the contract was originally deployed uint256 public immutable home; /// @notice Next token ID to be minted uint256 private _nextTokenId; - /// @notice Storage for proof handling - ProofHandler.ProofStorage private _proofStorage; + /// @notice Tracks token existence on each chain + mapping(uint256 => bool) public existsOnChain; - /// @notice Types of operations that can be synchronized across chains + /// @notice Operation types for cross-chain message verification + /// @dev Used to differentiate between different types of cross-chain operations enum OperationType { - MINT, - BURN, - SET_METADATA, - DELEGATE + MINT, // Mint new token + BURN, // Burn existing token + SET_METADATA // Update token metadata } - /// @notice Emitted when a membership is claimed - /// @param tokenId The ID of the claimed token - /// @param member The address receiving the membership - /// @param nonce Operation sequence number - event MembershipClaimed(uint256 indexed tokenId, address indexed member, uint256 nonce); - - /// @notice Emitted when a membership is revoked - /// @param tokenId The ID of the revoked token - /// @param member The address losing membership - /// @param nonce Operation sequence number - event MembershipRevoked(uint256 indexed tokenId, address indexed member, uint256 nonce); - - /// @notice Emitted when metadata is updated - /// @param tokenId The ID of the updated token - /// @param newUri The new metadata URI - /// @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 + /** + * @notice Emitted when a membership is claimed on a new chain + * @param tokenId The ID of the claimed token + * @param member The address receiving the membership + * @param claimer The address executing the claim + */ + event MembershipClaimed( + uint256 indexed tokenId, + address indexed member, + address indexed claimer + ); + + /** + * @notice Emitted when a membership is revoked + * @param tokenId The ID of the revoked token + * @param member The address losing membership + */ + event MembershipRevoked(uint256 indexed tokenId, address indexed member); + + /** + * @notice Emitted when a token's metadata is updated + * @param tokenId The ID of the updated token + * @param newUri The new metadata URI + */ + event MetadataUpdated(uint256 indexed tokenId, string newUri); + + /** + * @notice Restricts operations to the home chain + * @dev Used to ensure certain operations only occur on the chain where the contract was originally deployed + */ modifier onlyHomeChain() { require(block.chainid == home, "Operation only allowed on home chain"); _; } /** - * @notice Initializes the NFT contract - * @param _home Chain ID where contract is considered home - * @param initialOwner Initial contract owner + * @notice Initializes the NFT contract with initial members + * @dev Sets up ERC721 parameters and mints initial tokens + * @param _home The chain ID where this contract is considered home + * @param initialOwner The initial contract owner (typically governance) * @param _firstMembers Array of initial member addresses * @param _uri Initial token URI * @param _name Token collection name @@ -94,128 +97,212 @@ contract NFT is ) ERC721(_name, _symbol) Ownable(initialOwner) EIP712(_name, "1") { home = _home; for (uint i; i < _firstMembers.length; i++) { - _govMint(_firstMembers[i], _uri); + _mint(_firstMembers[i], _uri); + _delegate(_firstMembers[i], _firstMembers[i]); } } - /// @notice Adds a new member to the DAO - /// @dev Mints a new NFT to the specified address - /// @param to The address of the new member - /// @param uri The metadata URI for the new NFT + // Home Chain Operations + + /** + * @notice Mints a new membership token + * @dev Only callable by owner on home chain + * @param to Recipient address + * @param uri Token metadata URI + */ function safeMint(address to, string memory uri) public onlyOwner onlyHomeChain { - _govMint(to, uri); + _mint(to, uri); + _delegate(to, to); } /** - * @notice Burns token on home chain - * @dev Only callable by owner (governance) on home chain + * @notice Revokes a membership + * @dev Only callable by owner on home chain * @param tokenId ID of token to burn */ function govBurn(uint256 tokenId) public onlyOwner onlyHomeChain { - uint256 nonce = _proofStorage.incrementNonce(uint8(OperationType.BURN)); - address owner = ownerOf(tokenId); - _burn(tokenId); - emit MembershipRevoked(tokenId, owner, nonce); + _govBurn(tokenId); } /** - * @notice Updates token metadata on home chain - * @dev Only callable by owner (governance) on home chain + * @notice Updates a token's metadata + * @dev Only callable by owner on home chain * @param tokenId ID of token to update * @param uri New metadata URI */ function setMetadata(uint256 tokenId, string memory uri) public onlyOwner onlyHomeChain { - uint256 nonce = _proofStorage.incrementNonce(uint8(OperationType.SET_METADATA)); - _setTokenURI(tokenId, uri); - emit MetadataUpdated(tokenId, uri, nonce); + _updateTokenMetadata(tokenId, uri); + } + + // Cross-chain Operation Proofs + + /** + * @notice Generates proof for cross-chain minting + * @dev Creates a signed message proving token ownership and metadata + * @param tokenId ID of token + * @return Encoded proof data containing token details and signature + */ + function generateMintProof(uint256 tokenId) external view returns (bytes memory) { + require(block.chainid == home, "Proofs can only be generated on home chain"); + address to = ownerOf(tokenId); + string memory uri = tokenURI(tokenId); + + bytes32 message = keccak256( + abi.encodePacked(address(this), uint8(OperationType.MINT), tokenId, to, uri) + ); + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message)); + + return abi.encode(tokenId, to, uri, digest); } /** - * @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 + * @notice Generates proof for cross-chain burning + * @dev Creates a signed message proving burn authorization + * @param tokenId ID of token to burn + * @return Encoded proof data containing burn details and signature */ - function delegate(address delegatee) public virtual override onlyHomeChain { - uint256 nonce = _proofStorage.incrementNonce(uint8(OperationType.DELEGATE)); - _delegate(_msgSender(), delegatee); - emit DelegationSynced(_msgSender(), delegatee, nonce); + function generateBurnProof(uint256 tokenId) external view returns (bytes memory) { + require(block.chainid == home, "Proofs can only be generated on home chain"); + bytes32 message = keccak256( + abi.encodePacked(address(this), uint8(OperationType.BURN), tokenId) + ); + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message)); + return abi.encode(tokenId, digest); } /** - * @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 + * @notice Generates proof for cross-chain metadata updates + * @dev Creates a signed message proving metadata update authorization + * @param tokenId Token ID to update + * @param uri New metadata URI + * @return Encoded proof data containing update details and signature */ - function generateOperationProof( - uint8 operationType, - bytes memory params + function generateMetadataProof( + uint256 tokenId, + string memory uri ) external view returns (bytes memory) { - require(block.chainid == home, "Proofs only generated on home chain"); - uint256 nextNonce = _proofStorage.getNextNonce(operationType); - return ProofHandler.generateProof(address(this), operationType, params, nextNonce); + require(block.chainid == home, "Proofs can only be generated on home chain"); + bytes32 message = keccak256( + abi.encodePacked(address(this), uint8(OperationType.SET_METADATA), tokenId, uri) + ); + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message)); + return abi.encode(tokenId, uri, digest); + } + + /** + * @notice Claims a membership on a foreign chain + * @dev Verifies proof and mints token on foreign chain + * @param proof Proof generated on home chain + */ + function claimMint(bytes memory proof) external { + (uint256 tokenId, address to, string memory uri, bytes32 digest) = abi.decode( + proof, + (uint256, address, string, bytes32) + ); + + require(!existsOnChain[tokenId], "Token already exists on this chain"); + + bytes32 message = keccak256( + abi.encodePacked(address(this), uint8(OperationType.MINT), tokenId, to, uri) + ); + bytes32 expectedDigest = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", message) + ); + require(digest == expectedDigest, "Invalid mint proof"); + _mint(to, uri); + emit MembershipClaimed(tokenId, to, msg.sender); } - // Claim operations + /** + * @notice Claims a burn operation on a foreign chain + * @dev Verifies proof and burns token on foreign chain + * @param proof Proof generated on home chain + */ + function claimBurn(bytes memory proof) external { + (uint256 tokenId, bytes32 digest) = abi.decode(proof, (uint256, bytes32)); + + bytes32 message = keccak256( + abi.encodePacked(address(this), uint8(OperationType.BURN), tokenId) + ); + bytes32 expectedDigest = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", message) + ); + require(digest == expectedDigest, "Invalid burn proof"); + + address owner = ownerOf(tokenId); + _update(address(0), tokenId, owner); + existsOnChain[tokenId] = false; + + emit MembershipRevoked(tokenId, owner); + } /** - * @notice Claims an NFT operation on a foreign chain - * @param proof Proof generated by home chain + * @notice Claims a metadata update on a foreign chain + * @dev Verifies proof and updates token metadata on foreign chain + * @param proof Proof generated on home chain */ - function claimOperation(bytes memory proof) external { - (uint8 operationType, bytes memory params, uint256 nonce) = ProofHandler - .verifyAndClaimProof(proof, address(this), _proofStorage); - - if (operationType == uint8(OperationType.MINT)) { - (uint256 tokenId, address owner, string memory uri) = abi.decode( - params, - (uint256, address, string) - ); - - try this.ownerOf(tokenId) returns (address) { - revert("Token already exists"); - } catch { - _govMint(owner, uri); - emit MembershipClaimed(_nextTokenId - 1, owner, nonce); - } - } else if (operationType == uint8(OperationType.BURN)) { - uint256 tokenId = abi.decode(params, (uint256)); - address owner = ownerOf(tokenId); - _burn(tokenId); - emit MembershipRevoked(tokenId, owner, nonce); - } else if (operationType == uint8(OperationType.SET_METADATA)) { - (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); - } + function claimMetadataUpdate(bytes memory proof) external { + (uint256 tokenId, string memory uri, bytes32 digest) = abi.decode( + proof, + (uint256, string, bytes32) + ); + + bytes32 message = keccak256( + abi.encodePacked(address(this), uint8(OperationType.SET_METADATA), tokenId, uri) + ); + bytes32 expectedDigest = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", message) + ); + require(digest == expectedDigest, "Invalid metadata proof"); + + _setTokenURI(tokenId, uri); + existsOnChain[tokenId] = true; + emit MetadataUpdated(tokenId, uri); } + // Internal Functions + /** - * @notice Internal function for minting without proof verification - * @param to Address to receive token - * @param uri Token metadata URI + * @dev Internal function to mint new token with metadata + * @param to Address receiving the token + * @param uri Metadata URI for the token */ - function _govMint(address to, string memory uri) internal { + function _mint(address to, string memory uri) private { uint256 tokenId = _nextTokenId++; _safeMint(to, tokenId); _setTokenURI(tokenId, uri); - _delegate(to, to); + existsOnChain[tokenId] = true; } - // Required overrides + /** + * @dev Internal function to burn token through governance + * @param tokenId ID of token to burn + */ + function _govBurn(uint256 tokenId) private { + address owner = ownerOf(tokenId); + _update(address(0), tokenId, owner); + existsOnChain[tokenId] = false; + emit MembershipRevoked(tokenId, owner); + } /** - * @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 + * @dev Internal function to update token metadata + * @param tokenId ID of token to update + * @param uri New metadata URI + */ + function _updateTokenMetadata(uint256 tokenId, string memory uri) private { + _setTokenURI(tokenId, uri); + emit MetadataUpdated(tokenId, uri); + } + + // Required Overrides + + /** + * @dev Override of ERC721's _update to make tokens non-transferable + * @param to Target address (only allowed to be zero address for burns) + * @param tokenId Token ID being updated + * @param auth Address initiating the update + * @return Previous owner of the token */ function _update( address to, @@ -227,9 +314,9 @@ contract NFT is } /** - * @notice Increments account balance - * @dev Internal override to maintain compatibility - * @param account Account to update + * @notice Increases an account's token balance + * @dev Internal function required by inherited contracts + * @param account Address to increase balance for * @param value Amount to increase by */ function _increaseBalance( @@ -240,9 +327,10 @@ contract NFT is } /** - * @notice Gets token URI - * @param tokenId Token ID to query - * @return URI string + * @notice Gets the token URI + * @dev Returns the metadata URI for a given token + * @param tokenId ID of the token + * @return URI string for the token metadata */ function tokenURI( uint256 tokenId @@ -251,9 +339,10 @@ contract NFT is } /** - * @notice Checks interface support + * @notice Checks if the contract supports a given interface + * @dev Implements interface detection for ERC721 and extensions * @param interfaceId Interface identifier to check - * @return bool True if interface is supported + * @return bool True if the interface is supported */ function supportsInterface( bytes4 interfaceId @@ -262,8 +351,8 @@ contract NFT is } /** - * @notice Gets current timestamp - * @dev Used for voting snapshots + * @notice Gets the current timestamp + * @dev Used for voting snapshots, returns block timestamp as uint48 * @return Current block timestamp */ function clock() public view override returns (uint48) { @@ -271,8 +360,9 @@ contract NFT is } /** - * @notice Gets clock mode description - * @return String indicating timestamp-based voting + * @notice Gets the clock mode for voting snapshots + * @dev Returns a description of how the clock value should be interpreted + * @return String indicating timestamp-based clock mode */ function CLOCK_MODE() public pure override returns (string memory) { return "mode=timestamp"; diff --git a/contracts/variants/crosschain/ProofHandler.sol b/contracts/variants/crosschain/ProofHandler.sol deleted file mode 100644 index 1a88d56..0000000 --- a/contracts/variants/crosschain/ProofHandler.sol +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.20; - -/** - * @title Proof Handler Library - * @author Web3 Hackers Collective - * @notice Library for standardized cross-chain proof generation and verification - * @dev Handles proof generation, verification, and tracking for cross-chain operations - * @custom:security-contact julien@strat.cc - */ -library ProofHandler { - /// @notice Tracks which proofs have been applied and nonces for operations that require them - struct ProofStorage { - mapping(bytes32 => bool) updateAppliedOnChain; - mapping(uint8 => uint256) currentNonce; - } - - /// @dev Emitted when a proof is claimed - event ProofClaimed(uint8 indexed operationType, bytes params, uint256 nonce); - - /** - * @notice Generates a proof for cross-chain operations - * @param contractAddress Address of contract generating the proof - * @param operationType Type of operation being performed - * @param params Operation parameters - * @param nonce Current nonce for this operation type (0 for nonce-free operations) - * @return proof Encoded proof data - */ - function generateProof( - address contractAddress, - uint8 operationType, - bytes memory params, - uint256 nonce - ) public pure returns (bytes memory proof) { - bytes32 message = keccak256( - abi.encodePacked(contractAddress, operationType, params, nonce) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", message)); - - return abi.encode(operationType, params, nonce, digest); - } - - /** - * @notice Verifies and claims a proof - * @param proof Proof to verify and claim - * @param contractAddress Address of contract claiming the proof - * @param storage_ Proof storage struct - * @return operationType The type of operation being performed - * @return params The operation parameters - * @return nonce The operation sequence number - */ - function verifyAndClaimProof( - bytes memory proof, - address contractAddress, - ProofStorage storage storage_ - ) public returns (uint8 operationType, bytes memory params, uint256 nonce) { - bytes32 digest; - (operationType, params, nonce, digest) = abi.decode( - proof, - (uint8, bytes, uint256, bytes32) - ); - - bytes32 message = keccak256( - abi.encodePacked(contractAddress, operationType, params, nonce) - ); - bytes32 expectedDigest = keccak256( - abi.encodePacked("\x19Ethereum Signed Message:\n32", message) - ); - require(digest == expectedDigest, "Invalid proof"); - - if (operationType > 1) { - bytes32 proofHash = keccak256(proof); - require(!storage_.updateAppliedOnChain[proofHash], "Proof already claimed"); - require(nonce == storage_.currentNonce[operationType] + 1, "Invalid nonce"); - - storage_.updateAppliedOnChain[proofHash] = true; - storage_.currentNonce[operationType] = nonce; - } - - emit ProofClaimed(operationType, params, nonce); - - return (operationType, params, nonce); - } - - /** - * @notice Gets the next nonce for an operation type - * @param storage_ Proof storage struct - * @param operationType Type of operation - * @return nonce Next nonce value - */ - function getNextNonce( - ProofStorage storage storage_, - uint8 operationType - ) public view returns (uint256 nonce) { - if (operationType <= 1) return 0; // MINT or BURN operations don't use nonces - return storage_.currentNonce[operationType] + 1; - } - - /** - * @notice Increments the nonce for an operation type - * @param storage_ Proof storage struct - * @param operationType Type of operation - * @return nonce New nonce value - */ - function incrementNonce( - ProofStorage storage storage_, - uint8 operationType - ) public returns (uint256 nonce) { - if (operationType <= 1) return 0; // MINT or BURN operations don't use nonces - storage_.currentNonce[operationType]++; - return storage_.currentNonce[operationType]; - } -} diff --git a/deploy/deploy-crosschain-gov.ts b/deploy/deploy-crosschain-gov.ts index 1cfee4d..1f00d44 100644 --- a/deploy/deploy-crosschain-gov.ts +++ b/deploy/deploy-crosschain-gov.ts @@ -3,7 +3,6 @@ import { DeployFunction } from "hardhat-deploy/types" import color from "cli-color" var msg = color.xterm(39).bgXterm(128) import { - homeChain, firstMembers, uri, name, @@ -20,40 +19,23 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre const { deterministic } = deployments const { deployer } = await getNamedAccounts() - const salt = "-v1" + const salt = hre.ethers.id("Dec-12-v2") + const homeChainId = 11155420 function wait(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } - // Deploy ProofHandler library - const { address: proofHandlerAddress, deploy: deployProofHandler } = - await deterministic("ProofHandler", { - from: deployer, - contract: - "contracts/variants/crosschain/ProofHandler.sol:ProofHandler", - salt: hre.ethers.id("ProofHandler" + salt), - log: true, - waitConfirmations: 1 - }) - - console.log("ProofHandler library address:", msg(proofHandlerAddress)) - await deployProofHandler() - // Deploy NFT const { address: nftAddress, deploy: deployNFT } = await deterministic( "CrosschainNFT", { from: deployer, contract: "contracts/variants/crosschain/NFT.sol:NFT", - args: [homeChain, deployer, firstMembers, uri, name, symbol], - libraries: { - ProofHandler: proofHandlerAddress - }, - salt: hre.ethers.id("NFT" + salt), + args: [homeChainId, deployer, firstMembers, uri, name, symbol], + salt: salt, log: true, - waitConfirmations: 1, - gasLimit: 10000000 + waitConfirmations: 1 } ) @@ -67,7 +49,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { from: deployer, contract: "contracts/variants/crosschain/Gov.sol:Gov", args: [ - homeChain, + homeChainId, nftAddress, manifesto, daoName, @@ -76,10 +58,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { votingThreshold, quorum ], - libraries: { - ProofHandler: proofHandlerAddress - }, - salt: hre.ethers.id("Gov" + salt), + salt: salt, log: true, waitConfirmations: 5 } @@ -89,50 +68,21 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { console.log("Gov contract address:", msg(govAddress)) // Transfer NFT ownership to Gov - try { - let txOptions = {} - - switch (hre.network.name) { - case "arbitrum": - case "arbitrumSepolia": - case "sepolia": - case "opSepolia": - txOptions = { gasLimit: 500000 } - break - default: - txOptions = {} - } - - const nft = await hre.ethers.getContractAt( - "contracts/variants/crosschain/NFT.sol:NFT", - nftAddress - ) - await nft.transferOwnership(govAddress, txOptions) - console.log("NFT ownership transferred to Gov") - } catch (e: any) { - console.warn("error during ownership transfer", e) - } + const nft = await hre.ethers.getContractAt( + "contracts/variants/crosschain/NFT.sol:NFT", + nftAddress + ) + await nft.transferOwnership(govAddress) + console.log("NFT ownership transferred to Gov") if (hre.network.name !== "hardhat") { - console.log("\nVerifying ProofHandler library...") - try { - await hre.run("verify:verify", { - address: proofHandlerAddress, - contract: - "contracts/variants/crosschain/ProofHandler.sol:ProofHandler" - }) - console.log("ProofHandler verification done ✅") - } catch (err) { - console.log("ProofHandler verification failed:", err) - } - console.log("\nVerifying NFT contract...") try { await hre.run("verify:verify", { address: nftAddress, contract: "contracts/variants/crosschain/NFT.sol:NFT", constructorArguments: [ - homeChain, + homeChainId, deployer, firstMembers, uri, @@ -151,7 +101,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { address: govAddress, contract: "contracts/variants/crosschain/Gov.sol:Gov", constructorArguments: [ - homeChain, + homeChainId, nftAddress, manifesto, daoName, diff --git a/hardhat.config.ts b/hardhat.config.ts index 8e13295..adc3b23 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -2,7 +2,6 @@ import { HardhatUserConfig } from "hardhat/config" import "@nomicfoundation/hardhat-toolbox" import "@nomicfoundation/hardhat-verify" import "hardhat-deploy" -import "@typechain/hardhat" import * as dotenv from "dotenv" dotenv.config() @@ -37,8 +36,7 @@ const config: HardhatUserConfig = { SEPOLIA_RPC_ENDPOINT_URL || "https://ethereum-sepolia.publicnode.com", accounts: - SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [], - allowUnlimitedContractSize: true + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, optimism: { chainId: 10, @@ -46,59 +44,53 @@ const config: HardhatUserConfig = { OPTIMISM_MAINNET_RPC_ENDPOINT_URL || "https://mainnet.optimism.io", accounts: - SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [], - allowUnlimitedContractSize: true + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, base: { chainId: 8453, url: BASE_MAINNET_RPC_ENDPOINT_URL || "https://mainnet.base.org", accounts: - SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [], - allowUnlimitedContractSize: true + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, arbitrum: { chainId: 42161, url: ARBITRUM_MAINNET_RPC_ENDPOINT_URL || - "https://endpoints.omniatech.io/v1/arbitrum/one/public", + "https://arb1.arbitrum.io/rpc", accounts: - SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [], - allowUnlimitedContractSize: true + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, - opSepolia: { + + "op-sepolia": { chainId: 11155420, url: OP_SEPOLIA_RPC_ENDPOINT_URL || "https://ethereum-sepolia.publicnode.com", accounts: - SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [], - allowUnlimitedContractSize: true + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, - baseSepolia: { + "base-sepolia": { chainId: 84532, url: BASE_SEPOLIA_RPC_ENDPOINT_URL || "https://sepolia.base.org", accounts: - SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [], - allowUnlimitedContractSize: true + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] }, - arbitrumSepolia: { + "arbitrum-sepolia": { chainId: 421614, url: ARBITRUM_SEPOLIA_RPC_ENDPOINT_URL || "https://sepolia-rollup.arbitrum.io/rpc", accounts: - SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [], - allowUnlimitedContractSize: true + SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : [] } }, solidity: { - version: "0.8.20", + version: "0.8.22", settings: { optimizer: { enabled: true, runs: 200 - }, - viaIR: false + } } }, sourcify: { @@ -111,13 +103,13 @@ const config: HardhatUserConfig = { arbitrum: ARBITRUM_ETHERSCAN_API_KEY || "", sepolia: ETHERSCAN_API_KEY || "", optimisticEthereum: OP_ETHERSCAN_API_KEY || "", - opSepolia: OP_ETHERSCAN_API_KEY || "", - baseSepolia: BASE_ETHERSCAN_API_KEY || "", - arbitrumSepolia: ARBITRUM_ETHERSCAN_API_KEY || "" + "op-sepolia": OP_ETHERSCAN_API_KEY || "", + "base-sepolia": BASE_ETHERSCAN_API_KEY || "", + "arbitrum-sepolia": ARBITRUM_ETHERSCAN_API_KEY || "" }, customChains: [ { - network: "opSepolia", + network: "op-sepolia", chainId: 11155420, urls: { apiURL: "https://api-sepolia-optimistic.etherscan.io/api", @@ -125,12 +117,20 @@ const config: HardhatUserConfig = { } }, { - network: "baseSepolia", + network: "base-sepolia", chainId: 84532, urls: { apiURL: "https://api-sepolia.basescan.org/api", browserURL: "https://basescan.org/" } + }, + { + network: "arbitrum-sepolia", + chainId: 421614, + urls: { + apiURL: "https://api-sepolia.arbiscan.io/api", + browserURL: "https://sepolia.arbiscan.io" + } } ] } diff --git a/package.json b/package.json index 6874347..9ebfde8 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,13 @@ "deploy:optimism": "hardhat deploy --network optimism --reset", "deploy:base": "hardhat deploy --network base --reset", "deploy:sepolia": "hardhat deploy --network sepolia --reset", - "deploy:opSepolia": "hardhat deploy --network opSepolia --reset", - "deploy:baseSepolia": "hardhat deploy --network baseSepolia --reset", - "deploy:arbitrumSepolia": "hardhat deploy --network arbitrumSepolia --reset", + "deploy:op-sepolia": "hardhat deploy --network op-sepolia --reset", + "deploy:arbitrum-sepolia": "hardhat deploy --network arbitrum-sepolia --reset", + "deploy:base-sepolia": "hardhat deploy --network base-sepolia --reset", "crosschain:sepolia": "hardhat deploy --network sepolia --tags CrosschainGov --reset", - "crosschain:opSepolia": "hardhat deploy --network opSepolia --tags CrosschainGov --reset", - "crosschain:baseSepolia": "hardhat deploy --network baseSepolia --tags CrosschainGov --reset", - "crosschain:arbitrumSepolia": "hardhat deploy --network arbitrumSepolia --tags CrosschainGov --reset", + "crosschain:op-sepolia": "hardhat deploy --network op-sepolia --tags CrosschainGov --reset", "bal": "npx hardhat run scripts/check-my-balance.ts", + "verify:setup": "hardhat run scripts/verify-crosschain-setup.ts", "prettier": "prettier --write \"**/*.ts\"", "prettier-check": "prettier --check \"**/*.ts\"" }, @@ -42,7 +41,7 @@ "@types/mocha": "^10.0.10", "@types/node": "^22.9.3", "chai": "^4.2.0", - "hardhat": "^2.22.16", + "hardhat": "^2.22.17", "hardhat-gas-reporter": "^1.0.8", "prettier": "^2.8.8", "prettier-plugin-solidity": "^1.4.1", diff --git a/scenario.sh b/scenario.sh deleted file mode 100755 index de2181d..0000000 --- a/scenario.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -pnpm test - -pnpm crosschain:sepolia -pnpm crosschain:opSepolia -pnpm crosschain:baseSepolia -pnpm crosschain:arbitrumSepolia - -npx hardhat run scripts/check-token-existence.ts - -npx hardhat run scripts/propose.ts --network sepolia - -if [ -f .env ]; then - source .env - if [ -z "$TOKENID" ]; then - echo "Error: TOKENID not found in .env file" - exit 1 - fi - echo "Using Token ID: $TOKENID" -else - echo "Error: .env file not found" - exit 1 -fi - -npx hardhat run scripts/verify-proof.ts --network sepolia - -source .env - -if [ -z "$PROOF" ]; then - echo "Error: No proof generated" - exit 1 -fi - -npx hardhat run scripts/claim-membership.ts --network opSepolia -npx hardhat run scripts/claim-membership.ts --network baseSepolia -npx hardhat run scripts/claim-membership.ts --network arbitrumSepolia - -npx hardhat run scripts/check-token-existence.ts --network sepolia - -sed -i.bak '/^TOKENID=/d' .env -echo "TOKENID=2" >> .env -source .env - -npx hardhat run scripts/verify-metadata-proof.ts --network sepolia -source .env -npx hardhat run scripts/claim-metadata-update.ts --network opSepolia \ No newline at end of file diff --git a/scenario1.sh b/scenario1.sh new file mode 100755 index 0000000..d91f18f --- /dev/null +++ b/scenario1.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Color codes +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}Starting cross-chain deployment process...${NC}\n" + +# Deploy to OP Sepolia +echo -e "${BLUE}Deploying to OP Sepolia...${NC}" +if pnpm crosschain:op-sepolia; then + echo -e "${GREEN}✓ OP Sepolia deployment successful${NC}" +else + echo -e "${RED}✗ OP Sepolia deployment failed${NC}" + exit 1 +fi + +# Wait a bit to ensure deployment is fully confirmed +echo -e "\n${BLUE}Waiting 30 seconds before proceeding to Arbitrum deployment...${NC}" +sleep 30 + +# Deploy to Arbitrum Sepolia +echo -e "\n${BLUE}Deploying to Arbitrum Sepolia...${NC}" +if pnpm crosschain:arbitrum-sepolia; then + echo -e "${GREEN}✓ Arbitrum Sepolia deployment successful${NC}" +else + echo -e "${RED}✗ Arbitrum Sepolia deployment failed${NC}" + exit 1 +fi + +# Wait for deployment to be fully confirmed +echo -e "\n${BLUE}Waiting 30 seconds before running verification...${NC}" +sleep 30 + +# Run verification script +echo -e "\n${BLUE}Running cross-chain setup verification...${NC}" +if pnpm verify:setup; then + echo -e "${GREEN}✓ Cross-chain setup verification successful${NC}" + echo -e "\n${GREEN}✓ Deployment and verification completed successfully!${NC}" + exit 0 +else + echo -e "${RED}✗ Cross-chain setup verification failed${NC}" + exit 1 +fi + +# Create proposal on OP Sepolia +echo -e "\n${BLUE}Creating proposal on OP Sepolia...${NC}" +if npx hardhat run scripts/propose.ts --network op-sepolia; then + echo -e "${GREEN}✓ Proposal creation successful${NC}" + echo -e "\n${GREEN}✓ Deployment, verification, and proposal creation completed successfully!${NC}" + exit 0 +else + echo -e "${RED}✗ Proposal creation failed${NC}" + exit 1 +fi + +# Generate proof from OP Sepolia +echo -e "\n${BLUE}Generating proof from OP Sepolia...${NC}" +if npx hardhat run scripts/verify-proof.ts --network op-sepolia; then + echo -e "${GREEN}✓ Proof generation successful${NC}" +else + echo -e "${RED}✗ Proof generation failed${NC}" + exit 1 +fi + +# Claim membership on Arbitrum Sepolia +echo -e "\n${BLUE}Claiming membership on Arbitrum Sepolia...${NC}" +if npx hardhat run scripts/claim-membership.ts --network arbitrum-sepolia; then + echo -e "${GREEN}✓ Membership claim successful${NC}" + echo -e "\n${GREEN}✓ All steps completed successfully!${NC}" + exit 0 +else + echo -e "${RED}✗ Membership claim failed${NC}" + exit 1 +fi \ No newline at end of file diff --git a/scripts/check-my-balance.ts b/scripts/check-my-balance.ts index e4cc948..f859ac3 100644 --- a/scripts/check-my-balance.ts +++ b/scripts/check-my-balance.ts @@ -16,23 +16,56 @@ async function main() { try { console.log(color.magenta(`\nSwitching to network: ${networkName}`)) - // Ensure network has an RPC URL and accounts - const { url, accounts } = networkConfig as any + // Skip hardhat and localhost networks + if (networkName === "hardhat" || networkName === "localhost") { + console.log( + color.yellow( + `Skipping local network "${networkName}" - only checking remote networks.` + ) + ) + continue + } + + // Type assertion for network config + const config = networkConfig as { + url?: string + accounts?: string[] + } - if (!url || accounts.length === 0) { - console.error( + // Check if network is properly configured + if ( + !config.url || + !config.accounts || + config.accounts.length === 0 + ) { + console.log( color.yellow( - `Skipping network "${networkName}" due to missing RPC URL or accounts.` + `Skipping network "${networkName}" - missing configuration in .env file` ) ) continue } - // Create provider and signer - const provider = new ethers.JsonRpcProvider(url) - const signer = new ethers.Wallet(accounts[0], provider) + // Create provider with retry options + const provider = new ethers.JsonRpcProvider(config.url, undefined, { + maxRetries: 3, + timeout: 10000 + }) + + // Test provider connection + try { + await provider.getNetwork() + } catch (error) { + console.log( + color.yellow( + `Failed to connect to network "${networkName}" - check RPC URL` + ) + ) + continue + } - // Get balance + // Create signer and get balance + const signer = new ethers.Wallet(config.accounts[0], provider) const balance = await provider.getBalance(signer.address) console.log( diff --git a/scripts/check-nonce-state.ts b/scripts/check-nonce-state.ts deleted file mode 100644 index 05cdbbd..0000000 --- a/scripts/check-nonce-state.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ethers } from "hardhat" -import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__factory" -import { ProofHandler__factory } from "../typechain-types" - -async function main() { - const GOV_ADDRESS = "0x87b094e13DDe7e8d7F2793bD2Ac8636C7C0EcFD7" - const NEW_VOTING_DELAY = 250n - const PROOF_HANDLER_ADDRESS = "0x7342BA0E0C855B403287A2EB00d48257b85496a8" - const OPERATION_TYPE = 1 // UPDATE_VOTING_DELAY - - // Connect to contracts - const provider = new ethers.JsonRpcProvider( - process.env.SEPOLIA_RPC_ENDPOINT_URL - ) - - console.log("\nReading ProofHandler storage...") - - // Calculate storage slot for currentNonce - const nonceSlot = ethers.keccak256( - ethers.solidityPacked( - ["uint8", "uint256"], - [OPERATION_TYPE, 0] // mapping key and base slot - ) - ) - - // Read nonce state - const nonceData = await provider.getStorage( - PROOF_HANDLER_ADDRESS, - nonceSlot - ) - const currentNonce = parseInt(nonceData, 16) - console.log("Storage slot:", nonceSlot) - console.log("Raw data:", nonceData) - console.log("Current nonce:", currentNonce) - - // Check if the nonce was already claimed on target chain - const opSepoliaProvider = new ethers.JsonRpcProvider( - process.env.OP_SEPOLIA_RPC_ENDPOINT_URL - ) - const opNonceData = await opSepoliaProvider.getStorage( - PROOF_HANDLER_ADDRESS, - nonceSlot - ) - const opCurrentNonce = parseInt(opNonceData, 16) - console.log("\nOP Sepolia nonce:", opCurrentNonce) - - // Check proof hash storage - for (let nonce = 1; nonce <= 3; nonce++) { - const proofValue = ethers.solidityPacked(["uint48"], [NEW_VOTING_DELAY]) - const message = ethers.keccak256( - ethers.solidityPacked( - ["address", "uint8", "bytes", "uint256"], - [GOV_ADDRESS, OPERATION_TYPE, proofValue, nonce] - ) - ) - const digest = ethers.keccak256( - ethers.solidityPacked( - ["string", "bytes32"], - ["\x19Ethereum Signed Message:\n32", message] - ) - ) - console.log(`\nNonce ${nonce} digest:`, digest) - - // Check if proof was applied - const proofHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode( - ["uint8", "bytes", "uint256", "bytes32"], - [OPERATION_TYPE, proofValue, nonce, digest] - ) - ) - const proofSlot = ethers.keccak256( - ethers.solidityPacked( - ["bytes32", "uint256"], - [proofHash, 1] // mapping key and base slot for updateAppliedOnChain - ) - ) - const appliedData = await opSepoliaProvider.getStorage( - PROOF_HANDLER_ADDRESS, - proofSlot - ) - console.log( - `Proof ${nonce} applied:`, - appliedData !== - "0x0000000000000000000000000000000000000000000000000000000000000000" - ) - } -} - -main().catch(error => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/check-nonces.ts b/scripts/check-nonces.ts deleted file mode 100644 index 128e35c..0000000 --- a/scripts/check-nonces.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ethers } from "hardhat" -import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" - -async function checkNonceAndTokens( - chainName: string, - provider: ethers.JsonRpcProvider, - nftAddress: string -) { - const OPERATION_TYPE = 0 // MINT operation - - // Calculate storage slot for nonce - const proofSlot = ethers.keccak256( - ethers.solidityPacked( - ["uint8", "uint256"], - [OPERATION_TYPE, 0] // mapping key and base slot - ) - ) - const nonceData = await provider.getStorage(nftAddress, proofSlot) - const currentNonce = parseInt(nonceData, 16) - - console.log(`\n${chainName}:`) - console.log(`Current nonce: ${currentNonce}`) - - // Get NFT contract instance - const nft = NFT__factory.connect(nftAddress, provider) - - // Check tokens - try { - const totalSupply = await nft.totalSupply() - console.log(`Total supply: ${totalSupply}`) - - for (let i = 0; i < Number(totalSupply); i++) { - try { - const owner = await nft.ownerOf(i) - console.log(`Token ${i} owner: ${owner}`) - } catch (e) { - console.log(`Token ${i} doesn't exist or is burned`) - } - } - } catch (e) { - console.log("Error getting token details:", e) - } -} - -async function main() { - const NFT_ADDRESS = "0xfFCB28995DFAC5a90bf52195B6570DFF7e3e8dBD" - - // Check Sepolia - const sepoliaProvider = new ethers.JsonRpcProvider( - process.env.SEPOLIA_RPC_ENDPOINT_URL - ) - await checkNonceAndTokens( - "Sepolia (Home Chain)", - sepoliaProvider, - NFT_ADDRESS - ) - - // Check OP Sepolia - const opSepoliaProvider = new ethers.JsonRpcProvider( - process.env.OP_SEPOLIA_RPC_ENDPOINT_URL - ) - await checkNonceAndTokens("OP Sepolia", opSepoliaProvider, NFT_ADDRESS) - - // Check stored proofs - console.log("\nChecking stored proofs:") - const fs = require("fs") - const proofs = JSON.parse(fs.readFileSync("./proofs.json", "utf8")) - for (const p of proofs) { - console.log(`\nToken ${p.tokenId}:`) - // Parse the proof to get embedded nonce - const decoded = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], - p.proof - ) - console.log(`Nonce in proof: ${decoded[2]}`) - } -} - -main().catch(error => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/check-token-existence.ts b/scripts/check-token-existence.ts deleted file mode 100644 index 3fc0918..0000000 --- a/scripts/check-token-existence.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ethers } from "hardhat" -import { JsonRpcProvider } from "ethers" -import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" -import { NFT } from "../typechain-types/contracts/variants/crosschain/NFT" - -function getRpcUrl(network: string): string { - switch (network) { - case "opSepolia": - if (!process.env.OP_SEPOLIA_RPC_ENDPOINT_URL) { - throw new Error("OP_SEPOLIA_RPC_ENDPOINT_URL not set in .env") - } - return process.env.OP_SEPOLIA_RPC_ENDPOINT_URL - case "baseSepolia": - if (!process.env.BASE_SEPOLIA_RPC_ENDPOINT_URL) { - throw new Error("BASE_SEPOLIA_RPC_ENDPOINT_URL not set in .env") - } - return process.env.BASE_SEPOLIA_RPC_ENDPOINT_URL - case "arbitrumSepolia": - if (!process.env.ARBITRUM_SEPOLIA_RPC_ENDPOINT_URL) { - throw new Error( - "ARBITRUM_SEPOLIA_RPC_ENDPOINT_URL not set in .env" - ) - } - return process.env.ARBITRUM_SEPOLIA_RPC_ENDPOINT_URL - case "sepolia": - if (!process.env.SEPOLIA_RPC_ENDPOINT_URL) { - throw new Error("SEPOLIA_RPC_ENDPOINT_URL not set in .env") - } - return process.env.SEPOLIA_RPC_ENDPOINT_URL - default: - throw new Error(`Unsupported network: ${network}`) - } -} - -async function checkToken(nft: NFT, tokenId: number): Promise { - try { - const owner = await nft.ownerOf(tokenId) - return owner - } catch (error) { - return null - } -} - -async function main() { - console.log("\nChecking token existence...") - - // Get deployment information - const deploymentsNFT = require("../deployments/sepolia/CrosschainNFT.json") - const NFT_ADDRESS = deploymentsNFT.address - const networks = ["sepolia", "opSepolia", "baseSepolia", "arbitrumSepolia"] - const tokenIds = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - - console.log("\nNFT contract address:", NFT_ADDRESS, "\n") - - // Create a map of networks to their contract instances - const contracts = new Map() - for (const network of networks) { - const provider = new ethers.JsonRpcProvider(getRpcUrl(network)) - const nft = NFT__factory.connect(NFT_ADDRESS, provider) - contracts.set(network, nft) - } - - // Print header - console.log("Token ID | " + networks.map(n => n.padEnd(20)).join(" | ")) - console.log("-".repeat(120)) - - // Check each token ID - for (const tokenId of tokenIds) { - const results = await Promise.all( - networks.map(async network => { - const nft = contracts.get(network)! - const owner = await checkToken(nft, tokenId) - return owner ? owner.slice(0, 8) + "..." : "Not Found" - }) - ) - console.log( - `Token ${tokenId.toString().padEnd(7)} | ${results - .map(r => r.padEnd(20)) - .join(" | ")}` - ) - } -} - -main().catch(error => { - console.error("\nError:", error) - process.exitCode = 1 -}) diff --git a/scripts/claim-delegation.ts b/scripts/claim-delegation.ts deleted file mode 100644 index d5a5929..0000000 --- a/scripts/claim-delegation.ts +++ /dev/null @@ -1,72 +0,0 @@ -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 = "0x147613E970bbA94e19a70A8b0f9106a13B4d7cbE" - 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/claim-gov-burn.ts b/scripts/claim-gov-burn.ts deleted file mode 100644 index a720d8d..0000000 --- a/scripts/claim-gov-burn.ts +++ /dev/null @@ -1,52 +0,0 @@ -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 = "0x147613E970bbA94e19a70A8b0f9106a13B4d7cbE" - 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-gov-burn-proof.ts - const proof = - "0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001c95697158b3963a86e2b3624f08fcb72511d8079ee12a5d1d5c957ec2292181000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000bdc0e420ab9ba144213588a95fa1e5e63ceff1be0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005268747470733a2f2f6261666b726569636a36326c35787536706b3278783778376e3662377270756e78623465686c6837666576796a6170696433353536736d757a34792e697066732e7733732e6c696e6b2f0000000000000000000000000000" - - try { - console.log("Simulating burn claim...") - await nft.claimOperation.staticCall(proof) - console.log("✅ Simulation successful") - - console.log("Submitting burn claim...") - const tx = await nft.claimOperation(proof, { - gasLimit: 500000 - }) - - console.log("Transaction submitted:", tx.hash) - await tx.wait() - console.log("Token burned successfully!") - } 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/claim-manifesto-update.ts b/scripts/claim-manifesto-update.ts deleted file mode 100644 index 974689f..0000000 --- a/scripts/claim-manifesto-update.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ethers } from "hardhat" -import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__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 GOV_ADDRESS = "0x87b094e13DDe7e8d7F2793bD2Ac8636C7C0EcFD7" - const provider = new ethers.JsonRpcProvider( - process.env.OP_SEPOLIA_RPC_ENDPOINT_URL - ) - const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) - - const gov = Gov__factory.connect(GOV_ADDRESS, aliceSigner) - - // Replace with actual proof from verify-manifesto-proof.ts - const proof = - "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010c852d851b4a3959284ec1aace6db3d03fab053c9c530a2352a82e50ea3fc6f6000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000016697066733a2f2f6e65774d616e69666573746f43494400000000000000000000" - - try { - console.log("Submitting manifesto update claim...") - const tx = await gov.claimParameterUpdate(proof, { - gasLimit: 500000 - }) - - console.log("Transaction submitted:", tx.hash) - await tx.wait() - console.log("Manifesto updated successfully!") - } catch (error: any) { - console.error("\nError details:", error) - if (error.data) { - try { - const decodedError = gov.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/claim-membership.ts b/scripts/claim-membership.ts index ce08382..534c26c 100644 --- a/scripts/claim-membership.ts +++ b/scripts/claim-membership.ts @@ -1,92 +1,108 @@ import hre, { ethers } from "hardhat" import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" -import * as dotenv from "dotenv" +import * as fs from "fs" +import * as path from "path" +import color from "cli-color" +var msg = color.xterm(39).bgXterm(128) -async function main() { - dotenv.config() +function getDeployedAddress(network: string, contractName: string): string { + try { + const deploymentPath = path.join( + __dirname, + "..", + "deployments", + network, + `${contractName}.json` + ) + const deployment = JSON.parse(fs.readFileSync(deploymentPath, "utf8")) + return deployment.address + } catch (error) { + throw new Error( + `Failed to read deployment for ${contractName} on ${network}: ${error}` + ) + } +} - console.log("\nClaiming proof...") +function getProofFromData(): string { + try { + const dataPath = path.join(__dirname, "..", "data.json") + const data = JSON.parse(fs.readFileSync(dataPath, "utf8")) + return data.proof + } catch (error) { + throw new Error(`Failed to read proof from data.json: ${error}`) + } +} - if (!process.env.SIGNER_PRIVATE_KEY) { +async function main() { + const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY + if (!SIGNER_PRIVATE_KEY) { throw new Error("Please set SIGNER_PRIVATE_KEY in your .env file") } - if (!process.env.PROOF) { - throw new Error( - "No proof found in .env file. Please run verify-proof.ts first" - ) - } + // Get the network from hardhat config + const networkName = hre.network.name + + // Get deployed address from deployment files + const NFT_ADDRESS = getDeployedAddress(networkName, "CrosschainNFT") + console.log("Using NFT contract address:", NFT_ADDRESS) - const deploymentsNFT = require("../deployments/sepolia/CrosschainNFT.json") - const NFT_ADDRESS = deploymentsNFT.address - const network = hre.network.name + // Get RPC URL based on network + let provider = new ethers.JsonRpcProvider( + networkName === "op-sepolia" + ? process.env.OP_SEPOLIA_RPC_ENDPOINT_URL + : process.env.ARBITRUM_SEPOLIA_RPC_ENDPOINT_URL + ) + const signerZero = new ethers.Wallet(SIGNER_PRIVATE_KEY, provider) - // Setup provider and signer - const provider = new ethers.JsonRpcProvider(getRpcUrl(network)) - const signer = new ethers.Wallet(process.env.SIGNER_PRIVATE_KEY, provider) + console.log("Using address:", signerZero.address) - console.log("\nNetwork:", network) - console.log("Signer address:", signer.address) + const nft = NFT__factory.connect(NFT_ADDRESS, signerZero) - const nft = NFT__factory.connect(NFT_ADDRESS, signer) + // Get proof from data.json + const proof = getProofFromData() + console.log("\nUsing proof:", proof) try { - console.log("\nClaiming token...") - // Simulate first - await nft.claimOperation.staticCall(process.env.PROOF) + console.log("Simulating claim transaction...") + await nft.claimMint.staticCall(proof) console.log("✅ Simulation successful") - // Submit transaction - const tx = await nft.claimOperation(process.env.PROOF, { + console.log("Submitting claim transaction...") + const tx = await nft.claimMint(proof, { gasLimit: 500000 }) - console.log("Transaction submitted:", tx.hash) + + console.log("Transaction submitted:", msg(tx.hash)) + console.log("Waiting for confirmation...") const receipt = await tx.wait() - if (receipt?.status === 1) { - console.log("Token claimed successfully!") - - // Verify the new owner from the Transfer event - const transferEvent = receipt?.logs.find(log => { - try { - const parsed = nft.interface.parseLog(log as any) - return parsed?.name === "Transfer" - } catch { - return false - } - }) - - if (transferEvent) { - const parsedEvent = nft.interface.parseLog(transferEvent as any) - console.log(`New owner: ${parsedEvent?.args?.to}`) + console.log("Membership claimed successfully!") + + // Get token ID from event + const claimEvent = receipt?.logs.find(log => { + try { + return nft.interface.parseLog(log)?.name === "MembershipClaimed" + } catch { + return false } + }) + + if (claimEvent) { + const parsedEvent = nft.interface.parseLog(claimEvent) + const tokenId = parsedEvent?.args?.tokenId + console.log("Claimed token ID:", tokenId) } } catch (error: any) { - console.error(`\nFailed to claim token:`) + console.error("\nError details:", error) if (error.data) { try { const decodedError = nft.interface.parseError(error.data) - console.error("Error reason:", decodedError) + console.error("Decoded error:", decodedError) } catch (e) { - console.error(error) + console.error("Raw error data:", error.data) } } - } -} - -// Helper to get RPC URL -function getRpcUrl(network: string): string { - switch (network) { - case "opSepolia": - return process.env.OP_SEPOLIA_RPC_ENDPOINT_URL || "" - case "baseSepolia": - return process.env.BASE_SEPOLIA_RPC_ENDPOINT_URL || "" - case "arbitrumSepolia": - return process.env.ARBITRUM_SEPOLIA_RPC_ENDPOINT_URL || "" - case "sepolia": - return process.env.SEPOLIA_RPC_ENDPOINT_URL || "" - default: - throw new Error(`Unsupported network: ${network}`) + throw error } } diff --git a/scripts/claim-metadata-update.ts b/scripts/claim-metadata-update.ts deleted file mode 100644 index e4b6d30..0000000 --- a/scripts/claim-metadata-update.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ethers } from "hardhat" -import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" -import * as dotenv from "dotenv" - -async function main() { - // Load environment variables - dotenv.config() - - if (!process.env.ALICE) { - throw new Error("Please set ALICE private key in your .env file") - } - - if (!process.env.PROOF) { - throw new Error( - "No proof found in .env file. Please run verify-metadata-proof.ts first" - ) - } - - if (!process.env.TOKENID && process.env.TOKENID !== "0") { - throw new Error("No token ID specified in .env file") - } - - // Load contract addresses from deployment files - const deploymentsNFT = require("../deployments/sepolia/CrosschainNFT.json") - const NFT_ADDRESS = deploymentsNFT.address - - const provider = new ethers.JsonRpcProvider( - process.env.OP_SEPOLIA_RPC_ENDPOINT_URL - ) - const aliceSigner = new ethers.Wallet(process.env.ALICE, provider) - const nft = NFT__factory.connect(NFT_ADDRESS, aliceSigner) - - const TOKEN_ID = parseInt(process.env.TOKENID) - - try { - console.log("\nSimulating metadata update claim...") - console.log("Using proof for token:", TOKEN_ID) - await nft.claimOperation.staticCall(process.env.PROOF) - console.log("✅ Simulation successful") - - console.log("\nSubmitting metadata update claim...") - const tx = await nft.claimOperation(process.env.PROOF, { - gasLimit: 500000 - }) - - console.log("Transaction submitted:", tx.hash) - const receipt = await tx.wait() - - if (receipt?.status === 1) { - console.log("\nMetadata updated successfully! 🎉") - try { - const tokenURI = await nft.tokenURI(TOKEN_ID) - console.log("New token URI:", tokenURI) - } catch (e) { - console.log("Could not fetch new token URI") - } - } - } 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/claim-voting-delay.ts b/scripts/claim-voting-delay.ts deleted file mode 100644 index a146fbc..0000000 --- a/scripts/claim-voting-delay.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ethers } from "hardhat" -import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__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 GOV_ADDRESS = "0x87b094e13DDe7e8d7F2793bD2Ac8636C7C0EcFD7" - const provider = new ethers.JsonRpcProvider( - process.env.OP_SEPOLIA_RPC_ENDPOINT_URL - ) - const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) - - const gov = Gov__factory.connect(GOV_ADDRESS, aliceSigner) - - // The proof with nonce 1 we just generated - const proof = - "0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001d6e24169a86f648dc2eb3d5607c83873e546b036409af044ff2fe5cfcf32e56300000000000000000000000000000000000000000000000000000000000000060000000000fa0000000000000000000000000000000000000000000000000000" - - console.log("Starting claim process...") - console.log("\nCurrent state:") - console.log("Address:", aliceSigner.address) - console.log("Current voting delay:", await gov.votingDelay()) - - // Decode proof for logging - const decodedProof = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], - proof - ) - console.log("\nProof details:") - console.log("Operation type:", decodedProof[0]) - console.log("Value:", decodedProof[1]) - console.log("Nonce:", decodedProof[2]) - console.log("Digest:", decodedProof[3]) - - try { - console.log("\nSimulating claim...") - await gov.claimParameterUpdate.staticCall(proof) - console.log("✅ Simulation successful") - - console.log("\nSubmitting claim transaction...") - const tx = await gov.claimParameterUpdate(proof, { - gasLimit: 500000 - }) - - console.log("Transaction submitted:", tx.hash) - const receipt = await tx.wait() - - if (receipt?.status === 1) { - console.log("\n🎉 Claim successful!") - console.log("New voting delay:", await gov.votingDelay()) - } else { - throw new Error("Transaction failed") - } - } catch (error: any) { - console.error("\nError details:", error) - if (error.data) { - try { - const decodedError = gov.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 deleted file mode 100644 index 6aac1aa..0000000 --- a/scripts/gov-burn.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { ethers } from "hardhat" -import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__factory" -import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" - -function getProposalState(state: number): string { - const states = [ - "Pending", - "Active", - "Canceled", - "Defeated", - "Succeeded", - "Queued", - "Expired", - "Executed" - ] - return states[state] -} - -async function main() { - const ALICE_PRIVATE_KEY = process.env.ALICE - 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") - } - - const NFT_ADDRESS = "0x147613E970bbA94e19a70A8b0f9106a13B4d7cbE" - const GOV_ADDRESS = "0x87b094e13DDe7e8d7F2793bD2Ac8636C7C0EcFD7" - const TOKEN_ID = 2 // Token ID to burn - - const provider = new ethers.JsonRpcProvider( - process.env.SEPOLIA_RPC_ENDPOINT_URL - ) - const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) - const sepoliaSigner = new ethers.Wallet(SIGNER_PRIVATE_KEY, provider) - - console.log("Connected with address:", aliceSigner.address) - - const gov = Gov__factory.connect(GOV_ADDRESS, aliceSigner) - const nft = NFT__factory.connect(NFT_ADDRESS, aliceSigner) - - // Check voting power and delegate if needed - const votingPower = await nft.getVotes(aliceSigner.address) - console.log("Current voting power:", votingPower.toString()) - - if (votingPower === 0n) { - console.log("Delegating voting power...") - const tx = await nft.delegate(aliceSigner.address) - await tx.wait(3) - console.log("Delegation completed") - console.log( - "New voting power:", - (await nft.getVotes(aliceSigner.address)).toString() - ) - } - - const burnCall = nft.interface.encodeFunctionData("govBurn", [TOKEN_ID]) - const description = `Burn token ${TOKEN_ID} ${Date.now()}` - - try { - console.log("\nCreating proposal to burn token", TOKEN_ID) - const tx = await gov.propose([nft.target], [0], [burnCall], description) - - console.log("Proposal transaction submitted:", tx.hash) - const receipt = await tx.wait() - if (!receipt) throw new Error("No receipt received") - - const proposalId = - receipt.logs[0] instanceof ethers.EventLog - ? receipt.logs[0].args?.[0] - : null - - console.log("Proposal ID:", proposalId) - if (!proposalId) throw new Error("No proposal ID found") - - // Wait for proposal to become active - console.log("\nWaiting for proposal to become active...") - let state = await gov.state(proposalId) - let currentState = getProposalState(Number(state)) - console.log("Current state:", currentState) - - while (currentState === "Pending") { - await new Promise(r => setTimeout(r, 5000)) - state = await gov.state(proposalId) - currentState = getProposalState(Number(state)) - process.stdout.write(`Current state: ${currentState}\r`) - } - console.log("\nProposal is now", currentState) - - if (currentState === "Active") { - console.log("\nCasting vote...") - const voteTx = await gov.castVote(proposalId, 1) - console.log("Vote transaction submitted:", voteTx.hash) - await voteTx.wait() - console.log("Vote cast successfully") - - // Wait for proposal to succeed - console.log("\nWaiting for proposal to succeed...") - let successCounter = 0 - const maxAttempts = 60 // 5 minutes with 5s intervals - - while (successCounter < maxAttempts) { - state = await gov.state(proposalId) - currentState = getProposalState(Number(state)) - process.stdout.write( - `Current state: ${currentState} (Attempt ${ - successCounter + 1 - }/${maxAttempts})\r` - ) - - if (currentState === "Succeeded") { - console.log("\nProposal has succeeded!") - break - } else if ( - currentState === "Defeated" || - currentState === "Expired" - ) { - throw new Error(`Proposal ${currentState.toLowerCase()}`) - } - - successCounter++ - await new Promise(r => setTimeout(r, 5000)) - } - - if (successCounter >= maxAttempts) { - throw new Error("Timeout waiting for proposal to succeed") - } - - // Execute proposal - console.log("\nExecuting proposal...") - const executeTx = await gov - .connect(sepoliaSigner) - .execute([nft.target], [0], [burnCall], ethers.id(description)) - - console.log("Execution transaction submitted:", executeTx.hash) - await executeTx.wait() - console.log("\nToken burned successfully! 🎉") - } else { - throw new Error(`Unexpected proposal state: ${currentState}`) - } - } catch (error: any) { - console.error("\nError details:", error) - if (error.data) { - try { - const decodedError = gov.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-manifesto-edit.ts b/scripts/gov-manifesto-edit.ts deleted file mode 100644 index b70ba14..0000000 --- a/scripts/gov-manifesto-edit.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { ethers } from "hardhat" -import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__factory" -import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" - -function getProposalState(state: number): string { - const states = [ - "Pending", - "Active", - "Canceled", - "Defeated", - "Succeeded", - "Queued", - "Expired", - "Executed" - ] - return states[state] -} - -async function main() { - const ALICE_PRIVATE_KEY = process.env.ALICE - 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") - } - - const NFT_ADDRESS = "0x147613E970bbA94e19a70A8b0f9106a13B4d7cbE" - const GOV_ADDRESS = "0x87b094e13DDe7e8d7F2793bD2Ac8636C7C0EcFD7" - const NEW_MANIFESTO = "ipfs://newManifestoCID" // Replace with your new manifesto CID - - const provider = new ethers.JsonRpcProvider( - process.env.SEPOLIA_RPC_ENDPOINT_URL - ) - const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) - const sepoliaSigner = new ethers.Wallet(SIGNER_PRIVATE_KEY, provider) - - console.log("Connected with address:", aliceSigner.address) - - const gov = Gov__factory.connect(GOV_ADDRESS, aliceSigner) - const nft = NFT__factory.connect(NFT_ADDRESS, aliceSigner) - - // Check voting power and delegate if needed - const votingPower = await nft.getVotes(aliceSigner.address) - console.log("Current voting power:", votingPower.toString()) - - if (votingPower === 0n) { - console.log("Delegating voting power...") - const tx = await nft.delegate(aliceSigner.address) - await tx.wait() - console.log("Delegation completed") - console.log( - "New voting power:", - (await nft.getVotes(aliceSigner.address)).toString() - ) - } - - // Get current manifesto for reference - const currentManifesto = await gov.manifesto() - console.log("\nCurrent manifesto:", currentManifesto) - console.log("New manifesto:", NEW_MANIFESTO) - - const manifestoCall = gov.interface.encodeFunctionData("setManifesto", [ - NEW_MANIFESTO - ]) - const description = `Update manifesto to ${NEW_MANIFESTO} ${Date.now()}` - - try { - console.log("\nCreating proposal to update manifesto...") - const tx = await gov.propose( - [gov.target], - [0], - [manifestoCall], - description - ) - - console.log("Proposal transaction submitted:", tx.hash) - const receipt = await tx.wait() - if (!receipt) throw new Error("No receipt received") - - const proposalId = - receipt.logs[0] instanceof ethers.EventLog - ? receipt.logs[0].args?.[0] - : null - - console.log("Proposal ID:", proposalId) - if (!proposalId) throw new Error("No proposal ID found") - - // Wait for proposal to become active - console.log("\nWaiting for proposal to become active...") - let state = await gov.state(proposalId) - let currentState = getProposalState(Number(state)) - console.log("Current state:", currentState) - - while (currentState === "Pending") { - await new Promise(r => setTimeout(r, 5000)) - state = await gov.state(proposalId) - currentState = getProposalState(Number(state)) - process.stdout.write(`Current state: ${currentState}\r`) - } - console.log("\nProposal is now", currentState) - - if (currentState === "Active") { - console.log("\nCasting vote...") - const voteTx = await gov.castVote(proposalId, 1) - console.log("Vote transaction submitted:", voteTx.hash) - await voteTx.wait() - console.log("Vote cast successfully") - - // Wait for proposal to succeed - console.log("\nWaiting for proposal to succeed...") - let successCounter = 0 - const maxAttempts = 60 // 5 minutes with 5s intervals - - while (successCounter < maxAttempts) { - state = await gov.state(proposalId) - currentState = getProposalState(Number(state)) - process.stdout.write( - `Current state: ${currentState} (Attempt ${ - successCounter + 1 - }/${maxAttempts})\r` - ) - - if (currentState === "Succeeded") { - console.log("\nProposal has succeeded!") - break - } else if ( - currentState === "Defeated" || - currentState === "Expired" - ) { - throw new Error(`Proposal ${currentState.toLowerCase()}`) - } - - successCounter++ - await new Promise(r => setTimeout(r, 5000)) - } - - if (successCounter >= maxAttempts) { - throw new Error("Timeout waiting for proposal to succeed") - } - - // Execute proposal - console.log("\nExecuting proposal...") - const executeTx = await gov - .connect(sepoliaSigner) - .execute( - [gov.target], - [0], - [manifestoCall], - ethers.id(description) - ) - - console.log("Execution transaction submitted:", executeTx.hash) - await executeTx.wait() - - // Verify the update - const newManifesto = await gov.manifesto() - console.log("\nManifesto updated successfully!") - console.log("New manifesto value:", newManifesto) - } else { - throw new Error(`Unexpected proposal state: ${currentState}`) - } - } catch (error: any) { - console.error("\nError details:", error) - if (error.data) { - try { - const decodedError = gov.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-voting-delay.ts b/scripts/gov-voting-delay.ts deleted file mode 100644 index 85d95cf..0000000 --- a/scripts/gov-voting-delay.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { ethers } from "hardhat" -import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__factory" -import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" - -// Utility functions -function getProposalState(state: number): string { - const states = [ - "Pending", - "Active", - "Canceled", - "Defeated", - "Succeeded", - "Queued", - "Expired", - "Executed" - ] - return states[state] -} - -async function sleep(ms: number) { - return new Promise(r => setTimeout(r, ms)) -} - -async function waitForProposalState( - gov: any, - proposalId: bigint, - targetState: string, - maxAttempts = 60 -) { - let currentState = "" - let attempt = 0 - - while (attempt < maxAttempts) { - const state = await gov.state(proposalId) - currentState = getProposalState(Number(state)) - - process.stdout.write( - `Current state: ${currentState} (Attempt ${ - attempt + 1 - }/${maxAttempts})\r` - ) - - if (currentState === targetState) { - console.log(`\nReached ${targetState} state!`) - return true - } - - if (["Defeated", "Expired", "Canceled"].includes(currentState)) { - throw new Error(`Proposal ${currentState.toLowerCase()}`) - } - - attempt++ - await sleep(5000) - } - - throw new Error(`Timeout waiting for ${targetState} state`) -} - -async function main() { - // Configuration - const config = { - nftAddress: "0x147613E970bbA94e19a70A8b0f9106a13B4d7cbE", - govAddress: "0x87b094e13DDe7e8d7F2793bD2Ac8636C7C0EcFD7", - newVotingDelay: 250n, - rpcUrl: process.env.SEPOLIA_RPC_ENDPOINT_URL, - aliceKey: process.env.ALICE, - sepoliaKey: process.env.SIGNER_PRIVATE_KEY - } - - // Validate environment - if (!config.aliceKey || !config.sepoliaKey) { - throw new Error("Missing required environment variables") - } - - // Setup providers and signers - const provider = new ethers.JsonRpcProvider(config.rpcUrl) - const aliceSigner = new ethers.Wallet(config.aliceKey, provider) - const sepoliaSigner = new ethers.Wallet(config.sepoliaKey, provider) - - console.log("Network:", await provider.getNetwork()) - console.log("Connected with address:", aliceSigner.address) - console.log("Block number:", await provider.getBlockNumber()) - - // Contract connections - const gov = Gov__factory.connect(config.govAddress, aliceSigner) - const nft = NFT__factory.connect(config.nftAddress, aliceSigner) - - // Check voting power - const votingPower = await nft.getVotes(aliceSigner.address) - console.log("Current voting power:", votingPower.toString()) - - if (votingPower === 0n) { - console.log("\nDelegating voting power...") - const tx = await nft.delegate(aliceSigner.address) - console.log("Delegation tx:", tx.hash) - await tx.wait(3) - const newPower = await nft.getVotes(aliceSigner.address) - console.log("New voting power:", newPower.toString()) - } - - // Prepare proposal - const description = `Update voting delay to ${ - config.newVotingDelay - } blocks [${Date.now()}]` - const delayCall = gov.interface.encodeFunctionData("setVotingDelay", [ - config.newVotingDelay - ]) - - try { - // Create proposal - console.log("\nCreating proposal...") - console.log("Description:", description) - - const proposeTx = await gov.propose( - [gov.target], - [0], - [delayCall], - description - ) - console.log("Proposal tx submitted:", proposeTx.hash) - - const receipt = await proposeTx.wait() - if (!receipt) throw new Error("No receipt received") - - const proposalId = - receipt.logs[0] instanceof ethers.EventLog - ? receipt.logs[0].args?.[0] - : null - - if (!proposalId) throw new Error("No proposal ID found") - console.log("Proposal ID:", proposalId) - - // Wait for active state - console.log("\nWaiting for proposal to become active...") - await waitForProposalState(gov, proposalId, "Active") - - // Cast vote - console.log("\nCasting vote...") - const voteTx = await gov.castVote(proposalId, 1) // 1 = For - await voteTx.wait() - console.log("Vote cast successfully:", voteTx.hash) - - // Wait for success - console.log("\nWaiting for proposal to succeed...") - await waitForProposalState(gov, proposalId, "Succeeded") - - // Execute proposal - console.log("\nExecuting proposal...") - const executeTx = await gov - .connect(sepoliaSigner) - .execute([gov.target], [0], [delayCall], ethers.id(description)) - - console.log("Execute tx submitted:", executeTx.hash) - await executeTx.wait() - - // Verify result - const newDelay = await gov.votingDelay() - console.log("\nVoting delay updated successfully! 🎉") - console.log("New voting delay:", newDelay.toString(), "blocks") - } catch (error: any) { - console.error("\nError details:") - console.error("Message:", error.message) - - if (error.data) { - try { - const decodedError = gov.interface.parseError(error.data) - console.error("Decoded error:", decodedError) - } catch (e) { - console.error("Raw error data:", error.data) - } - } - - if (error.transaction) { - console.error("\nTransaction details:") - console.error("To:", error.transaction.to) - console.error("Data:", error.transaction.data) - } - - throw error - } -} - -main().catch(error => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/propose.ts b/scripts/propose.ts index 70cf58e..de70ffd 100644 --- a/scripts/propose.ts +++ b/scripts/propose.ts @@ -1,4 +1,4 @@ -import { ethers } from "hardhat" +import hre, { ethers } from "hardhat" import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__factory" import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" import * as fs from "fs" @@ -8,9 +8,25 @@ function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) } -async function main() { - console.log("\nCreating proposal...") +function getDeployedAddress(network: string, contractName: string): string { + try { + const deploymentPath = path.join( + __dirname, + "..", + "deployments", + network, + `${contractName}.json` + ) + const deployment = JSON.parse(fs.readFileSync(deploymentPath, "utf8")) + return deployment.address + } catch (error) { + throw new Error( + `Failed to read deployment for ${contractName} on ${network}: ${error}` + ) + } +} +async function main() { const ALICE_PRIVATE_KEY = process.env.ALICE const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY if (!ALICE_PRIVATE_KEY) { @@ -21,22 +37,34 @@ async function main() { } const JUNGLE_ADDRESS = "0xBDC0E420aB9ba144213588A95fa1E5e63CEFf1bE" - const deploymentsGov = require("../deployments/sepolia/CrosschainGov.json") - const GOV_ADDRESS = deploymentsGov.address - const deploymentsNFT = require("../deployments/sepolia/CrosschainNFT.json") - const NFT_ADDRESS = deploymentsNFT.address + // Get the network from hardhat config + const networkName = hre.network.name + + // Get deployed addresses from deployment files + const NFT_ADDRESS = getDeployedAddress(networkName, "CrosschainNFT") + const GOV_ADDRESS = getDeployedAddress(networkName, "CrosschainGov") - console.log("\nGov address:", GOV_ADDRESS) - console.log("NFT address:", NFT_ADDRESS) + console.log("Using contract addresses:") + console.log("NFT:", NFT_ADDRESS) + console.log("Gov:", GOV_ADDRESS) - // Create provider and signers properly const provider = new ethers.JsonRpcProvider( - process.env.SEPOLIA_RPC_ENDPOINT_URL + (() => { + switch (networkName) { + case "op-sepolia": + return process.env.OP_SEPOLIA_RPC_ENDPOINT_URL + case "arbitrum-sepolia": + return process.env.ARBITRUM_SEPOLIA_RPC_ENDPOINT_URL + default: + throw new Error(`Unsupported network: ${networkName}`) + } + })() ) + const aliceSigner = new ethers.Wallet(ALICE_PRIVATE_KEY, provider) - const sepoliaSigner = new ethers.Wallet(SIGNER_PRIVATE_KEY, provider) + const signerZero = new ethers.Wallet(SIGNER_PRIVATE_KEY, provider) console.log("Using address for proposals:", aliceSigner.address) - console.log("Using address for execution:", sepoliaSigner.address) + console.log("Using address for execution:", signerZero.address) const gov = Gov__factory.connect(GOV_ADDRESS, aliceSigner) const nft = NFT__factory.connect(NFT_ADDRESS, aliceSigner) @@ -122,6 +150,7 @@ async function main() { throw new Error("Transaction failed - no receipt received") } + console.log("proposalId:", proposalId) if (receipt) { console.log("Proposal confirmed in block:", receipt.blockNumber) const proposalIdFromEvent = @@ -139,202 +168,145 @@ async function main() { let currentState = Number(state) let attempts = 0 - const maxAttempts = 50 // 5 seconds * 50 = ~4 minutes max wait + const maxAttempts = 10 + + while (currentState === 0 && attempts < maxAttempts) { + console.log("Waiting for proposal to become active...") + await sleep(30000) - while (currentState !== 1 && attempts < maxAttempts) { - // Check current state const newState = await gov.state(proposalId) currentState = Number(newState) console.log( "Current proposal state:", getProposalState(currentState) ) - - if (currentState === 1) { - // Active - console.log( - "\nProposal is now active! Performing pre-vote checks..." - ) - - // Check contract and signer - console.log("\nContract checks:") - console.log("- Gov contract address:", gov.target) - - // Check proposal parameters - console.log("\nProposal parameters:") - console.log("- Proposal ID:", proposalId) - console.log("- Vote support (1 = For):", 1) - - // Check signer details - const signerBalance = await provider.getBalance( - aliceSigner.address - ) - console.log("\nSigner checks:") - console.log("- Address:", aliceSigner.address) - console.log( - "- Balance:", - ethers.formatEther(signerBalance), - "ETH" - ) - console.log( - "- Voting power:", - await nft.getVotes(aliceSigner.address) - ) - - // Check if already voted - const hasVoted = await gov.hasVoted( - proposalId, - aliceSigner.address - ) - console.log("- Already voted:", hasVoted) - - console.log("\nCasting vote...") - const voteTx = await gov - .connect(aliceSigner) - .castVote(proposalId, 1) - await voteTx.wait(1) - console.log("Vote cast successfully!") - } - attempts++ - await sleep(5000) // Wait 5 seconds before next check - process.stdout.write("\x1b[1A\x1b[K") // Clear previous line } - let isSucceeded = false - console.log("\nStarting to check proposal state...") - - while (!isSucceeded) { - const state = await gov.state(proposalId) - console.log( - "Current proposal state:", - getProposalState(Number(state)) - ) - - if (getProposalState(Number(state)) === "Succeeded") { - isSucceeded = true - console.log( - "\nProposal succeeded! Preparing for execution..." - ) + if (proposalId) { + if (currentState === 1) { + console.log("Casting vote...") + const voteTx = await gov.castVote(proposalId, 1) + const voteReceipt = await voteTx.wait() + console.log("Vote cast successfully!") - try { - console.log("Execution parameters:") - console.log("- Targets:", targets) - console.log("- Values:", values) - console.log("- Calldatas:", calldatas) - console.log( - "- Description hash:", - ethers.id(description) - ) + let isSucceeded = false + console.log("\nStarting to check proposal state...") + while (!isSucceeded) { + const state = await gov.state(proposalId) console.log( - "\nSubmitting execution transaction from Sepolia signer..." + "Current proposal state:", + getProposalState(Number(state)) ) - // Connect with sepoliaSigner for execution - const executeTx = await gov - .connect(sepoliaSigner) - .execute( - targets, - values, - calldatas, - ethers.id(description) + if (getProposalState(Number(state)) === "Succeeded") { + isSucceeded = true + console.log( + "\nProposal succeeded! Preparing for execution..." ) - console.log( - "Execution transaction submitted:", - executeTx.hash - ) - console.log("Waiting for confirmation...") - - const executeReceipt = await executeTx.wait() - console.log( - "Proposal executed successfully in block:", - executeReceipt?.blockNumber - ) - - try { - const totalSupply = await nft.totalSupply() - console.log("NFT total supply:", totalSupply) - const newOwner = await nft.ownerOf(totalSupply - 1n) - console.log("NFT successfully minted to:", newOwner) - - // Add the new token ID to .env file - const envPath = path.resolve(__dirname, "../.env") - let envContent = "" - try { - // Read existing .env content if file exists - if (fs.existsSync(envPath)) { - envContent = fs.readFileSync( - envPath, - "utf8" - ) - } - - // Remove existing TOKENID line if it exists - envContent = envContent.replace( - /^TOKENID=.*$/m, - "" + console.log("Execution parameters:") + console.log("- Targets:", targets) + console.log("- Values:", values) + console.log("- Calldatas:", calldatas) + console.log( + "- Description hash:", + ethers.id(description) ) - // Add new line if content doesn't end with one - if ( - envContent.length > 0 && - !envContent.endsWith("\n") - ) { - envContent += "\n" - } + console.log( + "\nSubmitting execution transaction from Sepolia signer..." + ) - // Add the new TOKENID - envContent += `TOKENID=${totalSupply - 1n}\n` + // Connect with sepoliaSigner for execution + const executeTx = await gov + .connect(signerZero) + .execute( + targets, + values, + calldatas, + ethers.id(description) + ) - // Write back to .env file - fs.writeFileSync(envPath, envContent) console.log( - "\nToken ID has been written to .env file" + "Execution transaction submitted:", + executeTx.hash ) - } catch (error) { - console.error( - "Error updating .env file:", - error + console.log("Waiting for confirmation...") + + const executeReceipt = await executeTx.wait() + console.log( + "Proposal executed successfully in block:", + executeReceipt?.blockNumber ) - } - } catch (error) { - console.log("Could not verify NFT minting:", error) - } - break - } catch (error: any) { - console.error("\nError executing proposal:") - console.error("Error message:", error.message) + try { + const totalSupply = await nft.totalSupply() + console.log( + "NFT total supply:", + totalSupply + ) + const newOwner = await nft.ownerOf( + totalSupply - 1n + ) + console.log( + "NFT successfully minted to:", + newOwner + ) + } catch (error) { + console.log( + "Could not verify NFT minting:", + error + ) + } + + break + } catch (error: any) { + console.error("\nError executing proposal:") + console.error("Error message:", error.message) + + if (error.data) { + try { + const decodedError = + gov.interface.parseError(error.data) + console.error( + "Decoded error:", + decodedError + ) + } catch (e) { + console.error( + "Raw error data:", + error.data + ) + } + } - if (error.data) { - try { - const decodedError = gov.interface.parseError( - error.data - ) - console.error("Decoded error:", decodedError) - } catch (e) { - console.error("Raw error data:", error.data) + if (error.transaction) { + console.error("\nTransaction details:") + console.error("To:", error.transaction.to) + console.error( + "Data:", + error.transaction.data + ) + } + throw error } } - if (error.transaction) { - console.error("\nTransaction details:") - console.error("To:", error.transaction.to) - console.error("Data:", error.transaction.data) - } - throw error + console.log( + "Waiting 1 minute before next state check..." + ) + await sleep(60000) } + } else { + console.log( + `Could not reach active state. Current state: ${getProposalState( + currentState + )}` + ) } - - console.log("Waiting 1 minute before next state check...") - await sleep(60000) - } - - if (attempts >= maxAttempts) { - throw new Error("Timeout waiting for proposal to become active") } } } catch (error: any) { diff --git a/scripts/verify-crosschain-setup.ts b/scripts/verify-crosschain-setup.ts new file mode 100644 index 0000000..95aadcb --- /dev/null +++ b/scripts/verify-crosschain-setup.ts @@ -0,0 +1,171 @@ +import { ethers } from "hardhat" +import color from "cli-color" +import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__factory" +import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" +import * as fs from "fs" +import * as path from "path" + +const msg = color.xterm(39).bgXterm(128) + +function getDeployedAddress(network: string, contractName: string): string { + try { + const deploymentPath = path.join( + __dirname, + "..", + "deployments", + network, + `${contractName}.json` + ) + const deployment = JSON.parse(fs.readFileSync(deploymentPath, "utf8")) + return deployment.address + } catch (error) { + throw new Error( + `Failed to read deployment for ${contractName} on ${network}: ${error}` + ) + } +} + +async function verifyNetwork(networkName: string, rpcUrl: string) { + console.log(color.magenta(`\nVerifying setup on ${networkName}...`)) + + try { + // Get deployed addresses from deployment files + const nftAddress = getDeployedAddress(networkName, "CrosschainNFT") + const govAddress = getDeployedAddress(networkName, "CrosschainGov") + + console.log(msg("\nDeployed Addresses:")) + console.log(`NFT: ${nftAddress}`) + console.log(`Gov: ${govAddress}`) + + // Create provider with correct options + const provider = new ethers.JsonRpcProvider(rpcUrl) + + // Test provider connection + const network = await provider.getNetwork() + console.log( + `Connected to ${networkName} (Chain ID: ${network.chainId})` + ) + + // Verify NFT Contract + const nft = NFT__factory.connect(nftAddress, provider) + console.log("\nVerifying NFT contract...") + + try { + const nftName = await nft.name() + const nftSymbol = await nft.symbol() + const totalSupply = await nft.totalSupply() + const homeChain = await nft.home() + + console.log(msg("NFT Contract Details:")) + console.log(`- Address: ${nftAddress}`) + console.log(`- Name: ${nftName}`) + console.log(`- Symbol: ${nftSymbol}`) + console.log(`- Total Supply: ${totalSupply}`) + console.log(`- Home Chain ID: ${homeChain}`) + } catch (error) { + console.error(color.red("Failed to verify NFT contract"), error) + return false + } + + // Verify Gov Contract + const gov = Gov__factory.connect(govAddress, provider) + console.log("\nVerifying Gov contract...") + + try { + const name = await gov.name() + const votingDelay = await gov.votingDelay() + const votingPeriod = await gov.votingPeriod() + const manifesto = await gov.manifesto() + const homeChain = await gov.home() + + console.log(msg("Gov Contract Details:")) + console.log(`- Address: ${govAddress}`) + console.log(`- Name: ${name}`) + console.log(`- Voting Delay: ${votingDelay} blocks`) + console.log(`- Voting Period: ${votingPeriod} blocks`) + console.log(`- Manifesto CID: ${manifesto}`) + console.log(`- Home Chain ID: ${homeChain}`) + + // Verify NFT ownership + const nftOwner = await nft.owner() + const expectedOwner = gov.target + + // Convert addresses to strings for comparison + const ownerStr = nftOwner.toString() + const expectedStr = expectedOwner.toString() + + if (ownerStr.toLowerCase() === expectedStr.toLowerCase()) { + console.log( + msg("\n✅ NFT ownership correctly transferred to Gov") + ) + } else { + console.log( + color.red("\n❌ NFT ownership not transferred to Gov") + ) + console.log(`Current owner: ${ownerStr}`) + console.log(`Expected owner: ${expectedStr}`) + } + } catch (error) { + console.error(color.red("Failed to verify Gov contract"), error) + return false + } + + return true + } catch (error) { + console.error(color.red(`Failed to verify ${networkName}`), error) + return false + } +} + +async function main() { + console.log( + color.cyanBright("\nStarting cross-chain setup verification...\n") + ) + + const networks = { + "op-sepolia": { + rpcUrl: + process.env.OP_SEPOLIA_RPC_ENDPOINT_URL || + "https://sepolia.optimism.io" + }, + "arbitrum-sepolia": { + rpcUrl: + process.env.ARBITRUM_SEPOLIA_RPC_ENDPOINT_URL || + "https://sepolia-rollup.arbitrum.io/rpc" + } + } + + let success = true + for (const [networkName, config] of Object.entries(networks)) { + try { + const networkSuccess = await verifyNetwork( + networkName, + config.rpcUrl + ) + if (!networkSuccess) success = false + } catch (error) { + console.error(color.red(`Failed to verify ${networkName}`), error) + success = false + } + } + + if (success) { + console.log( + color.green( + "\n✅ Cross-chain setup verification completed successfully!" + ) + ) + } else { + console.log( + color.red( + "\n❌ Cross-chain setup verification failed on one or more networks" + ) + ) + process.exitCode = 1 + } +} + +main().catch(error => { + console.error(color.red("\nScript failed:"), error) + process.exitCode = 1 +}) diff --git a/scripts/verify-delegation-proof.ts b/scripts/verify-delegation-proof.ts deleted file mode 100644 index 0f01395..0000000 --- a/scripts/verify-delegation-proof.ts +++ /dev/null @@ -1,47 +0,0 @@ -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 = "0x147613E970bbA94e19a70A8b0f9106a13B4d7cbE" - const DELEGATOR = "0xD8a394e7d7894bDF2C57139fF17e5CBAa29Dd977" - const DELEGATEE = "0xD8a394e7d7894bDF2C57139fF17e5CBAa29Dd977" - const PROOF_HANDLER_ADDRESS = "0x7342BA0E0C855B403287A2EB00d48257b85496a8" - - 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/scripts/verify-gov-burn-proof.ts b/scripts/verify-gov-burn-proof.ts deleted file mode 100644 index cf1a74c..0000000 --- a/scripts/verify-gov-burn-proof.ts +++ /dev/null @@ -1,74 +0,0 @@ -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 NFT_ADDRESS = "0x147613E970bbA94e19a70A8b0f9106a13B4d7cbE" - const TOKEN_ID = 2 // Token ID that was burned - - // Add the ProofHandler library address - const PROOF_HANDLER_ADDRESS = "0x7342BA0E0C855B403287A2EB00d48257b85496a8" - - // Get contract factory with library linking - const NFTFactory = await ethers.getContractFactory( - "contracts/variants/crosschain/NFT.sol:NFT", - { - libraries: { - ProofHandler: PROOF_HANDLER_ADDRESS - } - } - ) - const nft = NFT__factory.connect(NFT_ADDRESS, NFTFactory.runner) as NFT - - // First confirm the token is burned - try { - await nft.ownerOf(TOKEN_ID) - console.log( - "\n⚠️ Warning: Token", - TOKEN_ID, - "still has an owner - it may not be burned" - ) - return - } catch (error) { - console.log("\n✅ Confirmed token", TOKEN_ID, "is burned") - } - - try { - console.log("\nGenerating burn proof...") - - const validParams = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "string"], - [ - "0xBDC0E420aB9ba144213588A95fa1E5e63CEFf1bE", - "https://bafkreicj62l5xu6pk2xx7x7n6b7rpunxb4ehlh7fevyjapid3556smuz4y.ipfs.w3s.link/" - ] - ) - - const proof = await nft.generateOperationProof(TOKEN_ID, validParams) - console.log("\nProof for claiming burn on other chains:", proof) - - // Log instructions for next steps - console.log("\nTo claim this burn on another chain:") - console.log("1. Save this proof") - console.log( - "2. Run claim-gov-burn.ts with this proof on the target chain" - ) - console.log("3. Use that script to submit the burn claim") - } catch (error: any) { - console.error("\nError generating proof:", 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/verify-manifesto-proof.ts b/scripts/verify-manifesto-proof.ts deleted file mode 100644 index 5fe0c37..0000000 --- a/scripts/verify-manifesto-proof.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ethers } from "hardhat" -import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__factory" -import { Gov } from "../typechain-types/contracts/variants/crosschain/Gov" - -async function main() { - const GOV_ADDRESS = "0x87b094e13DDe7e8d7F2793bD2Ac8636C7C0EcFD7" - const NEW_MANIFESTO = "ipfs://newManifestoCID" - - const PROOF_HANDLER_ADDRESS = "0x7342BA0E0C855B403287A2EB00d48257b85496a8" - - const GovFactory = await ethers.getContractFactory( - "contracts/variants/crosschain/Gov.sol:Gov", - { - libraries: { - ProofHandler: PROOF_HANDLER_ADDRESS - } - } - ) - const gov = Gov__factory.connect(GOV_ADDRESS, GovFactory.runner) as Gov - - const validParams = ethers.AbiCoder.defaultAbiCoder().encode( - ["string"], - [NEW_MANIFESTO] - ) - - console.log("Generating manifesto update proof...") - const proof = await gov.generateParameterProof(0, validParams) - console.log("\nProof:", proof) -} - -main().catch(error => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/verify-metadata-proof.ts b/scripts/verify-metadata-proof.ts deleted file mode 100644 index f51e34a..0000000 --- a/scripts/verify-metadata-proof.ts +++ /dev/null @@ -1,103 +0,0 @@ -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" -import * as fs from "fs" -import * as path from "path" -import * as dotenv from "dotenv" - -async function main() { - // Ensure environment variables are loaded - dotenv.config() - - // Load contract addresses from deployment files - const deploymentsNFT = require("../deployments/sepolia/CrosschainNFT.json") - const NFT_ADDRESS = deploymentsNFT.address - const PROOF_HANDLER_ADDRESS = deploymentsNFT.libraries.ProofHandler - - // Get token info - if (!process.env.TOKENID && process.env.TOKENID !== "0") { - throw new Error("No token ID specified in .env") - } - const TOKEN_ID = parseInt(process.env.TOKENID) - - const NEW_URI = - "https://bafkreifnnreoxxgkhty7v2w3qwiie6cfxpv3vcco2xldekfvbiem3nm6dm.ipfs.w3s.link/" - - console.log("\nGenerating metadata update proof...") - console.log("NFT Address:", NFT_ADDRESS) - console.log("ProofHandler Address:", PROOF_HANDLER_ADDRESS) - console.log("Token ID:", TOKEN_ID) - console.log("New URI:", NEW_URI) - - // Get contract factory with library linking - const NFTFactory = await ethers.getContractFactory( - "contracts/variants/crosschain/NFT.sol:NFT", - { - libraries: { - ProofHandler: PROOF_HANDLER_ADDRESS - } - } - ) - - // Connect to the contract - const nft = NFT__factory.connect(NFT_ADDRESS, NFTFactory.runner) as NFT - - try { - // Encode parameters for the metadata update - const encodedParams = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint256", "string"], - [TOKEN_ID, NEW_URI] - ) - - // Generate the operation proof (Operation type 2 is SET_METADATA) - const proof = await nft.generateOperationProof(2, encodedParams) - - console.log("\nProof generated successfully!") - console.log("\nProof:", proof) - - // Update .env file with the proof - const envPath = path.resolve(__dirname, "../.env") - let envContent = "" - - try { - // Read existing .env content if file exists - if (fs.existsSync(envPath)) { - envContent = fs.readFileSync(envPath, "utf8") - } - - // Remove existing PROOF line if it exists - envContent = envContent.replace(/^PROOF=.*$/m, "") - - // Add new line if content doesn't end with one - if (envContent.length > 0 && !envContent.endsWith("\n")) { - envContent += "\n" - } - - // Add the new PROOF - envContent += `PROOF=${proof}\n` - - // Write back to .env file - fs.writeFileSync(envPath, envContent) - console.log("\nProof has been saved to .env file") - } catch (error) { - console.error("Error updating .env file:", error) - throw error - } - } catch (error: any) { - console.error("\nError generating proof:", 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/verify-proof.ts b/scripts/verify-proof.ts index 04184c0..eff04e3 100644 --- a/scripts/verify-proof.ts +++ b/scripts/verify-proof.ts @@ -1,87 +1,57 @@ -import { ethers } from "hardhat" +import hre, { ethers } from "hardhat" import { NFT__factory } from "../typechain-types/factories/contracts/variants/crosschain/NFT__factory" +import { NFT } from "../typechain-types/contracts/variants/crosschain/NFT" import * as fs from "fs" -import * as dotenv from "dotenv" - -async function main() { - console.log("\nVerifying and generating proof...") - - const deploymentsNFT = require("../deployments/sepolia/CrosschainNFT.json") - const NFT_ADDRESS = deploymentsNFT.address - console.log("\nNFT Address:", NFT_ADDRESS) - - // Get token info - const tokenId = process.env.TOKENID - ? parseInt(process.env.TOKENID) - : undefined - if (!tokenId) { - throw new Error("No token ID specified in .env") - } +import * as path from "path" +function getDeployedAddress(network: string, contractName: string): string { try { - const sepoliaProvider = new ethers.JsonRpcProvider( - process.env.SEPOLIA_RPC_ENDPOINT_URL - ) - const nft = NFT__factory.connect(NFT_ADDRESS, sepoliaProvider) - - // Get owner and URI from chain A - const owner = await nft.ownerOf(tokenId) - const uri = await nft.tokenURI(tokenId) - console.log(`\nToken ${tokenId} on chain A:`) - console.log(`Owner: ${owner}`) - console.log(`URI: ${uri}`) - - // Package all token data in params - const params = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint256", "address", "string"], - [tokenId, owner, uri] - ) - - const message = ethers.keccak256( - ethers.solidityPacked( - ["address", "uint8", "bytes", "uint256"], - [NFT_ADDRESS, 0, params, 0] // operationType = 0 (MINT), nonce = 0 - ) - ) - - const digest = ethers.keccak256( - ethers.solidityPacked( - ["string", "bytes32"], - ["\x19Ethereum Signed Message:\n32", message] - ) + const deploymentPath = path.join( + __dirname, + "..", + "deployments", + network, + `${contractName}.json` ) - - const proof = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint8", "bytes", "uint256", "bytes32"], - [0, params, 0, digest] + const deployment = JSON.parse(fs.readFileSync(deploymentPath, "utf8")) + return deployment.address + } catch (error) { + throw new Error( + `Failed to read deployment for ${contractName} on ${network}: ${error}` ) + } +} - // Update .env file with the proof - const envPath = ".env" - let envContent = "" +async function main() { + const networkName = hre.network.name + const NFT_ADDRESS = getDeployedAddress(networkName, "CrosschainNFT") - if (fs.existsSync(envPath)) { - envContent = fs.readFileSync(envPath, "utf8") - } + console.log("Using NFT contract address:", NFT_ADDRESS) - // Remove existing PROOF line if it exists - envContent = envContent.replace(/^PROOF=.*$/m, "") + // Get contract factory and instance + const NFTFactory = await ethers.getContractFactory( + "contracts/variants/crosschain/NFT.sol:NFT" + ) + const nft = NFT__factory.connect(NFT_ADDRESS, NFTFactory.runner) as NFT - // Add new line if content doesn't end with one - if (envContent.length > 0 && !envContent.endsWith("\n")) { - envContent += "\n" - } + // Get owner of token ID 2 for verification + const owner = await nft.ownerOf(2) + console.log("\nToken owner:", owner) - // Add the new PROOF - envContent += `PROOF=${proof}\n` + // Generate proof for token ID 2 + console.log("Generating proof for token ID 2...") + const proof = await nft.generateMintProof(2) + console.log("\nProof:", proof) - fs.writeFileSync(envPath, envContent) - console.log("\nProof generated and saved to .env:") - console.log(proof) - } catch (error) { - console.error("Error:", error) - process.exitCode = 1 + // Write proof to data.json + const data = { + proof: proof } + fs.writeFileSync( + path.join(__dirname, "..", "data.json"), + JSON.stringify(data, null, 2) + ) + console.log("\nProof written to data.json") } main().catch(error => { diff --git a/scripts/verify-voting-delay-proof.ts b/scripts/verify-voting-delay-proof.ts deleted file mode 100644 index fe58bea..0000000 --- a/scripts/verify-voting-delay-proof.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ethers } from "hardhat" -import { Gov__factory } from "../typechain-types/factories/contracts/variants/crosschain/Gov__factory" - -async function main() { - const GOV_ADDRESS = "0x87b094e13DDe7e8d7F2793bD2Ac8636C7C0EcFD7" - const NEW_VOTING_DELAY = 250n - const OPERATION_TYPE = 1 // UPDATE_VOTING_DELAY - const NONCE = 1n // Force nonce 1 - - // Pack parameters - const value = ethers.solidityPacked(["uint48"], [NEW_VOTING_DELAY]) - console.log("Packed delay value:", value) - - // Create message and digest manually - const message = ethers.keccak256( - ethers.solidityPacked( - ["address", "uint8", "bytes", "uint256"], - [GOV_ADDRESS, OPERATION_TYPE, value, NONCE] - ) - ) - - const digest = ethers.keccak256( - ethers.solidityPacked( - ["string", "bytes32"], - ["\x19Ethereum Signed Message:\n32", message] - ) - ) - - // Encode the proof - const proof = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint8", "bytes", "uint256", "bytes32"], - [OPERATION_TYPE, value, NONCE, digest] - ) - - console.log("\nGenerated proof details:") - console.log("Operation type:", OPERATION_TYPE) - console.log("Value:", value) - console.log("Nonce:", NONCE.toString()) - console.log("Digest:", digest) - - console.log("\nFull proof to use in claim script:") - console.log(proof) - - // Verify the digest matches what we saw in storage - console.log("\nVerification:") - const expectedDigest = - "0xd6e24169a86f648dc2eb3d5607c83873e546b036409af044ff2fe5cfcf32e563" - console.log("Expected digest:", expectedDigest) - console.log("Generated digest matches:", digest === expectedDigest) -} - -main().catch(error => { - console.error(error) - process.exitCode = 1 -}) diff --git a/test/Gov-crosschain.ts b/test/Gov-crosschain.ts index 9c82cc5..3e141b2 100644 --- a/test/Gov-crosschain.ts +++ b/test/Gov-crosschain.ts @@ -1,111 +1,65 @@ import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers" import { expect } from "chai" -import { ethers } from "hardhat" -import type { NFT, Gov, ProofHandler } from "../typechain-types" -import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" -import type { Contract, EventLog } from "ethers" +import { ethers, network } from "hardhat" +import { NFT } from "../typechain-types/contracts/variants/crosschain/NFT" +import { Gov } from "../typechain-types/contracts/variants/crosschain/Gov" +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers" +import { EventLog } from "ethers" describe("Crosschain Gov", function () { - let gov: Gov & Contract - let nft: NFT & Contract - let proofHandler: ProofHandler & Contract + let gov: Gov + let nft: NFT let owner: HardhatEthersSigner let alice: HardhatEthersSigner let bob: HardhatEthersSigner let charlie: HardhatEthersSigner let david: HardhatEthersSigner - async function findProposalId(receipt: any): Promise { - const log = receipt.logs.find( - (x: any) => x.fragment?.name === "ProposalCreated" - ) - if (!log) throw new Error("ProposalCreated event not found") - return log.args[0] - } - - // For finding tokenId in mint events - async function findTokenId(receipt: any): Promise { - const log = receipt.logs.find( - (x: any) => - x.fragment?.name === "Transfer" && - x.args[1] !== ethers.ZeroAddress - ) - if (!log) throw new Error("Transfer event not found") - return log.args[2] - } - async function deployContracts() { ;[owner, alice, bob, charlie, david] = await ethers.getSigners() - // Deploy ProofHandler library first - const ProofHandlerFactory = await ethers.getContractFactory( - "contracts/variants/crosschain/ProofHandler.sol:ProofHandler" - ) - const proofHandler = await ProofHandlerFactory.deploy() - await proofHandler.waitForDeployment() - - // Deploy NFT with library linking + // Deploy NFT with initial members const NFTFactory = await ethers.getContractFactory( - "contracts/variants/crosschain/NFT.sol:NFT", - { - libraries: { - ProofHandler: await proofHandler.getAddress() - } - } + "contracts/variants/crosschain/NFT.sol:NFT" ) - const nft = await NFTFactory.deploy( - BigInt(1337), // Chain ID for local network + const nftContract = (await NFTFactory.deploy( + 1337, owner.address, - [alice.address, bob.address], + [alice.address, bob.address], // Only Alice and Bob get NFTs initially "ipfs://testURI", "TestNFT", "TNFT" - ) + )) as unknown as NFT + await nftContract.waitForDeployment() + nft = nftContract - // Deploy Gov with library linking + // Deploy Gov contract const GovFactory = await ethers.getContractFactory( - "contracts/variants/crosschain/Gov.sol:Gov", - { - libraries: { - ProofHandler: await proofHandler.getAddress() - } - } + "contracts/variants/crosschain/Gov.sol:Gov" ) - const gov = await GovFactory.deploy( - BigInt(1337), + const govContract = (await GovFactory.deploy( + 1337, await nft.getAddress(), "ipfs://testManifesto", - "TestDAO", - 0, - 50400, - 1, - 10 - ) - - // Transfer NFT ownership to Gov + "TestGov", + 1, // votingDelay + 50, // votingPeriod + 1, // proposalThreshold + 1 // quorum + )) as unknown as Gov + await govContract.waitForDeployment() + gov = govContract + + // Transfer NFT contract ownership to Gov await nft.transferOwnership(await gov.getAddress()) - // Delegate voting power - await nft.connect(alice).delegate(alice.address) - await nft.connect(bob).delegate(bob.address) - - return { - gov: gov as Gov & Contract, - nft: nft as NFT & Contract, - proofHandler: proofHandler as ProofHandler & Contract, - owner, - alice, - bob, - charlie, - david - } + return { gov, nft, owner, alice, bob, charlie, david } } beforeEach(async function () { const contracts = await loadFixture(deployContracts) gov = contracts.gov nft = contracts.nft - proofHandler = contracts.proofHandler owner = contracts.owner alice = contracts.alice bob = contracts.bob @@ -131,106 +85,85 @@ describe("Crosschain Gov", function () { }) it("should allow the DAO to mint new NFTs", async function () { - const mintCalldata = nft.interface.encodeFunctionData("safeMint", [ - charlie.address, - "ipfs://newURI" - ]) - + const targets = [await nft.getAddress()] + const values = [0] + const calldatas = [ + nft.interface.encodeFunctionData("safeMint", [ + charlie.address, + "ipfs://newURI" + ]) + ] + const description = "Mint new member NFT" + + // Alice creates and votes on proposal const tx = await gov .connect(alice) - .propose( - [await nft.getAddress()], - [0], - [mintCalldata], - "Mint new NFT" - ) + .propose(targets, values, calldatas, description) const receipt = await tx.wait() - const proposalId = await findProposalId(receipt) - - // Skip voting delay - await ethers.provider.send("evm_mine", []) - - // Vote on proposal + const proposalId = ( + receipt?.logs?.find( + log => + log instanceof EventLog && + log.eventName === "ProposalCreated" + ) as EventLog + )?.args?.[0] + + await time.increase(2) await gov.connect(alice).castVote(proposalId, 1) - await gov.connect(bob).castVote(proposalId, 1) - - // Wait for voting period to end - for (let i = 0; i < 50400; i++) { - await ethers.provider.send("evm_mine", []) - } - - // Execute proposal - const execTx = await gov - .connect(alice) - .execute( - [await nft.getAddress()], - [0], - [mintCalldata], - ethers.id("Mint new NFT") - ) - const execReceipt = await execTx.wait() + await time.increase(51) + await gov.execute( + targets, + values, + calldatas, + ethers.id(description) + ) - // Verify the mint - expect(await nft.ownerOf(2)).to.equal(charlie.address) + expect(await nft.balanceOf(charlie.address)).to.equal(1) }) it("should generate membership proof", async function () { // Get Alice's token ID (should be 0 as she was first member) const aliceTokenId = 0 - const uri = await nft.tokenURI(aliceTokenId) - - const params = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "string"], - [alice.address, uri] - ) // Generate the proof - const proof = await nft.connect(alice).generateOperationProof( - 0, // MINT - params + expect( + await nft.connect(alice).generateMintProof(aliceTokenId) + ).to.be.equal( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c800000000000000000000000000000000000000000000000000000000000000805f94c8cd397e8c8823da171013dfc02b9f0d1812fb747295e6b0534f0270bf57000000000000000000000000000000000000000000000000000000000000000e697066733a2f2f74657374555249000000000000000000000000000000000000" ) - const [operationType, proofParams, nonce, digest] = - ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], - proof - ) - - expect(operationType).to.equal(0) // MINT - expect(proofParams).to.equal(params) + expect(await nft.connect(alice).generateMintProof(aliceTokenId)).to + .be.reverted }) it("should verify membership proof correctly", async function () { + // Get Alice's token ID (0) const aliceTokenId = 0 - const uri = await nft.tokenURI(aliceTokenId) - - const params = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "string"], - [alice.address, uri] - ) // Generate proof on "source" chain const proof = await nft .connect(alice) - .generateOperationProof(0, params) + .generateMintProof(aliceTokenId) - // Decode the proof - const [operationType, proofParams, nonce, digest] = + // Decode the proof to verify its contents + const [tokenId, to, uri, digest] = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], + ["uint256", "address", "string", "bytes32"], proof ) - // Verify the decoded values - expect(operationType).to.equal(0) // MINT - expect(proofParams).to.equal(params) + // Verify the decoded basic values + expect(tokenId).to.equal(aliceTokenId) + expect(to).to.equal(alice.address) + expect(uri).to.equal(await nft.tokenURI(aliceTokenId)) - // Reproduce the proof verification logic + // Reproduce the proof verification logic from the contract const nftAddress = await nft.getAddress() const message = ethers.solidityPackedKeccak256( - ["address", "uint8", "bytes", "uint256"], - [nftAddress, operationType, proofParams, nonce] + ["address", "uint8", "uint256", "address", "string"], + [nftAddress, 0, tokenId, to, uri] ) + // Create the expected digest (mimicking the contract's verification) const expectedDigest = ethers.keccak256( ethers.solidityPacked( ["string", "bytes32"], @@ -238,6 +171,7 @@ describe("Crosschain Gov", function () { ) ) + // Verify the digest matches expect(digest).to.equal( expectedDigest, "Proof digest verification failed" @@ -245,44 +179,61 @@ describe("Crosschain Gov", function () { }) it("should reject invalid membership proof", async function () { + // Get Alice's token ID (0) const aliceTokenId = 0 - const uri = await nft.tokenURI(aliceTokenId) - - // Generate valid params - const validParams = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "string"], - [alice.address, uri] - ) // Generate valid proof - const validProof = await nft + const proof = await nft .connect(alice) - .generateOperationProof(0, validParams) - - // Create invalid params - const invalidParams = ethers.AbiCoder.defaultAbiCoder().encode( - ["address", "string"], - [bob.address, uri] - ) + .generateMintProof(aliceTokenId) - // Create invalid proof with wrong digest + // Create an invalid proof by changing the tokenId and digest const invalidProof = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint8", "bytes", "uint256", "bytes32"], - [0, invalidParams, 1, ethers.id("invalid")] + ["uint256", "address", "string", "bytes32"], + [ + aliceTokenId + 1, + bob.address, + await nft.tokenURI(aliceTokenId), + ethers.id("invalid") + ] ) + // Decode the invalid proof + const [invalidTokenId, invalidTo, invalidUri, invalidDigest] = + ethers.AbiCoder.defaultAbiCoder().decode( + ["uint256", "address", "string", "bytes32"], + invalidProof + ) + const nftAddress = await nft.getAddress() + const expectedMessage = ethers.solidityPackedKeccak256( + ["address", "uint8", "uint256", "address", "string"], + [nftAddress, 0, invalidTokenId, invalidTo, invalidUri] // 0 is OperationType.MINT + ) - // Verify valid proof works - const [validOpType, validProofParams, validNonce, validDigest] = + const expectedDigest = ethers.keccak256( + ethers.solidityPacked( + ["string", "bytes32"], + ["\x19Ethereum Signed Message:\n32", expectedMessage] + ) + ) + + // Verify the invalid proof's digest doesn't match the expected digest + expect(invalidDigest).to.not.equal( + expectedDigest, + "Invalid proof should not verify" + ) + + // Verify the original proof is still valid + const [tokenId, to, validUri, digest] = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], - validProof + ["uint256", "address", "string", "bytes32"], + proof ) const validMessage = ethers.solidityPackedKeccak256( - ["address", "uint8", "bytes", "uint256"], - [nftAddress, validOpType, validProofParams, validNonce] + ["address", "uint8", "uint256", "address", "string"], + [nftAddress, 0, tokenId, to, validUri] ) const validExpectedDigest = ethers.keccak256( @@ -292,40 +243,38 @@ describe("Crosschain Gov", function () { ) ) - expect(validDigest).to.equal( + expect(digest).to.equal( validExpectedDigest, "Valid proof should verify" ) }) it("should generate and verify burn proof correctly", async function () { + // Get Alice's token ID (0) const aliceTokenId = 0 - const params = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint256"], - [aliceTokenId] - ) - // Generate burn proof + // Generate burn proof on "source" chain const proof = await nft .connect(alice) - .generateOperationProof(1, params) // BURN + .generateBurnProof(aliceTokenId) - const [operationType, proofParams, nonce, digest] = - ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], - proof - ) + // Decode the proof to verify its contents + const [tokenId, digest] = ethers.AbiCoder.defaultAbiCoder().decode( + ["uint256", "bytes32"], + proof + ) - expect(operationType).to.equal(1) // BURN - expect(proofParams).to.equal(params) + // Verify the decoded basic values + expect(tokenId).to.equal(aliceTokenId) - // Verify the proof + // Reproduce the proof verification logic from the contract const nftAddress = await nft.getAddress() const message = ethers.solidityPackedKeccak256( - ["address", "uint8", "bytes", "uint256"], - [nftAddress, operationType, proofParams, nonce] + ["address", "uint8", "uint256"], + [nftAddress, 1, tokenId] // 1 is OperationType.BURN ) + // Create the expected digest (mimicking the contract's verification) const expectedDigest = ethers.keccak256( ethers.solidityPacked( ["string", "bytes32"], @@ -333,42 +282,40 @@ describe("Crosschain Gov", function () { ) ) + // Verify the digest matches expect(digest).to.equal( expectedDigest, - "Burn proof verification failed" + "Burn proof digest verification failed" ) }) - it("should generate and verify metadata proof correctly", async function () { const aliceTokenId = 0 const newUri = "ipfs://newURI" - const params = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint256", "string"], - [aliceTokenId, newUri] - ) - - // Generate metadata proof + // Generate metadata proof on "source" chain const proof = await nft .connect(alice) - .generateOperationProof(2, params) // SET_METADATA + .generateMetadataProof(aliceTokenId, newUri) - const [operationType, proofParams, nonce, digest] = + // Decode the proof to verify its contents + const [tokenId, uri, digest] = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], + ["uint256", "string", "bytes32"], proof ) - expect(operationType).to.equal(2) // SET_METADATA - expect(proofParams).to.equal(params) + // Verify the decoded basic values + expect(tokenId).to.equal(aliceTokenId) + expect(uri).to.equal(newUri) - // Verify the proof + // Reproduce the proof verification logic from the contract const nftAddress = await nft.getAddress() const message = ethers.solidityPackedKeccak256( - ["address", "uint8", "bytes", "uint256"], - [nftAddress, operationType, proofParams, nonce] + ["address", "uint8", "uint256", "string"], + [nftAddress, 2, tokenId, newUri] // 2 is OperationType.SET_METADATA ) + // Create the expected digest (mimicking the contract's verification) const expectedDigest = ethers.keccak256( ethers.solidityPacked( ["string", "bytes32"], @@ -376,46 +323,33 @@ describe("Crosschain Gov", function () { ) ) + // Verify the digest matches expect(digest).to.equal( expectedDigest, - "Metadata proof verification failed" + "Metadata proof digest verification failed" ) }) it("should generate and verify manifesto proof correctly", async function () { const newManifesto = "ipfs://newManifesto" // Generate manifesto proof on "source" chain - const proof = await gov.generateParameterProof( - 0, // OperationType.SET_MANIFESTO - ethers.AbiCoder.defaultAbiCoder().encode( - ["string"], - [newManifesto] - ) - ) + const proof = await gov.generateManifestoProof(newManifesto) // Decode the proof to verify its contents - const [operationType, value, nonce, digest] = + const [manifestoValue, digest] = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], + ["string", "bytes32"], proof ) - // Decode the value back to string - const manifestoValue = ethers.AbiCoder.defaultAbiCoder().decode( - ["string"], - value - )[0] - // Verify the decoded basic values - expect(operationType).to.equal(0) // SET_MANIFESTO expect(manifestoValue).to.equal(newManifesto) - expect(nonce).to.equal(0) // First update should have nonce 0 // Reproduce the proof verification logic from the contract const govAddress = await gov.getAddress() const message = ethers.solidityPackedKeccak256( - ["address", "uint8", "bytes", "uint256"], - [govAddress, operationType, value, nonce] + ["address", "uint8", "string"], + [govAddress, 0, newManifesto] // 0 is OperationType.SET_MANIFESTO ) // Create the expected digest (mimicking the contract's verification) @@ -432,7 +366,6 @@ 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 @@ -442,28 +375,30 @@ describe("Crosschain Gov", function () { ) // Generate proof on home chain - const proof = await gov.generateParameterProof(1, value) // UPDATE_VOTING_DELAY + const proof = await gov.generateParameterProof( + 1, // UPDATE_VOTING_DELAY + value + ) // Decode the proof to verify its contents - const [operationType, proofValue, nonce, digest] = + const [operationType, proofValue, digest] = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], + ["uint8", "bytes", "bytes32"], proof ) // Verify the decoded basic values expect(operationType).to.equal(1) // UPDATE_VOTING_DELAY expect(proofValue).to.equal(value) - expect(nonce).to.equal(0) // First update should have nonce 0 // Reproduce the proof verification logic from the contract const govAddress = await gov.getAddress() const message = ethers.solidityPackedKeccak256( - ["address", "uint8", "bytes", "uint256"], - [govAddress, operationType, value, nonce] + ["address", "uint8", "bytes"], + [govAddress, operationType, value] ) - // Create the expected digest + // Create the expected digest (mimicking the contract's verification) const expectedDigest = ethers.keccak256( ethers.solidityPacked( ["string", "bytes32"], @@ -471,6 +406,7 @@ describe("Crosschain Gov", function () { ) ) + // Verify the digest matches expect(digest).to.equal( expectedDigest, "Proof digest verification failed" @@ -484,22 +420,28 @@ describe("Crosschain Gov", function () { [newVotingPeriod] ) - const proof = await gov.generateParameterProof(2, value) // UPDATE_VOTING_PERIOD + // Generate proof on home chain + const proof = await gov.generateParameterProof( + 2, // UPDATE_VOTING_PERIOD + value + ) - const [operationType, proofValue, nonce, digest] = + // Decode the proof to verify its contents + const [operationType, proofValue, digest] = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], + ["uint8", "bytes", "bytes32"], proof ) + // Verify the decoded basic values expect(operationType).to.equal(2) // UPDATE_VOTING_PERIOD expect(proofValue).to.equal(value) - expect(nonce).to.equal(1) + // Reproduce the proof verification logic from the contract const govAddress = await gov.getAddress() const message = ethers.solidityPackedKeccak256( - ["address", "uint8", "bytes", "uint256"], - [govAddress, operationType, value, nonce] + ["address", "uint8", "bytes"], + [govAddress, operationType, value] ) const expectedDigest = ethers.keccak256( @@ -522,22 +464,28 @@ describe("Crosschain Gov", function () { [newThreshold] ) - const proof = await gov.generateParameterProof(3, value) // UPDATE_PROPOSAL_THRESHOLD + // Generate proof on home chain + const proof = await gov.generateParameterProof( + 3, // UPDATE_PROPOSAL_THRESHOLD + value + ) - const [operationType, proofValue, nonce, digest] = + // Decode the proof to verify its contents + const [operationType, proofValue, digest] = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], + ["uint8", "bytes", "bytes32"], proof ) + // Verify the decoded basic values expect(operationType).to.equal(3) // UPDATE_PROPOSAL_THRESHOLD expect(proofValue).to.equal(value) - expect(nonce).to.equal(1) + // Reproduce the proof verification logic from the contract const govAddress = await gov.getAddress() const message = ethers.solidityPackedKeccak256( - ["address", "uint8", "bytes", "uint256"], - [govAddress, operationType, value, nonce] + ["address", "uint8", "bytes"], + [govAddress, operationType, value] ) const expectedDigest = ethers.keccak256( @@ -560,22 +508,28 @@ describe("Crosschain Gov", function () { [newQuorum] ) - const proof = await gov.generateParameterProof(4, value) // UPDATE_QUORUM + // Generate proof on home chain + const proof = await gov.generateParameterProof( + 4, // UPDATE_QUORUM + value + ) - const [operationType, proofValue, nonce, digest] = + // Decode the proof to verify its contents + const [operationType, proofValue, digest] = ethers.AbiCoder.defaultAbiCoder().decode( - ["uint8", "bytes", "uint256", "bytes32"], + ["uint8", "bytes", "bytes32"], proof ) + // Verify the decoded basic values expect(operationType).to.equal(4) // UPDATE_QUORUM expect(proofValue).to.equal(value) - expect(nonce).to.equal(1) + // Reproduce the proof verification logic from the contract const govAddress = await gov.getAddress() const message = ethers.solidityPackedKeccak256( - ["address", "uint8", "bytes", "uint256"], - [govAddress, operationType, value, nonce] + ["address", "uint8", "bytes"], + [govAddress, operationType, value] ) const expectedDigest = ethers.keccak256( @@ -590,44 +544,6 @@ describe("Crosschain Gov", function () { "Proof digest verification failed" ) }) - - // Add tests for preventing duplicate and old proofs - it("should reject duplicate proofs", async function () { - const newQuorum = 20n - const value = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint256"], - [newQuorum] - ) - const proof = await gov.generateParameterProof(4, value) - - await gov.claimParameterUpdate(proof) - await expect( - gov.claimParameterUpdate(proof) - ).to.be.revertedWith("Proof already claimed") - }) - - it("should reject old proofs", async function () { - const value1 = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint256"], - [20n] - ) - const value2 = ethers.AbiCoder.defaultAbiCoder().encode( - ["uint256"], - [30n] - ) - - // Generate both proofs first (they'll have sequential nonces) - const proof1 = await gov.generateParameterProof(4, value1) - const proof2 = await gov.generateParameterProof(4, value2) - - // Apply the newer proof first - await gov.claimParameterUpdate(proof2) - - // Now try to apply the older proof - should fail because of older nonce - await expect( - gov.claimParameterUpdate(proof1) - ).to.be.revertedWith("Invalid nonce") - }) }) }) @@ -650,45 +566,33 @@ 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 () { + xdescribe("Delegation Transfers", function () { it("should properly transfer voting power when changing delegates", async function () { - // Initial state - Alice and Bob each have 1 vote - expect(await nft.getVotes(alice.address)).to.equal(1n) - expect(await nft.getVotes(bob.address)).to.equal(1n) + // Initial delegation + await nft.connect(alice).delegate(david.address) + expect(await nft.getVotes(david.address)).to.equal(1) - // Alice delegates to Bob + // Change delegation await nft.connect(alice).delegate(bob.address) - - // Bob should have his own vote plus Alice's delegation - expect(await nft.getVotes(bob.address)).to.equal(2n) - expect(await nft.getVotes(alice.address)).to.equal(0n) + expect(await nft.getVotes(david.address)).to.equal(0) + expect(await nft.getVotes(bob.address)).to.equal(1) }) it("should maintain zero voting power for non-holders across multiple delegations", async function () { + // First check initial state + const initialBobVotes = await nft.getVotes(bob.address) + + // Charlie (non-holder) performs multiple delegations + await nft.connect(charlie).delegate(david.address) await nft.connect(charlie).delegate(alice.address) await nft.connect(charlie).delegate(bob.address) - // Charlie doesn't own any NFTs, so their delegation shouldn't affect voting power - expect(await nft.getVotes(charlie.address)).to.equal(0) + expect(await nft.getVotes(david.address)).to.equal(0) + expect(await nft.getVotes(alice.address)).to.equal(0) + // Bob should maintain only his original voting power if any + expect(await nft.getVotes(bob.address)).to.equal( + initialBobVotes + ) }) }) @@ -812,77 +716,221 @@ describe("Crosschain Gov", function () { describe("Voting", function () { let proposalId: bigint + let targets: string[] + let values: number[] + let calldatas: string[] + let description: string beforeEach(async function () { - const mintCalldata = nft.interface.encodeFunctionData( - "safeMint", - [charlie.address, "ipfs://newURI"] - ) + // Setup standard proposal + targets = [await gov.getAddress()] + values = [0] + calldatas = [ + gov.interface.encodeFunctionData("setManifesto", [ + "New Manifesto" + ]) + ] + description = "Test Proposal" + // Alice creates proposal + await nft.connect(alice).delegate(alice.address) const tx = await gov .connect(alice) - .propose( - [await nft.getAddress()], - [0], - [mintCalldata], - "Mint new NFT" - ) + .propose(targets, values, calldatas, description) const receipt = await tx.wait() - proposalId = await findProposalId(receipt) - - // Skip voting delay - await ethers.provider.send("evm_mine", []) + proposalId = ( + receipt?.logs?.find( + log => + log instanceof EventLog && + log.eventName === "ProposalCreated" + ) as EventLog + )?.args?.[0] + + // Move past voting delay + await time.increase(2) }) it("should allow NFT holders to vote", async function () { await expect(gov.connect(alice).castVote(proposalId, 1)).to.not .be.reverted }) + + it("should allow delegated votes to be cast", async function () { + await nft.connect(alice).delegate(david.address) + await expect(gov.connect(david).castVote(proposalId, 1)).to.not + .be.reverted + }) + + it("should prevent non-holders from voting", async function () { + await expect(gov.connect(charlie).castVote(proposalId, 1)).to + .not.be.reverted // The tx succeeds but... + + const proposalVotes = await gov.proposalVotes(proposalId) + expect(proposalVotes[1]).to.equal(0) // ...but the vote doesn't count + }) + + it("should track voting power at time of proposal creation", async function () { + // Initial vote from Alice + await gov.connect(alice).castVote(proposalId, 1) + + // Create and execute proposal to mint new NFT to Charlie + const mintTargets = [await nft.getAddress()] + const mintValues = [0] + const mintCalldata = [ + nft.interface.encodeFunctionData("safeMint", [ + charlie.address, + "ipfs://newURI" + ]) + ] + const mintDescription = "Mint new member NFT" + + // Create and vote on mint proposal + const mintTx = await gov + .connect(alice) + .propose( + mintTargets, + mintValues, + mintCalldata, + mintDescription + ) + const mintReceipt = await mintTx.wait() + const mintProposalId = ( + mintReceipt?.logs?.find( + log => + log instanceof EventLog && + log.eventName === "ProposalCreated" + ) as EventLog + )?.args?.[0] + + // Wait for voting delay + await time.increase(2) + + // Vote and wait for voting period + await gov.connect(alice).castVote(mintProposalId, 1) + await time.increase(51) + + // Execute mint proposal + await gov.execute( + mintTargets, + mintValues, + mintCalldata, + ethers.id(mintDescription) + ) + + // Check that Charlie got their NFT + expect(await nft.balanceOf(charlie.address)).to.equal(1) + + // Charlie delegates to themselves and tries to vote on original proposal + await nft.connect(charlie).delegate(charlie.address) + + // Check proposal state before Charlie tries to vote + const state = await gov.state(proposalId) + // Only try to vote if proposal is still active + if (state === BigInt(1)) { + // Active state + await gov.connect(charlie).castVote(proposalId, 1) + } + + // Check votes - should only count Alice's original vote + const proposalVotes = await gov.proposalVotes(proposalId) + expect(proposalVotes[1]).to.equal(1) + }) }) describe("Proposal Execution", function () { it("should execute successful proposals", async function () { - const mintCalldata = nft.interface.encodeFunctionData( - "safeMint", - [charlie.address, "ipfs://newURI"] - ) + // Setup + await nft.connect(alice).delegate(alice.address) + await nft.connect(bob).delegate(bob.address) + const targets = [await gov.getAddress()] + const values = [0] + const newManifesto = "New Manifesto" + const calldatas = [ + gov.interface.encodeFunctionData("setManifesto", [ + newManifesto + ]) + ] + const description = "Update Manifesto" + + // Create proposal const tx = await gov .connect(alice) - .propose( - [await nft.getAddress()], - [0], - [mintCalldata], - "Mint new NFT" - ) + .propose(targets, values, calldatas, description) const receipt = await tx.wait() - const proposalId = await findProposalId(receipt) - - // Skip voting delay - await ethers.provider.send("evm_mine", []) + const proposalId = ( + receipt?.logs?.find( + log => + log instanceof EventLog && + log.eventName === "ProposalCreated" + ) as EventLog + )?.args?.[0] // Vote + await time.increase(2) await gov.connect(alice).castVote(proposalId, 1) await gov.connect(bob).castVote(proposalId, 1) // Wait for voting period to end - for (let i = 0; i < 50400; i++) { - await ethers.provider.send("evm_mine", []) - } + await time.increase(51) // Execute - await expect( - gov.execute( - [await nft.getAddress()], - [0], - [mintCalldata], - ethers.id("Mint new NFT") - ) - ).to.not.be.reverted + await gov.execute( + targets, + values, + calldatas, + ethers.id(description) + ) + + // Verify + expect(await gov.manifesto()).to.equal(newManifesto) }) it("should not execute failed proposals", async function () { - // Similar structure but with failing conditions + // Setup similar to above but with opposing votes + await nft.connect(alice).delegate(alice.address) + await nft.connect(bob).delegate(bob.address) + + const targets = [await gov.getAddress()] + const values = [0] + const newManifesto = "New Manifesto" + const calldatas = [ + gov.interface.encodeFunctionData("setManifesto", [ + newManifesto + ]) + ] + const description = "Update Manifesto" + + const tx = await gov + .connect(alice) + .propose(targets, values, calldatas, description) + const receipt = await tx.wait() + const proposalId = ( + receipt?.logs?.find( + log => + log instanceof EventLog && + log.eventName === "ProposalCreated" + ) as EventLog + )?.args?.[0] + + await time.increase(2) + await gov.connect(alice).castVote(proposalId, 1) // For + await gov.connect(bob).castVote(proposalId, 0) // Against + + await time.increase(51) + + // Attempt to execute should fail + await expect( + gov.execute( + targets, + values, + calldatas, + ethers.id(description) + ) + ).to.be.revertedWithCustomError( + gov, + "GovernorUnexpectedProposalState" + ) }) }) })