WEB3DEV

Cover image for Matemática em Solidity (Parte 4: Juros Compostos)
Yan Luiz
Yan Luiz

Posted on

Matemática em Solidity (Parte 4: Juros Compostos)

Este artigo é o quarto de uma série de artigos sobre como usar matemática em Solidity. Dessa vez, o tópico é: juros compostos.

Tradução de Mikhail Vladimirov feita por Fatima Lima e você pode encontrar a versão original aqui.

Introdução

Em nosso artigo anterior falamos sobre porcentagens e como elas são calculadas em Solidity. Em matemática financeira, porcentagens são comumente associadas a juros pagos em empréstimos e depósitos. Ao final de cada período de tempo, digamos um mês ou um ano, um certo percentual do valor principal é pago ao credor ou depositante. Esse esquema é conhecido como juros simples e o percentual pago por período é chamado de taxa de juros periódica.

Nos programas de computador, é comum usar percentual de juros ao invés de taxa de juros. Por exemplo, para uma taxa de juros de 3% o percentual é 0.03. Assim, o valor do pagamento do juro por um período pode ser calculado como o percentual do juro multiplicado pelo valor principal. Do artigo anterior, nós já sabemos como fazer isso, exatamente e efetivamente, em Solidity.

O esquema de juros simples é fácil, mas as coisas se tornam mais complicadas se os juros não são pagos imediatamente para o credor ou o depositante, mas são acrescentados ao valor principal. Nesse caso, os juros acumulados durante os períodos passados influenciam no valor dos juros a serem cobrados no futuro.

Nesse artigo nós discutimos como esse esquema poderia ser implantado em Solidity e o nome desse esquema é: juros compostos.

Composição de Juros Periódicos

Nós já sabemos como calcular juros simples. A solução simples para calcular juros compostos é calcular os juros simples no final de cada período e então, adicionar esses juros calculados ao valor principal. Em linguagem de alto nível, como JavaScript, fica assim:

principal += ratio * principal; // Faz após cada período de tempo
Enter fullscreen mode Exit fullscreen mode

Aqui ratio é praticamente, sem dúvida, um número fracionário, mas o Solidity não suporta frações. Então, em Solidity devemos escrever algo assim:

principal += mulDiv (ratio, principal, 10^18);
Enter fullscreen mode Exit fullscreen mode

Nós usamos a função mulDiv do artigo anterior e assumimos que ratio é um número de ponto fixo com 18 casas decimais.

O código acima vai funcionar na maioria dos casos, mas sua operação += pode causar um overflow. Então, para tornar o código seguro, precisamos alterá-lo assim:

principal = add (principal, mulDiv (ratio, principal, 10^18));
Enter fullscreen mode Exit fullscreen mode

Essa variável é provavelmente boa para produção, mas difícil de ler. Nesse artigo, vamos usar operações matemáticas simples para simplificar, como se o Solidity suportasse frações e como se as operações matemáticas não produzissem overflow. No código real essas operações deveriam ser trocadas por funções adequadas.

Uma vez que sabemos como fazer juros compostos para um período simples, a questão é:

Como Poderíamos Disparar os Juros Compostos no Final de Cada Período?

Spoiler: não poderíamos

Ao contrário dos aplicativos tradicionais , os contratos inteligentes não podem ter nenhuma atividade em segundo plano. O bytecode do contrato é executado somente quando a transação chama o contrato, ou diretamente, ou por meio de outros contratos. Pode-se contar com serviços de terceiros, como o Provable (anteriormente chamado de Oraclize) para chamar um específico contrato inteligente regularmente ou pode-se motivar economicamente pessoas comuns a fazê-lo.

Essa abordagem funciona, mas possui um número de desvantagens. Primeiramente, alguém deve pagar pelo gás, ou seja, não é grátis. Em segundo lugar, os juros devem ser compostos no final de cada período, mesmo se ninguém acessar o valor principal atualizado durante o período seguinte. Terceiro, quanto menor o período, mais frequentemente os juros compostos devem ser calculados e consequentemente, mais gás será consumido. Quarto, para curtos períodos esse método não é preciso, já que o tempo de mineração da transação é imprevisível e pode ser muito grande nos momentos de alta carga de rede.

Assim, se a composição no fim de cada período não é uma boa ideia para o Solidity, então

Quando Devemos Usar os Juros Compostos?

Spoiler: juros compostos “preguiçosos"

