عمیق تر در قراردادهای هوشمند (تمرین کیف پول چند امضایی)

به گزارش کوین ایران به نقل از بایننس، Multisig مخفف چند امضایی یا Multi-signature است. چند امضایی نوع خاصی از امضای دیجیتال است که امضای یک سند توسط دو یا چند کاربر را امکان پذیر می نماید. بنابراین، چند امضایی از طریق ترکیب چندین امضای منحصر به فرد تولید می شود. چند امضایی در حال حاضر در دنیای رمز ارز ها مورد استفاده قرار می گیرد ولی اصول مربوط به آن پیش تر از بیت کوین نیز وجود داشته است. در حوزه رمز ارز ها، این فناوری برای اولین بار در سال 2012 در مورد آدرس های بیت کوین مورد استفاده قرار گرفت که در نهایت، یک سال بعد با معرفی کیف پول های چند امضایی همراه شد. آدرس های چند امضایی ممکن است در حوزه های مختلفی استفاده شوند ولی مهم ترین استفاده آنها در زمینه مسائل امنیتی است.

کیف پول های چند امضایی چگونه کار می کنند؟

به عنوان یک مثال ساده، می توان یک صندوق سپرده امن را در نظر گرفت که دو قفل و دو کلید دارد. یک کلید توسط آلیس نگهداری می شود و کلید دیگر در دست باب است. تنها راهی که آن ها می توانند جعبه را باز کنند این است که کلید ها را در یک زمان در کنار هم داشته باشند؛ بنابراین هیچ‌ کدام نمی توانند صندوق را بدون رضایت دیگری باز کنند.

همانند مثال ذکر شده، منابع مالی ذخیره شده در یک آدرس چند امضایی تنها با استفاده از دو یا چند امضاء قابل دسترسی است. بدین ترتیب، استفاده از کیف پول چند امضایی یک لایه امنیتی بیشتر برای کاربران آنها به ارمغان می آورد. اما برای فهم بهتر این موضوع، ابتدا باید درک پایه ای در مورد آدرس استاندارد بیت کوین داشته باشیم. آدرس های استاندارد بیت کوین تنها از یک کلید استفاده می کنند و آدرس های تک کلیدی هستند.
برای مطالعه بیشتر لطفا این لینک را چک کنید.

توجه: پیشنهاد نمی شود که از این کیف پول چند امضایی با دارایی واقعی استفاده کنید، بلکه از یک کیف پول بسیار عمیق تر مانند کیف پول Gnosis multisignature wallet. استفاده کنید.

اهداف:
برای یادگیری نحوه مدیریت تعاملات پیچیده بین چندین کاربر در یک قرارداد
نحوه اجتناب از حلقه ها و اجرای رای گیری را بیاموزید
برای یادگیری ارزیابی حملات احتمالی به قراردادها.

pragma solidity ^0.5.0;

contract MultiSignatureWallet {

    struct Transaction {
      bool executed;
      address destination;
      uint value;
      bytes data;
    }

    event Deposit(address indexed sender, uint value);

    /// @dev Fallback function allows to deposit ether.
    function()
    	external
        payable
    {
        if (msg.value > 0) {
            emit Deposit(msg.sender, msg.value);
	}
    }

    /*
     * Public functions
     */
    /// @dev Contract constructor sets initial owners and required number of confirmations.
    /// @param _owners List of initial owners.
    /// @param _required Number of required confirmations.
    constructor(address[] memory _owners, uint _required) public {}

    /// @dev Allows an owner to submit and confirm a transaction.
    /// @param destination Transaction target address.
    /// @param value Transaction ether value.
    /// @param data Transaction data payload.
    /// @return Returns transaction ID.
    function submitTransaction(address destination, uint value, bytes memory data) public returns (uint transactionId) {}

    /// @dev Allows an owner to confirm a transaction.
    /// @param transactionId Transaction ID.
    function confirmTransaction(uint transactionId) public {}

    /// @dev Allows an owner to revoke a confirmation for a transaction.
    /// @param transactionId Transaction ID.
    function revokeConfirmation(uint transactionId) public {}

    /// @dev Allows anyone to execute a confirmed transaction.
    /// @param transactionId Transaction ID.
    function executeTransaction(uint transactionId) public {}

		/*
		 * (Possible) Helper Functions
		 */
    /// @dev Returns the confirmation status of a transaction.
    /// @param transactionId Transaction ID.
    /// @return Confirmation status.
    function isConfirmed(uint transactionId) internal view returns (bool) {}

    /// @dev Adds a new transaction to the transaction mapping, if transaction does not exist yet.
    /// @param destination Transaction target address.
    /// @param value Transaction ether value.
    /// @param data Transaction data payload.
    /// @return Returns transaction ID.
    function addTransaction(address destination, uint value, bytes memory data) internal returns (uint transactionId) {}
}

