Auditoria
A lista de contratos da troca
Kit/Sources/EscaliburChains/Trade/TradeAllowlist.swift
A allowlist de routers, compilada.
| 1 | import EscaliburCore |
| 2 | import Foundation |
| 3 | |
| 4 | // A allowlist de routers, compilada. |
| 5 | // |
| 6 | // Para cada rede e provedor: o `to` aceito, o spender que recebe o approve exato, as |
| 7 | // funcoes aceitas (seletores tirados do ABI verificado) e como conferir que o codigo |
| 8 | // atras do endereco ainda e o que foi auditado. A allowlist vive no binario: a config |
| 9 | // remota so pode desligar provedor, nunca acrescentar endereco (docs/blockchain.md 3.5). |
| 10 | // |
| 11 | // Conferencia feita em 25/09/2026, para cada endereco e cada uma das 7 redes: |
| 12 | // - `eth_getCode` nao vazio em dois RPCs; |
| 13 | // - codigo verificado no Sourcify (sourcify.dev/server/v2/contract/<chainId>/<endereco>): |
| 14 | // AugustusV6 (solc 0.8.22), MetaAggregationRouterV2, GenericSwapFacetV3, |
| 15 | // OpenOceanExchange (implementacao atras do proxy); |
| 16 | // - o endereco devolvido pela API ao vivo (`to` da transacao e `approvalAddress`/ |
| 17 | // `tokenTransferProxy`/`routerAddress`) bateu com o daqui. |
| 18 | // |
| 19 | // Segunda leva, conferida em 26/09/2026 do mesmo jeito (codigo em dois RPCs, Sourcify ou |
| 20 | // explorador, `to` da API ao vivo): Plasma, Linea, Unichain e Sonic. Cada provedor so |
| 21 | // entra onde tudo fechou; o que ficou de fora esta dito na lista do provedor. |
| 22 | |
| 23 | /// Como conferir, antes de assinar, que o codigo atras do router e o que foi lido. |
| 24 | public enum TradeRouterPin: Sendable, Equatable { |
| 25 | /// Contrato sem proxy: o codigo e imutavel. A Augustus V6.2 e um diamond, mas |
| 26 | /// `swapExactAmountIn` esta no bytecode principal (Routers.sol), e funcao declarada |
| 27 | /// tem precedencia sobre o fallback de facetas. |
| 28 | case immutable |
| 29 | /// Proxy EIP-1967: a implementacao no slot |
| 30 | /// `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` tem de ser |
| 31 | /// esta. Upgrade do dono do proxy desliga o provedor ate a proxima versao do app. |
| 32 | case eip1967(implementation: EVMAddress) |
| 33 | /// Diamond EIP-2535: `facetAddress(seletor)` tem de devolver esta faceta para cada |
| 34 | /// seletor aceito. |
| 35 | case diamondFacet(EVMAddress) |
| 36 | |
| 37 | /// `keccak256("eip1967.proxy.implementation") - 1`. |
| 38 | public static let eip1967ImplementationSlot = [UInt8](hex: "360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")! |
| 39 | |
| 40 | /// `facetAddress(bytes4)` do DiamondLoupe (EIP-2535). |
| 41 | public static let facetAddressFunction = try! ABIFunction("facetAddress(bytes4)") // cdffacc6 |
| 42 | |
| 43 | /// O endereco que a leitura na cadeia tem de devolver; `nil` se nao ha o que ler. |
| 44 | public var expected: EVMAddress? { |
| 45 | switch self { |
| 46 | case .immutable: return nil |
| 47 | case .eip1967(let implementation): return implementation |
| 48 | case .diamondFacet(let facet): return facet |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | /// Um router aceito numa rede. |
| 54 | public struct TradeRouter: Sendable, Equatable { |
| 55 | public let provider: TradeProvider |
| 56 | public let chainID: UInt64 |
| 57 | /// O `to` da transacao de troca. |
| 58 | public let address: EVMAddress |
| 59 | /// Quem recebe o `approve` exato do token vendido. |
| 60 | public let spender: EVMAddress |
| 61 | public let pin: TradeRouterPin |
| 62 | /// As funcoes aceitas. Seletor fora desta lista e recusado antes de decodificar. |
| 63 | public let functions: [ABIFunction] |
| 64 | /// LI.FI: o contrato de taxa que pode aparecer dentro do `swapData`. |
| 65 | public let feeForwarder: EVMAddress? |
| 66 | |
| 67 | public var selectors: Set<[UInt8]> { Set(functions.map(\.selector)) } |
| 68 | |
| 69 | /// O router puxa o token pelo Permit2 (Uniswap): o approve exato vai ao Permit2, e o |
| 70 | /// plano soma a permissao do Permit2 a este router (`EVMPermit2Approval`). |
| 71 | public var viaPermit2: Bool { spender == EIP712ValidatedMessage.permit2 } |
| 72 | |
| 73 | /// A calldata de `facetAddress(seletor)` para cada seletor aceito (so diamond). |
| 74 | public var facetQueries: [[UInt8]] { |
| 75 | guard case .diamondFacet = pin else { return [] } |
| 76 | return functions.compactMap { function in |
| 77 | try? TradeRouterPin.facetAddressFunction.encodeCall([.fixedBytes(function.selector)]) |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | public enum TradeAllowlist { |
| 83 | static func address(_ hex: String) -> EVMAddress { |
| 84 | EVMAddress(uncheckedBytes: [UInt8](hex: hex)!) |
| 85 | } |
| 86 | |
| 87 | /// As redes EVM com troca, pelo chainId compilado: as 7 da v1 e Plasma, Linea, |
| 88 | /// Unichain e Sonic. |
| 89 | static let evmChainIDs: [UInt64] = [1, 42161, 8453, 10, 137, 56, 43114, 9745, 59144, 130, 146] |
| 90 | |
| 91 | // MARK: Velora (ex-ParaSwap), Augustus V6.2 |
| 92 | |
| 93 | /// Mesmo endereco em todas as redes da Velora. Fonte: developers.velora.xyz, |
| 94 | /// "Augustus v6.2 smart contracts"; Sourcify "AugustusV6" em cada uma; |
| 95 | /// `contractAddress` e `tokenTransferProxy` da API ao vivo. O spender e o proprio |
| 96 | /// router (v6.2 nao tem TokenTransferProxy separado). |
| 97 | static let velora = address("6a000f20005980200259b80c5102003040001068") |
| 98 | /// As 7 da v1 e a Unichain. A pagina da Velora nao lista mais Plasma nem Sonic, e a |
| 99 | /// API responde 503 para as duas. |
| 100 | static let veloraChainIDs: Set<UInt64> = [1, 42161, 8453, 10, 137, 56, 43114, 130] |
| 101 | |
| 102 | // MARK: KyberSwap, MetaAggregationRouterV2 |
| 103 | |
| 104 | /// Mesmo endereco em todas as redes. Fonte: docs.kyberswap.com (Aggregator API, |
| 105 | /// "router address"); Sourcify "MetaAggregationRouterV2" em cada uma (solc 0.8.9 ou |
| 106 | /// 0.8.17 conforme a rede); `routerAddress` da API ao vivo. Sem proxy: `Ownable` so |
| 107 | /// mexe em whitelist e resgate. |
| 108 | static let kyber = address("6131b5fae19ea4f9d964eac0408e4408b66337b5") |
| 109 | /// Todas as redes com troca: a API atende `plasma`, `linea`, `unichain` e `sonic` |
| 110 | /// (nao atende X Layer nem Celo). |
| 111 | static let kyberChainIDs: Set<UInt64> = [1, 42161, 8453, 10, 137, 56, 43114, 9745, 59144, 130, 146] |
| 112 | |
| 113 | // MARK: LI.FI, LiFiDiamond |
| 114 | |
| 115 | /// Mesmo diamond nas 7 redes e na Sonic. Fonte: github.com/lifinance/contracts, |
| 116 | /// deployments/<rede>.json (`LiFiDiamond`, `GenericSwapFacetV3`, `FeeForwarder`), |
| 117 | /// conferido com `facetAddress(seletor)` na cadeia em 25/09/2026. O |
| 118 | /// `swapTokensGeneric` antigo nao esta mais no diamond (facetAddress devolve zero), |
| 119 | /// entao a v1 so aceita as seis funcoes da GenericSwapFacetV3. |
| 120 | /// |
| 121 | /// Sonic (26/09/2026): diamond e faceta `0xf24c...1715` com Sourcify, os seis |
| 122 | /// seletores devolvem a faceta em dois RPCs, FeeForwarder com o mesmo bytecode da |
| 123 | /// Base. Plasma, Linea e Unichain ficam de fora: la o diamond e o FeeForwarder estao |
| 124 | /// em outros enderecos (deployments/plasma.json, linea.json, unichain.json), e esta |
| 125 | /// allowlist so conhece um diamond. |
| 126 | static let lifiDiamond = address("1231deb6f5749ef6ce6943a275a1d3e7486f4eae") |
| 127 | /// FeeForwarder 2.0.0, mesmo endereco e mesmo codehash nas 7 redes e na Sonic |
| 128 | /// (Sourcify na Base). |
| 129 | static let lifiFeeForwarder = address("ce40449b773a3e6e5e769adb4e567179d4828cbd") |
| 130 | static let lifiFacets: [UInt64: EVMAddress] = [ |
| 131 | 1: address("8c9dba771220ed09580b77f0765e7153fbde7790"), |
| 132 | 10: address("8dfdaebb42655a4a4e2b89687dd117074ae8c665"), |
| 133 | 42161: address("31a9b1835864706af10103b31ea2b79bdb995f5f"), |
| 134 | 8453: address("31a9b1835864706af10103b31ea2b79bdb995f5f"), |
| 135 | 137: address("31a9b1835864706af10103b31ea2b79bdb995f5f"), |
| 136 | 56: address("31a9b1835864706af10103b31ea2b79bdb995f5f"), |
| 137 | 43114: address("31a9b1835864706af10103b31ea2b79bdb995f5f"), |
| 138 | 146: address("f24c9914d3a89f16ad9c87fa549ff24359a31715"), |
| 139 | ] |
| 140 | |
| 141 | // MARK: De¹ (ex-OpenOcean), OpenOceanExchange |
| 142 | |
| 143 | /// Proxy transparente, mesmo endereco em todas as redes; a implementacao muda por |
| 144 | /// rede e fica fixada aqui. So entram as redes em que a implementacao esta |
| 145 | /// verificada: Sourcify em Ethereum, Arbitrum, Base, OP, BNB e Linea; Blockscout na |
| 146 | /// Polygon e na Unichain, todas "OpenOceanExchange" com solc 0.8.9. Ficam de fora a |
| 147 | /// Avalanche (`0x6a27...f993`) e a Plasma (`0xed85...4615`), com implementacao sem |
| 148 | /// codigo verificado em nenhum explorador que conferimos, e a Sonic: la a |
| 149 | /// implementacao `0x8d2b...be74` esta verificada (SonicScan), mas a API devolve a |
| 150 | /// venda de S sem o endereco 0xEeee e com `flags` 0, e a validacao recusa a calldata |
| 151 | /// (conferido ao vivo em 26/09/2026). |
| 152 | static let de1Proxy = address("6352a56caadc4f1e25cd6c75970fa768a3304e64") |
| 153 | static let de1Implementations: [UInt64: EVMAddress] = [ |
| 154 | 1: address("e29b6b6e96befe1e9c6f948bcb5a3f71058bda9b"), |
| 155 | 42161: address("94e8da4a7707b2edd1688c1ab3b09fd0fb8267eb"), |
| 156 | 8453: address("201263cea08e8f1d6e2fdd1fd2ca44bf6145e2af"), |
| 157 | 10: address("dcbf4cb83d27c408b30dd7f39bfcabd7176b1ba3"), |
| 158 | 137: address("2691f337abeb0146f16441ca4f82f363275851d5"), |
| 159 | 56: address("86f2058ba5e1466260055d1f3dab38b15f98be09"), |
| 160 | 59144: address("1aa298ae7c53d8dafa200ed49608649bfa76a446"), |
| 161 | 130: address("170100a288dc3d7e83fea20441f98166b15b6df0"), |
| 162 | ] |
| 163 | |
| 164 | // MARK: Consulta |
| 165 | |
| 166 | /// O router de um provedor numa rede, ou `nil` se o provedor nao atende ali. |
| 167 | public static func router(for provider: TradeProvider, on chain: Chain) -> TradeRouter? { |
| 168 | guard chain.family == .evm, let chainID = chain.evmChainID, evmChainIDs.contains(chainID) else { return nil } |
| 169 | switch provider { |
| 170 | case .velora: |
| 171 | guard veloraChainIDs.contains(chainID) else { return nil } |
| 172 | return TradeRouter(provider: .velora, chainID: chainID, address: velora, spender: velora, |
| 173 | pin: .immutable, functions: VeloraCalldata.functions, feeForwarder: nil) |
| 174 | case .kyberSwap: |
| 175 | guard kyberChainIDs.contains(chainID) else { return nil } |
| 176 | return TradeRouter(provider: .kyberSwap, chainID: chainID, address: kyber, spender: kyber, |
| 177 | pin: .immutable, functions: KyberCalldata.functions, feeForwarder: nil) |
| 178 | case .lifi: |
| 179 | guard let facet = lifiFacets[chainID] else { return nil } |
| 180 | return TradeRouter(provider: .lifi, chainID: chainID, address: lifiDiamond, spender: lifiDiamond, |
| 181 | pin: .diamondFacet(facet), functions: LiFiCalldata.functions, feeForwarder: lifiFeeForwarder) |
| 182 | case .de1: |
| 183 | guard let implementation = de1Implementations[chainID] else { return nil } |
| 184 | return TradeRouter(provider: .de1, chainID: chainID, address: de1Proxy, spender: de1Proxy, |
| 185 | pin: .eip1967(implementation: implementation), functions: De1Calldata.functions, feeForwarder: nil) |
| 186 | case .uniswap: |
| 187 | // O token vendido e aprovado ao Permit2, e o Permit2 da ao router a permissao |
| 188 | // exata de 30 minutos (`TradeRouter.viaPermit2`). Router sem proxy. |
| 189 | guard let deployment = UniswapDeployment.all[chainID] else { return nil } |
| 190 | return TradeRouter(provider: .uniswap, chainID: chainID, address: deployment.router, spender: EIP712ValidatedMessage.permit2, |
| 191 | pin: .immutable, functions: UniswapCalldata.functions, feeForwarder: nil) |
| 192 | case .pancakeswap: |
| 193 | // SmartRouter sem proxy, com o approve exato a ele mesmo. O `Ownable` dele so |
| 194 | // mexe na rota de stable swap, que a carteira nao monta. |
| 195 | guard let deployment = PancakeDeployment.all[chainID] else { return nil } |
| 196 | return TradeRouter(provider: .pancakeswap, chainID: chainID, address: deployment.router, spender: deployment.router, |
| 197 | pin: .immutable, functions: PancakeCalldata.functions, feeForwarder: nil) |
| 198 | case .shibaswap: |
| 199 | // UniswapV2Router02 sem proxy e sem dono, com o approve exato a ele mesmo. |
| 200 | guard let deployment = ShibaSwapDeployment.all[chainID] else { return nil } |
| 201 | return TradeRouter(provider: .shibaswap, chainID: chainID, address: deployment.router, spender: deployment.router, |
| 202 | pin: .immutable, functions: ShibaSwapCalldata.functions, feeForwarder: nil) |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | /// Todos os routers aceitos numa rede. |
| 207 | public static func routers(on chain: Chain) -> [TradeRouter] { |
| 208 | TradeProvider.allCases.compactMap { router(for: $0, on: chain) } |
| 209 | } |
| 210 | |
| 211 | /// Os spenders de troca de uma rede, para a tela de aprovacoes vigentes e revogacao |
| 212 | /// (docs/seguranca.md 4.4), incluindo o VaultRelayer da CoW. |
| 213 | public static func spenders(on chain: Chain) -> Set<EVMAddress> { |
| 214 | var out = Set(routers(on: chain).map(\.spender)) |
| 215 | if CoWProtocol.supports(chain) { out.insert(CoWProtocol.vaultRelayer) } |
| 216 | return out |
| 217 | } |
| 218 | } |