

Escrow Smart Contract
Klien dan penyedia menyetor dana ke smart contract. Penyedia dibayar setelah pekerjaan selesai dan kedua pihak setuju.
Cara kerjanya
Lima langkah menuju kontrak
Buat smart contract dengan wizard yang sederhana.
Buat smart contract dengan wizard yang sederhana.
- Langkah 1Peran dalam kontrakPilih peran Anda dalam kontrak. Hanya ada dua peran: klien dan penyedia. Tidak ada arbitrase pihak ketiga.9:41LTE
Peran dalam kontrak
- Langkah 2Pilih mata uangPilih mata uang kontrak. GRAM, mata uang asli TON, cocok untuk transaksi cepat dan kecil karena cukup volatil. USDT adalah mata uang yang lebih stabil untuk transaksi besar dan jangka panjang.9:41LTE
Pilih mata uang
- Langkah 3Nilai kontrakMasukkan nilai kontrak dalam mata uang yang dipilih. Jumlah ini akan ditransfer ke penyedia setelah pekerjaan selesai.9:41LTE
Nilai kontrak
Nilai kontrak adalah harga pekerjaan. Disetor oleh klien.
Dari 10% hingga 90%. Default: 30%.
0,4% dari nilai, tetapi tidak lebih dari 15 GRAM.
- Langkah 4Alamat dompetTempel alamat dompet. Selanjutnya, dompet ini akan digunakan untuk menyetor dan menarik dana.9:41LTE
Alamat dompet
- Langkah 5Periksa kontrakPeriksa data kontrak yang dimasukkan sebelum deploy.9:41LTE
Periksa kontrak
Peran dalam kontrak
Setelah dibuat
Kartu kontrak
Anda adalah klien, mata uangnya USDT. Enam cuplikan kartu yang sama seperti di dasbor sebenarnya, dengan semua tindakan yang tersedia di setiap langkah.
Baru dibuat
Saldo masih nol. Anda bisa menyetor nilai transaksi, langsung menyetor dengan tambahan hingga 30%, atau menghapus kontrak selama belum ada yang menyetor.
Sudah diisi, menunggu
Nilai transaksi sudah ada di kontrak, tetapi deposit penyedia belum. Selama status belum “Aktif”, Anda bisa menarik dana Anda. Deposit tambahan hingga 30% masih bisa ditambahkan.
Penyedia sudah mengisi
Kedua jumlah yang disepakati sudah ada, jadi kontrak aktif. Dana dibekukan, USDT tidak bisa ditarik. Anda bisa menerima pekerjaan, mengusulkan bagian lain, atau menambah deposit tambahan (jaminan ekstra dari klien).
Menambahkan deposit 20%
Ada +200 USDT di atas nilai transaksi. Masih 100 USDT lagi hingga batas 30%. Penarikan tetap ditutup: kontrak aktif menahan nilai kontrak ditambah deposit tambahan.
Mengusulkan 80% ke penyedia.
Penyedia akan menerima 800 USDT jika setuju. Tombol “Terima pekerjaan” hilang; tombol itu hanya muncul lagi jika Anda menetapkan 100%. Usulan bisa diganti, dan deposit tambahan bisa terus ditambah.
Penyedia setuju
Kontrak selesai: 800 USDT masuk ke penyedia, 4 USDT adalah royalti layanan. Anda masih memiliki 400 USDT yang bisa ditarik.
Q&A
Pertanyaan umum
Di mana pihak ketiga?
Tidak ada. Penyedia mempertaruhkan depositnya, jadi ia diuntungkan jika menyelesaikan pekerjaan sampai tuntas.
Secara default, deposit penyedia adalah 30% dari nilai transaksi. Mungkin terlihat bahwa ia lebih diuntungkan: ia mempertaruhkan 30%, sedangkan Anda 100%. Tetapi yang penting bukan persentasenya, melainkan uangnya. 30% itu adalah uangnya sendiri, dan kehilangannya bisa sama menyakitkannya bagi dia seperti kehilangan 100% bagi Anda. Tidak ada yang mau mengorbankan uangnya sendiri begitu saja.
Risiko lain: klien sudah menerima pekerjaan dan mungkin tidak membayar. Ada dua pilihan. Pertama, saat pekerjaan hampir selesai, tawarkan untuk melunasi lebih awal. Kedua, minta klien menyetor tambahan hingga 30% di atas nilai (deposit). Setelah kontrak selesai, klien akan mendapatkan depositnya kembali, jadi ia punya alasan untuk menutup transaksi (kontrak).
Mengapa saya harus memercayai Anda?
Anda tidak perlu percaya. Smart contract ini bersifat publik. Alamat kontrak Anda adalah hash dari data awal dan kodenya. Jika kodenya diganti, alamatnya akan berbeda.
Kontrak bisa dibuka dan diperiksa (misalnya di Tonviewer). Satu-satunya hal: Anda perlu memahami kodenya — di situlah masalah utamanya. Namun ini bukti 100%. Anda juga bisa memberikan kodenya ke model AI. Di bawah ini adalah kode kontrak dengan penjelasan yang sesederhana mungkin.
ContractUSDT
Ini daftar impor: konstanta dan fungsi bantu.
1import "@contracts/common-messages"2import "@contracts/errors"3import "@contracts/contract_USDT/usdt-utils"4import "fees-management"5import "messages"6import "storage"Gambaran singkat kontrak.
8contract ContractUSDT {9 author: "Alexandr_K"10 version: "0.0.4"11 description: "Escrow contract USDT"12 storage: StorageUsdt13 incomingMessages: AllowedMessage14}Daftar tindakan yang tersedia pada kontrak.
16type AllowedMessage =17 | TransferNotificationForRecipient18 | ChangePercent19 | AcceptPercent20 | RefundUsdt21 | ErrRefund22 | ErrMode23 | BurnContractFungsi bantu: apakah kontrak aktif atau tidak. Setelah semua pihak menyetor jumlahnya, kontrak menjadi aktif dan dana tidak bisa lagi ditarik begitu saja.
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}Fungsi untuk kembalian. Setiap pesan membawa sedikit GRAM untuk memproses transaksi. Kelebihannya dikembalikan oleh fungsi ini.
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}Menghitung alamat dompet jetton. Alamat ini bergantung pada alamat kontrak.
78fun contractUsdtWallet(): address {79 return calcAddressOfJettonWallet(contract.getAddress());80}Jika transfer gagal, uang kembali dan kontrak memperbaiki catatannya: siapa memiliki berapa.
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}Jika isi pesan kosong, tidak ada yang dilakukan.
131fun onInternalMessage(in: InMessage) {132 if (in.body.isEmpty()) {133 return;134 }Selanjutnya deklarasi variabel.
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;Isi saldo USDT: menerima transfer dan mencatat berapa USDT yang dimiliki masing-masing pihak.
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}Mengubah persentase pembayaran. Jika pekerjaan baru selesai separuh, Anda bisa mengusulkan 50%. Jika penyedia setuju, kontrak terpenuhi. Jika tidak, lanjutkan negosiasi.
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}Jika penyedia setuju, kontrak membayarkan bagiannya ditambah depositnya, sisanya untuk Anda. Pada 0%, seluruh transaksi dibatalkan dan semua orang mendapatkan uangnya kembali. Dalam hal ini, biaya 0,4% tidak dibayarkan ke kontrak.
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}Blok terbesar: mengelola pengembalian dana.
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}Mode kesalahan. Jika dompet hilang atau dana masuk tanpa tercatat, satu pihak dapat menyerahkan kendali kepada pihak lain. Situasi “tidak tercatat” sebenarnya mustahil, kecuali dilakukan dengan sengaja. Tetapi biarlah ada. Sedangkan lupa atau kehilangan dompet sangat mungkin terjadi.
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: penarikan darurat jetton yang masuk tanpa notifikasi dan tidak tercatat. Atau saat dompet hilang (lebih tepatnya, kata sandinya hilang).
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: memusnahkan kontrak agar cangkang kosong tidak tertinggal di jaringan. Yang bisa membakarnya adalah pembuatnya (klien atau penyedia) atau admin, dan hanya jika saldo klien dan penyedia nol.
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}Kode sumber terverifikasi dari tiga kontrak. Hash kode cocok dengan yang ada di jaringan. Setiap tautan membuka kode di TON Verifier.
ContractMinterContractUSDTContractTON