Esta é a versão de teste.

MainnetJettons de teste
TON Blockchain

Escrow Smart Contract

Cliente e prestador depositam o dinheiro em um smart contract. O prestador recebe quando o trabalho está feito e os dois lados concordam.

Como funciona

Cinco passos até o contrato

Crie um smart contract com um assistente simples.

  1. Passo 1Papel no contratoEscolha seu papel no contrato. Há apenas dois papéis: cliente e prestador. Não existe arbitragem de terceiros.
    9:41LTE

    Papel no contrato

    Papel no contrato

    O cliente é quem contrata o serviço ou o produto. Paga 100% antecipadamente, mais um depósito reembolsável opcional de até 30%.

    O prestador é quem fornece o serviço ou o produto. Faz um depósito reembolsável (por padrão, 30% do valor do contrato).

  2. Passo 2Escolha da moedaEscolha a moeda do contrato. GRAM, a moeda nativa da TON, serve para negócios rápidos e pequenos, já que é bastante volátil. USDT é uma moeda mais estável para negócios grandes e longos.
    9:41LTE

    Escolha da moeda

    Moeda do contrato

    GRAM: criação mais rápida. Você pode criar o contrato e depositar o valor em uma única transação.

    USDT: criação em duas etapas. Primeiro o contrato é criado (sua carteira precisa ter a moeda nativa GRAM ou suportar transações sem gás; GRAM é preferível) e depois você deposita USDT.O USDT é menos volátil que o GRAM.

  3. Passo 3Valor do contratoDigite o valor do contrato na moeda escolhida. Esse valor será transferido ao prestador depois que o trabalho for feito.
    9:41LTE

    Valor do contrato

    GRAM

    O valor do contrato é o preço do trabalho. Quem deposita é o cliente.

    30%

    De 10% a 90%. Padrão: 30%.

    0,4%

    0,4% do valor, mas no máximo 15 GRAM.

  4. Passo 4Endereços das carteirasCole os endereços das carteiras. Depois, essas carteiras serão usadas para depositar e sacar valores.
    9:41LTE

    Endereços das carteiras

  5. Passo 5Revisão do contratoConfira os dados do contrato antes do deploy.
    9:41LTE

    Revisão do contrato

    Condições do contrato
    Moeda
    GRAM
    Valor do contrato
    1 000 GRAM
    Depósito do prestador
    300 GRAM · 30%
    Taxa do serviço
    4 GRAM · 0,4%

Depois da criação

Cartão do contrato

Você é o cliente, a moeda é USDT. Seis capturas do mesmo cartão do painel real, com todas as ações possíveis em cada etapa.

  1. Aguardando depósitos
    1 000 USDT
    Contrato de 21.09.2026 - EQDe…0001
    Cliente (você)
    0 / 1 000
    Prestador
    0 / 300

    *Mais um depósito reembolsável de até 30%

    USDT

    Recém-criado

    Os saldos estão zerados. Você pode depositar o valor do contrato, depositá-lo já com um adicional de até 30% ou excluir o contrato enquanto ninguém tiver depositado nada.

  2. Aguardando depósitos
    1 000 USDT
    Contrato de 21.09.2026 - EQDe…0001
    Cliente (você)
    1 000 / 1 000
    Prestador
    0 / 300

    *Mais um depósito reembolsável de até 30%

    USDT

    Recarregado, aguardando

    O valor do contrato já está no contrato, mas o depósito do prestador ainda não. Enquanto o status não for “Ativo”, você pode sacar seu dinheiro. Ainda é possível adicionar o depósito adicional de até 30%.

  3. Ativo
    1 000 USDT
    Contrato de 21.09.2026 - EQDe…0001
    Cliente (você)
    1 000 / 1 000
    Prestador
    300 / 300
    USDT

    O prestador precisa concordar. O valor será arredondado para um percentual inteiro do valor do contrato.

    *Mais um depósito reembolsável de até 30%

    USDT

    O prestador recarregou

    Os dois valores combinados estão lá: o contrato está ativo. Os valores estão congelados e não é possível sacar USDT. Você pode aceitar o trabalho, propor outra parte ou adicionar o depósito adicional (garantia extra do cliente).

  4. Ativo
    1 000 USDT
    Contrato de 21.09.2026 - EQDe…0001
    Cliente (você)
    1 200 / 1 000 + 200 · 20%
    Prestador
    300 / 300
    USDT

    O prestador precisa concordar. O valor será arredondado para um percentual inteiro do valor do contrato.

    *Mais um depósito reembolsável de até 30%

    USDT

    Depósito de 20% adicionado

    Há +200 USDT acima do valor do contrato. Faltam 100 USDT para o teto de 30%. O saque continua bloqueado: um contrato ativo retém o valor do contrato mais o depósito adicional.

  5. Pagamento proposto 800 USDT
    1 000 USDT
    Contrato de 21.09.2026 - EQDe…0001
    Cliente (você)
    1 200 / 1 000 + 200 · 20%
    Prestador
    300 / 300
    USDT

    No momento estão propostos 800 USDT · 80%. Você pode substituir.

    *Mais um depósito reembolsável de até 30%

    USDT

    80% propostos ao prestador.

    O prestador receberá 800 USDT se concordar. O botão “Aceitar o trabalho” sumiu; ele só volta se você definir 100%. A proposta pode ser substituída, e o depósito adicional, complementado.

  6. Concluído 800 USDT
    1 000 USDT
    Contrato de 21.09.2026 - EQDe…0001
    Cliente (você)
    400
    Prestador
    1 096

    O prestador concordou

    Contrato concluído: 800 USDT foram para o prestador, 4 USDT são os royalties do serviço. Restam 400 USDT para você, que podem ser sacados.

