Oráculos: usando dados externos dentro da blockchain

Lucas Vieira
Block3 Research
Published in
14 min readJun 6, 2022

Na sua definição padrão, oráculos são sistemas (alguns chamam de entidades) que conectam blockchains a serviços externos. Eles surgiram de uma limitação fundamental dos contratos inteligentes: eles não podem por natureza interagir com dados e sistemas que existem fora da blockchain em que ele habita. Recursos que se encontram fora da blockchain normalmente são chamados de off-chain e recursos que se encontram dentro dela on-chain.

Através dos oráculos, se torna possível a conexão entre os mundos da Web3 e da Web2. Hoje muitos dizem que vivemos a era da Web2.5, uma transição entre o mundo centralizado e descentralizado. Eu tendo a concordar com essa visão, acredito que a transição não é feita de uma hora para outra e que temos que nos beneficiar dos fatores positivos de cada um desses universos. Com a ajuda dos oráculos, contratos inteligentes podem ser integrados com bases de dados já existentes. Permitindo assim que eles sejam executados com entradas vindas do mundo externo (Web2) e também alimentem esses sistemas com dados oriundos de blockchains.

Costumamos então dizer que os oráculos são uma ponte entre blockchains e o mundo externo. Um exemplo bastante utilizado para exemplificar essa situação é o caso de uso de uma seguradora. Imagine que você tem uma propriedade rural. Hoje muitos proprietários contam com a aquisição de apólices de seguros para ter mais segurança em sua produção perante a fatores externos. O problema é o mesmo que toda relação com instituição centralizada oferece. Como o proprietário tem a garantia de que no caso de um sinistro, se os termos impostos no contrato do seguro ocorrerem, ele de fato vai receber a quantia devida? Ele não tem. As relações são baseadas na confiança que o proprietário tem com a seguradora de que vai ser pago conforme o combinado. E é ai que podemos tomar vantagem de tecnologias descentralizadoras como blockchains e contratos inteligentes.

Poderíamos escrever um contrato inteligente (um programa de computador armazenado na blockchain) o qual definisse que, por exemplo, se a temperatura no local de sua propriedade passar de um limite (digamos, 45 graus) o contrato irá ser executado e automaticamente transferiria a quantia devida ao proprietário. Incrível, não? Dessa forma não precisaríamos mais de uma entidade no meio do caminho gerando burocracia e ineficiência.

Mas como o contrato inteligente consegue saber qual a temperatura atual numa localidade específica? Com os oráculos! E além de temperaturas, oráculos podem fornecer diversos dados úteis para as blockchains, por exemplo, se um pagamento foi efetuado corretamente, quais foram os resultados da última eleição ou partida de futebol, qual a cotação atual de certas moedas ou até se o sensor de segurança da sua casa foi acionado. As possibilidades são infinitas e nos trazem uma forma de implementar soluções usando tecnologia de blockchain em muitos problemas reais. No caso abaixo podemos ver um fluxo de oráculos e blockchains sendo usados para ressarcimento no caso de voos atrasados.

Os oráculos que alimentam blockchain com dados do mundo real são chamados de input oracles, enquanto os oráculos que usam dados de blockchains no mundo externo (como, por exemplo, para acionar uma ação) são chamados de output oracles. Mas esses não são os únicos tipos de oráculos que existem, temos os cross-chain oracles que leem e escrevem dados entre diferentes blockchains (usando dados de uma para acionar uma ação na outra ou para compartilhamento de recursos) e por final, temos os compute-enabled oracles que usam recursos computacionais off-chain (por segurança ou eficiência) como ferramenta para operações on-chain. Um exemplo é na geração de números randômicos(aleatórios), que tem muita utilidade desde um mint aleatório de um NFT e até num possível contrato inteligente de uma loteria.

Imagino que neste ponto vocês devem estar pensando o mesmo que eu: mas se eu tenho uma fonte externa centralizada alimentando os contratos inteligentes dentro de uma blockchain que, em teoria, eram para operar de maneira descentralizada, de que adianta eu usar a blockchain para início de conversa? E você tem toda a razão! E esse é chamado de "o problema dos oráculos". Se usarmos uma entidade centralizadora como oráculo estamos introduzindo um ponto único de falha, tirando o propósito de uso de uma blockchain descentralizada e ainda correndo o risco de receber informações corrompidas que podem gerar resultados errados e desastrosos. A solução para esse problema está nas redes descentralizadas de oráculos, as DONs. Que nos fornece múltiplos nós(“nodes”) como oráculos, garantindo uma fonte descentralizada e confiável da informação.

A Chainlink é um dos serviços que nos oferece uma solução para esse problema. Um de seus DONs é o Chainlink Data Feed que nos fornece informações sobre cotações de moedas que são extremamente úteis em diversas aplicações como soluções de DeFi, por exemplo. Vamos agora botar a mão na massa e fazer o passo-a-passo para implementar um oráculo usando o Chainlink Price Feed num contrato inteligente escrito em Solidity! Você pode acessar o código completo no meu Github :)

