ShadowVote

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}

abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

/**
 * @dev Library for reading and writing primitive types to specific storage slots.
 *
 * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.
 * This library helps with reading and writing to such slots without the need for inline assembly.
 *
 * The functions in this library return Slot structs that contain a `value` member that can be used to read or write.
 *
 */
library StorageSlot {
    struct AddressSlot {
        address value;
    }

    struct BooleanSlot {
        bool value;
    }

    struct Bytes32Slot {
        bytes32 value;
    }

    struct Uint256Slot {
        uint256 value;
    }

    struct Int256Slot {
        int256 value;
    }

    struct StringSlot {
        string value;
    }

    struct BytesSlot {
        bytes value;
    }

    /**
     * @dev Returns an `AddressSlot` with member `value` located at `slot`.
     */
    function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `BooleanSlot` with member `value` located at `slot`.
     */
    function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `Bytes32Slot` with member `value` located at `slot`.
     */
    function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `Uint256Slot` with member `value` located at `slot`.
     */
    function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `Int256Slot` with member `value` located at `slot`.
     */
    function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `StringSlot` with member `value` located at `slot`.
     */
    function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns an `StringSlot` representation of the string storage pointer `store`.
     */
    function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {
        assembly ("memory-safe") {
            r.slot := store.slot
        }
    }

    /**
     * @dev Returns a `BytesSlot` with member `value` located at `slot`.
     */
    function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.
     */
    function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {
        assembly ("memory-safe") {
            r.slot := store.slot
        }
    }
}

