neo-devpack-solidity 0.22.0

Production-focused Solidity-to-NeoVM compilation system
Documentation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title SimpleDAO — On-chain Governance
 * @notice Neo N3 adaptation of Compound Governor / MolochDAO.
 *
 * Members stake GAS for voting power. Proposals require quorum (20%)
 * and pass after a voting period + timelock before execution.
 *
 * Compiler constraints respected:
 *   - No {value: ...} — uses NativeCalls.gasTransfer()
 *   - No receive()/fallback() — uses onNEP17Payment()
 *   - Uses Syscalls.contractCall() for Neo-native cross-contract execution
 *   - Import devpack via -I devpack
 */
contract SimpleDAO {
    address private constant GAS_TOKEN = NativeCalls.GAS_CONTRACT;

    /// @notice Quorum: 20% of total staked (scaled by 100).
    uint256 public constant QUORUM_PERCENT = 20;

    /// @notice Voting period in blocks.
    uint256 public votingPeriod;

    /// @notice Timelock delay in blocks after voting ends.
    uint256 public timelockDelay;

    enum ProposalState { Pending, Active, Succeeded, Defeated, Executed, Cancelled }

    struct Proposal {
        uint256 id;
        address proposer;
        string description;
        address target;
        string methodName;
        bytes args;
        uint256 startBlock;
        uint256 endBlock;
        uint256 executeAfter;
        uint256 forVotes;
        uint256 againstVotes;
        bool executed;
        bool cancelled;
    }

    mapping(address => uint256) public stakes;
    uint256 public totalStaked;

    uint256 public proposalCount;
    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    event Staked(address indexed member, uint256 amount);
    event Unstaked(address indexed member, uint256 amount);
    event ProposalCreated(uint256 indexed proposalId, address indexed proposer, string description);
    event VoteCast(uint256 indexed proposalId, address indexed voter, bool support, uint256 weight);
    event ProposalExecuted(uint256 indexed proposalId);
    event ProposalCancelled(uint256 indexed proposalId);

    /// @notice Default voting period (100 blocks) and timelock delay (10 blocks).
    constructor() {
        votingPeriod = 100;
        timelockDelay = 10;
    }

    /// @notice NEP-17 callback — receives GAS stakes.
    function onNEP17Payment(address from, uint256 amount, Any calldata /*data*/) external {
        address caller = Syscalls.getCallingScriptHash();
        require(caller == GAS_TOKEN, "DAO: only GAS accepted");
        require(amount > 0, "DAO: zero stake");

        stakes[from] += amount;
        totalStaked += amount;

        emit Staked(from, amount);
    }

    /// @notice Unstake GAS and withdraw.
    function unstake(uint256 amount) external {
        require(stakes[msg.sender] >= amount, "DAO: insufficient stake");

        stakes[msg.sender] -= amount;
        totalStaked -= amount;

        bool ok = NativeCalls.gasTransfer(address(this), msg.sender, amount, "");
        require(ok, "DAO: transfer failed");

        emit Unstaked(msg.sender, amount);
    }

    /// @notice Create a new proposal.
    /// @param description Human-readable description.
    /// @param target Contract to call on execution.
    /// @param methodName Method name to invoke (e.g. "setValue").
    /// @param args ABI-encoded arguments for the method.
    function propose(
        string memory description,
        address target,
        string memory methodName,
        bytes memory args
    ) external returns (uint256 proposalId) {
        require(stakes[msg.sender] > 0, "DAO: must be staker");
        require(target != address(0), "DAO: zero target");

        proposalId = proposalCount;
        proposalCount += 1;

        Proposal storage p = proposals[proposalId];
        p.id = proposalId;
        p.proposer = msg.sender;
        p.description = description;
        p.target = target;
        p.methodName = methodName;
        p.args = args;
        p.startBlock = block.number;
        p.endBlock = block.number + votingPeriod;
        p.executeAfter = block.number + votingPeriod + timelockDelay;

        emit ProposalCreated(proposalId, msg.sender, description);
    }

    /// @notice Cast a vote on a proposal.
    function vote(uint256 proposalId, bool support) external {
        require(proposalId < proposalCount, "DAO: invalid proposal");
        Proposal storage p = proposals[proposalId];

        require(block.number >= p.startBlock, "DAO: voting not started");
        require(block.number <= p.endBlock, "DAO: voting ended");
        require(!hasVoted[proposalId][msg.sender], "DAO: already voted");
        require(stakes[msg.sender] > 0, "DAO: no voting power");

        uint256 weight = stakes[msg.sender];
        hasVoted[proposalId][msg.sender] = true;

        if (support) {
            p.forVotes += weight;
        } else {
            p.againstVotes += weight;
        }

        emit VoteCast(proposalId, msg.sender, support, weight);
    }

    /// @notice Execute a succeeded proposal after timelock.
    function execute(uint256 proposalId) external {
        require(proposalId < proposalCount, "DAO: invalid proposal");
        Proposal storage p = proposals[proposalId];

        require(!p.executed, "DAO: already executed");
        require(!p.cancelled, "DAO: cancelled");
        require(block.number > p.endBlock, "DAO: voting not ended");
        require(block.number >= p.executeAfter, "DAO: timelock active");

        // Check quorum: forVotes >= 20% of totalStaked.
        uint256 quorum = (totalStaked * QUORUM_PERCENT) / 100;
        require(p.forVotes >= quorum, "DAO: quorum not reached");
        require(p.forVotes > p.againstVotes, "DAO: proposal defeated");

        p.executed = true;

        // Neo-native cross-contract call via Syscalls.
        Syscalls.contractCall(p.target, p.methodName, p.args);

        emit ProposalExecuted(proposalId);
    }

    /// @notice Cancel a proposal (only proposer).
    function cancel(uint256 proposalId) external {
        require(proposalId < proposalCount, "DAO: invalid proposal");
        Proposal storage p = proposals[proposalId];

        require(msg.sender == p.proposer, "DAO: not proposer");
        require(!p.executed, "DAO: already executed");

        p.cancelled = true;
        emit ProposalCancelled(proposalId);
    }

    /// @notice Get the current state of a proposal.
    function getProposalState(uint256 proposalId) external view returns (uint256) {
        require(proposalId < proposalCount, "DAO: invalid proposal");
        Proposal storage p = proposals[proposalId];

        if (p.cancelled) return uint256(ProposalState.Cancelled);
        if (p.executed) return uint256(ProposalState.Executed);
        if (block.number <= p.endBlock) return uint256(ProposalState.Active);

        uint256 quorum = (totalStaked * QUORUM_PERCENT) / 100;
        if (p.forVotes >= quorum && p.forVotes > p.againstVotes) {
            return uint256(ProposalState.Succeeded);
        }
        return uint256(ProposalState.Defeated);
    }
}