Constructor

ما می توانیم یک modifier ایجاد کنیم که این شرایط را بررسی کند

    modifier validRequirement(uint ownerCount, uint _required) {
        if (   _required > ownerCount || _required == 0 || ownerCount == 0)
            revert();
        _;
    }

و وقتی سازنده اجرا میشه modifier را صدا کنید.

constructor(address[] memory _owners, uint _required) public 
            validRequirement(_owners.length, _required)
        {...}

ما می خواهیم owners و _required را برای طول مدت قرارداد حفظ کنیم ، بنابراین برای ذخیره آنها باید متغیرهای ذخیره شده را اعلام کنیم.

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

ما همچنین mapping آدرسهای owner را به booleans اضافه کردیم تا بتوانیم سریعاً (بدون نیاز به جستجوی آرایه owners) ارجاع دهیم که آدرس خاصی owner است یا خیر.

همه این متغیرها در سازنده تنظیم می شوند.

constructor(address[] memory _owners, uint _required) public 
        validRequirement(_owners.length, _required)
    {
        for (uint i=0; i<_owners.length; i++) {
            isOwner[_owners[i]] = true;
        }
        owners = _owners;
        required = _required;
    }

Submit Transaction

عملکرد submitTransaction به owner اجازه می دهد یک تراکنش را ارسال و تأیید کند.
ابتدا باید این عملکرد را محدود کنیم تا فقط توسط یک owner قابل انجام باشد.

require(isOwner[msg.sender]);

با نگاهی به بقیه قسمتهای قرارداد ، متوجه خواهید شد که دو تابع دیگر در قرارداد وجود دارد که می تواند به شما در پیاده سازی این تابع کمک کند ، یکی addTransaction نامیده می شود که ورودی های مشابه submitTransaction را می گیرد و یک transactionId uint را برمی گرداند. مورد دیگر confirmTransaction است که یک uint transactionId را می گیرد.

ما می توانیم به راحتی با کمک این توابع ، submitTransaction را پیاده سازی کنیم:

function submitTransaction(address destination, uint value, bytes memory data) 
        public 
        returns (uint transactionId) 
    {
        require(isOwner[msg.sender]);
        transactionId = addTransaction(destination, value, data);
        confirmTransaction(transactionId);
    }

Add Transaction

بیایید به تابع addTransaction برویم و آن را پیاده سازی کنیم. اگر هنوز تراکنش موجود نباشد ، این عملکرد یک تراکنش جدید را به mapping تراکنش (که در شرف ایجاد آن هستیم) اضافه می کند.

function addTransaction(address destination, uint value, bytes memory data) internal returns (uint transactionId);

A transaction is a data structure that is defined in the contract stub.

    struct Transaction {
        address destination;
        uint value;
        bytes data;
        bool executed;
    }

ما باید ورودی های تابع addTransaction را در Transaction struct ذخیره کنیم و یک transaction id برای تراکنش ایجاد کنیم. بیایید دو متغیر ذخیره سازی دیگر ایجاد کنیم تا transaction ids و transaction mapping را پیگیری کنیم.

 uint public transactionCount;
    mapping (uint => Transaction) public transactions;

