neo-devpack-solidity 0.22.0

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

/**
 * @title MultiSigWalletNEP17
 * @notice Neo N3 multisig-style wallet for native GAS/NEO (NEP-17).
 *
 * Neo N3 does not support EVM-style `{value: ...}` calls. To move value you must
 * explicitly transfer NEP-17 tokens and handle deposits via `onNEP17Payment`.
 */
contract MultiSigWalletNEP17 {
    // Native contract hashes exposed by the devpack (lowered as compiler intrinsics).
    address private constant NEO = NativeCalls.NEO_CONTRACT;
    address private constant GAS = NativeCalls.GAS_CONTRACT;

    struct TransferTx {
        address token;
        address to;
        uint256 amount;
        bytes data;
        bool executed;
        uint256 confirmations;
    }

    address[] public owners;
    mapping(address => bool) public isOwner;
    uint256 public required;

    uint256 public transactionCount;
    mapping(uint256 => TransferTx) public transactions;
    mapping(uint256 => mapping(address => bool)) public confirmed;

    event Deposit(address token, address from, uint256 amount, bytes data);
    event Submission(uint256 indexed txId, address indexed token, address indexed to, uint256 amount);
    event Confirmation(address indexed owner, uint256 indexed txId);
    event Execution(uint256 indexed txId);

    modifier onlyOwner() {
        require(isOwner[msg.sender], "not owner");
        _;
    }

    modifier txExists(uint256 txId) {
        require(txId < transactionCount, "tx does not exist");
        _;
    }

    modifier notExecuted(uint256 txId) {
        require(!transactions[txId].executed, "tx already executed");
        _;
    }

    modifier notConfirmed(uint256 txId, address owner) {
        require(!confirmed[txId][owner], "tx already confirmed");
        _;
    }

    constructor(address[] memory _owners, uint256 _required) {
        require(_owners.length > 0, "owners required");
        require(_required > 0 && _required <= _owners.length, "invalid required");

        required = _required;

        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            require(owner != address(0), "invalid owner");
            require(!isOwner[owner], "duplicate owner");
            isOwner[owner] = true;
            owners.push(owner);
        }
    }

    function onNEP17Payment(address from, uint256 amount, bytes memory data) external {
        address token = Syscalls.getCallingScriptHash();
        require(token == GAS || token == NEO, "unsupported token");
        emit Deposit(token, from, amount, data);
    }

    function submitTransfer(
        address token,
        address to,
        uint256 amount,
        bytes memory data
    ) external onlyOwner returns (uint256 txId) {
        require(token == GAS || token == NEO, "unsupported token");
        require(to != address(0), "invalid recipient");

        txId = transactionCount;
        transactionCount = txId + 1;

        TransferTx storage txn = transactions[txId];
        txn.token = token;
        txn.to = to;
        txn.amount = amount;
        txn.data = data;
        txn.executed = false;
        txn.confirmations = 0;

        emit Submission(txId, token, to, amount);

        confirm(txId);
    }

    function confirm(uint256 txId)
        public
        onlyOwner
        txExists(txId)
        notExecuted(txId)
        notConfirmed(txId, msg.sender)
    {
        confirmed[txId][msg.sender] = true;
        transactions[txId].confirmations += 1;
        emit Confirmation(msg.sender, txId);

        if (transactions[txId].confirmations >= required) {
            execute(txId);
        }
    }

    function execute(uint256 txId) public onlyOwner txExists(txId) notExecuted(txId) {
        TransferTx storage txn = transactions[txId];
        require(txn.confirmations >= required, "insufficient confirmations");

        txn.executed = true;
        bool success = _transfer(txn.token, txn.to, txn.amount, txn.data);
        require(success, "transfer failed");

        emit Execution(txId);
    }

    function _transfer(
        address token,
        address to,
        uint256 amount,
        bytes memory data
    ) internal returns (bool) {
        if (token == GAS) {
            return NativeCalls.gasTransfer(address(this), to, amount, data);
        }
        if (token == NEO) {
            return NativeCalls.neoTransfer(address(this), to, amount, data);
        }
        return false;
    }
}