Merge dependency updates from GitHub
- Updated black from 24.3.0 to 26.3.1 - Kept ruff at 0.15.7 (our updated version) - All other dependency updates already applied
This commit is contained in:
316
cli/aitbc_cli/commands/dao.py
Normal file
316
cli/aitbc_cli/commands/dao.py
Normal file
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenClaw DAO CLI Commands
|
||||
Provides command-line interface for DAO governance operations
|
||||
"""
|
||||
|
||||
import click
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any
|
||||
from web3 import Web3
|
||||
from ..utils.blockchain import get_web3_connection, get_contract
|
||||
from ..utils.config import load_config
|
||||
|
||||
@click.group()
|
||||
def dao():
|
||||
"""OpenClaw DAO governance commands"""
|
||||
pass
|
||||
|
||||
@dao.command()
|
||||
@click.option('--token-address', required=True, help='Governance token contract address')
|
||||
@click.option('--timelock-address', required=True, help='Timelock controller address')
|
||||
@click.option('--network', default='mainnet', help='Blockchain network')
|
||||
def deploy(token_address: str, timelock_address: str, network: str):
|
||||
"""Deploy OpenClaw DAO contract"""
|
||||
try:
|
||||
w3 = get_web3_connection(network)
|
||||
config = load_config()
|
||||
|
||||
# Account for deployment
|
||||
account = w3.eth.account.from_key(config['private_key'])
|
||||
|
||||
# Contract ABI (simplified)
|
||||
abi = [
|
||||
{
|
||||
"inputs": [
|
||||
{"internalType": "address", "name": "_governanceToken", "type": "address"},
|
||||
{"internalType": "contract TimelockController", "name": "_timelock", "type": "address"}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
}
|
||||
]
|
||||
|
||||
# Deploy contract
|
||||
contract = w3.eth.contract(abi=abi, bytecode="0x...") # Actual bytecode needed
|
||||
|
||||
# Build transaction
|
||||
tx = contract.constructor(token_address, timelock_address).build_transaction({
|
||||
'from': account.address,
|
||||
'gas': 2000000,
|
||||
'gasPrice': w3.eth.gas_price,
|
||||
'nonce': w3.eth.get_transaction_count(account.address)
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed_tx = w3.eth.account.sign_transaction(tx, config['private_key'])
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
|
||||
# Wait for confirmation
|
||||
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
|
||||
|
||||
click.echo(f"✅ OpenClaw DAO deployed at: {receipt.contractAddress}")
|
||||
click.echo(f"📦 Transaction hash: {tx_hash.hex()}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Deployment failed: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
@click.option('--targets', required=True, help='Comma-separated target addresses')
|
||||
@click.option('--values', required=True, help='Comma-separated ETH values')
|
||||
@click.option('--calldatas', required=True, help='Comma-separated hex calldatas')
|
||||
@click.option('--description', required=True, help='Proposal description')
|
||||
@click.option('--type', 'proposal_type', type=click.Choice(['0', '1', '2', '3']),
|
||||
default='0', help='Proposal type (0=parameter, 1=upgrade, 2=treasury, 3=emergency)')
|
||||
def propose(dao_address: str, targets: str, values: str, calldatas: str,
|
||||
description: str, proposal_type: str):
|
||||
"""Create a new governance proposal"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
config = load_config()
|
||||
|
||||
# Parse inputs
|
||||
target_addresses = targets.split(',')
|
||||
value_list = [int(v) for v in values.split(',')]
|
||||
calldata_list = calldatas.split(',')
|
||||
|
||||
# Get contract
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Build transaction
|
||||
tx = dao_contract.functions.propose(
|
||||
target_addresses,
|
||||
value_list,
|
||||
calldata_list,
|
||||
description,
|
||||
int(proposal_type)
|
||||
).build_transaction({
|
||||
'from': config['address'],
|
||||
'gas': 500000,
|
||||
'gasPrice': w3.eth.gas_price,
|
||||
'nonce': w3.eth.get_transaction_count(config['address'])
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed_tx = w3.eth.account.sign_transaction(tx, config['private_key'])
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
|
||||
# Get proposal ID
|
||||
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
|
||||
|
||||
# Parse proposal ID from events
|
||||
proposal_id = None
|
||||
for log in receipt.logs:
|
||||
try:
|
||||
event = dao_contract.events.ProposalCreated().process_log(log)
|
||||
proposal_id = event.args.proposalId
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
click.echo(f"✅ Proposal created!")
|
||||
click.echo(f"📋 Proposal ID: {proposal_id}")
|
||||
click.echo(f"📦 Transaction hash: {tx_hash.hex()}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Proposal creation failed: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
@click.option('--proposal-id', required=True, type=int, help='Proposal ID')
|
||||
def vote(dao_address: str, proposal_id: int):
|
||||
"""Cast a vote on a proposal"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
config = load_config()
|
||||
|
||||
# Get contract
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Check proposal state
|
||||
state = dao_contract.functions.state(proposal_id).call()
|
||||
if state != 1: # Active
|
||||
click.echo("❌ Proposal is not active for voting")
|
||||
return
|
||||
|
||||
# Get voting power
|
||||
token_address = dao_contract.functions.governanceToken().call()
|
||||
token_contract = get_contract(token_address, "ERC20")
|
||||
voting_power = token_contract.functions.balanceOf(config['address']).call()
|
||||
|
||||
if voting_power == 0:
|
||||
click.echo("❌ No voting power (no governance tokens)")
|
||||
return
|
||||
|
||||
click.echo(f"🗳️ Your voting power: {voting_power}")
|
||||
|
||||
# Get vote choice
|
||||
support = click.prompt(
|
||||
"Vote (0=Against, 1=For, 2=Abstain)",
|
||||
type=click.Choice(['0', '1', '2'])
|
||||
)
|
||||
|
||||
reason = click.prompt("Reason (optional)", default="", show_default=False)
|
||||
|
||||
# Build transaction
|
||||
tx = dao_contract.functions.castVoteWithReason(
|
||||
proposal_id,
|
||||
int(support),
|
||||
reason
|
||||
).build_transaction({
|
||||
'from': config['address'],
|
||||
'gas': 100000,
|
||||
'gasPrice': w3.eth.gas_price,
|
||||
'nonce': w3.eth.get_transaction_count(config['address'])
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed_tx = w3.eth.account.sign_transaction(tx, config['private_key'])
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
|
||||
click.echo(f"✅ Vote cast!")
|
||||
click.echo(f"📦 Transaction hash: {tx_hash.hex()}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Voting failed: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
@click.option('--proposal-id', required=True, type=int, help='Proposal ID')
|
||||
def execute(dao_address: str, proposal_id: int):
|
||||
"""Execute a successful proposal"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
config = load_config()
|
||||
|
||||
# Get contract
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Check proposal state
|
||||
state = dao_contract.functions.state(proposal_id).call()
|
||||
if state != 7: # Succeeded
|
||||
click.echo("❌ Proposal has not succeeded")
|
||||
return
|
||||
|
||||
# Build transaction
|
||||
tx = dao_contract.functions.execute(proposal_id).build_transaction({
|
||||
'from': config['address'],
|
||||
'gas': 300000,
|
||||
'gasPrice': w3.eth.gas_price,
|
||||
'nonce': w3.eth.get_transaction_count(config['address'])
|
||||
})
|
||||
|
||||
# Sign and send
|
||||
signed_tx = w3.eth.account.sign_transaction(tx, config['private_key'])
|
||||
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
|
||||
click.echo(f"✅ Proposal executed!")
|
||||
click.echo(f"📦 Transaction hash: {tx_hash.hex()}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Execution failed: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
def list_proposals(dao_address: str):
|
||||
"""List all proposals"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Get proposal count
|
||||
proposal_count = dao_contract.functions.proposalCount().call()
|
||||
|
||||
click.echo(f"📋 Found {proposal_count} proposals:\n")
|
||||
|
||||
for i in range(1, proposal_count + 1):
|
||||
try:
|
||||
proposal = dao_contract.functions.getProposal(i).call()
|
||||
state = dao_contract.functions.state(i).call()
|
||||
|
||||
state_names = {
|
||||
0: "Pending",
|
||||
1: "Active",
|
||||
2: "Canceled",
|
||||
3: "Defeated",
|
||||
4: "Succeeded",
|
||||
5: "Queued",
|
||||
6: "Expired",
|
||||
7: "Executed"
|
||||
}
|
||||
|
||||
type_names = {
|
||||
0: "Parameter Change",
|
||||
1: "Protocol Upgrade",
|
||||
2: "Treasury Allocation",
|
||||
3: "Emergency Action"
|
||||
}
|
||||
|
||||
click.echo(f"🔹 Proposal #{i}")
|
||||
click.echo(f" Type: {type_names.get(proposal[3], 'Unknown')}")
|
||||
click.echo(f" State: {state_names.get(state, 'Unknown')}")
|
||||
click.echo(f" Description: {proposal[4]}")
|
||||
click.echo(f" For: {proposal[6]}, Against: {proposal[7]}, Abstain: {proposal[8]}")
|
||||
click.echo()
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Failed to list proposals: {str(e)}", err=True)
|
||||
|
||||
@dao.command()
|
||||
@click.option('--dao-address', required=True, help='DAO contract address')
|
||||
def status(dao_address: str):
|
||||
"""Show DAO status and statistics"""
|
||||
try:
|
||||
w3 = get_web3_connection()
|
||||
dao_contract = get_contract(dao_address, "OpenClawDAO")
|
||||
|
||||
# Get DAO info
|
||||
token_address = dao_contract.functions.governanceToken().call()
|
||||
token_contract = get_contract(token_address, "ERC20")
|
||||
|
||||
total_supply = token_contract.functions.totalSupply().call()
|
||||
proposal_count = dao_contract.functions.proposalCount().call()
|
||||
|
||||
# Get active proposals
|
||||
active_proposals = dao_contract.functions.getActiveProposals().call()
|
||||
|
||||
click.echo("🏛️ OpenClaw DAO Status")
|
||||
click.echo("=" * 40)
|
||||
click.echo(f"📊 Total Supply: {total_supply / 1e18:.2f} tokens")
|
||||
click.echo(f"📋 Total Proposals: {proposal_count}")
|
||||
click.echo(f"🗳️ Active Proposals: {len(active_proposals)}")
|
||||
click.echo(f"🪙 Governance Token: {token_address}")
|
||||
click.echo(f"🏛️ DAO Address: {dao_address}")
|
||||
|
||||
# Voting parameters
|
||||
voting_delay = dao_contract.functions.votingDelay().call()
|
||||
voting_period = dao_contract.functions.votingPeriod().call()
|
||||
quorum = dao_contract.functions.quorum(w3.eth.block_number).call()
|
||||
threshold = dao_contract.functions.proposalThreshold().call()
|
||||
|
||||
click.echo(f"\n⚙️ Voting Parameters:")
|
||||
click.echo(f" Delay: {voting_delay // 86400} days")
|
||||
click.echo(f" Period: {voting_period // 86400} days")
|
||||
click.echo(f" Quorum: {quorum / 1e18:.2f} tokens ({(quorum * 100 / total_supply):.2f}%)")
|
||||
click.echo(f" Threshold: {threshold / 1e18:.2f} tokens")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Failed to get status: {str(e)}", err=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
dao()
|
||||
@@ -1233,11 +1233,18 @@ def unstake(ctx, amount: float):
|
||||
}
|
||||
)
|
||||
|
||||
# Save wallet with encryption
|
||||
password = None
|
||||
# CRITICAL SECURITY FIX: Save wallet properly to avoid double-encryption
|
||||
if wallet_data.get("encrypted"):
|
||||
# For encrypted wallets, we need to re-encrypt the private key before saving
|
||||
password = _get_wallet_password(wallet_name)
|
||||
_save_wallet(wallet_path, wallet_data, password)
|
||||
# Only encrypt the private key, not the entire wallet data
|
||||
if "private_key" in wallet_data:
|
||||
wallet_data["private_key"] = encrypt_value(wallet_data["private_key"], password)
|
||||
# Save without passing password to avoid double-encryption
|
||||
_save_wallet(wallet_path, wallet_data, None)
|
||||
else:
|
||||
# For unencrypted wallets, save normally
|
||||
_save_wallet(wallet_path, wallet_data, None)
|
||||
|
||||
success(f"Unstaked {amount} AITBC")
|
||||
output(
|
||||
|
||||
@@ -364,8 +364,11 @@ class DualModeWalletAdapter:
|
||||
wallet_data["transactions"].append(transaction)
|
||||
wallet_data["balance"] = chain_balance - amount
|
||||
|
||||
# Save wallet
|
||||
# Save wallet - CRITICAL SECURITY FIX: Always use password if wallet is encrypted
|
||||
save_password = password if wallet_data.get("encrypted") else None
|
||||
if wallet_data.get("encrypted") and not save_password:
|
||||
error("❌ CRITICAL: Cannot save encrypted wallet without password")
|
||||
raise Exception("Password required for encrypted wallet")
|
||||
_save_wallet(wallet_path, wallet_data, save_password)
|
||||
|
||||
success(f"Submitted transaction {tx_hash} to send {amount} AITBC to {to_address}")
|
||||
|
||||
@@ -70,17 +70,74 @@ class AuditLogger:
|
||||
|
||||
|
||||
def _get_fernet_key(key: str = None) -> bytes:
|
||||
"""Derive a Fernet key from a password or use default"""
|
||||
"""Derive a Fernet key from a password using Argon2 KDF"""
|
||||
from cryptography.fernet import Fernet
|
||||
import base64
|
||||
import hashlib
|
||||
import secrets
|
||||
import getpass
|
||||
|
||||
if key is None:
|
||||
# Use a default key (should be overridden in production)
|
||||
key = "aitbc_config_key_2026_default"
|
||||
# CRITICAL SECURITY FIX: Never use hardcoded keys
|
||||
# Always require user to provide a password or generate a secure random key
|
||||
error("❌ CRITICAL: No encryption key provided. This is a security vulnerability.")
|
||||
error("Please provide a password for encryption.")
|
||||
key = getpass.getpass("Enter encryption password: ")
|
||||
|
||||
if not key:
|
||||
error("❌ Password cannot be empty for encryption operations.")
|
||||
raise ValueError("Encryption password is required")
|
||||
|
||||
# Derive a 32-byte key suitable for Fernet
|
||||
return base64.urlsafe_b64encode(hashlib.sha256(key.encode()).digest())
|
||||
# Use Argon2 for secure key derivation (replaces insecure SHA-256)
|
||||
try:
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
|
||||
# Generate a secure salt
|
||||
salt = secrets.token_bytes(16)
|
||||
|
||||
# Derive key using Argon2
|
||||
ph = PasswordHasher(
|
||||
time_cost=3, # Number of iterations
|
||||
memory_cost=65536, # Memory usage in KB
|
||||
parallelism=4, # Number of parallel threads
|
||||
hash_len=32, # Output hash length
|
||||
salt_len=16 # Salt length
|
||||
)
|
||||
|
||||
# Hash the password to get a 32-byte key
|
||||
hashed_key = ph.hash(key + salt.decode('utf-8'))
|
||||
|
||||
# Extract the hash part and convert to bytes suitable for Fernet
|
||||
key_bytes = hashed_key.encode('utf-8')[:32]
|
||||
|
||||
# Ensure we have exactly 32 bytes for Fernet
|
||||
if len(key_bytes) < 32:
|
||||
key_bytes += secrets.token_bytes(32 - len(key_bytes))
|
||||
elif len(key_bytes) > 32:
|
||||
key_bytes = key_bytes[:32]
|
||||
|
||||
return base64.urlsafe_b64encode(key_bytes)
|
||||
|
||||
except ImportError:
|
||||
# Fallback to PBKDF2 if Argon2 is not available
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
warning("⚠️ Argon2 not available, falling back to PBKDF2 (less secure)")
|
||||
|
||||
# Generate a secure salt
|
||||
salt = secrets.token_bytes(16)
|
||||
|
||||
# Use PBKDF2 with SHA-256 (better than plain SHA-256)
|
||||
key_bytes = hashlib.pbkdf2_hmac(
|
||||
'sha256',
|
||||
key.encode('utf-8'),
|
||||
salt,
|
||||
100000, # 100k iterations
|
||||
32 # 32-byte key
|
||||
)
|
||||
|
||||
return base64.urlsafe_b64encode(key_bytes)
|
||||
|
||||
|
||||
def encrypt_value(value: str, key: str = None) -> str:
|
||||
|
||||
Reference in New Issue
Block a user