1. Chainlink Data Feed

Primeiro vamos criar o diretório e criar o projeto hardhat dentro dele.

mkdir smart-contract-oracles
cd smart-contract-oracles/
npx hardhat

Escolha a opção "Create a basic sample project" e aceite as próximas opções. Temos agora um projeto base com um contrato, um script de deploy e um script de testes base para começarmos. Vamos renomear o contrato contracts/Greeter.sol para contracts/CurrencyDataFeed.sol. Vamos também reescrever seu conteúdo para termos um contrato vazio no formato abaixo.

Para usar o Chainlink Data Feeds, além de outras bibliotecas da plataforma, é necessário fazer a instalação dos pacotes com o comando:

npm install @chainlink/contracts

Nosso contrato vai ser um consumidor dos dados fornecidos por um contrato agregador dessa informação. Para nos conectarmos com esses contratos precisamos usar a interface AggregatorV3Interface que está incluída na biblioteca que acabamos de instalar. Essa interface fornece ao nosso contrato algumas funções para retornar os dados do contrato agregador. É possível ver em detalhes o funcionamento da documentação da Chainlink. Agora no nosso contrato vamos precisar importar a biblioteca da interface que iremos usar.

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

Além disso vamos precisar definir uma variável para interagir com a interface do agregador que vamos usar, no caso vamos começar com o par ETH/USD. A Chainlink nos fornece uma página com todos os contratos agregadores disponíveis. Além dos pares, é possível escolher os contratos de diferentes redes como a Ethereum Mainnet, Polygon Mainnet, BSC Mainnet e outras. Escolhemos a Ethereum e podemos ver uma listagem dos pares disponíveis.

Escolhemos o par desejado e, nessa página, podemos ver inclusive os nós que operam esse agregador e o status deles! Além disso, temos o endereço do contrato agregador. Vamos copiar esse valor para inserir no nosso contrato consumidor e, nos permitir assim, interagir com o agregador do par desejado.

Vamos então importar a biblioteca da interface, definir a váriavel que vamos usar para interagir com a interface e setar ela com o endereço do contrato agregador dentro do construtor do nosso contrato.

Agora só precisamos escrever a função que vai consumir os dados do agregador chamando a função latestRoundData da interface que importamos. Para isso, vamos criar a função getLatestPrice no nosso contrato.

A função basicamente chama a função do agregador e retorna o preço do par que foi recebido. Simples assim! Escolhemos receber também o parâmetro com o timestamp da rodada de atualização ethTimestamp do agregador que estamos consumindo, caso ele seja 0, a rodada de dados não foi completada do lado deles e devemos então aguardar um pouco e refazer a chamada para termos valores acurados. Vamos agora reescrever o arquivo de testes para ver tudo funcionando!

Você deve estar se perguntando, se o contrato agregador está na Mainnet da Ethereum e os testes do hardhat rodam num nó da blockchain gerado localmente, como nosso contrato vai conseguir se comunicar com o contrato agregador nos testes? Se essa dúvida surgiu, você está coberto de razão. De fatos se rodarmos qualquer teste agora vamos receber um erro ao tentar acessar o contrato da interface pelo construtor! Para resolver esse problema precisamos fazer um Forking da Mainnet. Esse é basicamente o processo de simular o estado atual da rede Mainnet na nossa rede de desenvolvimento local. Dessa forma, conseguimos interagir com contratos e protocolos da rede principal nos nossos testes locais. Incrível, não?

Para isso primeiro precisamos criar um nó da Mainnet usando algum blockchain provider. No nosso caso iremos usar o Alchemy. Nesse post detalhamos o processo de criação de contas, de nós e como retornar a chave de acesso no Alchemy, mas o processo é muito simples e intuitivo!

Com o App criado no Alchemy, vamos pegar a chave de acesso http e inseri-la no arquivo de configuração do hardhat da seguinte maneira.

Note que além da URL do forking da Mainnet, já inserimos a URL do nó da rede Rinkeby para testes (que vai ser usado nos próximos passos) e o Gas Reporter com a chave de acesso da API do CoinMarketCap (é preciso instalar o hardhat-gas-reporter também), para retornar os custos reais das funções durante os nossos testes. Lembre-se de trocar os valores do código acima pelas suas chaves ou, como no caso do exemplo, usar variáveis de ambiente com o dotenv.

Ufa, agora vamos escrever nosso arquivo de testes. Vamos renomear o arquivo test/sample-test.js para test/currencyDataFeed.js e escrever um teste básico para chamar a função que criamos agora a pouco e checar o seu retorno.

Para rodar o teste usamos o comando do hardhat

npx hardhat test

