179 lines
6.2 KiB
Solidity
179 lines
6.2 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
|
|
/**
|
|
* @title DAOGovernance
|
|
* @dev Multi-jurisdictional DAO framework with regional councils and staking.
|
|
*/
|
|
contract DAOGovernance is Ownable, ReentrancyGuard {
|
|
using SafeERC20 for IERC20;
|
|
|
|
IERC20 public governanceToken;
|
|
|
|
// Staking Parameters
|
|
uint256 public minStakeAmount;
|
|
uint256 public unbondingPeriod = 7 days;
|
|
|
|
struct Staker {
|
|
uint256 amount;
|
|
uint256 unbondingAmount;
|
|
uint256 unbondingCompleteTime;
|
|
uint256 lastStakeTime;
|
|
}
|
|
|
|
mapping(address => Staker) public stakers;
|
|
uint256 public totalStaked;
|
|
|
|
// Proposal Parameters
|
|
enum ProposalState { Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed }
|
|
|
|
struct Proposal {
|
|
uint256 id;
|
|
address proposer;
|
|
string region; // "" for global
|
|
string descriptionHash;
|
|
uint256 forVotes;
|
|
uint256 againstVotes;
|
|
uint256 startTime;
|
|
uint256 endTime;
|
|
bool executed;
|
|
bool canceled;
|
|
mapping(address => bool) hasVoted;
|
|
}
|
|
|
|
uint256 public proposalCount;
|
|
mapping(uint256 => Proposal) public proposals;
|
|
|
|
// Regional Councils
|
|
mapping(string => mapping(address => bool)) public isRegionalCouncilMember;
|
|
mapping(string => address[]) public regionalCouncilMembers;
|
|
|
|
// Events
|
|
event Staked(address indexed user, uint256 amount);
|
|
event Unstaked(address indexed user, uint256 amount);
|
|
event ProposalCreated(uint256 indexed id, address proposer, string region, string descriptionHash);
|
|
event VoteCast(address indexed voter, uint256 indexed proposalId, bool support, uint256 weight);
|
|
event ProposalExecuted(uint256 indexed id);
|
|
|
|
constructor(address _governanceToken, uint256 _minStakeAmount) {
|
|
governanceToken = IERC20(_governanceToken);
|
|
minStakeAmount = _minStakeAmount;
|
|
}
|
|
|
|
// --- Staking ---
|
|
|
|
function stake(uint256 _amount) external nonReentrant {
|
|
require(_amount > 0, "Cannot stake 0");
|
|
|
|
governanceToken.safeTransferFrom(msg.sender, address(this), _amount);
|
|
|
|
stakers[msg.sender].amount += _amount;
|
|
stakers[msg.sender].lastStakeTime = block.timestamp;
|
|
totalStaked += _amount;
|
|
|
|
require(stakers[msg.sender].amount >= minStakeAmount, "Below min stake");
|
|
|
|
emit Staked(msg.sender, _amount);
|
|
}
|
|
|
|
function initiateUnstake(uint256 _amount) external nonReentrant {
|
|
Staker storage staker = stakers[msg.sender];
|
|
require(_amount > 0 && staker.amount >= _amount, "Invalid amount");
|
|
require(staker.unbondingAmount == 0, "Unbonding already in progress");
|
|
|
|
staker.amount -= _amount;
|
|
staker.unbondingAmount = _amount;
|
|
staker.unbondingCompleteTime = block.timestamp + unbondingPeriod;
|
|
totalStaked -= _amount;
|
|
}
|
|
|
|
function completeUnstake() external nonReentrant {
|
|
Staker storage staker = stakers[msg.sender];
|
|
require(staker.unbondingAmount > 0, "Nothing to unstake");
|
|
require(block.timestamp >= staker.unbondingCompleteTime, "Unbonding not complete");
|
|
|
|
uint256 amount = staker.unbondingAmount;
|
|
staker.unbondingAmount = 0;
|
|
|
|
governanceToken.safeTransfer(msg.sender, amount);
|
|
|
|
emit Unstaked(msg.sender, amount);
|
|
}
|
|
|
|
// --- Proposals & Voting ---
|
|
|
|
function createProposal(string calldata _region, string calldata _descriptionHash, uint256 _votingPeriod) external returns (uint256) {
|
|
require(stakers[msg.sender].amount >= minStakeAmount, "Must be staked to propose");
|
|
|
|
// If regional, must be a council member
|
|
if (bytes(_region).length > 0) {
|
|
require(isRegionalCouncilMember[_region][msg.sender], "Not a council member");
|
|
}
|
|
|
|
proposalCount++;
|
|
Proposal storage p = proposals[proposalCount];
|
|
p.id = proposalCount;
|
|
p.proposer = msg.sender;
|
|
p.region = _region;
|
|
p.descriptionHash = _descriptionHash;
|
|
p.startTime = block.timestamp;
|
|
p.endTime = block.timestamp + _votingPeriod;
|
|
|
|
emit ProposalCreated(p.id, msg.sender, _region, _descriptionHash);
|
|
return p.id;
|
|
}
|
|
|
|
function castVote(uint256 _proposalId, bool _support) external {
|
|
Proposal storage p = proposals[_proposalId];
|
|
require(block.timestamp >= p.startTime && block.timestamp <= p.endTime, "Voting closed");
|
|
require(!p.hasVoted[msg.sender], "Already voted");
|
|
|
|
uint256 weight = stakers[msg.sender].amount;
|
|
require(weight > 0, "No voting weight");
|
|
|
|
// If regional, must be a council member
|
|
if (bytes(p.region).length > 0) {
|
|
require(isRegionalCouncilMember[p.region][msg.sender], "Not a council member");
|
|
weight = 1; // 1 member = 1 vote in council
|
|
}
|
|
|
|
p.hasVoted[msg.sender] = true;
|
|
|
|
if (_support) {
|
|
p.forVotes += weight;
|
|
} else {
|
|
p.againstVotes += weight;
|
|
}
|
|
|
|
emit VoteCast(msg.sender, _proposalId, _support, weight);
|
|
}
|
|
|
|
function executeProposal(uint256 _proposalId) external nonReentrant {
|
|
Proposal storage p = proposals[_proposalId];
|
|
require(block.timestamp > p.endTime, "Voting not ended");
|
|
require(!p.executed && !p.canceled, "Already executed or canceled");
|
|
require(p.forVotes > p.againstVotes, "Proposal defeated");
|
|
|
|
p.executed = true;
|
|
// The actual execution logic (e.g., transferring treasury funds) would happen here
|
|
// Usually involves calling other contracts via target[] and callData[] arrays.
|
|
|
|
emit ProposalExecuted(_proposalId);
|
|
}
|
|
|
|
// --- Admin Functions ---
|
|
|
|
function setRegionalCouncilMember(string calldata _region, address _member, bool _status) external onlyOwner {
|
|
isRegionalCouncilMember[_region][_member] = _status;
|
|
if (_status) {
|
|
regionalCouncilMembers[_region].push(_member);
|
|
}
|
|
// Simplified array management for hackathon/demo purposes
|
|
}
|
|
}
|