Q&A

Perguntas frequentes

Onde está o terceiro?

Não existe. O prestador arrisca o depósito, então é vantajoso para ele terminar o trabalho.

Por padrão, o depósito do prestador é de 30% do valor do negócio. Pode parecer que ele leva vantagem: ele arrisca 30%, e você, 100%. Mas o que importa não é o percentual, e sim o dinheiro. Esses 30% são dinheiro dele, e perdê-los pode doer tanto para ele quanto perder os seus 100% doeria para você. Ninguém abre mão do próprio dinheiro à toa.

Outro risco: o cliente recebe o trabalho e pode não pagar. Há duas opções. A primeira: quando o trabalho estiver quase pronto, propor acertar as contas antes. A segunda: pedir que o cliente deposite um adicional de até 30% sobre o valor (depósito). Depois que o contrato é concluído, o cliente recebe o depósito de volta, então ele tem motivo para fechar o negócio.

Por que eu deveria confiar em vocês?

Não é preciso confiar. O smart contract é público. O endereço do seu contrato é um hash dos dados iniciais e do código. Se o código fosse trocado, o endereço seria outro.

Você pode abri-lo e conferir (por exemplo, no Tonviewer). A única coisa é que é preciso entender o código — esse é o principal problema. Mas, em compensação, é uma prova 100%. Você também pode entregar o código a um modelo de IA. Abaixo está o código do contrato com a explicação mais simples possível.

ContractUSDT

Esta é a lista de importações: constantes e funções auxiliares.

contract_usdt.tolk
1import "@contracts/common-messages"
2import "@contracts/errors"
3import "@contracts/contract_USDT/usdt-utils"
4import "fees-management"
5import "messages"
6import "storage"

Breve descrição do contrato.

8contract ContractUSDT {
9 author: "Alexandr_K"
10 version: "0.0.4"
11 description: "Escrow contract USDT"
12 storage: StorageUsdt
13 incomingMessages: AllowedMessage
14}

Lista de ações disponíveis com o contrato.

16type AllowedMessage =
17 | TransferNotificationForRecipient
18 | ChangePercent
19 | AcceptPercent
20 | RefundUsdt
21 | ErrRefund
22 | ErrMode
23 | BurnContract

Função auxiliar: indica se o contrato está ativo ou não. Quando todos depositam seus valores, o contrato fica ativo e já não dá para sacar simplesmente.

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}

Função do troco. Cada mensagem leva um pouco de GRAM para processar a transação. O que sobra, ela devolve.

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}

Calcula o endereço da carteira de jettons. Ele depende do endereço do contrato.

78fun contractUsdtWallet(): address {
79 return calcAddressOfJettonWallet(contract.getAddress());
80}

Se uma transferência falhar, o dinheiro volta e o contrato corrige os registros de quanto cada um tem.

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}

Se o corpo da mensagem estiver vazio, não fazemos nada.

131fun onInternalMessage(in: InMessage) {
132 if (in.body.isEmpty()) {
133 return;
134 }

Em seguida, a declaração das variáveis.

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;

Recarga de USDT: aceita transferências e registra quanto USDT cada um tem.

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}

Altera o percentual do pagamento. Se o trabalho foi feito só pela metade, você pode propor 50%. Se o prestador concordar, o contrato é cumprido. Se não, continuem negociando.

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}

Se o prestador concordar, o contrato paga a ele a parte dele mais o depósito, e o restante volta para você. Com 0%, o negócio inteiro é cancelado e todos recebem o dinheiro de volta. Nesse caso, a taxa de 0,4% não é cobrada.

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}

O maior bloco: gerencia as devoluções de valores.

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 != null
214 ? msg.addressRefund
215 : 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.providerDepositInit
235 ) 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}

Modo de erro. Se uma carteira for perdida ou o dinheiro tiver entrado sem registro, uma parte pode passar o controle para a outra. A rigor, o dinheiro entrar sem registro é impossível, a não ser de propósito. Mas a opção fica, por via das dúvidas. Já esquecer ou perder uma carteira é bem possível.

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 int8
284 ) {
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 int8
298 ) {
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 = 1
12 ProviderFullRights = 2
13 ClientFullRightsAfterHalfYear = 16 // клиент получает полные права через полгода
14 ProviderFullRightsAfterHalfYear = 26 // исполнитель — через полгода
15 BlockErrorMode = 99
16}

ErrRefund: saque de emergência de jettons que chegaram sem notificação e não foram registrados. Ou quando a carteira foi perdida (mais precisamente, a senha dela).

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 != null
336 ? msg.addressRefund
337 : 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: destrói o contrato para que uma casca vazia não fique solta na rede. Quem pode queimá-lo é quem o criou (o cliente ou o prestador) ou o administrador, e só quando os saldos do cliente e do prestador estão zerados.

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.adminAddress
357 ) throw Errors.UnauthorizedSender;
358 assert (
359 storage.clientAmount == 0 && storage.providerDeposit == 0
360 ) 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}

Código-fonte verificado dos três contratos. O hash do código coincide com o que está na rede. Cada link abre o código no TON Verifier.

ContractMinterContractUSDTContractTON