در تابع addTransaction می توانیم transaction count را بدست آوریم ، تراکنش را در mapping ذخیره کرده و تعداد را افزایش دهیم. این عملکرد state را تغییر می دهد بنابراین صدا زدن یک رویداد یک تمرین خوب است.

ما یک Submission event را که یک transactionId را می گیرد ، emit خواهیم کرد. ابتدا رویداد را تعریف کنیم. رویدادها معمولاً در بالای یک قرارداد Solidity تعریف می شوند ، بنابراین این همان کاری است که ما انجام خواهیم داد. این خط را درست در زیر تعریف قرارداد اضافه کنید.

event Submission(uint indexed transactionId);

کلمه کلیدی ایندکس شده در تعریف رویداد ، رویداد را به راحتی قابل جستجو می کند و هنگام ساخت رابط کاربر که نیاز به تجزیه و تحلیل بسیاری از رویدادها دارد ، مفید است.

در بدنه function می توانیم رویداد را فراخوانی کنیم.

function addTransaction(address destination, uint value, bytes memory data)
        internal
        returns (uint transactionId)
    {
        transactionId = transactionCount;
        transactions[transactionId] = Transaction({
            destination: destination,
            value: value,
            data: data,
            executed: false
        });
        transactionCount += 1;
        emit Submission(transactionId);
    }

unit transactionId برای تابع submitTransaction بازگردانده می شود تا به تابع confirm Transaction تحویل شود.

Confirm Transaction

function confirmTransaction(uint transactionId) public {}

تابع confirm transaction به owner اجازه می دهد تا تراکنش اضافه شده را تأیید کند.

این کار به یک متغیر ذخیره سازی دیگر نیاز دارد ، یک confirmations mapping که mapping مقادیر بولین را در آدرس owner ذخیره می کند. این متغیر پیگیری می کند که آدرس های owner کدام تراکنش ها را تأیید کرده اند.

mapping (uint => mapping (address => bool)) public confirmations;

چندین مورد وجود دارد که ما می خواهیم قبل از اجرای این تراکنش تأیید کنیم.
اول ، فقط دارندگان کیف پول باید بتوانند این تابع را صدا بزنند.
دوم ، ما می خواهیم بررسی کنیم که یک تراکنش با این transactionId وجود دارد.
در نهایت، ما می خواهیم بررسی کنیم که msg.sender قبلاً این تراکنش را تأیید نکرده است.

require(isOwner[msg.sender]);
require(transactions[transactionId].destination != 0);
require(confirmations[transactionId][msg.sender] == false);

هنگامی که تراکنش تعداد تأییدهای مورد نیاز را دریافت کرد ، تراکنش باید اجرا شود ، بنابراین متغیر با true باید مقداردهی شود.

confirmations[transactionId][msg.sender] = true;

از آنجا که ما در این تابع state را تغییر می دهیم، ثبت یک رویداد روش خوبی است. در ابتدا ، ما رویدادی به نام Confirmation را تعریف می کنیم که آدرس تأییدکنندگان و همچنین transactionId را ثبت می کند. تراکنش هایی که تأیید شده اند.
هر دو این پارامترهای تابع رویداد ایندکس می شوند تا رویداد را به راحتی جستجو کنید. حال می توانیم رویداد را در تابع فراخوانی کنیم.
پس از ورود به سیستم ، می توانیم تراکنش را اجرا کنیم.

executeTransaction(transactionId);

بنابراین کل تابع باید به این شکل باشد:

function confirmTransaction(uint transactionId)
        public
    {
        require(isOwner[msg.sender]);
        require(transactions[transactionId].destination != address(0));
        require(confirmations[transactionId][msg.sender] == false);
        confirmations[transactionId][msg.sender] = true;
        emit Confirmation(msg.sender, transactionId);
        executeTransaction(transactionId);
    }

