Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add delegation proofs #156

Merged
merged 6 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
# Signer Private Key (signer[0])
SIGNER_PRIVATE_KEY="88888"

# Optimism Mainnet
OPTIMISM_MAINNET_RPC_ENDPOINT_URL="https://mainnet.optimism.io"
OPTIMISM_MAINNET_PRIVATE_KEY="88888"
OP_ETHERSCAN_API_KEY="88888"

# Base Mainnet
BASE_MAINNET_RPC_ENDPOINT_URL="https://mainnet.base.org"
BASE_MAINNET_PRIVATE_KEY="88888"
BASE_ETHERSCAN_API_KEY="88888"

# Sepolia
SEPOLIA_RPC_ENDPOINT_URL="https://ethereum-sepolia.publicnode.com"
SEPOLIA_PRIVATE_KEY="88888"
SEPOLIA_RPC_ENDPOINT_URL="https://sepolia.infura.io/v3/88888"
ETHERSCAN_API_KEY="88888"

# OP Sepolia
OP_SEPOLIA_RPC_ENDPOINT_URL="https://sepolia.optimism.io"
OP_SEPOLIA_PRIVATE_KEY="88888"

# Base Sepolia
BASE_SEPOLIA_RPC_ENDPOINT_URL="https://sepolia.base.org"
BASE_SEPOLIA_PRIVATE_KEY="88888"

