

Escrow Smart Contract
El cliente y el proveedor depositan el dinero en un smart contract. El proveedor cobra cuando el trabajo está hecho y ambas partes están de acuerdo.
Cómo funciona
Cinco pasos hasta el contrato
Cree un smart contract con un asistente sencillo.
Cree un smart contract con un asistente sencillo.
- Paso 1Rol en el contratoElija su rol en el contrato. Solo hay dos roles: cliente y proveedor. No hay arbitraje de terceros.9:41LTE
Rol en el contrato
- Paso 2Elección de monedaElija la moneda del contrato. GRAM, la moneda nativa de TON, sirve para tratos rápidos y pequeños, ya que es bastante volátil. USDT es una moneda más estable para tratos grandes y largos.9:41LTE
Elección de moneda
- Paso 3Monto del contratoIngrese el monto del contrato en la moneda elegida. Este monto se transferirá al proveedor una vez realizado el trabajo.9:41LTE
Monto del contrato
El monto del contrato es el precio del trabajo. Lo deposita el cliente.
Del 10% al 90%. Por defecto: 30%.
0,4% del monto, pero no más de 15 GRAM.
- Paso 4Direcciones de billeteraPegue las direcciones de billetera. Más adelante, estas billeteras se usarán para depositar y retirar fondos.9:41LTE
Direcciones de billetera
- Paso 5Revisión del contratoRevise los datos del contrato antes del despliegue.9:41LTE
Revisión del contrato
Rol en el contrato
Después de crearlo
Tarjeta del contrato
Usted es el cliente y la moneda es USDT. Seis capturas de la misma tarjeta que en el panel real, con todas las acciones posibles en cada paso.
Recién creado
Los saldos están en cero. Puede depositar el monto del contrato, el monto con un adicional de hasta el 30% o eliminar el contrato mientras nadie haya depositado nada.
Recargado, en espera
El monto del contrato ya está en el contrato, pero aún falta el depósito del proveedor. Mientras el estado no sea «Activo», puede retirar su dinero. Todavía puede agregar el depósito adicional de hasta el 30%.
El proveedor recargó
Ambos montos acordados ya están: el contrato está activo. Los fondos están bloqueados y no se puede retirar USDT. Puede aceptar el trabajo, proponer otra parte o agregar el depósito adicional (garantía extra del cliente).
Se agregó un depósito del 20%
Hay +200 USDT por encima del monto del contrato. Faltan 100 USDT para el tope del 30%. El retiro sigue cerrado: un contrato activo retiene el monto del contrato más el depósito adicional.
Se propuso el 80% al proveedor.
El proveedor recibirá 800 USDT si acepta. El botón «Aceptar el trabajo» desapareció; solo volverá a aparecer si fija el 100%. La propuesta se puede reemplazar y el depósito adicional se puede seguir aumentando.
El proveedor aceptó
Contrato completado: 800 USDT fueron al proveedor y 4 USDT son las regalías del servicio. A usted le quedan 400 USDT, que puede retirar.
Q&A
Preguntas frecuentes
¿Dónde está el tercero?
No la hay. El proveedor arriesga su depósito, así que le conviene terminar el trabajo.
Por defecto, el depósito del proveedor es el 30% del monto del trato. Puede parecer que tiene ventaja: él arriesga el 30% y usted el 100%. Pero lo que importa no son los porcentajes, sino el dinero. Ese 30% es su propio dinero, y perderlo puede dolerle tanto como a usted perder su 100%. Nadie sacrifica su dinero porque sí.
Otro riesgo: el cliente recibe el trabajo y podría no pagar. Hay dos opciones. La primera: cuando el trabajo esté casi listo, proponer liquidar por adelantado. La segunda: pedir al cliente un depósito adicional de hasta el 30% sobre el monto. Al cerrarse el contrato, el cliente recupera ese depósito, así que tiene motivos para cerrar el trato.
¿Por qué debería confiar en ustedes?
No hace falta confiar. El smart contract es público. La dirección de su contrato es un hash de los datos iniciales y del código. Si alguien cambiara el código, la dirección sería otra.
Puede abrirlo y revisarlo (por ejemplo, en Tonviewer). Lo único es que hay que entender el código: ahí está el problema principal. Pero, a cambio, es una prueba al 100%. También puede darle el código a un modelo de IA. Abajo está el código del contrato con la explicación más sencilla posible.
ContractUSDT
Esta es la lista de importaciones: constantes y funciones auxiliares.
1import "@contracts/common-messages"2import "@contracts/errors"3import "@contracts/contract_USDT/usdt-utils"4import "fees-management"5import "messages"6import "storage"Breve descripción del contrato.
8contract ContractUSDT {9 author: "Alexandr_K"10 version: "0.0.4"11 description: "Escrow contract USDT"12 storage: StorageUsdt13 incomingMessages: AllowedMessage14}Lista de acciones disponibles con el contrato.
16type AllowedMessage =17 | TransferNotificationForRecipient18 | ChangePercent19 | AcceptPercent20 | RefundUsdt21 | ErrRefund22 | ErrMode23 | BurnContractFunción auxiliar: indica si el contrato está activo. Cuando todos han depositado sus montos, el contrato se activa y ya no se puede retirar el dinero sin más.
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}Función para el cambio. Cada mensaje lleva un poco de GRAM para procesar la transacción. Lo que sobra, esta función lo devuelve.
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 la dirección de la billetera de jettons. Depende de la dirección del contrato.
78fun contractUsdtWallet(): address {79 return calcAddressOfJettonWallet(contract.getAddress());80}Si una transferencia falla, el dinero regresa y el contrato corrige sus registros de cuánto tiene cada uno.
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}Si el cuerpo del mensaje está vacío, no hacemos nada.
131fun onInternalMessage(in: InMessage) {132 if (in.body.isEmpty()) {133 return;134 }A continuación, la declaración de variables.
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: acepta transferencias y registra cuántos USDT tiene cada parte.
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}Cambia el porcentaje del pago. Si el trabajo solo está hecho a medias, puede proponer el 50%. Si el proveedor acepta, el contrato se cumple. Si no, sigan 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}Si el proveedor acepta, el contrato le paga su parte más su depósito, y el resto vuelve a usted. Con 0% se cancela todo el trato y cada uno recupera su dinero. En ese caso, la comisión del 0,4% no se cobra.
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}El bloque más grande: gestiona las devoluciones de fondos.
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}Modo de error. Si se pierde una billetera o el dinero llegó sin quedar registrado, una parte puede ceder el control a la otra. En rigor, que el dinero llegue sin registrarse es imposible, salvo que se haga a propósito. Pero la opción se queda por si acaso. Olvidar o perder una billetera, en cambio, es bastante posible.
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: retiro de emergencia de jettons que llegaron sin notificación y no quedaron registrados. También sirve cuando se pierde una billetera (más exactamente, su contraseña).
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: destruye el contrato para que no quede una carcasa vacía en la red. Puede quemarlo quien lo creó (el cliente o el proveedor) o el administrador, y solo cuando los saldos del cliente y del proveedor son cero.
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}Código fuente verificado de los tres contratos. El hash del código coincide con el que está en la red. Cada enlace abre el código en TON Verifier.
ContractMinterContractUSDTContractTON