Execute Transaction

تابع execute transaction یک پارامتر واحد، transactionId را می گیرد.
ابتدا می خواهیم اطمینان حاصل کنیم که تراکنش مشخص شده قبلاً اجرا نشده است.

  require(transactions[trandsactionId].executed == false);

سپس می خواهیم تأیید کنیم که معامله حداقل تعداد تأیید مورد نیاز را دارد.

برای انجام این کار ما آرایه owners را مرور خواهیم کرد و تعداد owner هایی که تراکنش را تأیید کرده اند را چک میکنیم. اگر شمارش به مقدار لازم برسد ، می توانیم شمارش را متوقف کنیم (در مصرف gas صرفه جویی کنیم) و فقط بگوییم که شرط لازم برقرار است.

من تابع کمکی isConfusted را تعریف می کنم ، که می توانیم آن را از تابع executeTransaction فراخوانی کنیم.

function isConfirmed(uint transactionId)
        public
        view
        returns (bool)
    {
        uint count = 0;
        for (uint i=0; i<owners.length; i++) {
            if (confirmations[transactionId][owners[i]])
                count += 1;
            if (count == required)
                return true;
        }
    }

اگر تراکنش تأیید شود ، true برمی گرداند ، بنابراین در تابع executeTransaction ، اگر تأیید شده باشد ، می توانیم تراکنش را در قسمت مشخص شده اجرا کنیم ، در غیر این صورت آن را اجرا نمی کنیم و سپس Transaction struct را به روز می کنیم تا stateرا منعکس کند.

ما در حال به روزرسانی state هستیم ، بنابراین باید رویدادی را emit کنیم که تغییر را نشان می دهد.

event Execution(uint indexed transactionId);
    event ExecutionFailure(uint indexed transactionId);

ما دو نتیجه احتمالی از این تابع داریم - انجام موفقیت آمیز تراکنش تضمین نشده است. ما می خواهیم مشخص کنیم که آیا ارسال تراکنش با موفقیت انجام می شود یا شکست می خورد. توجه کنید که چگونه بدنه تابع یک معامله ناموفق را مدیریت کرده و آن را در contract state ثبت می کند.

function executeTransaction(uint transactionId)
        public
    {
        require(transactions[transactionId].executed == false);
        if (isConfirmed(transactionId)) {
            Transaction storage t = transactions[transactionId];  // using the "storage" keyword makes "t" a pointer to storage 
            t.executed = true;
            (bool success, bytes memory returnedData) = t.destination.call.value(t.value)(t.data);
            if (success)
                emit Execution(transactionId);
            else {
                emit ExecutionFailure(transactionId);
                t.executed = false;
            }
        }
    }

Additional Functions

تاکنون ، ما فقط عملکرد اصلی این کیف پول چند علامتی را پوشش داده ایم.

ادامه تمرین و کاوش در بقیه قرارداد را به شما می سپارم. این کد به خوبی توضیح داده شده است و شما باید بتوانید هدف هر عملکرد را در قرارداد تعیین و توضیح دهید.

4 پسندیده

سلام
من متوجه بخش constructor نشدم. این بخش کلا باید تو تابع constructor نوشته بشه؟
modifier validrequirement رو با متغیر ها تو تابع constructor تعریف کنیم؟ بعد اون حلقه for چیه دیگه
ممنون می شم یه توضیح کلی بدین این بخش رو

1 پسندیده

سلام
2 تا سئوال داشتم
1- از event کی باید استفاده بشه. docs.soliditylang رو خوندم ولی کامل جا نیافتده واسم
2- این بخش اخر که عکسشو گذاشتم متوجه نشدم چی شد ممنون می شم توضیح بدید
مرسی

1 پسندیده

سلام @Aysha
ممنون می شم در مورد سیوالاتی که در ارتباط با این تمرین داشتم منو راهنمایی کنید
و همچنین در مورد اون بخشی که خود ما باید انجام بدیم توضیح بدید دقیقا چه کاری باید انجام بدید ممنون می شم