abstract contract ReentrancyGuard {
    using StorageSlot for bytes32;

    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant REENTRANCY_GUARD_STORAGE =
        0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;

    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    /**
     * @dev Unauthorized reentrant call.
     */
    error ReentrancyGuardReentrantCall();

    constructor() {
        _reentrancyGuardStorageSlot().getUint256Slot().value = NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    /**
     * @dev A `view` only version of {nonReentrant}. Use to block view functions
     * from being called, preventing reading from inconsistent contract state.
     *
     * CAUTION: This is a "view" modifier and does not change the reentrancy
     * status. Use it only on view functions. For payable or non-payable functions,
     * use the standard {nonReentrant} modifier instead.
     */
    modifier nonReentrantView() {
        _nonReentrantBeforeView();
        _;
    }

    function _nonReentrantBeforeView() private view {
        if (_reentrancyGuardEntered()) {
            revert ReentrancyGuardReentrantCall();
        }
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        _nonReentrantBeforeView();

        // Any calls to nonReentrant after this point will fail
        _reentrancyGuardStorageSlot().getUint256Slot().value = ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _reentrancyGuardStorageSlot().getUint256Slot().value = NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _reentrancyGuardStorageSlot().getUint256Slot().value == ENTERED;
    }

    function _reentrancyGuardStorageSlot() internal pure virtual returns (bytes32) {
        return REENTRANCY_GUARD_STORAGE;
    }
}

/**
 * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
 *
 * These functions can be used to verify that a message was signed by the holder
 * of the private keys of a given address.
 */
library ECDSA {
    enum RecoverError {
        NoError,
        InvalidSignature,
        InvalidSignatureLength,
        InvalidSignatureS
    }

    /**
     * @dev The signature derives the `address(0)`.
     */
    error ECDSAInvalidSignature();

    /**
     * @dev The signature has an invalid length.
     */
    error ECDSAInvalidSignatureLength(uint256 length);

    /**
     * @dev The signature has an S value that is in the upper half order.
     */
    error ECDSAInvalidSignatureS(bytes32 s);

    /**
     * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not
     * return address(0) without also returning an error description. Errors are documented using an enum (error type)
     * and a bytes32 providing additional information about the error.
     *
     * If no error is returned, then the address can be used for verification purposes.
     *
     * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
     * this function rejects them by requiring the `s` value to be in the lower
     * half order, and the `v` value to be either 27 or 28.
     *
     * NOTE: This function only supports 65-byte signatures. ERC-2098 short signatures are rejected. This restriction
     * is DEPRECATED and will be removed in v6.0. Developers SHOULD NOT use signatures as unique identifiers; use hash
     * invalidation or nonces for replay protection.
     *
     * IMPORTANT: `hash` _must_ be the result of a hash operation for the
     * verification to be secure: it is possible to craft signatures that
     * recover to arbitrary addresses for non-hashed data. A safe way to ensure
     * this is by receiving a hash of the original message (which may otherwise
     * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
     *
     * Documentation for signature generation:
     *
     * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
     * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
     */
    function tryRecover(
        bytes32 hash,
        bytes memory signature
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        if (signature.length == 65) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            // ecrecover takes the signature parameters, and the only way to get them
            // currently is to use assembly.
            assembly ("memory-safe") {
                r := mload(add(signature, 0x20))
                s := mload(add(signature, 0x40))
                v := byte(0, mload(add(signature, 0x60)))
            }
            return tryRecover(hash, v, r, s);
        } else {
            return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));
        }
    }

    /**
     * @dev Variant of {tryRecover} that takes a signature in calldata
     */
    function tryRecoverCalldata(
        bytes32 hash,
        bytes calldata signature
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        if (signature.length == 65) {
            bytes32 r;
            bytes32 s;
            uint8 v;
            // ecrecover takes the signature parameters, calldata slices would work here, but are
            // significantly more expensive (length check) than using calldataload in assembly.
            assembly ("memory-safe") {
                r := calldataload(signature.offset)
                s := calldataload(add(signature.offset, 0x20))
                v := byte(0, calldataload(add(signature.offset, 0x40)))
            }
            return tryRecover(hash, v, r, s);
        } else {
            return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));
        }
    }

    /**
     * @dev Returns the address that signed a hashed message (`hash`) with
     * `signature`. This address can then be used for verification purposes.
     *
     * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
     * this function rejects them by requiring the `s` value to be in the lower
     * half order, and the `v` value to be either 27 or 28.
     *
     * NOTE: This function only supports 65-byte signatures. ERC-2098 short signatures are rejected. This restriction
     * is DEPRECATED and will be removed in v6.0. Developers SHOULD NOT use signatures as unique identifiers; use hash
     * invalidation or nonces for replay protection.
     *
     * IMPORTANT: `hash` _must_ be the result of a hash operation for the
     * verification to be secure: it is possible to craft signatures that
     * recover to arbitrary addresses for non-hashed data. A safe way to ensure
     * this is by receiving a hash of the original message (which may otherwise
     * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
     */
    function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Variant of {recover} that takes a signature in calldata
     */
    function recoverCalldata(bytes32 hash, bytes calldata signature) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecoverCalldata(hash, signature);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
     *
     * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures]
     */
    function tryRecover(
        bytes32 hash,
        bytes32 r,
        bytes32 vs
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        unchecked {
            bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
            // We do not check for an overflow here since the shift operation results in 0 or 1.
            uint8 v = uint8((uint256(vs) >> 255) + 27);
            return tryRecover(hash, v, r, s);
        }
    }

    /**
     * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
     */
    function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Overload of {ECDSA-tryRecover} that receives the `v`,
     * `r` and `s` signature fields separately.
     */
    function tryRecover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
        // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
        // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
        // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
        // signatures from current libraries generate a unique signature with an s-value in the lower half order.
        //
        // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            return (address(0), RecoverError.InvalidSignatureS, s);
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            return (address(0), RecoverError.InvalidSignature, bytes32(0));
        }

        return (signer, RecoverError.NoError, bytes32(0));
    }

    /**
     * @dev Overload of {ECDSA-recover} that receives the `v`,
     * `r` and `s` signature fields separately.
     */
    function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
        (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s);
        _throwError(error, errorArg);
        return recovered;
    }

    /**
     * @dev Parse a signature into its `v`, `r` and `s` components. Supports 65-byte and 64-byte (ERC-2098)
     * formats. Returns (0,0,0) for invalid signatures.
     *
     * For 64-byte signatures, `v` is automatically normalized to 27 or 28.
     * For 65-byte signatures, `v` is returned as-is and MUST already be 27 or 28 for use with ecrecover.
     *
     * Consider validating the result before use, or use {tryRecover}/{recover} which perform full validation.
     */
    function parse(bytes memory signature) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
        assembly ("memory-safe") {
            // Check the signature length
            switch mload(signature)
            // - case 65: r,s,v signature (standard)
            case 65 {
                r := mload(add(signature, 0x20))
                s := mload(add(signature, 0x40))
                v := byte(0, mload(add(signature, 0x60)))
            }
            // - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098)
            case 64 {
                let vs := mload(add(signature, 0x40))
                r := mload(add(signature, 0x20))
                s := and(vs, shr(1, not(0)))
                v := add(shr(255, vs), 27)
            }
            default {
                r := 0
                s := 0
                v := 0
            }
        }
    }

    /**
     * @dev Variant of {parse} that takes a signature in calldata
     */
    function parseCalldata(bytes calldata signature) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
        assembly ("memory-safe") {
            // Check the signature length
            switch signature.length
            // - case 65: r,s,v signature (standard)
            case 65 {
                r := calldataload(signature.offset)
                s := calldataload(add(signature.offset, 0x20))
                v := byte(0, calldataload(add(signature.offset, 0x40)))
            }
            // - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098)
            case 64 {
                let vs := calldataload(add(signature.offset, 0x20))
                r := calldataload(signature.offset)
                s := and(vs, shr(1, not(0)))
                v := add(shr(255, vs), 27)
            }
            default {
                r := 0
                s := 0
                v := 0
            }
        }
    }

    /**
     * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided.
     */
    function _throwError(RecoverError error, bytes32 errorArg) private pure {
        if (error == RecoverError.NoError) {
            return; // no error: do nothing
        } else if (error == RecoverError.InvalidSignature) {
            revert ECDSAInvalidSignature();
        } else if (error == RecoverError.InvalidSignatureLength) {
            revert ECDSAInvalidSignatureLength(uint256(errorArg));
        } else if (error == RecoverError.InvalidSignatureS) {
            revert ECDSAInvalidSignatureS(errorArg);
        }
    }
}

