Skip to content

Commit

Permalink
Prevent minting when tokenId already exists (#148)
Browse files Browse the repository at this point in the history
* shorter voting delay

* fix generateMintProof

* fix tests

* new deployment

* add scripts

* add scenario
  • Loading branch information
julienbrg authored Nov 30, 2024
1 parent e844640 commit 1a26001
Show file tree
Hide file tree
Showing 29 changed files with 1,986 additions and 4,865 deletions.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,26 @@ pnpm crosschain:op-sepolia

Your DAO will be deployed on every networks at the same address.

This is an example of a member generating a membership proof on chain A (Sepolia), and claiming his membership on chain B (OP Seolia): https://sepolia-optimism.etherscan.io/tx/0x4e43854ae6ff26207285286e365dafb075a210a688678fb06fd5e2a854f69a62
Then you can follow these steps to verify that proofs of `safeMint`, `govBurn`, `setMetadata`, `setManifesto` can be generated on source chain and claimed on foreign chain:

```
pnpm crosschain:sepolia
pnpm crosschain:op-sepolia
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 op-sepolia
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 op-sepolia
npx hardhat run scripts/verify-metadata-proof.ts --network sepolia
npx hardhat run scripts/claim-metadata-update.ts --network op-sepolia
npx hardhat run scripts/verify-manifesto-proof.ts --network sepolia
npx hardhat run scripts/claim-manifesto-update.ts --network op-sepolia
```

## Security

Expand Down
143 changes: 116 additions & 27 deletions contracts/variants/crosschain/Gov.sol
Original file line number Diff line number Diff line change
@@ -1,32 +1,63 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;

// Gov.sol
import "@openzeppelin/contracts/governance/Governor.sol";
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";

/**
* @title Cross-chain Governance Contract
* @author Web3 Hackers Collective
* @notice Implements DAO governance with cross-chain support
* @dev Extends OpenZeppelin Governor with cross-chain manifesto updates
* @custom:security-contact [email protected]
*/
contract Gov is
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction
{
/// @notice Chain ID where this contract was originally deployed
uint256 public immutable home;

/// @notice CID of the DAO's manifesto
string public manifesto;

event ManifestoUpdated(string cid);
/// @dev Operation types for cross-chain actions
enum OperationType {
SET_MANIFESTO
}

/**
* @notice Emitted when the manifesto is updated
* @param oldManifesto Previous manifesto CID
* @param newManifesto New manifesto CID
*/
event ManifestoUpdated(string oldManifesto, string newManifesto);

/**
* @notice Restricts operations to the home chain
*/
modifier onlyHomeChain() {
require(block.chainid == home, "Operation only allowed on home chain");
_;
}

/// @notice Initializes the governance contract
/// @param _token The address of the token used for voting
/// @param _manifesto The initial CID of the manifesto
/// @param _name The name of the governance contract
/// @param _votingDelay The delay before voting starts
/// @param _votingPeriod The duration of the voting period
/// @param _votingThreshold The minimum number of votes required to create a proposal
/// @param _quorum The percentage of total supply that must participate for a vote to succeed
/**
* @notice Initializes the governance contract
* @param _home Chain ID where this contract is considered home
* @param _token The voting token contract
* @param _manifesto Initial manifesto CID
* @param _name Name of the governance contract
* @param _votingDelay Time before voting begins
* @param _votingPeriod Duration of voting
* @param _votingThreshold Minimum votes needed for proposal
* @param _quorum Minimum participation percentage
*/
constructor(
uint256 _home,
IVotes _token,
string memory _manifesto,
string memory _name,
Expand All @@ -40,30 +71,96 @@ contract Gov is
GovernorVotes(_token)
GovernorVotesQuorumFraction(_quorum)
{
home = _home;
manifesto = _manifesto;
}

/// @notice Returns the delay before voting on a proposal may take place
/**
* @notice Updates the manifesto CID on home chain
* @dev Only callable through governance on home chain
* @param newManifesto New manifesto CID
*/
function setManifesto(string memory newManifesto) public onlyGovernance onlyHomeChain {
_setManifestoOriginal(newManifesto);
}

/**
* @notice Generates proof for cross-chain manifesto updates
* @param newManifesto New manifesto CID
* @return Encoded proof data
*/
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
* @param proof Proof generated on 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");

_setManifestoOriginal(newManifesto);
}

/**
* @notice Internal function to update manifesto
* @param newManifesto New manifesto CID
*/
function _setManifestoOriginal(string memory newManifesto) private {
string memory oldManifesto = manifesto;
manifesto = newManifesto;
emit ManifestoUpdated(oldManifesto, newManifesto);
}

// Required Governor Overrides

/**
* @notice Returns the voting delay
* @return Delay before voting starts
*/
function votingDelay() public view override(Governor, GovernorSettings) returns (uint256) {
return super.votingDelay();
}

/// @notice Returns the duration of the voting period
/**
* @notice Returns the voting period duration
* @return Duration of the voting period
*/
function votingPeriod() public view override(Governor, GovernorSettings) returns (uint256) {
return super.votingPeriod();
}

/// @notice Returns the quorum for a specific block number
/// @param blockNumber The block number to check the quorum for
/// @return The number of votes required for a quorum
/**
* @notice Returns the quorum requirement
* @param blockNumber Block number to check quorum for
* @return Required number of votes for quorum
*/
function quorum(
uint256 blockNumber
) public view override(Governor, GovernorVotesQuorumFraction) returns (uint256) {
return super.quorum(blockNumber);
}

/// @notice Returns the proposal threshold
/// @return The minimum number of votes required to create a proposal
/**
* @notice Returns the proposal threshold
* @return Minimum votes needed to create a proposal
*/
function proposalThreshold()
public
view
Expand All @@ -72,12 +169,4 @@ contract Gov is
{
return super.proposalThreshold();
}

/// @notice Replaces the CID of the manifesto
/// @dev Must include the DAO mission statement
/// @param cid The CID of the new manifesto
function setManifesto(string memory cid) public onlyGovernance {
manifesto = cid;
emit ManifestoUpdated(cid);
}
}
Loading

0 comments on commit 1a26001

Please sign in to comment.