إنشاء محفظة متعددة التوقيع (Multi-sig Wallet) لحماية أموال الشركات عبر العقود الذكية

دقائق القراءة: 6

إنشاء محفظة متعددة التوقيع Multi-sig Wallet لحماية أموال الشركات عبر العقود الذكية

تُعد محفظة Multi-sig Wallet من أهم الأدوات العملية في بيئة Web3 عندما تحتاج الشركات إلى حماية خزينتها الرقمية من مخاطر القرار الفردي أو اختراق مفتاح واحد. الفكرة الأساسية بسيطة: لا يتم تنفيذ أي تحويل أموال إلا بعد موافقة عدد محدد من الموقّعين، مثل 2 من 3 أو 3 من 5.

هذا النموذج يرفع مستوى الحوكمة الداخلية ويقلل مخاطر إساءة الاستخدام، خصوصاً عند إدارة أرصدة Ether أو رموز ERC-20. وإذا كنت تريد أساساً نظرياً أعمق حول البنية العامة للويب اللامركزي، فمن المفيد مراجعة مدخل إلى Web3: ما هو البلوكتشين ولماذا يغير شكل الإنترنت والأنظمة المالية؟.

ما هي المحفظة متعددة التوقيع ولماذا تحتاجها الشركات؟

في المحافظ التقليدية المعتمدة على مفتاح خاص واحد، امتلاك المفتاح يعني امتلاك سلطة مطلقة على الأموال. هذا النموذج خطير في السياق المؤسسي، لأن فقدان الجهاز، أو تسريب العبارة السرية، أو خطأ موظف واحد قد يؤدي إلى خسارة لا يمكن التراجع عنها.

أما في تصميم Multi-sig فإن العقد الذكي يفرض منطق الموافقات برمجياً. بدلاً من الثقة في شخص واحد، تصبح الثقة موزعة على عدة عناوين. وهذا يرتبط مباشرة بمفاهيم المفاتيح العامة والخاصة التي تم شرحها في التشفير والمفاتيح: كيف تعمل المحافظ الرقمية (Public & Private Keys) برمجياً؟.

  • تقليل خطر Single Point of Failure.
  • فرض موافقات إدارية قبل التحويل.
  • تحسين قابلية التتبع عبر Events والمعاملات المسجلة على السلسلة.
  • إنشاء حوكمة مالية قابلة للتدقيق لاحقاً.

التصميم البرمجي للعقد الذكي

لبناء هذه المحفظة نحتاج إلى مجموعة عناصر واضحة داخل عقد Solidity: قائمة المالكين، عدد الموافقات المطلوبة، هيكل بيانات للمعاملة، وخريطة لتسجيل من وافق على كل طلب. هذا يعتمد على مفاهيم المصفوفات (Arrays) في Solidity والقواميس (Mappings) والهياكل (Structs).

منطق العمل يمر غالباً بالمراحل التالية:

  1. إيداع الأموال في العقد.
  2. إنشاء طلب تحويل جديد.
  3. موافقة الموقّعين على الطلب.
  4. تنفيذ المعاملة بعد بلوغ العدد المطلوب من الموافقات.

هيكل المعاملة داخل العقد

كل طلب تحويل يجب أن يخزن الوجهة، القيمة، البيانات الاختيارية، وحالة التنفيذ. كما نحتاج إلى عدّاد للموافقات. هنا تظهر أهمية فهم الدوال (Functions) في Solidity والمعدلات (Modifiers) لحماية الوصول.

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