O preço bate com o preço atual do Ethereum, checando no site do CoinMarketCap.

Note porém que o valor retornado é multiplicado por 10⁸, que é o valor retornado pela função decimals da interface do agregador! Para checar na formatação correta, basta dividir o valor retornado da cotação por este outro. Então, para ter a formatação correta basta adicionar a função no contrato.

function decimals() external view returns (uint8) {
return ethPriceFeed.decimals();
}

E chamá-la no nosso arquivo de testes.

const ethDecimals = await currencyDataFeed.decimals();
console.log("ETH Price returned in USD: $", ethPrice.toNumber() / 10 ** ethDecimals);

Pronto! Temos nosso oráculo de Data Feed funcional retornando dados externos para o nosso contrato. Mas e se quiséssemos retornar o valor do Ethereum em Reais? Para isso basta encontrarmos o agregador do par BRL/USD na listagem da Chainlink, usá-lo no nosso contrato e fazer as formatações necessárias!

Adicionamos um novo data feed, dessa vez para o par BRL/USD.

brlPriceFeed = AggregatorV3Interface(0x971E8F1B779A5F1C36e1cd7ef44Ba1Cc2F5EeE0f);

Em seguida ajustamos a função getLatestPrice para receber o preço do novo par que será dividido do primeiro para checarmos a ETH/BRL. Como os decimals de ambos os feeds é 8 ou seja, multiplicam o retorno por 10⁸, esse multiplicador é cortado dos dois lados na divisão. Poderíamos apenas dividir para ter o retorno formatado. Mas para evitar problemas clássico com decimais em Solidity e também para seguir o padrão dos data feeds, multiplicamos o preço do par ETH/USD por 10⁸ novamente e fazemos os ajustes na nossa função de decimals. Assim, o par ETH/BRL também virá multiplicado por 10⁸ como os demais! Com isso, o único ajuste que precisamos fazer no arquivo de testes é no texto do log da cotação.

console.log("ETH Price returned in BRL: R$", ethPrice.toNumber() / 10 ** ethDecimals);

Rodando o comando de testes podemos ver que nosso oráculo funciona corretamente!

Você pensa que acabou? Nana nina não! Vamos agora implementar um oráculo que retorna a mesma informação, mas dessa vez usando um "servidor" próprio. Apesar de no próximo caso usarmos um oráculo próprio para retornar a mesma informação, essa lógica pode ser aplicada em qualquer cenário em que você precise de algum dado diferente dos padrões oferecidos pelos serviços de redes descentralizada de oráculos, como a Chainlink. Podemos customizar esse fluxo para o caso de uso do seu produto!

2. Oráculo Customizado

Vamos começar criando o arquivo do nosso novo contrato contracts/CurrencyOracle.sol.

Criamos uma estrutura de dados CurrencyPrice para armazenar as informações das consultas feitas pelo oráculo, onde armazenamos o preço do par ETH/USD ethPrice e do par BRL/USD brlPrice, além da data retrievedAt em que as cotações foram medidas. Criamos também uma lista prices de CurrencyPrice retornadas. Além disso, temos a variável totalRequests que guarda o número de consultas feitas até o momento.

Um ponto importante na estrutura dos oráculos são os eventos. São através deles que o nosso servidor vai conseguir saber quando ele precisa receber uma atualização das cotações. No nosso caso, sempre que emitirmos um evento PriceRequested o nosso servidor vai escutar essa emissão e, em seguida, enviar os valores atualizados dos pares para o contrato.

No fluxo do nosso oráculo, temos um cliente que vai fazer a requisição do retorno dos preços atuais com a função getCurrentPrice. Essa função irá emitir o evento PirceRequested e o nosso servidor, ao escutar essa emissão, vai pegar os preços atuais dos pares e enviá-los para o contrato pela função updatePrice, com isso o cliente poderá resgatar essa informação com a função getLatestPrice. Vamos começar esse fluxo criando as respectivas funções no nosso contrato.

Emitimos o evento PriceRequested na função getCurrentPrice

emit PriceRequested();

E na função updatePrice recebemos as cotações atuais dos pares e os inserimos dentro da nossa lista prices, além de incrementar a variável totalRequests com o novo total de requisições.

prices.push(CurrencyPrice(_ethPrice, _brlPrice, block.timestamp));
totalRequests++;

Em seguida fazemos o cálculo do par ETH/BRL com base na ultima cotação recebida

int price = prices[totalRequests - 1].ethPrice * multiplier / prices[totalRequests - 1].brlPrice;

Além disso, incluímos a lógica de decimais para seguir o padrão do último data feed implementado. Agora vamos fazer outros dois últimos ajustes. Primeiro vamos incluir no construtor a inclusão da primeira CurrencyPrice, para que o contrato já tenho o funcionamento correto logo de início e incluir um modificador para restringir o acesso da função updatePrice, de forma que não seja possível qualquer usuário atualizar as cotações.