1 پسندیده

سلام و خدا قوت . من این خط رو متوجه نشدم چیکار میکنه ممنون میشم توضیح بدید .
(bool success, bytes memory returnedData) = t.destination.call.value(t.value)(t.data);
یا اگه منبعی هست که بتونم برم خودم مطالعه کنم معرفی بفرمایید ممنون میشم .

1 پسندیده

و سوال دومم اینکه این نمونه ای رو که قرار دادیدی رو مطالعه کردم واقعا عالی بود خیلی از سوالاتم برطرف شد
Gnosis multisignature wallet
فقط من یکی از توابع رو اصلا نفهمیدم اگه امکانش هست یکم توضیح بدید ممنونتون میشم .
این تابع external_call

1 پسندیده

سلام, متاسفم برای تاخیر.

لطفا مفهوم سازنده ها رو اینجا چک کنید:

constructor(address[] memory _owners, uint _required) public

تعریف سازنده

validRequirement(_owners.length, _required)

modifier ی که چک میکنه حداقل نفرات لازم امضارو ارسال کردن.
{
for (uint i=0; i<_owners.length; i++) {
به تعداد افرادی که امضا کردن حلقه داریم

isOwner[_owners[i]] = true;
و چک میکنیم این ادرس ها owner باشن.


        }
        owners = _owners;
        required = _required;
    }

در صورت برقرار بودن شرط متغیرهای ورودی رو ذخیره میکنیم.

1 پسندیده
> 
> Transaction storage t = transactions[transactionId];  // using the "storage" keyword makes "t" a pointer to storage

اینجا داره از struct استفاده میکنه که یه ساختار پیچیده محسوب میشه و محل ذخیره سازی ساختار دادهای پیچیده باید در فانکشن مشخص شه.
این struct:

struct Transaction {
      bool executed;
      address destination;
      uint value;
      bytes data;
    }
        t.executed = true;

داده struct رو اپدیت میکنه.

        (bool success, bytes memory returnedData) = t.destination.call.value(t.value)(t.data);

این خط هم نحوه ی خوندن خروجی از تابع با چند خروجی رو انجام داده.

مثلا این مثال رو چک کنید.


function A() returns (uint, uint) {

return (1,5)

}
uint a;
uint b

(a,b)= a();
1 پسندیده

در مورد بخش مربوط به شما:
این بیشتر یه مثال تمرینیه. کد ولت هارو چک کنید. میتونین فیچرهای خودتون رو تعریف کنین و بهش هر چیزی خواستین اضافه کنید.

1 پسندیده

سلام مرسی

(bool success, bytes memory returnedData) = t.destination.call.value(t.value)(t.data);

t نمونه ای از struct ه که در destination آدرس ذخیره میکنه.
call هم قراردادی که تو اون آدرس هست رو صدا میزنه.
t.value wei مقدار اتره
دومی هم برا مشخص کردن gas استفاده میشه فک کنم. و این ورژن قدیمیه انگار.
در مورد خروجی ها هم گفتم بالاتر.

1 پسندیده

البته این کد قدیمیه و این کلاس فک کنم دیگه کاربردی نداره.
لطفا این لینک هارو چک کنید.

1 پسندیده

سلام
خیلی ممنون از پاسخ های کاملتون

سلام من تازه با تیم کوین ایران آشنا شدم و دارم دوره ای که در یوتوب بارگذاری کردید رو میبینم و اینجا تاپیک هارو میخونم و …
اول از تمام اعضای تیم واقعا ممنونم خیلی زیاد …
این دوره بسیار ارزشمنده
برخی قسمتهایی که متوجه نمیشم رو میتونم سوال کنم یا اینکه دوره ای هست که بتونم بصورت آنلاین یا حضوری بعد از اتمام این دوره شرکت کنم؟

سپاس