contract MultiSigWallet {
    event Deposit(address indexed sender, uint256 amount, uint256 balance);
    event SubmitTransaction(
        address indexed owner,
        uint256 indexed txIndex,
        address indexed to,
        uint256 value,
        bytes data
    );
    event ConfirmTransaction(address indexed owner, uint256 indexed txIndex);
    event RevokeConfirmation(address indexed owner, uint256 indexed txIndex);
    event ExecuteTransaction(address indexed owner, uint256 indexed txIndex);

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

    struct Transaction {
        address to;
        uint256 value;
        bytes data;
        bool executed;
        uint256 numConfirmations;
    }

    Transaction[] public transactions;
    mapping(uint256 => mapping(address => bool)) public isConfirmed;

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

    modifier txExists(uint256 _txIndex) {
        require(_txIndex < transactions.length, "Tx does not exist");
        _;
    }

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

    modifier notConfirmed(uint256 _txIndex) {
        require(!isConfirmed[_txIndex][msg.sender], "Tx already confirmed");
        _;
    }

    constructor(address[] memory _owners, uint256 _required) {
        require(_owners.length > 0, "Owners required");
        require(
            _required > 0 && _required <= _owners.length,
            "Invalid confirmations count"
        );

        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            require(owner != address(0), "Invalid owner");
            require(!isOwner[owner], "Owner not unique");

            isOwner[owner] = true;
            owners.push(owner);
        }

        required = _required;
    }

    receive() external payable {
        emit Deposit(msg.sender, msg.value, address(this).balance);
    }

    function submitTransaction(
        address _to,
        uint256 _value,
        bytes memory _data
    ) external onlyOwner {
        uint256 txIndex = transactions.length;

        transactions.push(
            Transaction({
                to: _to,
                value: _value,
                data: _data,
                executed: false,
                numConfirmations: 0
            })
        );

        emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);
    }

    function confirmTransaction(
        uint256 _txIndex
    ) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) notConfirmed(_txIndex) {
        Transaction storage transaction = transactions[_txIndex];

        transaction.numConfirmations += 1;
        isConfirmed[_txIndex][msg.sender] = true;

        emit ConfirmTransaction(msg.sender, _txIndex);
    }

    function executeTransaction(
        uint256 _txIndex
    ) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
        Transaction storage transaction = transactions[_txIndex];

        require(
            transaction.numConfirmations >= required,
            "Not enough confirmations"
        );

        transaction.executed = true;

        (bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
        require(success, "Tx failed");

        emit ExecuteTransaction(msg.sender, _txIndex);
    }

    function revokeConfirmation(
        uint256 _txIndex
    ) external onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
        require(isConfirmed[_txIndex][msg.sender], "Tx not confirmed");

        Transaction storage transaction = transactions[_txIndex];
        transaction.numConfirmations -= 1;
        isConfirmed[_txIndex][msg.sender] = false;

        emit RevokeConfirmation(msg.sender, _txIndex);
    }

    function getOwners() external view returns (address[] memory) {
        return owners;
    }

    function getTransactionCount() external view returns (uint256) {
        return transactions.length;
    }

    function getTransaction(
        uint256 _txIndex
    )
        external
        view
        returns (
            address to,
            uint256 value,
            bytes memory data,
            bool executed,
            uint256 numConfirmations
        )
    {
        Transaction storage transaction = transactions[_txIndex];

        return (
            transaction.to,
            transaction.value,
            transaction.data,
            transaction.executed,
            transaction.numConfirmations
        );
    }
}

كيف يعمل هذا العقد فعلياً؟

الدالة constructor تستقبل قائمة المالكين وعدد التوقيعات المطلوبة، ثم تتحقق من عدم وجود عناوين مكررة أو عنوان صفري. هذا يمنع أخطاء التهيئة التي قد تجعل العقد غير صالح للاستخدام منذ لحظة النشر.

عند استدعاء submitTransaction يتم حفظ طلب التحويل فقط دون تنفيذ فوري. بعد ذلك يستخدم المالكون الدالة confirmTransaction لإضافة موافقاتهم.

إذا بلغ عدد الموافقات القيمة المخزنة في required يمكن لأي مالك تنفيذ الطلب عبر executeTransaction. العقد يستخدم الاستدعاء منخفض المستوى call لدعم إرسال Ether أو استدعاء وظائف في عقود أخرى، وهو امتداد مهم إذا كنت مهتماً بـ التفاعل بين العقود الذكية.

النشر والاختبار باستخدام Hardhat

