

Escrow Smart Contract
مشتری و ارائهدهنده پول را در قرارداد هوشمند واریز میکنند. وقتی کار انجام شد و هر دو طرف موافق بودند، پرداخت به ارائهدهنده میرسد.
چگونه کار میکند
پنج گام تا قرارداد
با یک راهنمای ساده، قرارداد هوشمند بسازید.
با یک راهنمای ساده، قرارداد هوشمند بسازید.
- گام 1نقش در قراردادنقش خود را در قرارداد انتخاب کنید. قرارداد فقط دو نقش دارد — مشتری و ارائهدهنده. داور شخص ثالثی وجود ندارد.9:41LTE
نقش در قرارداد
- گام 2انتخاب ارزارز قرارداد را انتخاب کنید. GRAM — ارز بومی TON — برای معاملات سریع و کوچک مناسب است، چون نسبتاً پرنوسان است. USDT ارز پایدارتری برای معاملات بزرگ و بلندمدت است.9:41LTE
انتخاب ارز
- گام 3مبلغ قراردادمبلغ قرارداد را به ارز انتخابشده وارد کنید. این مبلغ پس از انجام کار به ارائهدهنده منتقل میشود.9:41LTE
مبلغ قرارداد
مبلغ قرارداد بهای کار است. مشتری آن را واریز میکند.
از 10٪ تا 90٪. پیشفرض: 30%.
0,4% از مبلغ، اما نه بیشتر از 15 GRAM.
- گام 4نشانیهای کیف پولنشانیهای کیف پول را جایگذاری کنید. بعدها واریز و برداشت پول از طریق همین کیف پولها انجام میشود.9:41LTE
نشانیهای کیف پول
- گام 5بررسی قراردادپیش از دیپلوی، اطلاعات واردشدهٔ قرارداد را بررسی کنید.9:41LTE
بررسی قرارداد
نقش در قرارداد
پس از ایجاد
کارت قرارداد
شما مشتری هستید و ارز USDT است. شش تصویر از همان کارتی که در پنل واقعی هست، با همهٔ اقدامات ممکن در هر مرحله.
تازه ایجاد شده
موجودیها صفر است. میتوانید مبلغ معامله را، یا یکباره همراه با مبلغ اضافی تا 30٪ واریز کنید، یا تا وقتی کسی چیزی واریز نکرده، قرارداد را حذف کنید.
مبلغ واریز شد، در انتظاریم
مبلغ معامله در قرارداد است، اما سپردهٔ ارائهدهنده هنوز نیامده. تا وقتی وضعیت «فعال» نشده، میتوانید پول خود را برداشت کنید. هنوز میتوانید مبلغ اضافی تا 30٪ را تکمیل کنید.
ارائهدهنده واریز کرد
هر دو مبلغ توافقشده موجود است — قرارداد فعال است. مبالغ مسدود شدهاند و برداشت USDT ممکن نیست. میتوانید کار را بپذیرید، سهم دیگری پیشنهاد دهید یا مبلغ اضافی (سپردهٔ اضافی مشتری) واریز کنید.
سپردهٔ 20٪ اضافه شد
+200 USDT بیش از مبلغ معامله وجود دارد. تا سقف 30٪ هنوز 100 USDT جا هست. برداشت همچنان بسته است: قرارداد فعال مبلغ قرارداد بهعلاوهٔ مبلغ اضافی را نگه میدارد.
80٪ به ارائهدهنده پیشنهاد شد.
اگر ارائهدهنده موافقت کند، 800 USDT دریافت میکند. دکمهٔ «پذیرش کار» ناپدید شد؛ فقط اگر 100٪ تعیین کنید دوباره ظاهر میشود. پیشنهاد را میتوان عوض کرد و مبلغ اضافی را افزایش داد.
ارائهدهنده موافقت کرد
قرارداد تکمیل شد: 800 USDT به ارائهدهنده رسید و 4 USDT حقالامتیاز سرویس شد. 400 USDT برای شما مانده که میتوانید برداشت کنید.
Q&A
پرسشهای متداول
طرف سوم کجاست؟
وجود ندارد. ارائهدهنده سپردهاش را به خطر میاندازد. به نفع اوست که کار را تا آخر انجام دهد.
سپردهٔ ارائهدهنده بهطور پیشفرض 30% مبلغ معامله است. شاید به نظر برسد که او برتری دارد: او 30% را به خطر میاندازد و شما 100% را. اما آنچه مهم است درصد نیست، بلکه پول است. آن 30% پول خود اوست و از دست دادنش ممکن است برای او همانقدر دردناک باشد که از دست دادن 100% برای شما. هیچکس بیدلیل پول خودش را فدا نمیکند.
خطر دیگر: مشتری کار را تحویل گرفته و ممکن است پول را ندهد. در این حالت دو راه هست. اول — وقتی کار تقریباً آماده است، پیشنهاد تسویه زودتر بدهید. دوم — از مشتری بخواهید مبلغ اضافی تا 30٪ بیش از مبلغ (سپرده) واریز کند. پس از تکمیل قرارداد، مشتری سپردهاش را پس میگیرد، پس دلیلی برای بستن معامله (قرارداد) دارد.
چرا باید به شما اعتماد کنم؟
لازم نیست اعتماد کنید. قرارداد هوشمند عمومی است. نشانی قرارداد شما هش دادههای اولیه و کد است. اگر کد عوض شود، نشانی دیگری خواهد بود.
میتوان آن را باز کرد و دید (مثلاً در Tonviewer). تنها نکته این است که باید کد را فهمید — مشکل اصلی همین است. اما در عوض، این اثبات 100٪ است. میتوانید کد را به یک مدل هوش مصنوعی هم بدهید. در ادامه کد قرارداد با سادهترین توضیح ممکن آمده است.
ContractUSDT
این فهرست واردسازیهاست: ثابتها و توابع کمکی.
1import "@contracts/common-messages"2import "@contracts/errors"3import "@contracts/contract_USDT/usdt-utils"4import "fees-management"5import "messages"6import "storage"توصیف کوتاه قرارداد.
8contract ContractUSDT {9 author: "Alexandr_K"10 version: "0.0.4"11 description: "Escrow contract USDT"12 storage: StorageUsdt13 incomingMessages: AllowedMessage14}فهرست اقدامات در دسترس روی قرارداد.
16type AllowedMessage =17 | TransferNotificationForRecipient18 | ChangePercent19 | AcceptPercent20 | RefundUsdt21 | ErrRefund22 | ErrMode23 | BurnContractتابع کمکی: قرارداد فعال است یا نه. وقتی همه مبالغ خود را واریز کردند، قرارداد فعال میشود — دیگر نمیتوان همینطوری برداشت کرد.
28fun StorageUsdt.isActive(self, extra: ExtraFieldsUsdt): bool {29 if (extra.completed) {30 return false;31 }32 return self.clientAmount >= extra.clientAmountInit &&33 self.providerDeposit >= extra.providerDepositInit;34}تابع باقیمانده. به هر پیام کمی GRAM برای پردازش تراکنش ضمیمه میشود. این تابع مازاد را برمیگرداند.
67fun sendExcessesTon(to: address, amount: coins) {68 if (amount > MIN_MESSAGE_VALUE_BOUND) {69 val excessesMsg = createMessage({70 bounce: BounceMode.NoBounce,71 dest: to,72 value: amount,73 });74 excessesMsg.send(SEND_MODE_IGNORE_ERRORS);75 }76}نشانی کیف پول جتون را محاسبه میکند. این نشانی به نشانی قرارداد بستگی دارد.
78fun contractUsdtWallet(): address {79 return calcAddressOfJettonWallet(contract.getAddress());80}اگر انتقال ناموفق باشد — پول برمیگردد و قرارداد سوابق را اصلاح میکند: چه کسی چقدر دارد.
105fun onBouncedMessage(in: InMessageBounced) {106 in.bouncedBody.skipBouncedPrefix();107 val msg = lazy BounceOpToHandle.fromSlice(in.bouncedBody);108 var storage = lazy StorageUsdt.load();109 match (msg) {110 TransferBounce => {111 if (msg.queryId == QueryRole.Client as uint64) {112 storage.clientAmount += msg.jettonAmount;113 } else if (msg.queryId == QueryRole.Provider as uint64) {114 storage.providerDeposit += msg.jettonAmount;115 } else if (msg.queryId == QueryRole.Admin as uint64) {116 var extra: ExtraFieldsUsdt = lazy storage.extra.load();117 extra.royaltyInit += msg.jettonAmount;118 storage.extra = extra.toCell();119 } else {120 // ErrRefund (QueryRole.None) и неизвестные id: учёт не уменьшали121 return;122 }123 }124 }125 storage.save();126}اگر بدنهٔ پیام خالی باشد — کاری نمیکنیم.
131fun onInternalMessage(in: InMessage) {132 if (in.body.isEmpty()) {133 return;134 }در ادامه، تعریف متغیرها.
137val msg = lazy AllowedMessage.fromSlice(in.body);138var storage: StorageUsdt = lazy StorageUsdt.load();139val tonBalanceBeforeMsg = contract.getOriginalBalance() - in.valueCoins;140val storageFee = MIN_TONS_FOR_STORAGE_THIS_CONTRACT -141min(tonBalanceBeforeMsg, MIN_TONS_FOR_STORAGE_THIS_CONTRACT);142var msgValue = in.valueCoins - storageFee - CONTRACT_GAS_CONSUMPTION;واریز USDT: انتقالها را میپذیرد و ثبت میکند که هر کس چقدر USDT دارد.
145TransferNotificationForRecipient => {146 assert (in.senderAddress == contractUsdtWallet()) throw Errors.InvalidWallet;147 assert (msg.transferInitiator != null) throw Errors.InvalidData;148 if (msg.transferInitiator == storage.clientAddress) {149 storage.clientAmount += msg.jettonAmount;150 storage.save();151 } else if (msg.transferInitiator == storage.providerAddress) {152 storage.providerDeposit += msg.jettonAmount;153 storage.save();154 }155 sendExcessesTon(msg.transferInitiator, msgValue);156}درصد پرداخت را تغییر میدهد. اگر کار فقط نیمهتمام است، میتوان 50٪ پیشنهاد داد. ارائهدهنده موافقت کرد — قرارداد اجرا شد. نه — باز هم مذاکره کنید.
162ChangePercent => {163 assert (msgValue > 0) throw Errors.NotEnoughGas;164 assert (in.senderAddress == storage.clientAddress) throw Errors.UnauthorizedSender;165 assert (msg.percent >= 0 && msg.percent <= 100) throw Errors.WrongPercent;166 var extra: ExtraFieldsUsdt = lazy storage.extra.load();167 assert (extra.completed == false) throw Errors.ContractIsCompleted;168 assert (storage.isActive(extra)) throw Errors.ContractIsNotActive;169 extra.percent = msg.percent;170 if (msg.percent == 100) {171 extra.completed = true;172 storage.providerDeposit += extra.clientAmountInit - extra.royaltyInit;173 storage.clientAmount -= extra.clientAmountInit;174 }175 storage.extra = extra.toCell();176 storage.save();177 sendExcessesTon(in.senderAddress, msgValue);178}اگر ارائهدهنده موافقت کند، قرارداد سهم او بهعلاوهٔ سپردهاش را به او میپردازد و باقیمانده به شما میرسد. در 0٪ کل معامله لغو میشود و همه پولشان را پس میگیرند. در این حالت کارمزد ۰٫۴% به قرارداد پرداخت نمیشود.
183AcceptPercent => {184 assert (msgValue > 0) throw Errors.NotEnoughGas;185 assert (in.senderAddress == storage.providerAddress) throw Errors.UnauthorizedSender;186 var extra: ExtraFieldsUsdt = lazy storage.extra.load();187 assert (extra.completed == false) throw Errors.ContractIsCompleted;188 assert (extra.percent != 100) throw Errors.WrongPercent;189 assert (msg.percent == extra.percent) throw Errors.WrongPercent;190 extra.completed = true;191 val percentValue = extra.clientAmountInit / 100 * msg.percent;192 if (msg.percent == 0) {193 extra.royaltyInit = 0;194 }195 storage.providerDeposit += percentValue - extra.royaltyInit;196 storage.clientAmount -= percentValue;197 storage.extra = extra.toCell();198 storage.save();199 sendExcessesTon(in.senderAddress, msgValue);200}بزرگترین بخش — بازگشت وجوه را مدیریت میکند.
208RefundUsdt => {209 msgValue -= (MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT);210 assert (msgValue > 0) throw Errors.NotEnoughGas;211 assert (msg.jettonAmount > 0) throw Errors.InvalidData;212 var extra: ExtraFieldsUsdt = lazy storage.extra.load();213 val transferRecipient: address = msg.addressRefund != null214 ? msg.addressRefund215 : in.senderAddress;216 val isActive = storage.isActive(extra);217 var queryId: uint64;218 var jettonAmount: coins;219 if (in.senderAddress == storage.clientAddress) {220 queryId = QueryRole.Client;221 if (isActive) {222 val deposit: uint72 = (extra.clientAmountInit / 100) * 30;223 val locked = extra.clientAmountInit + deposit;224 assert (storage.clientAmount > locked) throw Errors.NotEnoughJettons;225 jettonAmount = min(storage.clientAmount - locked, msg.jettonAmount);226 } else {227 jettonAmount = min(storage.clientAmount, msg.jettonAmount);228 }229 storage.clientAmount -= jettonAmount;230 } else if (in.senderAddress == storage.providerAddress) {231 queryId = QueryRole.Provider;232 if (isActive) {233 assert (234 storage.providerDeposit > extra.providerDepositInit235 ) throw Errors.NotEnoughJettons;236 jettonAmount = min(237 storage.providerDeposit - extra.providerDepositInit,238 msg.jettonAmount,239 );240 } else {241 jettonAmount = min(storage.providerDeposit, msg.jettonAmount);242 }243 storage.providerDeposit -= jettonAmount;244 } else if (in.senderAddress == extra.adminAddress) {245 queryId = QueryRole.Admin;246 val maxRefundUsdt: uint72 = extra.completed ? extra.royaltyInit : 0;247 jettonAmount = min(maxRefundUsdt, msg.jettonAmount);248 extra.royaltyInit -= jettonAmount;249 storage.extra = extra.toCell();250 } else {251 throw Errors.UnauthorizedSender;252 }253 assert (jettonAmount > 0) throw Errors.NotEnoughJettons;254 storage.save();255 sendAskToTransferUsdt(256 queryId,257 jettonAmount,258 transferRecipient,259 msgValue + MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT,260 );261}حالت خطا. اگر کیف پول گم شود یا پول خارج از ثبت عبور کرده باشد، یک طرف میتواند کنترل را به طرف دیگر بدهد. وضعیت «خارج از ثبت» در واقع ناممکن است، مگر اینکه عمداً انجام شود. اما بگذار باشد. ولی فراموش کردن یا گم کردن کیف پول کاملاً ممکن است.
271ErrMode => {272 assert (msgValue > 0) throw Errors.NotEnoughGas;273 var extra: ExtraFieldsUsdt = lazy storage.extra.load();274 var noticeTo: address? = null;275 var noticeAboutClient: bool = false;276 if (in.senderAddress == storage.clientAddress) {277 if (msg.value == ErrModeVal.ProviderFullRights as int8) {278 extra.errMode = ErrModeVal.ProviderFullRights;279 } else if (msg.value == ErrModeVal.BlockErrorMode as int8) {280 extra.errMode = ErrModeVal.BlockErrorMode;281 } else if (282 msg.value == ErrModeVal.ClientFullRightsAfterHalfYear as int8 &&283 extra.errMode != ErrModeVal.BlockErrorMode as int8284 ) {285 extra.errMode = ErrModeVal.ClientFullRightsAfterHalfYear;286 extra.errModeAt = blockchain.now() as uint64;287 noticeTo = storage.providerAddress;288 noticeAboutClient = true;289 }290 } else if (in.senderAddress == storage.providerAddress) {291 if (msg.value == ErrModeVal.ClientFullRights as int8) {292 extra.errMode = ErrModeVal.ClientFullRights;293 } else if (msg.value == ErrModeVal.BlockErrorMode as int8) {294 extra.errMode = ErrModeVal.BlockErrorMode;295 } else if (296 msg.value == ErrModeVal.ProviderFullRightsAfterHalfYear as int8 &&297 extra.errMode != ErrModeVal.BlockErrorMode as int8298 ) {299 extra.errMode = ErrModeVal.ProviderFullRightsAfterHalfYear;300 extra.errModeAt = blockchain.now() as uint64;301 noticeTo = storage.clientAddress;302 noticeAboutClient = false;303 }304 } else {305 throw Errors.UnauthorizedSender;306 }307 if (noticeTo != null) {308 assert (msgValue > HALF_YEAR_NOTICE_VALUE) throw Errors.NotEnoughGas;309 msgValue -= HALF_YEAR_NOTICE_VALUE;310 }311 storage.extra = extra.toCell();312 storage.save();313 if (noticeTo != null) {314 sendHalfYearControlNotice(noticeTo, noticeAboutClient);315 }316 sendExcessesTon(in.senderAddress, msgValue);317}10enum ErrModeVal {11 ClientFullRights = 112 ProviderFullRights = 213 ClientFullRightsAfterHalfYear = 16 // клиент получает полные права через полгода14 ProviderFullRightsAfterHalfYear = 26 // исполнитель — через полгода15 BlockErrorMode = 9916}ErrRefund — برداشت اضطراری جتونهایی که بدون اعلان رسیدهاند و ثبت نشدهاند. یا وقتی کیف پول گم شده است (دقیقتر بگوییم، رمز عبور آن گم شده است).
321ErrRefund => {322 msgValue -= (MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT);323 assert (msgValue > 0) throw Errors.NotEnoughGas;324 assert (msg.jettonAmount > 0) throw Errors.InvalidData;325 val extra: ExtraFieldsUsdt = lazy storage.extra.load();326 if (327 !extra.allowsErrRefund(328 in.senderAddress,329 storage.clientAddress,330 storage.providerAddress,331 )332 ) {333 throw Errors.UnauthorizedSender;334 }335 val transferRecipient: address = msg.addressRefund != null336 ? msg.addressRefund337 : in.senderAddress;338 sendAskToTransferUsdt(339 QueryRole.None,340 msg.jettonAmount as coins,341 transferRecipient,342 msgValue + MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT,343 );344}36fun ExtraFieldsUsdt.allowsErrRefund(37 self,38 sender: address,39 client: address,40 provider: address,41): bool {42 if (sender == client) {43 if (self.errMode == ErrModeVal.ClientFullRights as int8) {44 return true;45 }46 return self.errMode == ErrModeVal.ClientFullRightsAfterHalfYear as int8 &&47 (blockchain.now() as uint64) >= self.errModeAt + HALF_YEAR;48 }49 if (sender == provider) {50 if (self.errMode == ErrModeVal.ProviderFullRights as int8) {51 return true;52 }53 return self.errMode == ErrModeVal.ProviderFullRightsAfterHalfYear as int8 &&54 (blockchain.now() as uint64) >= self.errModeAt + HALF_YEAR;55 }56 return false;57}Burn — نابود کردن قرارداد تا پوستهٔ خالی در شبکه سرگردان نماند. کسی که آن را ایجاد کرده (مشتری یا ارائهدهنده) یا ادمین میتواند آن را بسوزاند — و فقط وقتی موجودی مشتری و ارائهدهنده صفر باشد.
351BurnContract => {352 assert (msgValue > 0) throw Errors.NotEnoughGas;353 val extra: ExtraFieldsUsdt = lazy storage.extra.load();354 val deployer = extra.deployerAddress(storage.clientAddress, storage.providerAddress);355 assert (356 in.senderAddress == deployer || in.senderAddress == extra.adminAddress357 ) throw Errors.UnauthorizedSender;358 assert (359 storage.clientAmount == 0 && storage.providerDeposit == 0360 ) throw Errors.BalancesNotEmpty;361 if (extra.completed && extra.royaltyInit > 0) {362 val jettonTon = MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT;363 msgValue -= jettonTon;364 assert (msgValue > 0) throw Errors.NotEnoughGas;365 sendAskToTransferUsdt(366 QueryRole.Admin,367 extra.royaltyInit as coins,368 extra.adminAddress,369 msgValue + jettonTon,370 );371 }372 val burn = createMessage({373 bounce: BounceMode.NoBounce,374 dest: in.senderAddress,375 value: 0,376 });377 burn.send(SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_DESTROY);378}59fun ExtraFieldsUsdt.deployerAddress(self, client: address, provider: address): address {60 if (self.mode == QueryRole.Client as int8) {61 return client;62 }63 assert (self.mode == QueryRole.Provider as int8) throw Errors.UnauthorizedSender;64 return provider;65}کد منبع تأییدشدهٔ سه قرارداد. هش کد با آنچه روی شبکه است یکی است. هر پیوند منبع را در TON Verifier باز میکند.
ContractMinterContractUSDTContractTON