Agora vamos criar nosso arquivo de testes locais para nosso novo contrato mas, antes disso, precisamos criar uma função para retornar os preços atuais da cotações dos pares. Para isso vamos usar a API do CoinMarketCap. Vamos usar o axios para fazer as chamadas e o instalamos usando o seguinte comando:

npm install axios 

Em seguida vamos criar um diretório para as lógicas auxiliares e um novo arquivo em helpers/currencies.js. Nesse post temos em detalhes como criar uma chave de API da CoinMarketCap, vamos precisar dela para fazer a chamada. Para nossa função vamos usar a API de Price Conversion, implementada com uma chamada get onde precisamos passar os identificadores de ETH (1027) e BRL (2783) para os pares relativos.

Note que multiplicamos o valor retornado por 10⁸ para já termos os dados no formato de decimais usado no nosso contrato. A resposta da chamada de API vem no seguinte formato:

Vamos agora criar nosso arquivo de testes test/currencyOracle.js e definir 3 testes para checar a funcionalidade do contrato:

  • Ao fazer o deploy já deve ser possível fazer o retorno da cotação
  • Fazer a requisição de uma nova cotação e certificar que o valor foi atualizado
  • Tentar fazer a atualização da cotação através de uma conta sem ser a do dono do contrato e receber um erro

Note que incluímos um helper com uma função sleep para adicionarmos tempos de espera entre as chamadas. Dessa forma conseguimos notar uma evolução nos valores das cotações. Podemos ver a definição da função a seguir.

const sleep = ms => {
return new Promise(resolve => {
return setTimeout(resolve, ms);
});
};

E podemos então rodar o teste com o seguinte comando.

npx hardhat test --grep Oracle

Podemos ver que os testes passaram corretamente e as cotações atualizadas dos pares foram retornadas!

Mas para ver ainda mais na prática a funcionalidade, vamos agora criar de fato um "servidor" como oráculo dos dados externos, fazer o deploy do contrato para a rede Rinkeby e checar o fluxo de comunicação entre eles durante a requisição de um usuário do contrato. Vamos começar criando o script para fazer o deploy do nosso contrato alterando o arquivo no diretório de scripts para scripts/deployOracle.js e incluir a seguinte lógica nele:

Além disso, precisamos importar o script dentro do arquivo de configuração hardhat.config.js.

require("./scripts/deployOracle");

Vamos agora fazer o deploy, mas lembre-se de criar um App no serviços de blockchain node (como Alchemy) na rede Rinkeby e incluir a chave de acesso no arquivo de configuração. Após isso feito podemos rodar o comando:

npx hardhat deploy --network rinkeby

Agora vamos copiar o endereço do contrato que foi logado no terminal após o deploy.

Vamos criar agora nosso servidor local, para isso vamos precisar instalar a biblioteca fs para fazer a leitura do arquivo JSON ABI (contém definições das funções e variáveis do contrato) gerado após a compilação e da biblioteca ethers para escutar os eventos emitidos pelo contrato que subimos na rede Rinkeby.

npm install fs ethers

Como falamos, vamos precisar da localização da ABI do contrato que fica em artifacts/contracts/CurrencyOracle.sol/CurrencyOracle.json, além disso precisamos também da chave websocket do nosso nó na rede Rinkbey, que pegamos pelo dashboard do Alchemy, do endereço do contrato que copiamos a pouco e da chave privada da conta que usamos no deploy. Os eventos podem ser escutados com o seguinte comando:

contract.on("PriceRequested", async () => {});

Para iniciar o servidor, usamos:

node scripts/oracleServer.js

Vamos agora incluir mais um script para fazer a requisição da cotação atualizada do par em scripts/getLatestPrice.js que deve ter o seguinte formato:

Além disso, precisamos importar o script no arquivo de configuração usando

require("./scripts/getLatestPrice");

Agora vamos rodar o script com o comando

npx hardhat getprice --network rinkeby

Mas lembre-se de que precisar deixar o servidor local rodando antes disso! Note que as cotações foram requisitadas no servidor

a medida que eles foram feitos no script getprice

E agora podemos ver na prática como um oráculo funciona por baixo dos panos! Com esse fluxo é possível fazer um oráculo customizado para o retorno de qualquer informação para um contrato que roda dentro de alguma blockchain. No próximo post vamos usar o que foi construído aqui para criar um nova funcionalidade para nosso contrato de tokens fungíveis que foi construído aqui.

Obrigado por lerem até aqui! Se tiverem alguma sugestão do que abordar num post futuro, não deixem de compartilhar comigo :)

--

--

Lucas Vieira
Block3 Research

Hi! I'm a brazilian software engineer who love to learn and build new things. My goal here is to share I little about that on the way :) https://bushi.solutions