في المشاريع الاحترافية لا يكفي اختبار العقد داخل المتصفح فقط، بل يجب إعداد بيئة تطوير منظمة. يمكنك البدء من الانتقال إلى بيئة العمل الاحترافية: تثبيت إطار عمل Hardhat باستخدام Node.js ثم مراجعة إعداد مشروع Hardhat وكتابة أول سكربت JavaScript لترجمة (Compile) العقد الذكي.

خطوات التحقق العملية تكون كالتالي:

  • نشر العقد بثلاثة عناوين مالكة مثلاً.
  • ضبط عدد الموافقات المطلوبة على 2.
  • إرسال رصيد تجريبي إلى العقد.
  • إنشاء معاملة سحب إلى عنوان مستفيد.
  • تأكيدها من محفظتين مختلفتين.
  • تنفيذها ومراقبة الحدث ExecuteTransaction.

ولضمان الاعتمادية، يُستحسن كتابة Unit Tests كما في اختبار العقود الذكية محلياً: كتابة اختبارات الوحدة (Unit Tests) باستخدام Chai & Mocha للتحقق من سيناريوهات مثل رفض التنفيذ المبكر أو منع التأكيد المكرر.

ربط المحفظة بواجهة ويب عبر Ethers.js

بعد نشر العقد، يمكن بناء لوحة تحكم داخلية للشركة لعرض الطلبات وحالة التوقيع. في هذه المرحلة ستحتاج إلى هندسة الويب اللامركزي (Web3.js & Ethers.js): كيف نربط الواجهات بالعقود الذكية؟ وكتابة البيانات وإرسال المعاملات (Transactions) من واجهة الويب إلى العقد الذكي.

واجهة الشركة يمكن أن تعرض:

  • الرصيد الحالي للعقد.
  • قائمة المالكين.
  • عدد التوقيعات المطلوبة.
  • المعاملات غير المنفذة.
  • زر موافقة أو إلغاء موافقة لكل مالك.

كما يمكن استخدام Events لتحديث الواجهة فورياً، وهو ما يرتبط مباشرة بمقال الاستماع إلى الأحداث (Events) وتحديث واجهة React لحظياً عند تغير البيانات.

أمنياً، يجب ألا تعتبر محفظة Multi-sig بديلاً كاملاً عن التدقيق. إذا كانت المعاملة تستدعي عقداً خارجياً عبر call فيجب تحليل مخاطر إعادة الدخول ومراجعة مبادئ ثغرة إعادة الدخول (Reentrancy Attack) وطرق الحماية منها.

لتقليل Gas Fees، حاول تقليل عمليات التخزين غير الضرورية، واستخدم دوال view لقراءة البيانات دون معاملات، وراجع تحسين استهلاك الـ Gas: حيل وأسرار متقدمة في Solidity لتقليل رسوم المعاملات لفهم تحسينات أعمق على مستوى التخزين والهيكلة.

أفضل الممارسات المؤسسية قبل الاعتماد الفعلي

قبل استخدام المحفظة لحفظ أموال حقيقية، ينبغي التفكير بمنطق الحوكمة وليس الكود فقط. من يملك مفاتيح التوقيع؟ ماذا يحدث إذا غادر أحد المدراء؟ هل هناك سياسة لتدوير العناوين أو لنشر نسخة مطورة؟ هذه الأسئلة مهمة بقدر أهمية كتابة العقد نفسه.

خلاصة

محفظة Multi-sig Wallet ليست مجرد عقد ذكي لتحويل الأموال، بل هي طبقة حوكمة مالية قابلة للبرمجة. بفضل Solidity يمكن تحويل سياسات الموافقة داخل الشركات إلى قواعد تنفيذية غير قابلة للتلاعب على البلوكتشين.

وعندما يتم دمج هذا النموذج مع اختبارات قوية، واجهة مبنية عبر Ethers.js، ومراجعات أمنية دقيقة، فإنه يصبح من أقوى الحلول العملية لحماية الخزائن الرقمية المؤسسية في عالم Blockchain.

2 comments

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *