Auditoria
A conferência do plano
Kit/Sources/EscaliburChains/PlanIntentCheck.swift
O plano faz o que a tela pediu? Conferido pelo app antes de mostrar a revisão e de novo antes de assinar (auditoria 2, M1).
| 1 | import EscaliburCore |
| 2 | import Foundation |
| 3 | |
| 4 | /// O plano faz o que a tela pediu? Conferido pelo app antes de mostrar a revisao e de |
| 5 | /// novo antes de assinar (auditoria 2, M1). |
| 6 | /// |
| 7 | /// A revisao e montada pelo planejador da rede a partir das transacoes; esta conferencia |
| 8 | /// amarra essa revisao ao pedido do dono: o ativo e o valor que saem, o minimo e o ativo |
| 9 | /// que entram, e quem recebe. Falha fechada: movimento ausente e recusado. |
| 10 | public enum PlanIntentCheck { |
| 11 | public enum Mismatch: Error, Equatable, Sendable { |
| 12 | case missingMovement |
| 13 | case wrongAsset |
| 14 | /// Sai mais (ou, num envio de valor exato, outro valor) do que o pedido. |
| 15 | case wrongAmount |
| 16 | case wrongBuyAsset |
| 17 | case belowMinimum |
| 18 | case wrongBeneficiary |
| 19 | } |
| 20 | |
| 21 | /// Envio. Valor exato: sai exatamente `amount`. Enviar tudo (`amount` nil): sai no |
| 22 | /// maximo `ceiling`, o saldo que a tela mostrou. |
| 23 | public static func send(_ review: PlanReview, asset: Asset, amount: BigUInt?, ceiling: BigUInt, chain: Chain) throws { |
| 24 | guard let outgoing = review.outgoing else { throw Mismatch.missingMovement } |
| 25 | guard sameAsset(outgoing.assetID, asset, chain: chain) else { throw Mismatch.wrongAsset } |
| 26 | if let amount { |
| 27 | guard outgoing.amount == amount else { throw Mismatch.wrongAmount } |
| 28 | } else { |
| 29 | guard outgoing.amount <= ceiling else { throw Mismatch.wrongAmount } |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | /// Troca ou ordem limite: vende `sell`, nunca mais que `amountIn`; garante pelo |
| 34 | /// menos `minimumOut` de `buy`; e o que entra vai para `owner`. |
| 35 | public static func trade( |
| 36 | _ review: PlanReview, sell: Asset, amountIn: BigUInt, buy: Asset, minimumOut: BigUInt, owner: String, chain: Chain |
| 37 | ) throws { |
| 38 | guard let outgoing = review.outgoing, let incoming = review.incomingMinimum else { throw Mismatch.missingMovement } |
| 39 | guard sameAsset(outgoing.assetID, sell, chain: chain) else { throw Mismatch.wrongAsset } |
| 40 | guard outgoing.amount <= amountIn else { throw Mismatch.wrongAmount } |
| 41 | guard sameAsset(incoming.assetID, buy, chain: chain) else { throw Mismatch.wrongBuyAsset } |
| 42 | guard incoming.amount >= minimumOut else { throw Mismatch.belowMinimum } |
| 43 | guard Address.sameRecipient(review.beneficiary, owner, chain: chain) else { throw Mismatch.wrongBeneficiary } |
| 44 | } |
| 45 | |
| 46 | /// Ponte: sai exatamente `amount` de `sell` na rede de origem; chega pelo menos |
| 47 | /// `minimumOut` de `receive` na rede de destino; e quem recebe e `owner`, a mesma |
| 48 | /// conta. Os ativos sao de redes diferentes: cada um e conferido na sua. |
| 49 | public static func bridge( |
| 50 | _ review: PlanReview, sell: Asset, amount: BigUInt, from: Chain, receive: Asset, minimumOut: BigUInt, to: Chain, owner: String |
| 51 | ) throws { |
| 52 | guard review.kind == .bridge, let outgoing = review.outgoing, let incoming = review.incomingMinimum else { |
| 53 | throw Mismatch.missingMovement |
| 54 | } |
| 55 | guard sameAsset(outgoing.assetID, sell, chain: from) else { throw Mismatch.wrongAsset } |
| 56 | guard outgoing.amount == amount else { throw Mismatch.wrongAmount } |
| 57 | guard sameAsset(incoming.assetID, receive, chain: to) else { throw Mismatch.wrongBuyAsset } |
| 58 | guard incoming.amount >= minimumOut else { throw Mismatch.belowMinimum } |
| 59 | guard Address.sameRecipient(review.beneficiary, owner, chain: to) else { throw Mismatch.wrongBeneficiary } |
| 60 | } |
| 61 | |
| 62 | /// A tag, o memo ou o comentario do plano contra o digitado (auditoria 2, B7). A tag |
| 63 | /// do XRP Ledger e um numero: "0123" e 123 sao a mesma. Memo da Stellar, comentario |
| 64 | /// da TON e memo da Tron sao comparados exatamente: o motor da Stellar so escolhe |
| 65 | /// memo ID para digitos sem zero a esquerda, e "0123" de texto nunca vale o ID 123. |
| 66 | public static func sameTag(_ planned: String?, _ typed: String?, chain: Chain) -> Bool { |
| 67 | let a = planned.flatMap { $0.isEmpty ? nil : $0 } |
| 68 | let b = typed.flatMap { $0.isEmpty ? nil : $0 } |
| 69 | guard let a, let b else { return a == nil && b == nil } |
| 70 | if chain.destinationTag == .xrplTag { |
| 71 | guard let x = UInt32(a), let y = UInt32(b) else { return false } |
| 72 | return x == y |
| 73 | } |
| 74 | return a == b |
| 75 | } |
| 76 | |
| 77 | /// O id do `TokenRegistry`. Na EVM o contrato pode vir em outra caixa (EIP-55), e |
| 78 | /// so la a comparacao ignora caixa: nas outras redes a caixa faz parte do endereco. |
| 79 | static func sameAsset(_ id: String, _ asset: Asset, chain: Chain) -> Bool { |
| 80 | chain.family == .evm ? id.lowercased() == asset.id.lowercased() : id == asset.id |
| 81 | } |
| 82 | } |