# Addresses used when running scripts for testing cross-chain scenarios
ALICE="88888" # Alice // 0xD8a394e7d7894bDF2C57139fF17e5CBAa29Dd977
JUNGLE="88888" # Jungle Fever // 0xBDC0E420aB9ba144213588A95fa1E5e63CEFf1bE
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ Then you can add your DAO in [Tally](https://www.tally.xyz/) and/or spin up your

### Crosschain

Make sure the contracts are deployed from the same account.
Make sure that the deployer wallet address is funded on each notwork you want to deploy to:

```
pnpm bal
```

Then, you can go ahead and deploy:

```bash
pnpm crosschain:sepolia
Expand All @@ -61,7 +67,7 @@ pnpm crosschain:op-sepolia

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

Then you can follow these steps to verify that proofs of `safeMint`, `govBurn`, `setMetadata`, `setManifesto`, `setVotingDelay`, and `delegate` can be generated on source chain and claimed on foreign chain:
Then you can follow these steps to verify that proofs can be generated on home chain and claimed on foreign chain:

```bash
npx hardhat run scripts/propose.ts --network sepolia
Expand All @@ -81,6 +87,9 @@ npx hardhat run scripts/claim-manifesto-update.ts --network op-sepolia
npx hardhat run scripts/gov-voting-delay.ts --network sepolia
npx hardhat run scripts/verify-voting-delay-proof.ts --network sepolia
npx hardhat run scripts/claim-voting-delay.ts --network op-sepolia

npx hardhat run scripts/verify-delegation-proof.ts --network sepolia
npx hardhat run scripts/claim-delegation.ts --network op-sepolia
```

## Security
Expand Down
2 changes: 2 additions & 0 deletions contracts/NFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ contract NFT is
) ERC721(_name, _symbol) Ownable(initialOwner) EIP712(_name, "1") {
for (uint i; i < _firstMembers.length; i++) {
safeMint(_firstMembers[i], _uri);
_delegate(_firstMembers[i], _firstMembers[i]);
}
}

Expand All @@ -63,6 +64,7 @@ contract NFT is
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
_delegate(to, to);
}

/// @notice Updates the NFT ownership
Expand Down
102 changes: 100 additions & 2 deletions contracts/variants/crosschain/NFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import "./ProofHandler.sol";
* @title Cross-chain Membership NFT Contract
* @author Web3 Hackers Collective
* @notice Non-transferable NFT implementation for DAO membership with cross-chain capabilities
* @dev Extends OpenZeppelin's NFT standards with cross-chain operation support
* @dev Extends OpenZeppelin's NFT standards with cross-chain operation support and delegation
* @custom:security-contact [email protected]
*/
contract NFT is
Expand All @@ -41,7 +41,8 @@ contract NFT is
enum OperationType {
MINT,
BURN,
SET_METADATA
SET_METADATA,
DELEGATE
}

/// @notice Emitted when a membership is claimed
Expand All @@ -62,6 +63,12 @@ contract NFT is
/// @param nonce Operation sequence number
event MetadataUpdated(uint256 indexed tokenId, string newUri, uint256 nonce);

/// @notice Emitted when delegation is synchronized across chains
/// @param delegator The address delegating their voting power
/// @param delegatee The address receiving the delegation
/// @param nonce Operation sequence number
event DelegationSynced(address indexed delegator, address indexed delegatee, uint256 nonce);

/// @notice Restricts functions to home chain
modifier onlyHomeChain() {
require(block.chainid == home, "Operation only allowed on home chain");
Expand All @@ -88,6 +95,7 @@ contract NFT is
home = _home;
for (uint i; i < _firstMembers.length; i++) {
_govMint(_firstMembers[i], _uri);
_delegate(_firstMembers[i], _firstMembers[i]);
}
}

Expand Down Expand Up @@ -127,8 +135,20 @@ contract NFT is
emit MetadataUpdated(tokenId, uri, nonce);
}

/**
* @notice Delegates voting power to another address on home chain
* @dev Overrides ERC721Votes delegate function to add cross-chain functionality
* @param delegatee Address to delegate voting power to
*/
function delegate(address delegatee) public virtual override onlyHomeChain {
uint256 nonce = _proofStorage.incrementNonce(uint8(OperationType.DELEGATE));
_delegate(_msgSender(), delegatee);
emit DelegationSynced(_msgSender(), delegatee, nonce);
}

/**
* @notice Generates proof for NFT operations
* @dev Only callable on home chain
* @param operationType Type of operation
* @param params Operation parameters
* @return Encoded proof data
Expand All @@ -142,6 +162,31 @@ contract NFT is
return ProofHandler.generateProof(address(this), operationType, params, nextNonce);
}

/**
* @notice Generates proof for delegation
* @dev Only callable on home chain
* @param delegator Address delegating voting power
* @param delegatee Address receiving delegation
* @return Encoded proof data
*/
function generateDelegationProof(
address delegator,
address delegatee
) external view returns (bytes memory) {
require(block.chainid == home, "Proofs only generated on home chain");
uint256 nextNonce = _proofStorage.getNextNonce(uint8(OperationType.DELEGATE));
bytes memory params = abi.encode(delegator, delegatee);
return
ProofHandler.generateProof(
address(this),
uint8(OperationType.DELEGATE),
params,
nextNonce
);
}

// Claim operations

/**
* @notice Claims an NFT operation on a foreign chain
* @param proof Proof generated by home chain
Expand All @@ -163,9 +208,28 @@ contract NFT is
(uint256 tokenId, string memory uri) = abi.decode(params, (uint256, string));
_setTokenURI(tokenId, uri);
emit MetadataUpdated(tokenId, uri, nonce);
} else if (operationType == uint8(OperationType.DELEGATE)) {
(address delegator, address delegatee) = abi.decode(params, (address, address));
_delegate(delegator, delegatee);
emit DelegationSynced(delegator, delegatee, nonce);
}
}

/**
* @notice Claims a delegation operation on a foreign chain
* @param proof Proof generated by home chain
*/
function claimDelegation(bytes memory proof) external {
(uint8 operationType, bytes memory params, uint256 nonce) = ProofHandler
.verifyAndClaimProof(proof, address(this), _proofStorage);

require(operationType == uint8(OperationType.DELEGATE), "Invalid operation type");
(address delegator, address delegatee) = abi.decode(params, (address, address));

_delegate(delegator, delegatee);
emit DelegationSynced(delegator, delegatee, nonce);
}

/**
* @notice Internal function for minting without proof verification
* @param to Address to receive token
Expand All @@ -175,10 +239,19 @@ contract NFT is
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
_delegate(to, to);
}

// Required overrides

/**
* @notice Updates token data
* @dev Overrides ERC721 _update to make NFTs non-transferable
* @param to Recipient address
* @param tokenId Token ID
* @param auth Address authorized for transfer
* @return Previous owner address
*/
function _update(
address to,
uint256 tokenId,
Expand All @@ -188,29 +261,54 @@ contract NFT is
return super._update(to, tokenId, auth);
}

/**
* @notice Increments account balance
* @dev Internal override to maintain compatibility
* @param account Account to update
* @param value Amount to increase by
*/
function _increaseBalance(
address account,
uint128 value
) internal override(ERC721, ERC721Enumerable, ERC721Votes) {
super._increaseBalance(account, value);
}

/**
* @notice Gets token URI
* @param tokenId Token ID to query
* @return URI string
*/
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}

/**
* @notice Checks interface support
* @param interfaceId Interface identifier to check
* @return bool True if interface is supported
*/
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721Enumerable, ERC721URIStorage) returns (bool) {
return super.supportsInterface(interfaceId);
}

/**
* @notice Gets current timestamp
* @dev Used for voting snapshots
* @return Current block timestamp
*/
function clock() public view override returns (uint48) {
return uint48(block.timestamp);
}

/**
* @notice Gets clock mode description
* @return String indicating timestamp-based voting
*/
function CLOCK_MODE() public pure override returns (string memory) {
return "mode=timestamp";
}
Expand Down
26 changes: 7 additions & 19 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,15 @@ import * as dotenv from "dotenv"
dotenv.config()

const {
SIGNER_PRIVATE_KEY,
OPTIMISM_MAINNET_RPC_ENDPOINT_URL,
OPTIMISM_MAINNET_PRIVATE_KEY,
OP_ETHERSCAN_API_KEY,
BASE_MAINNET_RPC_ENDPOINT_URL,
BASE_MAINNET_PRIVATE_KEY,
BASE_ETHERSCAN_API_KEY,
SEPOLIA_RPC_ENDPOINT_URL,
SEPOLIA_PRIVATE_KEY,
ETHERSCAN_API_KEY,
OP_SEPOLIA_RPC_ENDPOINT_URL,
OP_SEPOLIA_PRIVATE_KEY,
BASE_SEPOLIA_RPC_ENDPOINT_URL,
BASE_SEPOLIA_PRIVATE_KEY
BASE_SEPOLIA_RPC_ENDPOINT_URL
} = process.env

const config: HardhatUserConfig = {
Expand All @@ -37,43 +33,35 @@ const config: HardhatUserConfig = {
SEPOLIA_RPC_ENDPOINT_URL ||
"https://ethereum-sepolia.publicnode.com",
accounts:
SEPOLIA_PRIVATE_KEY !== undefined ? [SEPOLIA_PRIVATE_KEY] : []
SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : []
},
optimism: {
chainId: 10,
url:
OPTIMISM_MAINNET_RPC_ENDPOINT_URL ||
"https://mainnet.optimism.io",
accounts:
OPTIMISM_MAINNET_PRIVATE_KEY !== undefined
? [OPTIMISM_MAINNET_PRIVATE_KEY]
: []
SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : []
},
base: {
chainId: 8453,
url: BASE_MAINNET_RPC_ENDPOINT_URL || "https://mainnet.base.org",
accounts:
BASE_MAINNET_PRIVATE_KEY !== undefined
? [BASE_MAINNET_PRIVATE_KEY]
: []
SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : []
},
"op-sepolia": {
chainId: 11155420,
url:
OP_SEPOLIA_RPC_ENDPOINT_URL ||
"https://ethereum-sepolia.publicnode.com",
accounts:
OP_SEPOLIA_PRIVATE_KEY !== undefined
? [OP_SEPOLIA_PRIVATE_KEY]
: []
SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : []
},
"base-sepolia": {
chainId: 84532,
url: BASE_SEPOLIA_RPC_ENDPOINT_URL || "https://sepolia.base.org",
accounts:
BASE_SEPOLIA_PRIVATE_KEY !== undefined
? [BASE_SEPOLIA_PRIVATE_KEY]
: []
SIGNER_PRIVATE_KEY !== undefined ? [SIGNER_PRIVATE_KEY] : []
}
},
solidity: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"deploy:base-sepolia": "hardhat deploy --network base-sepolia --reset",
"crosschain:sepolia": "hardhat deploy --network sepolia --tags CrosschainGov --reset",
"crosschain:op-sepolia": "hardhat deploy --network op-sepolia --tags CrosschainGov --reset",
"bal": "npx hardhat run scripts/check-my-balance.ts --network",
"bal": "npx hardhat run scripts/check-my-balance.ts",
"prettier": "prettier --write \"**/*.ts\"",
"prettier-check": "prettier --check \"**/*.ts\""
},
Expand Down
Loading