آموزش نرخ بهره در دیفای

در نگاه اول، ساده‌ترین روش برای دنبال کردن سپرده های وام دهندگان این است که مقدار USDC و زمان واریز آن را مستقیماً ثبت کنیم. اما Compound V3 این مسیر را انتخاب نمی‌کند.

این پروتکل، مشابه الگوریتم استیکینگ MasterChef در SushiSwap، سود فرضی حاصل از وام دادن 1 دلار از ابتدای راه‌اندازی پروتکل را محاسبه می‌کند. اگر با الگوریتم MasterChef آشنا نیستید، بهتر است ابتدا منبع معرفی‌شده را مطالعه کنید.

متغیر baseSupplyIndex در فایل CometStorage.sol این سود فرضی را نشان می‌دهد. این متغیر عملکردی شبیه به rewardPerTokenAccumulator در SushiSwap دارد. مقدار اولیه آن برابر 1.0 است و هر زمان که یکی از عملیات‌های تغییردهنده وضعیت مانند سپرده‌گذاری، برداشت یا وام‌گیری اتفاق بیفتد، مقدار آن با توجه به زمان سپری‌شده و نرخ بهره دوره افزایش پیدا می‌کند.

برای مثال، فرض کنید 100 ثانیه گذشته باشد و نرخ بهره برابر با 0.001 در هر ثانیه باشد (مقداری اغراق‌آمیز اما مناسب برای درک ساده‌تر). در این شرایط، مقدار baseSupplyIndex به 1.1 می‌رسد. فرمول زیر این محاسبه را انجام می‌دهد:

فقط یک نقطه در کل کد مقدار baseSupplyIndex را تغییر می‌دهد: خط 403 از فایل Comet.sol در داخل تابع accruedInterestIndices.

تابع accrueInterestIndices

از آنجایی که نرخ بهره مستقیماً به میزان استفاده (Utilization) وابسته است، شاخص baseSupplyIndex در زمان‌هایی که استفاده بالا است سریع‌تر رشد می‌کند و در دوره‌هایی که استفاده پایین است، با سرعت کمتری افزایش می‌یابد.

نمودار فرضی زیر، نحوه افزایش baseSupplyIndex و baseBorrowIndex را در شرایط مختلف استفاده نشان می‌دهد. به طور کلی، وام‌گیرندگان بهره بیشتری پرداخت می‌کنند نسبت به بهره‌ای که وام‌دهندگان دریافت می‌کنند؛ به همین دلیل، شاخص baseBorrowIndex با سرعت بیشتری رشد می‌کند.

شاخص های بهره به عنوان تابعی از بهره وری

مثال زیر نشان می‌دهد که این متغیر چگونه مورد استفاده قرار می‌گیرد.

مثال و اصطلاحات کلیدی

فرض کنید “آلیس” مبلغ 1000 دلار را در زمانی سپرده گذاری می‌کند که مقدار baseSupplyIndex برابر با 2.5 است. در این حالت، سیستم Compound به‌جای ثبت مستقیم عدد 1000 دلار برای او، مقدار 400 دلار را به عنوان سپرده در نظر می‌گیرد؛ زیرا سپرده آلیس بر اساس شاخص فعلی تقسیم می‌شود (1000 دلار ÷ 2.5 = 400 دلار).

در نتیجه، حساب آلیس یک “ارزش اصلی” (principal value) معادل 400 دلار ثبت می‌کند (در کادر زرد). این عدد همان مقداری است که قرارداد هوشمند Compound در فایل CometStorage.sol برای کاربران ذخیره می‌کند.

ساختار userBasic

اگر آلیس بلافاصله پس از سپرده‌گذاری تصمیم به برداشت بگیرد، سیستم Compound موجودی او را برابر با حاصل‌ضرب مقدار “ارزش اصلی” (400 دلار) در شاخص فعلی baseSupplyIndex محاسبه می‌کند؛ یعنی 400 × 2.5 = 1000 دلار. بنابراین، او می‌تواند همان مبلغ اولیه را برداشت کند. Compound اصطلاح «ارزش فعلی» (present value) را برای حاصل‌ضرب ارزش اصلی در شاخص baseSupplyIndex به کار می‌برد.

نکته مهم اینجاست که Compound V3 هرگز مقدار واقعی سپرده اولیه را به‌صورت مستقیم ذخیره نمی‌کند. این مقدار تنها از طریق شاخص قابل استنباط است. چون شاخص در حال حاضر 2.5 است و مقدار ثبت‌شده برای سپرده آلیس 400 دلار می‌باشد، به‌طور ضمنی مشخص است که سپرده اولیه او 1000 دلار بوده است.

مقداری که Compound V3 در قرارداد ذخیره می‌کند، یک نسخه «کوچک‌شده» یا «مقیاس‌پایین‌رفته» از سپرده واقعی است و با عنوان “ارزش اصلی” (principal value) شناخته می‌شود. زمانی که این مقدار را در baseSupplyIndex ضرب کنیم، به «ارزش فعلی» (present value) می‌رسیم؛ که در واقع معادل روز سپرده فرد است.

اگر مخاطب با مفاهیم سنتی مالی آشنا باشد، ممکن است استفاده Compound از اصطلاحات “ارزش اصلی” و “ارزش فعلی” برایش گیج‌کننده باشد. پیشنهاد می‌کنیم این مفاهیم را صرفاً در بستر تعریف‌شده توسط Compound درک کنید و آن‌ها را با معانی رایج در مالی سنتی مقایسه نکنید.

حال اگر آلیس منتظر بماند تا مقدار baseSupplyIndex به 3.0 برسد، مقدار “ارزش اصلی” او همچنان برابر با 400 دلار باقی می‌ماند، اما “ارزش فعلی” به 1200 دلار افزایش پیدا می‌کند؛ چون 400 × 3.0 = 1200 دلار.

مثال رشد ارزش فعلی

در فایل CometCore.sol، دو رابطه اصلی تعریف شده‌اند:

  • برای محاسبه “ارزش اصلی”، باید “ارزش فعلی” را بر baseSupplyIndex تقسیم کنیم

  • برای محاسبه “ارزش فعلی”، باید “ارزش اصلی” را در baseSupplyIndex ضرب کنیم

توابع ارزش اصلی و فعلی

بنابراین، برای جمع بندی مفاهیم کلیدی:

ارزش اصلی (Principal Value)

ارزش اصلی برابر است با مقدار USDC واریزشده تقسیم بر مقدار baseSupplyIndex در زمان واریز. این عدد در یک متغیر ذخیره سازی مخصوص حساب کاربر نگهداری می‌شود و فقط زمانی تغییر می‌کند که کاربر سپرده جدیدی اضافه کند یا برداشت انجام دهد. معمولاً ارزش اصلی کمتر از مقدار واقعی سپرده است، چون مقدار baseSupplyIndex همیشه برابر یا بزرگ‌تر از 1 است.

ارزش فعلی (Present Value)

ارزش فعلی از حاصل‌ضرب ارزش اصلی در مقدار فعلی baseSupplyIndex به‌دست می‌آید. این مقدار در هیچ جایی ذخیره نمی‌شود و هر بار به‌صورت لحظه‌ای (on the fly) محاسبه می‌گردد.

درک درست از دو مفهوم ارزش اصلی و ارزش فعلی برای فهم عملکرد پروتکل Compound V3 کاملاً ضروری است.

فقط متغیر “ارزش اصلی” (Principal) برای وام دهندگان دنبال می‌شود

برای روشن‌تر شدن موضوع، بیایید بار دیگر به ساختار userBasic که در بخش قبل به آن اشاره شد، نگاهی بیندازیم:

ساختار userBasic دوباره

در این ساختار:

  • متغیر baseTrackingAccrued توسط ماژول CometRewards استفاده می‌شود تا مشخص کند چه مقدار توکن COMP به خاطر مشارکت کاربر در پروتکل به او تعلق می‌گیرد.

  • متغیر baseTrackingIndex به این فرآیند مرتبط است اما در حال حاضر کاربردی ندارد.

  • متغیر assetsIn فقط در سمت وام گیرندگان استفاده می‌شود تا نشان دهد چه دارایی‌هایی به‌عنوان وثیقه فعال شده‌اند.

  • متغیر _reserved نیز در حال حاضر هیچ کاربردی ندارد.

بنابراین، تنها متغیر کلیدی برای محاسبه وضعیت وام دهندگان، همان principal یا ارزش اصلی است. نکته مهم اینجاست که این متغیر از نوع signed (علامت‌دار) تعریف شده است — یعنی برای وام گیرندگان مقدار آن منفی در نظر گرفته می‌شود.

در مورد وام‌گیرندگان، علاوه بر principal باید مقدار وثیقه هایی که سپرده گذاری کرده‌اند نیز ثبت شود؛ اما این موضوع در مقاله‌ای جداگانه بررسی خواهد شد.

تابع balanceOf() — بررسی موجودی مثبت وام دهنده