Ao invés de fazer os juros compostos no fim de cada período, a melhor opção deveria ser fazer a composição somente quando alguém precisasse acessar o valor principal, ou o débito ou depósito e nesse ponto, fazer a composição para todos os períodos encerrados, desde o momento em que os juros compostos foram calculados pela última vez:

uint currentPeriod = block.timestamp / periodLength;
for (uint period = lastPeriod; period < currentPeriod; period++)
 principal += ratio * principal;
lastPeriod = currentPeriod;
Enter fullscreen mode Exit fullscreen mode

Esse código adiciona todos os juros ainda não compostos ao valor principal e deve ser executado sempre que alguém desejar acessar o principal. Essa abordagem é conhecida como composição “preguiçosa” e os cálculos reais são adiados até que alguém realmente precise do resultado.

Contudo, a implementação da composição “preguiçosa” mostrada acima tem um grande problema. O consumo real do gás depende linearmente do número de intervalos existentes, desde a última vez que a composição de juros foi realizada. Se o tempo for muito curto ou a composição foi realizada muito tempo atrás, então a quantidade de gás necessária para a composição dos juros para todos os períodos passados poderá exceder o limite de gás de bloco, impossibilitando efetivamente a composição de juros subsequente. Então, a pergunta é:

Como Realizar a Composição “Preguiçosa” com Mais Eficiência?

Spoiler: dobrar os intervalos

Antes de tudo, nós observamos que a composição de juros para um período simples pode ser escrito assim:

principal *= 1 + ratio;
Enter fullscreen mode Exit fullscreen mode

Para os dois intervalos de tempo, poderia ser:

principal *= (1 + ratio) * (1 + ratio);
Enter fullscreen mode Exit fullscreen mode

Depois observamos que (1+r)²=1+(2r+r²). Então, o percentual de juros para um intervalo duplo é 2r+r², onde r é o percentual de juros para um único intervalo. Se o número de intervalos de tempo para o qual queremos calcular os juros compostos for par, podemos dividir esse número pela metade, duplicando a duração do intervalo de tempo. Quando o número de intervalos for ímpar, podemos realizar a composição de juros uma vez, e então, fazer com que o número de intervalos restante seja par. Eis o código:

function compound (uint principal, uint ratio, uint n)
public pure returns (uint) {
 while (n > 0) {
   if (n % 2 == 1) {
     principal += principal * ratio;
     n -= 1;
   } else {
     ratio = 2 * ratio + ratio * ratio;
     n /= 2;
   }
 }
 return principal;
}
Enter fullscreen mode Exit fullscreen mode

O código acima possui uma complexidade logarítmica e funciona bem quando o principal e o ratio são grandes, pois o produto principal * ratio possui quantidade de decimais suficientes para uma precisão adequada. Contudo, se, o principal e o ratio forem pequenos, esse código acima pode produzir um resultado impreciso. Agora a pergunta é:

Como Melhorar a Precisão da Composição Preguiçosa?

Spoiler: exponenciação ao quadrado

No código mostrado acima, perde-se a precisão no seguinte:

principal += principal * ratio;
Enter fullscreen mode Exit fullscreen mode

Isso porque nós assumimos que o principal é um número inteiro, então a tarefa é arredondar o valor calculado. O arredondamento pode ser realizado várias vezes e os erros de arredondamento se somam.

Para resolver esse problema, podemos observar que, para n intervalos, os juros podem ser compostos assim:

principal *= (1 + ratio) ** n;
Enter fullscreen mode Exit fullscreen mode

Esse código funcionaria se o Solidity suportasse frações, mas já que não suporta, precisamos implementar, nós mesmos, a exponenciação. Usamos a mesma abordagem da complexidade logarítmica, como usamos na seção anterior. Assim, o código fica bem semelhante:

function pow (uint x, uint n)
public pure returns (uint r) {
 r = 1.0;
 while (n > 0) {
   if (n % 2 == 1) {
     r *= x;
     n -= 1;
   } else {
     x *= x;
     n /= 2;
   }
 }
}
function compound (uint principal, uint ratio, uint n)
public pure returns (uint) {
 return principal * pow (1 + ratio, n);
}
Enter fullscreen mode Exit fullscreen mode

Observe esta expressão: r = 1.0. Ela está aqui para lembrar que nós estamos trabalhando aqui com frações, como se o Solidity as suportasse, embora ela não suporte. Alguém terá que substituir todas as operações aritméticas por funções que implementam a matemática fracionária. Por exemplo, aqui está como o código real vai ficar, com o uso da biblioteca ABDK Math 64.64, que implementa as operações aritméticas para números com ponto fixo 64.64-bit:

function pow (int128 x, uint n)
public pure returns (int128 r) {
 r = ABDKMath64x64.fromUInt (1);
 while (n > 0) {
   if (n % 2 == 1) {
     r = ABDKMath64x64.mul (r, x);
     n -= 1;
   } else {
     x = ABDKMath64x64.mul (x, x);
     n /= 2;
   }
 }
}
function compound (uint principal, uint ratio, uint n)
public pure returns (uint) {
 return ABDKMath64x64.mulu (
   pow (
     ABDKMath64x64.add (
       ABDKMath64x64.fromUInt (1),
       ABDKMath64x64.divu (
         ratio,
         10**18)),
     n),
   principal);
}
Enter fullscreen mode Exit fullscreen mode

Na verdade, essa biblioteca já possui a função pow, que pode ser usada no lugar da nossa implementação.

O código acima é bastante preciso e direto, mas funciona apenas para intervalos de tempo discretos. E se nós precisarmos compor juros para intervalos de tempo arbitrários? Tal esquema é conhecido como

Composição de Juros Contínua

A ideia da composição contínua é calcular os juros para períodos arbitrários e, não para fixos. Uma forma de conseguir isso é usar número fracionário de períodos. Nós já sabemos como calcular juros compostos para n períodos:

principal *= (1 + ratio) ** n;
Enter fullscreen mode Exit fullscreen mode

Vamos assumir que o período seja um ano e nós desejamos calcular juros compostos para 1 mês, ou seja, para 1/12 do ano. Então, a fórmula deveria ser:

principal *= (1 + ratio) ** (1 / 12);
Enter fullscreen mode Exit fullscreen mode

Infelizmente, nem o Solidity, nem a função pow apresentada acima suportam exponenciais fracionários. Nós poderíamos implementá-los ou por potência ou raiz inteiras ou por logaritmo e expoente de base fixa, mas

Existe uma Maneira Mais Simples de Fazer Composição Contínua?

Spoiler: sim: não faça

O tempo na vida real é contínuo ou ao menos parece ser. O tempo no Ethereum é discreto. É medido em segundos e é representado por números inteiros. Assim, composição periódica com período de 1 segundo funciona como composição contínua, já que ninguém pode observar o valor principal no meio de um período.

A ideia de juros compostos a cada segundo pode parecer estranha à primeira vista, mas no Ethereum funciona surpreendentemente bem. A taxa de juros anual de 3% é efetivamente equivalente uma taxa de 0.000000093668115524% por segundo, ou um percentual de juros de 0.000000000936681155 por segundo representado por 18 casas decimais. Aqui assumimos que 1 ano possui 31556952 segundos.

Compondo por 1 ano (períodos de 31556952), usando a função apresentada acima, esse percentual dá uma taxa anual de 2.99999999895%, com quase 10 dígitos significativos de precisão. O suficiente para a maioria das aplicações. Usando números de ponto fixo de 128.128-bit ao invés de 64.64-bit ou mesmo, o ponto flutuante permitiria alcançar ainda, uma precisão maior.

Em nossos experimentos a composição periódica de juros por segundo para 1 ano consumiu cerca de 90K de gás. Isso é provavelmente acessível à maioria dos aplicativos, mas, em geral, é bastante alto. Em nosso próximo artigo vamos apresentar uma abordagem mais econômica que oferece aproximadamente a mesma precisão.

Conclusão

Os cálculos fracionários complicados, como esses necessários para taxas de juros compostos periódicos, poderiam ser desafiadores em Solidity, graças à falta de suporte para números fracionários.

Contudo, os juros compostos poderiam ser calculados eficientemente usando exponenciação, elevando ao quadrado o algoritmo e os números de ponto fixo emulados.

A abordagem sugerida é poderosa o suficiente para compor taxas de juros por segundo em intervalos de 1 ano (e até por tempo maior). Contudo, essa abordagem consome bastante gás.

Em nosso próximo artigo, iremos apresentar uma abordagem melhor e o próximo tópico será: exponencial e logaritmo.

Outros artigos nesta série:

· Parte 1: Números
· Parte 2: Overflow
· Parte 3: Porcentagens e Proporções
· Part 5: Exponenciação e Logaritmos

Latest comments (0)