מה זה חוזה חכם?
לפני שנתחיל בואו ניישר קו ונבין מהו חוזה חכם ולמה צריך אותו.
חוזה חכם הוא חוזה לביצוע עצמי כאשר תנאי ההסכם בין הקונה למוכר נכתבים ישירות לתוך שורות קוד. הקוד וההסכמים הכלולים בו קיימים ברשת בלוקצ’יין מבוזרת. הקוד שולט בביצוע, והעסקאות ניתנות למעקב ובלתי הפיכות. בגדול, הוא בעצם חשבון על האתריום; יש לו יתרה, והוא יכול לשלוח עסקאות דרך הרשת.
למה צריך את זה בכלל?
חוזים חכמים מאפשרים לבצע עסקאות והסכמים מהימנים בין צדדים אנונימיים שונים ללא צורך בסמכות מרכזית, מערכת משפטית או מנגנון אכיפה חיצוני.
Solidity – שפת תכנות
השפה איתה נכתבים חוזים חכמים היא שפת סולידיטי:
- שפה מונחת עצמים ליישום חוזים חכמים.
- שפה סטטית (סוג המשתנה ידוע בזמן הקומפילציה).
- תומכת בירושה.
- תומכת בספריות (ניתן ליצור קוד לשימוש חוזר וקריאה מחוזים אחרים).
מבנה החוזה
נתחיל בדוגמה בסיסית שקובעת את הערך של משתנה ונותנת גישת צפייה לחוזים אחרים.
רגע, אז מה בעצם ראינו פה?
השורה הראשונה אומרת לך שקוד המקור מורשה תחת גירסת GPL 3.0. חשוב בסביבה בה פרסום קוד המקור הוא ברירת המחדל.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}
// SPDX-License-Identifier: GPL-3.0
השורה הבאה מציינת את טווח הגרסאות של השפה בה אנו כותבים. זאת כדי להבטיח שהחוזה אינו ניתן לקומפילציה עם גרסת מהדר חדשה יותר, שבה היא (שפת סולידיטי) יכולה להתנהג אחרת.
pragma solidity >=0.7.0 <0.9.0;
חוזה במובן של Solidity הוא אוסף של קוד (functions) ונתונים (state) השוכנים בכתובת ספציפית ב-Ethereum blockchain. נגדיר אותו בדומה למחלקה, רק שפה במקום להגדיר אותו כ-class נגדיר אותו כחוזה, contract.
contract Storage {}
בחוזה נגדיר משתנה מסוג uint256 (שלם, אינו מסומן, בגודל 256 ביט) בשם number.
uint256 number;
כדי לגשת למשתנה (כמו משתנה מצב) בחוזה, לא נוסיף את הקידומת this פשוט ניגש אליו ישירות דרך השם שלו.
function store(uint256 num) public{number = num;}
החוזה הזה עדיין לא עושה הרבה מלבד לאפשר לכל אחד לאחסן מספר בודד שנגיש לכל אחד בעולם ללא דרך למנוע מאיתנו לפרסם את המספר הזה. כל אחד יכול להחליף את המספר שלנו, אבל המספר עדיין יהיה שמור בהיסטוריה של הבלוקצ’יין. מאוחר יותר, נראה כיצד אפשר להטיל הגבלות גישה כך שרק אנו נוכל לשנות את המספר.
איפה להריץ?
סביבת העבודה בה נריץ את הקוד היא Remix – Ethereum IDE. מדובר בכלי אינטרנטי מדהים ליצירת חוזים חכמים ולבדיקה שלהם על הרשת. למדריך מהיר כיצד להשתמש ברמיקס לחצו כאן.
על מנת לבחון את החוזה נצטרך להעלות אותו לרשת דמה מתוך אחד החשבונות שרמיקס מספק עם 100 אתריום בכל אחד (לצערנו גם אלו רק לטסטים).
ולבדוק כל פונקציה בחוזה בעזרת הממשק.
חוזה המטבע הדיגיטלי
החוזה הבא מיישם את הצורה הפשוטה ביותר של מטבע קריפטוגרפי:
- החוזה מאפשר רק ליוצרו ליצור מטבעות חדשים.
- כל אחד יכול לשלוח מטבעות אחד לשני ללא צורך ברישום עם שם משתמש וסיסמה, כל מה שצריך זה צמד כתובות של Ethereum.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Coin {
// The keyword "public" makes variables
// accessible from other contracts
address public minter;
mapping (address => uint) public balances;
// Events allow clients to react to specific
// contract changes you declare
event Sent(address from, address to, uint amount);
// Constructor code is only run when the contract
// is created
constructor() {
minter = msg.sender;
}
// Sends an amount of newly created coins to an address
// Can only be called by the contract creator
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
balances[receiver] += amount;
}
// Errors allow you to provide information about
// why an operation failed. They are returned
// to the caller of the function.
error InsufficientBalance(uint requested, uint available);
// Sends an amount of existing coins
// from any caller to an address
function send(address receiver, uint amount) public {
if (amount > balances[msg.sender])
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}
החוזה הזה חושף אותנו לרעיונות חדשים, מורכבים יותר מהדוגמה הקודמת. בואו נעבור על הדוגמה.
בשורה זו הכרזנו על משתנה מצב פומבי מסוג כתובת. מדובר במשתנה בגודל 160 ביט שלא מאפשר שום מניפולציה אריתמטית ומתאים לאיחסון כתובות של חוזים או ערך גיבוב מסויים.
מילת המפתח public יוצרת אוטומטית פונקציה המאפשרת גישה לערך הנוכחי של משתנה המצב מחוץ לחוזה.
address public minter;
גם פה אנו מגדירים משתנה מצב מסוג מיפוי. נעשה כאן מיפוי כתובות למספרים שלמים. המיפוי זהה למילון בכל שפה אחרת. ב-Solidity, זוהי טבלת גיבוב המאחסנת נתונים כזוגות מפתח-ערך.
- מפתח – יכול להיות כל טיפוס מובנה בתוספת בתים ומחרוזות. אין להשתמש בטיפוס הפניה (רפרנס) או באובייקטים מורכבים.
- ערך – יכול להיות כל טיפוס.
mapping (address => uint) public balances;
מכריז על “אירוע”, שנפלט בשורה האחרונה של הפונקציה send. לקוחות Ethereum כגון יישומי אינטרנט יכולים להאזין לאירועים אלו בבלוקצ’יין. ברגע שהוא נפלט, המאזין מקבל את הארגומנטים ״מ…״, ״אל…״ וסכום, מה שמאפשר לעקוב אחר עסקאות.
event Sent(address from, address to, uint amount);
הבנאי הוא פונקציה מיוחדת שמתבצעת במהלך יצירת החוזה ולא ניתן לקרוא לה לאחר מכן. במקרה זה, הוא מאחסן לצמיתות את הכתובת של יוצר החוזה. המשתנה msg הוא משתנה גלובלי מיוחד המכיל מאפיינים המאפשרים גישה לבלוקצ’יין. msg.sender היא תמיד הכתובת שממנה הגיעה קריאת הפונקציה הנוכחית.
constructor() {minter = msg.sender;}
הפונקציות השימושיות בחוזה שלנו הן mint ו- send.
הפונקציה mint שולחת כמות מטבעות שזה עתה נוצרו לכתובת אחרת. קריאת הפונקציה require מגדירה את התנאים לשימוש בפונקציה. בדוגמה זו, רק יוצר החוזה יכול להפעיל את הפונקציה mint.
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
balances[receiver] += amount;
}
השגיאות מאפשרות לנו לספק מידע נוסף לגבי הסיבה שמצב או פעולה נכשלו.
error InsufficientBalance(uint requested, uint available);
נעשה שימוש בשגיאות יחד עם revert. הצהרת revert מבטלת ומחזירה ללא תנאי את כל השינויים הדומים לפונקציית require, אך היא גם מאפשרת לנו לספק את שם השגיאה ונתונים נוספים שיסופקו למשתמש כך שניתן בקלות רבה יותר לנפות באגים.
פונקציית send יכולה לשמש כל אחד (שכבר יש לו חלק מהמטבעות הללו) כדי לשלוח מטבעות לכל אחד אחר. אם לשולח אין מספיק מטבעות לשלוח, התנאי if מוערך כאמת. כתוצאה מכך, revert תגרום לכישלון הפעולה תוך מתן פרטי שגיאה לשולח באמצעות השגיאה InsufficientBalance.
function send(address receiver, uint amount) public {
if (amount > balances[msg.sender])
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
ולסיום סיומת...
איזה כיף שהגעתם עד לכאן!
בפוסט הזה למדנו על קצה המזלג על כיצד נראה מבנה של חוזה, קיבלנו טעימה על איך לכתוב בשפת Solidity ואיך להריץ את הקוד שלנו. ראינו שאמנם המילה בלוקצ׳יין וחוזים חכמים הן מילים מפציצות אבל הבסיס הוא בסך הכל תכנות מונחה עצמים כמו שאנחנו מכירים 🙂
מוזמנים למצוא אותי בלינקדאין ולכתוב לי בקשות, הערות, המלצות, או סתם לדבר על בלוקצ׳יין!
כמה שניות היסטוריות…
חוזים חכמים הוצעו לראשונה בשנת 1994 על ידי ניק סאבו, מדען מחשבים אמריקאי שהמציא מטבע וירטואלי בשם "Bit Gold" בשנת 1998, במלואן 10 שנים לפני המצאת הביטקוין. למעשה, לעתים קרובות אומרים שסאבו הוא סאטושי נקמוטו האמיתי, הממציא האלמוני של הביטקוין, דבר שהוא הכחיש.