برای درک بهتر اینکه Compound V3 فقط مقدار “ارزش اصلی” (principal value) را ذخیره می‌کند ولی هنگام نمایش موجودی، از “ارزش فعلی” (present value) استفاده می‌کند، کافی است به نحوه عملکرد تابع balanceOf() در فایل Comet.sol توجه کنیم.

در این تابع که از نوع view است، ابتدا مقدار به‌روزشده baseSupplyIndex خوانده می‌شود، بدون آنکه تغییری در وضعیت قرارداد ایجاد کند. سپس مقدار principal یا همان موجودی اصلی وام دهنده خوانده می‌شود و در مقدار baseSupplyIndex ضرب می‌شود تا موجودی فعلی به‌دست آید.

از آنجا که سود حاصل از سپرده تابعی از زمان و نرخ استفاده (utilization) است، در صورتی که utilization صفر نباشد، هر بار که تابع balanceOf() فراخوانی شود، مقدار بازگشتی آن نسبت به دفعات قبلی بیشتر خواهد بود.

تابع balanceOf

اسکرین‌شات زیر رابطه بین موجودی وام دهنده در رابط کاربری اپلیکیشن غیرمتمرکز Compound V3 (dapp) و مقدار بازگشتی تابع balanceOf در Etherscan را برای همان آدرس نشان می‌دهد. هر دو عدد با یک کادر نارنجی مشخص شده‌اند تا مطابقت کامل آن‌ها قابل مشاهده باشد.

مانده حساب وام دهنده

مثال تصویری از رفتار baseSupplyIndex

زمانی که یک وام دهنده سپرده گذاری می‌کند، موجودی USDC او بر baseSupplyIndex در آن لحظه تقسیم می‌شود تا مقدار ارزش اصلی (principal value) محاسبه شود — این همان مقداری است که در قرارداد ذخیره می‌شود. هرچه زمان سپرده گذاری دیرتر باشد، مقدار baseSupplyIndex بزرگ‌تر خواهد بود و در نتیجه، کاربر اعتبار کمتری به‌عنوان ارزش اصلی دریافت می‌کند.

ارزش اصلی عددی ثابت است (مگر آنکه کاربر سپرده جدیدی وارد کند یا برداشت انجام دهد)، اما ارزش فعلی (present value) از ضرب ارزش اصلی در baseSupplyIndex به‌دست می‌آید و این مقدار به‌صورت مداوم در حال افزایش است.

در نمودار زیر:

  • آلیس مبلغ 1 دلار را زمانی سپرده گذاری کرد که baseSupplyIndex برابر با 1.01 بود. بنابراین، ارزش اصلی او برابر است با: 1 ÷ 1.01 = 0.99

  • باب نیز مبلغ 1 دلار سپرده گذاری کرد، اما در زمان دیرتری که مقدار baseSupplyIndex به 1.03 رسیده بود. بنابراین، ارزش اصلی او برابر است با: 1 ÷ 1.03 = 0.97

در نتیجه، با اینکه آلیس و باب هر دو مقدار یکسانی (1 دلار) واریز کردند، باب به‌دلیل سپرده گذاری در زمان دیرتر و رشد شاخص، مقدار کمتری به‌عنوان ارزش اصلی دریافت کرد.

نمودار ارزش فعلی و اصلی

توابع supplyBase() و withdrawBase()

زمانی که یک کاربر دارایی پایه (base asset) را سپرده گذاری یا برداشت می‌کند، مقدار ارزش اصلی (principal value) او دوباره محاسبه و به‌روزرسانی می‌شود. چنین توابعی در قراردادهای هوشمند، پایه ای ترین مفاهیم آموزش برنامه نویسی در حوزه دیفای به‌شمار می‌روند و درک نحوه عملکرد آن‌ها برای توسعه دهندگان کاملاً ضروری است.

فرض کنید آلیس 10 دلار را در زمانی سپرده گذاری کرده است که baseSupplyIndex برابر با 10 بوده. در این شرایط، ارزش اصلی او معادل 1 دلار خواهد بود (10 ÷ 10 = 1). حالا تصور کنید که با گذشت زمان، شاخص به عدد 20 افزایش پیدا می‌کند؛ بنابراین، ارزش فعلی (present value) او به 20 دلار می‌رسد (1 × 20).

در ادامه، آلیس تصمیم می‌گیرد 10 دلار دیگر هم واریز کند. پس ارزش فعلی جدید حساب او باید 30 دلار باشد (20 دلار قبلی + 10 دلار جدید).

حالا این سؤال مطرح می‌شود:

ارزش اصلی جدید او باید چقدر باشد؟
(کمی فکر کنید!)