interface IFeeGateway {
    function payAndPass(address user, bytes32 nonce) external payable;
}

interface ISemaphoreVerifier {
    function verifyProof(
        bytes32 merkleTreeRoot,
        bytes32 nullifierHash, // Input: computed off-chain
        bytes32 signal,
        uint[8] calldata proof // Semaphore Groth16: a[2], b[2][2], c[2]
    ) external view returns (bool); // Returns only validity
}

interface IExternalVotable {
    function castAggregatedVote(uint256 proposalId, uint8 support, bytes32 tallyProof) external;
    function castVote(uint256 proposalId, uint8 support) external; // Fallback for non-aggregated
}

/**
 * @title ShadowVote
 * @dev Robust shielded DAO voting with Semaphore zk-signals as default, MPC tally off-chain.
 * Standalone: create/signal/tally/execute proposals.
 * Wrapper: voteOnExternal for existing DAOs (designed for OpenZeppelin IGovernor.sol), with aggregated callback or proxy for non-compliant implementations.
 * Agent-first: Off-chain proofs in SDK - permissionless verification, no exposure.
 * Ties to stack: Oracle-informed, registry-weighted, settlement-paid.
 */

contract ShadowVote is ReentrancyGuard, Ownable {
    using ECDSA for bytes32;

    IFeeGateway public immutable FEE_GATEWAY = IFeeGateway(0x08Db140854AAb90463f6B61eB6F346C29BFB02EA);

    // Semaphore verifier (default for zk-signals)
    ISemaphoreVerifier public semaphoreVerifier;

    // Proposals: id => {executed, threshold, endTime, creator}
    mapping(uint256 => Proposal) public proposals;

    // Proposal creators
    mapping(uint256 => address) public proposalCreators;

    // Nullifiers (Semaphore + custom; prevents double-votes)
    mapping(bytes32 => bool) public nullifiers;

    // Tally proofs: proposalId => resultCommitment
    mapping(uint256 => bytes32) public tallyProofs;

    struct Proposal {
        bool executed;
        uint256 threshold;
        uint256 endTime;
        address creator;
        uint256 yes;
        uint256 no;
    }

    event VoteSignaled(address indexed agent, uint256 indexed proposalId, bytes32 signal, bytes32 nullifierHash);
    event TallySubmitted(uint256 indexed proposalId, uint256 yes, uint256 no, bytes32 tallyProof);
    event VoteExecuted(uint256 indexed proposalId, bool passed);
    event ExternalVoteWrapped(address indexed externalDAO, uint256 indexed proposalId, uint8 support, bytes32 tallyProof);
    event FeePaidAndPassed(bytes32 nonce);
    event NullifierSpent(bytes32 indexed nullifierHash, address indexed spender);

    constructor(address _semaphoreVerifier) Ownable(msg.sender) {
        semaphoreVerifier = ISemaphoreVerifier(_semaphoreVerifier); // Deploy Semaphore verifier first
    }

    /**
     * @dev Standalone vote signal: Semaphore zk-proof for anonymity.
     * Off-chain: SDK generates proof for group (DAO), signal = Poseidon(yes/no + weight), nullifierHash = Poseidon(nullifier + externalNullifier).
     * On-chain: Verifies membership/signal, nullifier spends.
     * @param proposalId ID.
     * @param merkleTreeRoot Group root.
     * @param signal Hashed vote.
     * @param nullifierHash Computed off-chain.
     * @param proof Semaphore proof (8 uints: a[2], b[2][2], c[2]).
     * @param nonce Fee pass.
     */
    function signalVote(
        uint256 proposalId,
        bytes32 merkleTreeRoot,
        bytes32 signal,
        bytes32 nullifierHash,
        uint[8] calldata proof,
        bytes32 nonce
    ) external payable nonReentrant {
        // Pay fee
        FEE_GATEWAY.payAndPass{value: msg.value}(msg.sender, nonce);

        // Semaphore verify (proves in group, signal valid)
        bool valid = semaphoreVerifier.verifyProof(merkleTreeRoot, nullifierHash, signal, proof);
        require(valid, "Invalid Semaphore proof");
        require(!nullifiers[nullifierHash], "Nullifier spent");
        nullifiers[nullifierHash] = true;

        emit VoteSignaled(msg.sender, proposalId, signal, nullifierHash);
        emit NullifierSpent(nullifierHash, msg.sender);
        emit FeePaidAndPassed(nonce);
    }

    /**
     * @dev Wrapper vote for external DAO: Signal + emit for off-chain tally/callback.
     * Off-chain: Swarm aggregates signals, calls external.castAggregatedVote.
     * @param externalDAO. Target contract (i.e. OpenZeppelin IGovernor.sol signatures).
     * @param proposalId. External ID.
     * @param merkleTreeRoot. Group root.
     * @param signal. Hashed vote.
     * @param nullifierHash. Computed off-chain.
     * @param proof. Semaphore proof.
     * @param nonce. Fee pass.
     */
    function voteOnExternal(
        address externalDAO,
        uint256 proposalId,
        bytes32 merkleTreeRoot,
        bytes32 signal,
        bytes32 nullifierHash,
        uint[8] calldata proof,
        bytes32 nonce
    ) external payable nonReentrant {
        // Pay fee
        FEE_GATEWAY.payAndPass{value: msg.value}(msg.sender, nonce);

        // Semaphore verify
        bool valid = semaphoreVerifier.verifyProof(merkleTreeRoot, nullifierHash, signal, proof);
        require(valid, "Invalid Semaphore proof");
        require(!nullifiers[nullifierHash], "Nullifier spent");
        nullifiers[nullifierHash] = true;

        emit VoteSignaled(msg.sender, proposalId, signal, nullifierHash); // Emits for Swarm tally
        emit NullifierSpent(nullifierHash, msg.sender);
        emit FeePaidAndPassed(nonce);
    }

    /**
     * @dev Submit tally: Off-chain MPC aggregates signals (yes/no).
     * Verifies total; executes standalone or calls external (aggregated or fallback individual).
     * @param proposalId ID.
     * @param yes Yes count.
     * @param no No count.
     * @param tallyProof Hash of tally (MPC output).
     * @param externalDAO Target for wrapper mode (address(0) for standalone).
     */
    function submitTally(
        uint256 proposalId,
        uint256 yes,
        uint256 no,
        bytes32 tallyProof,
        address externalDAO
    ) external {
        // Prod: Merkle proof for auth (like oracle)
        require(msg.sender == owner(), "Unauthorized tally"); // MVP

        require(tallyProofs[proposalId] == bytes32(0), "Tally submitted");
        tallyProofs[proposalId] = tallyProof;

        if (externalDAO == address(0)) {
            // Standalone: Update proposal
            Proposal storage prop = proposals[proposalId];
            prop.yes = yes;
            prop.no = no;
            emit TallySubmitted(proposalId, yes, no, tallyProof);
        } else {
            // Wrapper: Aggregated if supported, fallback individual castVote
            uint8 support = yes > no ? 1 : 0; // 1=For, 0=Against (abstain if tie)
            try IExternalVotable(externalDAO).castAggregatedVote(proposalId, support, tallyProof) {
                emit ExternalVoteWrapped(externalDAO, proposalId, support, tallyProof);
            } catch {
                // Fallback to individual (for unmodified Governors)
                IExternalVotable(externalDAO).castVote(proposalId, support);
                emit ExternalVoteWrapped(externalDAO, proposalId, support, bytes32(0)); // No proof in fallback
            }
        }
    }

    /**
     * @dev Execute standalone proposal: Creator or post-endTime.
     * @param proposalId ID.
     */
    function executeVote(uint256 proposalId) external {
        Proposal storage prop = proposals[proposalId];
        require(!prop.executed, "Already executed");
        require(msg.sender == prop.creator || block.timestamp > prop.endTime, "Unauthorized execution");
        require(tallyProofs[proposalId] != bytes32(0), "No tally");
        require(prop.yes + prop.no > 0, "No votes");

        require(prop.yes > prop.no && prop.yes >= prop.threshold, "Vote failed");

        prop.executed = true;
        emit VoteExecuted(proposalId, true);
    }

    /**
     * @dev Create standalone proposal.
     * @param threshold Quorum.
     * @param endTime End timestamp.
     */
    function createProposal(uint256 threshold, uint256 endTime) external returns (uint256) {
        uint256 id = uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp)));
        proposals[id] = Proposal(false, threshold, endTime, msg.sender, 0, 0);
        proposalCreators[id] = msg.sender;
        return id;
    }

    /**
     * @dev Set Semaphore verifier.
     */
    function setSemaphoreVerifier(address verifierAddr) external onlyOwner {
        semaphoreVerifier = ISemaphoreVerifier(verifierAddr);
    }

    receive() external payable {}
}

Tags:
Multisig, Voting, Upgradeable, Multi-Signature, Factory, Oracle|addr:0xe33b7fa8aafd262dab59cdc9d109690dd54e052d|verified:true|block:23700546|tx:0x31f2898c5abe2eb72a7fde0887ad51d986768313b39bbdc51d4ef4c1ec3f5e3d|first_check:1761990872

Submitted on: 2025-11-01 10:54:32

Comments

Log in to comment.

No comments yet.