מערכות דומיינים מבוזרות: חוזים חכמים ובדיקת ביקורת
כתובות קריפטוגרפיות אינן קריאות לבני אדם. 0x742d35Cc6634C0532925a3b844Bc454e4438f44e אינה כתובת, אלא מקור לשגיאות: עד 30% מהעסקאות נכשלות עקב כתובות שהועתקו באופן שגוי. מערכת דומיינים בלוקצ'יין מחליפה את הכתובת בשם קריא לאדם, ובמקביל הופכת את השם הזה לרשומת זהות ניידת. שירותים כאלה פותרים לא רק את בעיית השימושיות אלא גם את האבטחה: התקפות פישינג המבוססות על כתובות דומות הופכות לכמעט בלתי אפשריות בעת שימוש בשם מאומת. לדוגמה, הדומיין הבלוקצ'ייני alice.myns יכול לשמש כנקודת חיבור יחידה לארנקים ב-Ethereum, Polygon ו-Solana, וגם להפנות לאתר IPFS. אנו מפתחים מערכות כאלה במתכונת turnkey: מארכיטקטורה ועד ביקורת והשקה. קבלו ייעוץ — נבחן את הפרויקט שלכם כבר עכשיו.
ביקורת חוזים חכמים לדומיינים בלוקצ'יין
מרחב שמות ורג'יסטרי
הרכיב המרכזי הוא חוזה ה-Registry. הוא מאחסן מיפוי משם מהונדס (namehash) לבעלים ולכתובת Resolver. ויקיפדיה: שירות השמות של Ethereum משתמש בדיוק בארכיטקטורה זו: הפרדת בעלות (Registry) ואחסון נתונים (Resolver) מאפשרת שינוי ה-Resolver ללא אובדן בעלות.
contract DomainRegistry {
struct Record {
address owner;
address resolver;
uint64 ttl;
}
mapping(bytes32 => Record) private records;
mapping(bytes32 => mapping(address => bool)) private operators;
event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner);
event Transfer(bytes32 indexed node, address owner);
event NewResolver(bytes32 indexed node, address resolver);
function setOwner(bytes32 node, address _owner) external authorised(node) {
records[node].owner = _owner;
emit Transfer(node, _owner);
}
function setSubnodeOwner(bytes32 node, bytes32 label, address _owner) external authorised(node) returns (bytes32) {
bytes32 subnode = keccak256(abi.encodePacked(node, label));
records[subnode].owner = _owner;
emit NewOwner(node, label, _owner);
return subnode;
}
modifier authorised(bytes32 node) {
address owner = records[node].owner;
require(owner == msg.sender || operators[node][msg.sender], "Not authorised");
_;
}
}שמות מומרים ל-bytes32 באמצעות גיבוב רקורסיבי: contract DomainRegistry { struct Record { address owner; address resolver; uint64 ttl; } mapping(bytes32 => Record) private records; mapping(bytes32 => mapping(address => bool)) private operators; event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner); event Transfer(bytes32 indexed node, address owner); event NewResolver(bytes32 indexed node, address resolver); function setOwner(bytes32 node, address _owner) external authorised(node) { records[node].owner = _owner; emit Transfer(node, _owner); } function setSubnodeOwner(bytes32 node, bytes32 label, address _owner) external authorised(node) returns (bytes32) { bytes32 subnode = keccak256(abi.encodePacked(node, label)); records[subnode].owner = _owner; emit NewOwner(node, label, _owner); return subnode; } modifier authorised(bytes32 node) { address owner = records[node].owner; require(owner == msg.sender || operators[node][msg.sender], "Not authorised"); _; } } → alice.myns → keccak256(keccak256('' bytes32(0)) + keccak256('myns')). גישה זו היא הבסיס לשירות ENS, המשמש מיליוני משתמשים. מעל 2.5 מיליון דומיינים של ENS נרשמו ברחבי העולם נכון ל-2023.
חוזה Resolver
ה-Resolver מאחסן נתונים המשויכים לשם. Resolver אחד יכול לשרת שמות רבים.
contract PublicResolver {
DomainRegistry immutable registry;
mapping(bytes32 => mapping(uint256 => bytes)) private _addresses;
mapping(bytes32 => mapping(string => string)) private _textRecords;
mapping(bytes32 => bytes) private _contenthash;
event AddressChanged(bytes32 indexed node, uint256 coinType, bytes newAddress);
event TextChanged(bytes32 indexed node, string indexed key, string value);
event ContenthashChanged(bytes32 indexed node, bytes hash);
function setAddr(bytes32 node, address addr) external authorised(node) {
setAddr(node, 60, addressToBytes(addr));
}
function setAddr(bytes32 node, uint256 coinType, bytes calldata a) public authorised(node) {
_addresses[node][coinType] = a;
emit AddressChanged(node, coinType, a);
}
function setText(bytes32 node, string calldata key, string calldata value) external authorised(node) {
_textRecords[node][key] = value;
emit TextChanged(node, key, value);
}
function setContenthash(bytes32 node, bytes calldata hash) external authorised(node) {
_contenthash[node] = hash;
emit ContenthashChanged(node, hash);
}
modifier authorised(bytes32 node) {
require(registry.owner(node) == msg.sender, "Not authorised");
_;
}
} חוזה Registrar ו-NFT
שמות ברמה העליונה (TLD) נרשמים דרך ה-Registrar. כל שם רשום הוא NFT מסוג ERC-721, המאפשר מסחר ב-OpenSea ובשווקים אחרים. רישום דומיינים על הבלוקצ'יין אינו רק רשומה, אלא יצירת נכס נזיל.
contract BaseRegistrar is ERC721 {
DomainRegistry public registry;
bytes32 public baseNode;
mapping(uint256 => uint256) public expiries;
uint256 public constant GRACE_PERIOD = 90 days;
function available(uint256 id) public view returns (bool) {
return expiries[id] + GRACE_PERIOD < block.timestamp;
}
function register(uint256 id, address owner, uint256 duration) external onlyController returns (uint256) {
require(available(id), "Not available");
expiries[id] = block.timestamp + duration;
if (_exists(id)) {
_transfer(address(0), owner, id);
} else {
_mint(owner, id);
}
registry.setSubnodeOwner(baseNode, bytes32(id), owner);
return expiries[id];
}
function renew(uint256 id, uint256 duration) external onlyController returns (uint256) {
require(expiries[id] + GRACE_PERIOD >= block.timestamp, "Expired");
expiries[id] += duration;
return expiries[id];
}
} Oracle מחירים ורישום
מחירי הרישום תלויים בדרך כלל באורך השם. אנו משתמשים ב-Chainlink כדי לקבל את שער ETH/USD העדכני. דומיין בן 3 תווים עשוי לעלות $160 לשנה, בעוד דומיין בן 7+ תווים עולה $1 לשנה.
contract PriceOracle {
uint256[5] public rentPrices = [
160e18,
40e18,
10e18,
5e18,
1e18
];
AggregatorV3Interface public immutable usdOracle;
function price(string calldata name, uint256 duration) external view returns (uint256 weiAmount) {
uint256 len = strlen(name);
uint256 usdPrice = rentPrices[min(len - 1, 4)];
uint256 annualUsd = usdPrice * duration / 365 days;
(, int256 usdEthPrice,,,) = usdOracle.latestRoundData();
return annualUsd * 1e8 / uint256(usdEthPrice);
}
} יישום פתרון הפוך (Reverse Resolution)
פתרון ישיר: keccak256(result + keccak256('alice')) → contract PublicResolver { DomainRegistry immutable registry; mapping(bytes32 => mapping(uint256 => bytes)) private _addresses; mapping(bytes32 => mapping(string => string)) private _textRecords; mapping(bytes32 => bytes) private _contenthash; event AddressChanged(bytes32 indexed node, uint256 coinType, bytes newAddress); event TextChanged(bytes32 indexed node, string indexed key, string value); event ContenthashChanged(bytes32 indexed node, bytes hash); function setAddr(bytes32 node, address addr) external authorised(node) { setAddr(node, 60, addressToBytes(addr)); } function setAddr(bytes32 node, uint256 coinType, bytes calldata a) public authorised(node) { _addresses[node][coinType] = a; emit AddressChanged(node, coinType, a); } function setText(bytes32 node, string calldata key, string calldata value) external authorised(node) { _textRecords[node][key] = value; emit TextChanged(node, key, value); } function setContenthash(bytes32 node, bytes calldata hash) external authorised(node) { _contenthash[node] = hash; emit ContenthashChanged(node, hash); } modifier authorised(bytes32 node) { require(registry.owner(node) == msg.sender, "Not authorised"); _; } } . פתרון הפוך: contract BaseRegistrar is ERC721 { DomainRegistry public registry; bytes32 public baseNode; mapping(uint256 => uint256) public expiries; uint256 public constant GRACE_PERIOD = 90 days; function available(uint256 id) public view returns (bool) { return expiries[id] + GRACE_PERIOD < block.timestamp; } function register(uint256 id, address owner, uint256 duration) external onlyController returns (uint256) { require(available(id), "Not available"); expiries[id] = block.timestamp + duration; if (_exists(id)) { _transfer(address(0), owner, id); } else { _mint(owner, id); } registry.setSubnodeOwner(baseNode, bytes32(id), owner); return expiries[id]; } function renew(uint256 id, uint256 duration) external onlyController returns (uint256) { require(expiries[id] + GRACE_PERIOD >= block.timestamp, "Expired"); expiries[id] += duration; return expiries[id]; } } → contract PriceOracle { uint256[5] public rentPrices = [ 160e18, 40e18, 10e18, 5e18, 1e18 ]; AggregatorV3Interface public immutable usdOracle; function price(string calldata name, uint256 duration) external view returns (uint256 weiAmount) { uint256 len = strlen(name); uint256 usdPrice = rentPrices[min(len - 1, 4)]; uint256 annualUsd = usdPrice * duration / 365 days; (, int256 usdEthPrice,,,) = usdOracle.latestRoundData(); return annualUsd * 1e8 / uint256(usdEthPrice); } } . זה נחוץ להצגת שמות בממשקים. הוא מיושם באמצעות מרחב שמות הפוך מיוחד: הכתובת ממפה לרשומה alice.myns. המשתמש מגדיר את הרשומה ההפוכה בעצמו — זו הבחירה שלו איזה שם להציג.
contract ReverseRegistrar {
bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2;
function setName(string calldata name) external returns (bytes32) {
bytes32 node = claimWithResolver(msg.sender, address(defaultResolver));
defaultResolver.setName(node, name);
return node;
}
function node(address addr) public pure returns (bytes32) {
return keccak256(abi.encodePacked(ADDR_REVERSE_NODE, sha3HexAddress(addr)));
}
} האצלת תת-דומיינים ו-NameWrapper
בעל הדומיין יכול ליצור תת-דומיינים ולהאציל אותם: 0x742d..., 0x742d.... פרוטוקולים משתמשים בזה למערכות זהות מבוזרות של משתתפים. NameWrapper (תבנית ENS v2) הופך תת-דומיינים לטוקנים מסוג ERC-1155 ומוסיף מערכת הרשאות: fuses — לדוגמה, alice.myns (soulbound) או ...addr.reverse. זה מפחית את מספר החוזים ב-50% בהשוואה לגרסה הקודמת (ENS v1), מה שהופך את NameWrapper ליעיל פי 2. פרטי יישום ה-fuse: contract ReverseRegistrar { bytes32 constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2; function setName(string calldata name) external returns (bytes32) { bytes32 node = claimWithResolver(msg.sender, address(defaultResolver)); defaultResolver.setName(node, name); return node; } function node(address addr) public pure returns (bytes32) { return keccak256(abi.encodePacked(ADDR_REVERSE_NODE, sha3HexAddress(addr))); } } חוסם team.alice.myns, dao.alice.myns אוסר CANNOT_TRANSFER עבור תת-דומיינים. כל fuse הוא ביט ב-CANNOT_CREATE_SUBDOMAIN שנשרף בעת ההפעלה.
טבלת דגלי Fuse של NameWrapper
| Fuse | ערך | תיאור |
|---|---|---|
CANNOT_TRANSFER |
0x01 | אוסר העברת טוקן תת-הדומיין |
safeTransferFrom |
0x02 | אוסר יצירת תת-תת-דומיינים |
CANNOT_CREATE_SUBDOMAIN |
0x04 | אוסר שינוי ה-Resolver |
setSubnodeOwner |
0x08 | אוסר שינוי TTL |
Resolver מחוץ לשרשרת (CCIP-Read / EIP-3668)
לצורך סקלביליות, אנו משתמשים באחסון נתונים מחוץ לשרשרת עם אימות על השרשרת. ה-Resolver מחזיר שגיאת uint256 עם URL ונתוני בקשה. הלקוח שולח שאילתה ל-gateway החיצוני, מקבל תשובה חתומה, ומעביר אותה בחזרה לחוזה לאימות החתימה. CCIP-Read זול פי 10-100 מאחסון על השרשרת — אידיאלי לפרופילים ומספר גדול של רשומות טקסט. בנפח של 10,000 בקשות, gateway של CCIP-Read חוסך מעל $500 בחודש בהשוואה לאחסון מלא על השרשרת. לשילוב CCIP-Read, בצעו את השלבים הבאים:
- יישמו
CANNOT_TRANSFERב-Resolver שלכם. - פרסו שרת gateway (Node.js + ECDSA).
- הגדירו את הלקוח (ethers.js/viem) לטיפול בשגיאה.
מחסנית הטכנולוגיות של מערכת דומיינים מבוזרת ואינטגרציה
| רכיב | טכנולוגיה |
|---|---|
| חוזים חכמים | Solidity 0.8.x + OpenZeppelin |
| Chainlink Oracle | AggregatorV3Interface עבור ETH/USD |
| פתרון בצד הלקוח | ethers.js CANNOT_CREATE_SUBDOMAIN |
| אינדוקס | The Graph subgraph |
| CCIP-Read gateway | שרת Node.js + חתימת ECDSA |
פגיעויות אופייניות ושיטות הגנה
טבלת פגיעויות
| פגיעות | השלכות | הגנה |
|---|---|---|
| Reentrancy | גניבת כספים | ReentrancyGuard מ-OpenZeppelin |
| מניפולציית Oracle | מחיר שגוי | מספר Oracles + ממוצע משוקלל בזמן |
| גלישת מספרים שלמים | חישובים שגויים | בדיקה מובנית ב-Solidity 0.8+ |
| Front-running (MEV) | אובדן מחיר נוח | סכמות Commit-reveal או Subgraph |
מדוע חשובה ביקורת חוזים חכמים?
כל שגיאה ב-Registrar או ב-Price Oracle יכולה לעלות אלפי דולרים. מעל 80% מהפגיעויות הקריטיות מתגלות במהלך אימות פורמלי. אנו מבצעים ביקורות באמצעות Slither, Mythril, Echidna (fuzzing) ואימות פורמלי. הלקוחות שלנו כבר השיקו מעל 50 פרויקטים ברחבי העולם. לדוגמה, ביקורת לאחרונה על חוזה NameWrapper חשפה באג קריטי של reentrancy שיכול היה לגנוב $200,000 מכספי משתמשים.
כדי לחסוך ב-gas, השתמשו ב-CANNOT_SET_RESOLVER במקום CANNOT_SET_TTL, אצרו רשומות בקבוצות, ולגבי תת-דומיינים בחרו ב-NameWrapper עם ה-fuse OffchainLookup. זה מפחית את עלויות ה-gas בממוצע ב-20% — חיסכון של עד $2,000 לשנה בקנה מידה גדול.
שלבים להשקה מאובטחת:
- ניתוח סטטי עם Slither.
- ניתוח דינמי עם Mythril.
- Fuzzing עם Echidna.
- אימות פורמלי באמצעות Certora.
- סקירת קוד ידנית על ידי מבקרים בכירים.
שירות הביקורת שלנו עולה בין $5,000 ל-$15,000, אך מונע הפסדים פוטנציאליים של מעל $200,000. ההשקעה הכוללת לפתרון turnkey מלא נעה בדרך כלל בין $20,000 ל-$50,000, תלוי במורכבות. עסקים חוסכים עד $500 בחודש באמצעות CCIP-Read במקום אחסון מלא על השרשרת.
מה כלול בעבודה?
תוצרים
- תיעוד: תיעוד API, מדריך פריסה, הערות קוד
- גישה: מאגר Git, לוח ניהול, קישורים לסייר בלוקצ'יין
- הדרכה: סדנה של יומיים לצוות שלכם
- תמיכה לאחר השקה: 3 חודשים של תיקוני באגים ועדכונים
ההיקף המלא כולל:
- ניתוח דרישות ועיצוב ארכיטקטורה
- פיתוח חוזים חכמים (Registry, Resolver, Registrar, Price Oracle, NameWrapper)
- אינטגרציה של Chainlink Oracle ו-CCIP-Read gateway
- רכיב Frontend לפתרון שמות המבוסס על ethers.js/viem
- ביקורת קוד (Slither + Echidna) ותיקון פגיעויות
- תיעוד API ופריסה
- תמיכה לאחר השקה
לוחות זמנים משוערים
- שירות בסיסי (Registry + Resolver + Registrar + Price Oracle): 6–8 שבועות.
- מורחב (NameWrapper + פתרון הפוך + CCIP-Read gateway + אינטגרציה עם שוק): 10–14 שבועות.
- ביקורת היא חובה — 2–4 שבועות נוספים. העלות מחושבת באופן פרטני (בדרך כלל $5,000–$15,000 לביקורת מלאה). ההשקעה הכוללת לפתרון turnkey מלא נעה בדרך כלל בין $20,000 ל-$50,000, תלוי במורכבות.
הזמינו ייעוץ — נעריך את היקף העבודה ונציע את הפתרון האופטימלי. אם יש לכם שאלות על ארכיטקטורה, צרו קשר לניתוח מקדים.