در حال حاضر baseSupplyIndex برابر با 20 است و ارزش فعلی حساب برابر با 30 دلار است. پس ارزش اصلی جدید باید برابر باشد با:

30 ÷ 20 = 1.5

با همین منطق، تابع supplyBase() در فایل Comet.sol (در خط 829) دقیقاً همین فرآیند را هنگام سپرده گذاری یک وام دهنده انجام می‌دهد: ابتدا ارزش فعلی را محاسبه می‌کند، سپس با توجه به شاخص فعلی، مقدار ارزش اصلی جدید را محاسبه و ذخیره می‌کند.

تابع supplyBase

زمانی که آلیس سپرده گذاری می‌کند، ابتدا مقدار ارزش اصلی فعلی او از ذخیره سازی خوانده می‌شود (کادر نارنجی). سپس این مقدار به ارزش فعلی تبدیل می‌شود (کادر سبز). بعد، مقدار جدیدی که کاربر هم‌اکنون واریز کرده به آن اضافه می‌شود (کادر آبی).

مجموع به‌دست‌آمده که بیانگر ارزش فعلی جدید است، مجدداً با استفاده از شاخص baseSupplyIndex به ارزش اصلی جدید تبدیل می‌شود. این مقدار جدید در متغیری به نام dstPrincipalNew (کادر زرد) ذخیره می‌شود و در نهایت، سیستم آن را به‌عنوان ارزش اصلی به‌روزشده کاربر در ساختار داده ذخیره می‌کند (کادر قرمز).

تابع updateBasePrincipal() (کادر قرمز) وظیفه دارد مقدار UserBasic.principal مربوط به حساب کاربر را با مقدار جدید dstPrincipalNew جایگزین کند.

در سمت مقابل، تابع withdrawBase() (در خط 1051 فایل Comet.sol) همین منطق را به‌صورت معکوس پیاده سازی می‌کند: ابتدا ارزش اصلی را به ارزش فعلی تبدیل می‌کند، سپس مقدار برداشت‌شده را کم می‌کند و مقدار باقی‌مانده را مجدداً به عنوان ارزش اصلی جدید ذخیره می‌نماید.

شاخص baseBorrowIndex چیست؟

همان‌طور که baseSupplyIndex رشد سود یک دلار وام داده شده را از ابتدای راه اندازی پروتکل دنبال می‌کند، شاخص baseBorrowIndex نیز انباشت بدهی یک دلار وام گرفته شده را از همان نقطه آغاز، ردیابی می‌کند.

از آنجا که نرخ های بهره برای وام دهندگان و وام گیرندگان متفاوت است و هر کدام منحنی بهره مستقل خود را دارند، لازم است برای محاسبه دقیق، از دو شاخص مجزا استفاده شود:

  • baseSupplyIndex برای وام دهندگان

  • baseBorrowIndex برای وام گیرندگان

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

آیا ممکن است شاخص ها از حد مجاز عبور کنند؟ (Overflow)

هر دو شاخص baseSupplyIndex و baseBorrowIndex به‌صورت اعداد ممیز ثابت با 15 رقم اعشار در نظر گرفته می‌شوند. به‌عبارت دیگر، مقدار 1e15 معادل عدد 1.0 در این سیستم است.

بیشترین مقداری که یک عدد علامت‌دار 104 بیتی می‌تواند نگه دارد برابر است با 1.014e31. در نتیجه، با احتساب 15 رقم اعشار، شاخص تجمیعی (accumulator) حداکثر می‌تواند تا مقدار 1.014e16 افزایش یابد.

حال اگر فرض کنیم که نرخ بهره در پروتکل وام دهی هرگز از 100 درصد سالانه (APR) فراتر نرود، در آن صورت 53 سال طول می‌کشد تا شاخص دچار overflow شود. اگر نرخ بهره منطقی‌تری مانند 10 درصد سالانه را در نظر بگیریم، رسیدن یک دلار به مقدار 10 تریلیون دلار از طریق سود مرکب، 386 سال زمان می‌برد.

با این حساب، می‌توان با اطمینان گفت که احتمالاً نسخه چهارم پروتکل Compound خیلی زودتر از آن روز منتشر خواهد شد.

5/5 - (1 امتیاز)

راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.

دوره صفر تا صد آموزش بین المللی لینوکس
  • انتشار: ۲۶ تیر ۱۴۰۴

دسته بندی موضوعات

آخرین محصولات فروشگاه

مشاهده همه

نظرات

بازخوردهای خود را برای ما ارسال کنید