פיתוח מערכת ניהול אישורי טוקנים (ביטול הרשאות)
נתקלנו במצב: למשתמש יש מאות אישורי טוקנים שאינם בשימוש, אחד הפרוטוקולים נפרץ — והארנק מתרוקן. ללא מערכת ניהול אישורים, כל פרוטוקול שנפרץ יכול לרוקן את הכספים שלך. הצוות שלנו מפתח פתרונות מותאמים אישית לצפייה וביטול אישורי טוקנים — מממשק חד-שרשרתי פשוט ועד מערכת רב-שרשרתית עם דירוג סיכונים וביטול בכמות גדולה. פיתוח כזה אורך בין 2 ל-5 ימים בהתאם למורכבות, ואנחנו עושים זאת במפתח מלא: מניתוח נתונים ועד פריסה. צור קשר להערכת פרויקט.
יסודות טכניים: כיצד פועלים אישורים
ERC-20 Allowance
תקן ERC-20 מגדיר allowance(owner, spender) — כמה טוקנים המוציא (spender) יכול לבזבז בשם הבעלים. הוא מוגדר באמצעות approve(spender, amount). ערך של type(uint256).max (2^256-1) פירושו "בלתי מוגבל" — רוב הפרוטוקולים דורשים זאת לנוחות.
בעיה: לאישור אין תפוגה. אין מנגנון ביטול אוטומטי. אם פרוטוקול נפרץ שנה אחרי האישור שלך — האישור עדיין פעיל.
אישורי ERC-721 ו-ERC-1155
עבור NFT, ישנם שני סוגי אישורים:
-
approve(operator, tokenId)— הרשאה לטוקן ספציפי -
setApprovalForAll(operator, true)— גישה מלאה לכל האוסף
setApprovalForAll משמש את OpenSea, blur.io ופלטפורמות מסחר אחרות. זהו הסוג המסוכן ביותר — שוק אחד שנפרץ עם setApprovalForAll פעיל שווה לאוסף אבוד.
EIP-2612: Permit
permit(owner, spender, value, deadline, v, r, s) — חתימה במקום עסקה. הוא לא יוצר אישור קבוע, פועל פעם אחת עם מועד אחרון מוגדר. dApps מתוכננים היטב משתמשים ב-permit במקום approve.
אבל ל-permit יש ניואנס: אם DAI, USDC או טוקן אחר תומך ב-permit — האישור דרך permit עדיין ניתן לצפייה באמצעות allowance(). הם בלתי ניתנים להבחנה מ-approve רגיל.
קריאת נתוני אישורים
דרך אירוע Approval
קריאה ישירה ל-allowance(owner, spender) דורשת ידיעת כתובת המוציא. כדי לקבל את כל האישורים הפעילים עבור ארנק — יש לקרוא אירועים:
import { createPublicClient, http, parseAbi } from 'viem';
const ERC20_ABI = parseAbi([
'event Approval(address indexed owner, address indexed spender, uint256 value)',
'function allowance(address owner, address spender) view returns (uint256)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
]);
async function getTokenApprovals(ownerAddress: `0x${string}`) {
const client = createPublicClient({
chain: mainnet,
transport: http(RPC_URL)
});
const approvalLogs = await client.getLogs({
event: ERC20_ABI[0],
args: { owner: ownerAddress },
fromBlock: 0n,
toBlock: 'latest'
});
const latestApprovals = new Map<string, typeof approvalLogs[0]>();
for (const log of approvalLogs) {
const key = `${log.address}-${log.args.spender}`;
latestApprovals.set(key, log);
}
const results = await Promise.all(
Array.from(latestApprovals.values()).map(async (log) => {
const [allowance, symbol, decimals] = await Promise.all([
client.readContract({
address: log.address,
abi: ERC20_ABI,
functionName: 'allowance',
args: [ownerAddress, log.args.spender!]
}),
client.readContract({
address: log.address,
abi: ERC20_ABI,
functionName: 'symbol'
}),
client.readContract({
address: log.address,
abi: ERC20_ABI,
functionName: 'decimals'
}),
]);
return {
tokenAddress: log.address,
spenderAddress: log.args.spender!,
allowance,
symbol,
decimals,
isUnlimited: allowance === BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'),
};
})
);
return results.filter(r => r.allowance > 0n);
} בעיה עם נתונים היסטוריים
import { createPublicClient, http, parseAbi } from 'viem'; const ERC20_ABI = parseAbi([ 'event Approval(address indexed owner, address indexed spender, uint256 value)', 'function allowance(address owner, address spender) view returns (uint256)', 'function symbol() view returns (string)', 'function decimals() view returns (uint8)', ]); async function getTokenApprovals(ownerAddress: `0x${string}`) { const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) }); const approvalLogs = await client.getLogs({ event: ERC20_ABI[0], args: { owner: ownerAddress }, fromBlock: 0n, toBlock: 'latest' }); const latestApprovals = new Map<string, typeof approvalLogs[0]>(); for (const log of approvalLogs) { const key = `${log.address}-${log.args.spender}`; latestApprovals.set(key, log); } const results = await Promise.all( Array.from(latestApprovals.values()).map(async (log) => { const [allowance, symbol, decimals] = await Promise.all([ client.readContract({ address: log.address, abi: ERC20_ABI, functionName: 'allowance', args: [ownerAddress, log.args.spender!] }), client.readContract({ address: log.address, abi: ERC20_ABI, functionName: 'symbol' }), client.readContract({ address: log.address, abi: ERC20_ABI, functionName: 'decimals' }), ]); return { tokenAddress: log.address, spenderAddress: log.args.spender!, allowance, symbol, decimals, isUnlimited: allowance === BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'), }; }) ); return results.filter(r => r.allowance > 0n); } עם getLogs איטי ויקר עבור RPC ציבוריים. פתרונות:
- The Graph: אינדוקס של אירועי Approval דרך subgraph, שאילתות GraphQL מיידיות
- Etherscan/Alchemy API: נקודות קצה מוכנות לאישורי טוקנים (
fromBlock: 0n) - אינדוקס מצטבר: מעקב אחר הבלוק האחרון שמונפק, בקשת אירועים חדשים בלבד בכל עדכון
עבור מערכות ייצור, subgraph של The Graph הוא הפתרון האופטימלי. שאילתה אחת מחזירה את כל האישורים הפעילים עם מטא-דאטה.
מדוע ביטול בכמות גדולה הוא משימה מורכבת?
ביטול 10 אישורים דרך ERC-20 דורש 10 עסקאות נפרדות, כל אחת עם חתימת המשתמש. זה בלתי מקובל מבחינת חוויית משתמש. Multicall לא עוזר כי alchemy_getTokenAllowances הוא פעולה בשם המשתמש, הדורשת חתימה. פתרון — תור עסקאות עם המשך אוטומטי לאחר אישור. המשתמש חותם על כל אחת אבל רואה התקדמות "מבטל 3 מתוך 8…". לחלופין, שימוש ב-Permit2 לביטול בכמות גדולה, אם המשתמש העביר את האישורים שלו ל-Permit2 — זה מפחית עלויות גז בעד 70%.
כיצד מתבצע ביטול טוקנים?
ביטול ERC-20
ביטול = approve. עסקה אחת לכל זוג טוקן+מוציא.
async function revokeERC20Approval(
tokenAddress: `0x${string}`,
spenderAddress: `0x${string}`
) {
const { writeContract } = useWriteContract();
writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, 0n]
});
} ביטול ERC-721 / ERC-1155
approve(spender, 0) — ביטול גישה מלאה לאוסף. קריטי יותר, ולכן מודגש באדום בממשק. async function revokeERC20Approval( tokenAddress: `0x${string}`, spenderAddress: `0x${string}` ) { const { writeContract } = useWriteContract(); writeContract({ address: tokenAddress, abi: erc20Abi, functionName: 'approve', args: [spenderAddress, 0n] }); } ואחריו ביטול פחות קריטי — גישה לטוקן ספציפי.
עיצוב ממשק המשתמש של המערכת
הרכיב המרכזי הוא טבלה עם מיון וסינון:
| טוקן | מוציא | אישור | סיכון | פעולה |
|---|---|---|---|---|
| USDC | Uniswap V3 | בלתי מוגבל | בינוני | בטל |
| WETH | פרוטוקול ישן (לא פעיל) | בלתי מוגבל | גבוה | בטל |
| DAI | Aave V3 | 1,000 DAI | נמוך | בטל |
דירוג סיכונים הוא תכונת UX חשובה. כתובות מוציא מזוהות באמצעות:
- Etherscan Labels API
- מאגר פרוטוקולים של DefiLlama
- רשימה לבנה מותאמת אישית של פרוטוקולים ידועים
פרוטוקול מאומת = סיכון בינוני (קיים אישור, אבל הפרוטוקול אמין). חוזה לא ידוע = סיכון גבוה. חוזה לא פעיל/מת = סיכון קריטי.
סינונים: לפי רשת, לפי סוג (ERC-20 / NFT), לפי רמת סיכון, רק אישורים בלתי מוגבלים.
השוואת גישות לאחזור נתונים
| גישה | מהירות | מורכבות | גז עבור שאילתות |
|---|---|---|---|
| אירועי RPC | איטית | נמוכה | גבוה (קריאות רבות) |
| The Graph | פי 10 מהיר יותר | בינונית (נדרש subgraph) | אפס (GraphQL) |
| Alchemy API | מהירה | נמוכה (נקודת קצה מוכנה) | מבוסס מנוי |
מה כלול בעבודה שלנו
- פיתוח ממשק משתמש (React + wagmi + TanStack Table) עם טבלת אישורים
- אינטגרציה עם מקור נתונים נבחר (RPC, The Graph, Alchemy)
- יישום ביטול בכמות גדולה דרך תור עסקאות
- תמיכה רב-שרשרתית (Ethereum, Arbitrum, Polygon, Base, BNB Chain)
- דירוג סיכונים מבוסס רשימה לבנה ו-API חיצוניים
- תיעוד API והוראות משתמש
- פריסה על הדומיין שלך או white-label
החברה במספרים
מעל 5 שנות ניסיון בפיתוח בלוקצ'יין, למעלה מ-30 פרויקטים מיושמים, כולל פרוטוקולי DeFi ופלטפורמות NFT. הצוות שלנו מורכב ממהנדסים בכירים עם ידע עמוק ב-Solidity, Rust (Solana) ו-TypeScript.
לוחות זמנים משוערים
מערכת בסיסית לרשת אחת (ERC-20, אירועי RPC) — מיומיים. רב-שרשרתית עם NFT ו-The Graph — מ-5 ימים. העלות המדויקת מחושבת באופן אישי — צור קשר לייעוץ.







