WEB3DEV

Cover image for Calculando Datas e Horários na Blockchain: Um Estudo de Caso da DAO Maia
Rafael Ojeda
Rafael Ojeda

Posted on

Calculando Datas e Horários na Blockchain: Um Estudo de Caso da DAO Maia

Calculando Datas e Horários na Blockchain: Um Estudo de Caso da DAO Maia

Image description

Tabela de conteúdo:

  • Introdução
  • O que é o vMaia
  • Uma breve introdução ao ERC-4626
  • Implementação de cofre ERC-4626 com bloqueio temporal
  • Resumo do contrato inteligente do vMaia
  • Como o vMaia implementa um cofre com bloqueio temporal?
  • Como calcular o tempo na blockchain?
  • Conclusões
  • Referências

Introdução

Neste post do blog, explicarei como a DAO Maia implementa um cofre com bloqueio de tempo que é compatível com o padrão ERC-4626. Também discutiremos um dos requisitos do padrão ERC-4626, de que retiradas de tokens são permitidas apenas uma vez por mês, em um dia específico.

Se você tiver alguma dúvida ou feedback sobre este post do blog, sinta-se à vontade para entrar em contato comigo no Twitter em @s3rgiomazari3go, minhas DM’s estão sempre abertas.

O que é o vMaia

O que é vMaia: é um token MAIA compatível com ERC-4626 que distribui tokens utilitários bHermes (Peso, Governança) e Governança Maia em troca de staking de MAIA, a retirada desses tokens só é permitida uma vez por mês, durante a 1ª terça-feira (UTC+0) de cada mês.

  • O vMaia é um tipo de token que é usado para participar do ecossistema Maia.
  • Os tokens vMaia são cunhados quando os usuários fazem stake de tokens MAIA.
  • Os tokens vMaia podem ser usados para votar em propostas, ganhar recompensas e acessar recursos exclusivos.
  • As retiradas de tokens vMaia só são permitidas uma vez por mês, durante a 1ª terça-feira (UTC+0) de cada mês.

Uma breve introdução ao ERC-4626

O ERC-4626 é um padrão de cofre tokenizado que define um conjunto de regras sobre como os cofres devem ser implementados na blockchain da Ethereum. Os cofres são contratos inteligentes que permitem que os usuários depositem e retirem tokens e ganhem recompensas por isso. O ERC-4626 fornece uma API padrão para cofres, o que facilita a criação e o uso de cofres pelos desenvolvedores.

Os cofres ERC-4626 podem ser usados para ganhar uma variedade de recompensas, incluindo juros, recompensas de staking e airdrops, e também podem ser usados para acessar os serviços DeFi, como empréstimos e financiamentos.

Dois recursos dos cofres ERC-4626:

  • Padronização: o ERC-4626 fornece uma API padrão para cofres, o que facilita a criação e o uso de cofres pelos desenvolvedores.
  • Liquidez: os cofres ERC-4626 permitem que você acesse facilmente seus tokens. Você pode retirar seus tokens a qualquer momento ou quando o desenvolvedor especificar na implementação.

Implementação de cofre ERC-4626 com bloqueio de tempo

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
import {Ownable} from "solady/auth/Ownable.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";

import {ERC20} from "solmate/tokens/ERC20.sol";

import {bHermesVotes as vMaiaVotes} from "@hermes/tokens/bHermesVotes.sol";

import {DateTimeLib} from "./libraries/DateTimeLib.sol";
import {ERC4626PartnerManager, PartnerManagerFactory} from "./tokens/ERC4626PartnerManager.sol";

/**

* @title vMaia: MAIA com suporte de rendimento, reforço, votação e medidor ativado 
* @author DAO Maia  (https://github.com/Maia-DAO)
* @notice vMaia é um token MAIA compatível com ERC-4626 que:

*    distribui tokens de utilidade bHermes (Peso, Governança) e Governança Maia

*    em troca de stake de MAIA.

*   OBSERVAÇÃO: a retirada é permitida apenas uma vez por mês,

*                durante a primeira terça-feira (UTC+0) do mês.
*/
contract vMaia is ERC4626PartnerManager {
using SafeTransferLib for address;
using FixedPointMathLib for uint256;

perl
Copy code
/*//////////////////////////////////////////////////////////////
                     ESTADO DO vMAIA
//////////////////////////////////////////////////////////////*/

uint256 private currentMonth;
uint256 private unstakePeriodEnd;

/**
 * @notice Inicializa o token vMaia.
 * @param _factory A fábrica que criou este contrato.
 * @param _bHermesRate A taxa pela qual o bHermes pode ser reivindicado.
 * @param _partnerAsset O ativo que será usado para depositar e obter vMaia.
 * @param _name O nome do token.
 * @param _symbol O símbolo do token.
 * @param _bhermes O endereço do token bHermes.
 * @param _partnerVault O endereço do cofre do parceiro.
 * @param _owner O proprietário do token.
 */
constructor(
    PartnerManagerFactory _factory,
    uint256 _bHermesRate,
    ERC20 _partnerAsset,
    string memory _name,
    string memory _symbol,
    address _bhermes,
    address _partnerVault,
    address _owner
) ERC4626PartnerManager(_factory, _bHermesRate, _partnerAsset, _name, _symbol, _bhermes, _partnerVault, _owner) {
    // Define o mês atual como o mês atual.
    currentMonth = DateTimeLib.getMonth(block.timestamp);
}

/*///////////////////////////////////////////////////////////////
                        MODIFICADORES
//////////////////////////////////////////////////////////////*/

/// @dev Verifica se o peso disponível permite a chamada.
modifier checkWeight(uint256 amount) virtual override {
    if (balanceOf[msg.sender] < amount + userClaimedWeight[msg.sender]) {
        revert InsufficientShares();
    }
    _;
}

/// @dev Verifica se a governança disponível permite a chamada.
modifier checkGovernance(uint256 amount) virtual override {
    if (balanceOf[msg.sender] < amount + userClaimedGovernance[msg.sender]) {
        revert InsufficientShares();
    }
    _;
}

/// @dev Verifica se a governança do parceiro disponível permite a chamada.
modifier checkPartnerGovernance(uint256 amount) virtual override {
    if (balanceOf[msg.sender] < amount + userClaimedPartnerGovernance[msg.sender]) {
        revert InsufficientShares();
    }
    _;
}

/// @dev O aumento não pode ser reivindicado; não falha. É totalmente usado pelo cofre do parceiro.
function claimBoost(uint256 amount) public override {}

/*//////////////////////////////////////////////////////////////
                      LÓGICA DE GANCHOS INTERNOS
//////////////////////////////////////////////////////////////*/

/**
 * @notice Função que realiza as verificações necessárias antes que um usuário possa retirar sua posição vMaia.
 * Verifica se estamos dentro do período de desbloqueio, se sim, então o usuário pode retirar.
 * Se não estivermos no período de desbloqueio, haverá verificações para determinar se este é o início do mês.
 */
function beforeWithdraw(uint256, uint256) internal override {
    /// @dev Verifica se o período de desbloqueio ainda não terminou, continua se for o caso.
    if (unstakePeriodEnd >= block.timestamp) return;

    uint256 _currentMonth = DateTimeLib.getMonth(block.timestamp);
    if (_currentMonth == currentMonth) revert UnstakePeriodNotLive();

    (bool isTuesday, uint256 _unstakePeriodStart) = DateTimeLib.isTuesday(block.timestamp);
    if (!isTuesday) revert UnstakePeriodNotLive();

    currentMonth = _currentMonth;
    unstakePeriodEnd = _unstakePeriodStart + 1 days;
}

/*///////////////////////////////////////////////////////////////
                         ERROS
//////////////////////////////////////////////////////////////*/

/// @dev Erro lançado ao tentar fazer a retirada e não for a primeira terça-feira do mês.
error UnstakePeriodNotLive();
}


Enter fullscreen mode Exit fullscreen mode

Resumo do contrato inteligente vMaia

O vMaia é um token compatível com ERC-4626 que:

  • Distribui tokens de utilidade bHermes (Peso, Governança) e Governança Maia.
  • Em troca de staking de MAIA.
  • A retirada de Maia só é permitida uma vez por mês (durante a 1ª terça-feira (UTC+0) do mês).

Resumo de cada função do contrato inteligente vMaia:

constructor(
PartnerManagerFactory _factory,
uint256 _bHermesRate,
ERC20 _partnerAsset,
string memory _name,
string memory _symbol,
address _bhermes,
address _partnerVault,
address _owner
)
Enter fullscreen mode Exit fullscreen mode

Este construtor inicializa o token vMaia e recebe os seguintes parâmetros:

* `_factory`: a fábrica que criou este contrato.
* `_bHermesRate`: a taxa pela qual o bHermes pode ser reivindicado.
* `_partnerAsset`: o ativo que será usado para depositar e obter vMaia.
* `_name`: o nome do token.
* `_symbol`: o símbolo do token.
* `_bhermes`: o endereço do token bHermes.
* `_partnerVault`: o endereço do cofre do parceiro.
* `_owner`: o proprietário do token.
Enter fullscreen mode Exit fullscreen mode
modifier checkWeight(uint256 amount) virtual override
Enter fullscreen mode Exit fullscreen mode

Esse modificador verifica se o usuário tem vMaia suficiente para reivindicar a quantidade especificada de peso. Se o usuário não tiver vMaia suficiente, a função irá reverter.

modifier checkGovernance(uint256 amount) virtual override
Enter fullscreen mode Exit fullscreen mode

Esse modificador verifica se o usuário tem vMaia suficiente para reivindicar a quantidade especificada de governança. Se o usuário não tiver vMaia suficiente, a função irá reverter.

modifier checkPartnerGovernance(uint256 amount) virtual override
Enter fullscreen mode Exit fullscreen mode

Esse modificador verifica se o usuário tem vMaia suficiente para reivindicar a quantidade especificada de governança do parceiro. Se o usuário não tiver vMaia suficiente, a função irá reverter.

function claimBoost(uint256 amount) public override {}
Enter fullscreen mode Exit fullscreen mode

Essa função vazia reivindica a quantidade especificada de aumento (boost), uma vez que o aumento não pode ser reivindicado, essa função não falha.

function beforeWithdraw(uint256, uint256) internal override
Enter fullscreen mode Exit fullscreen mode

Esta função verifica se o usuário tem permissão para retirar seu vMaia. Se o período de desbloqueio ainda não terminou, a função retorna. Se o mês atual é igual ao mês anterior, a função reverte. Se não for terça-feira, a função reverte. Se todas as verificações forem bem-sucedidas, o mês atual é atualizado e o período de desbloqueio é definido para começar um dia a partir de agora.

error UnstakePeriodNotLive();
Enter fullscreen mode Exit fullscreen mode

Este erro é lançado quando o usuário tenta retirar seu vMaia fora do período de desbloqueio.

Como o vMaia implementa um cofre com bloqueio temporal?

Esta postagem no blog tem como foco apenas entender a implementação do bloqueio temporal de cofre vMaia. Primeiro, precisamos entender duas coisas:


/**

* @notice Função que realiza as verificações necessárias antes que um usuário possa retirar sua posição de vMaia.

* Verifica se estamos dentro do período de desbloqueio, se sim, então o usuário pode retirar.

* Se não estivermos no período de desbloqueio, haverá verificações para determinar se este é o início do mês.

*/

function beforeWithdraw(uint256, uint256) internal override {

/// @dev Verifica se o período de desbloqueio ainda não terminou, continua se for o caso.

if (unstakePeriodEnd >= block.timestamp) return;

java

Copy code

uint256 _currentMonth = DateTimeLib.getMonth(block.timestamp);

scss

Copy code

if (_currentMonth == currentMonth) revert UnstakePeriodNotLive();

(bool isTuesday, uint256 _unstakePeriodStart) = DateTimeLib.isTuesday(block.timestamp);

if (!isTuesday) revert UnstakePeriodNotLive();

currentMonth = _currentMonth;

unstakePeriodEnd = _unstakePeriodStart + 1 days;

}

Enter fullscreen mode Exit fullscreen mode

Como calcular o tempo na blockchain?

A função beforeWithdraw é o tópico principal desta postagem do blog, portanto, analisaremos cada linha de código e a entenderemos.

A função beforeWithdraw consiste no seguinte:

  • Primeiro, ela verifica se o valor de unstakePeriodEnd é maior ou igual a block.timestamp; essa é uma variável global no Solidity que retorna o registro de data e hora do bloco atual; o registro de data e hora é o número de segundos desde a época do Unix, que é 1º de janeiro de 1970, no UTC.
  • A segunda etapa é usar o método DateTimeLib.getMonth de uma versão modificada (aqui) da biblioteca Solidity (aqui), esse método obtém o valor atual do block.timestamp e o converte em um mês. Criei um contrato se você quiser experimentar (aqui).
  • A terceira etapa é verificar se hoje é terça-feira, o que é feito chamando a função DateTimeLib.isTuesday e passando o registro de data e hora do bloco atual. Se o resultado da função DateTimeLib.isTuesday for false, então hoje não é terça-feira e a função será revertida.
  • Se todas as três etapas anteriores tiverem sido aprovadas, a função atualizará a variável currentMonth e definirá a variável unstakePeriodEnd para iniciar em um dia a partir de agora.

// @dev Retorna (month) a partir do número de dias desde 1970-01-01.

/// Consulte: https://howardhinnant.github.io/date_algorithms.html

/// Observação: entradas fora das faixas suportadas resultam em comportamento indefinido.

/// Use {isSupportedDays} para verificar se as entradas são suportadas.

function getMonth(uint256 timestamp) internal pure returns (uint256 month) {

uint256 epochDay = timestamp / 86400;

    /// @solidity memory-safe-assembly

    assembly {

        epochDay := add(epochDay, 719468)

        let doe := mod(epochDay, 146097)

        let yoe := div(sub(sub(add(doe, div(doe, 36524)), div(doe, 1460)), eq(doe, 146096)), 365)

        let doy := sub(doe, sub(add(mul(365, yoe), shr(2, yoe)), div(yoe, 100)))

        let mp := div(add(mul(5, doy), 2), 153)

        month := sub(add(mp, 3), mul(gt(mp, 9), 12))

    }

}

Enter fullscreen mode Exit fullscreen mode

A função para obter o mês funciona da seguinte forma (descrição geral):

  • A primeira etapa é converter o número de dias desde 1970-01-01 para o número de dias desde a época do Unix. Isso é feito dividindo o número de dias por 86400, que é o número de segundos em um dia.
  • A próxima etapa é usar o Assembly para calcular o ano, o mês e o dia a partir do número de dias desde a época do Unix. O código do Assembly usa vários algoritmos baseados no calendário gregoriano proléptico.
  • A etapa final é retornar o mês.

Para a parte complicada, deixarei nosso melhor amigo ChatGPT explicar a função em detalhes.

  • uint256 epochDay = timestamp / 86400: esta linha de código divide o timestamp (registro de data e hora) por 86400 para obter o número de dias desde a época Unix. O número 86400 representa a quantidade de segundos em um dia. Ao dividir o timestamp por 86400, obtemos o número de dias desde a época Unix.
  • epochDay := add(epochDay, 719468): essa linha de código adiciona 719468 à variável epochDay, porque a época do Unix é 1º de janeiro de 1970 e o calendário gregoriano proléptico começa em 1º de janeiro de 4713 a.C, o número 719468 é o número de dias entre a época do Unix e o início do calendário gregoriano proléptico.
  • let doe := mod(epochDay, 146097): essa linha de código calcula o dia da época, usando a função mod para calcular o restante da divisão do número de dias desde a época do Unix por 146097, o número 146097 é o número de dias em 20871 anos, que é o período de tempo mais longo possível entre anos bissextos.
  • let yoe := div(sub(sub(add(doe, div(doe, 36524)), div(doe, 1460)), eq(doe, 146096)), 365): essa linha de código calcula o ano da época, isso é feito usando a função div para dividir o número de dias desde a época do Unix por 146097, o resultado é o número de anos desde o início do calendário gregoriano proléptico.
  • O número 146097 é o número de dias em 20871 anos, que é o período de tempo mais longo possível entre os anos bissextos. O número 146097 não está explicitamente na função, mas está implícito na divisão por 36524 e na subtração de 1460.
  • O código primeiro divide o número de dias desde a época do Unix por 36524, o que dá o número de ciclos de 400 anos, que é o período de tempo mais longo que é sempre um múltiplo de 4 anos.
  • Em seguida, o código subtrai o número de ciclos de 400 anos do número de dias desde a época do Unix, o que dá o número de dias que não estão em um ciclo de 400 anos.
  • O código então divide o número de dias que não estão em um ciclo de 400 anos por 1460, o que dá o número de ciclos de 28 anos, que é o período de tempo mais longo que é sempre um múltiplo de 4 anos e 7 anos.
  • Em seguida, o código subtrai o número de ciclos de 28 anos do número de dias que não estão em um ciclo de 400 anos, o que dá o número de dias que não estão em um ciclo de 28 anos.
  • Em seguida, o código verifica se o número de dias que não estão em um ciclo de 28 anos é igual a 146096. Se for, o código adiciona 1 ao resultado, pois o ano de 146096 é bissexto, mas o ano de 146097 não é.
  • O código então divide o número de dias que não estão em um ciclo de 28 anos por 365, o que dá o número de anos desde o início do calendário gregoriano proléptico.
  • let doy := sub(doe, sub(add(mul(365, yoe), shr(2, yoe)), div(yoe, 100))): essa linha de código calcula o dia do ano, usando a função sub para subtrair o número de anos desde o início do calendário gregoriano proléptico do número de dias desde a época do Unix; o resultado é o número de dias desde o início do ano atual.
  • let mp := div(add(mul(5, doy), 2), 153): essa linha de código calcula o mês da época, o que é feito usando a função div para dividir o número de dias desde o início do ano atual por 30; o resultado é o número de meses desde o início do ano atual.
  • month := sub(add(mp, 3), mul(gt(mp, 9), 12)): essa linha de código retorna o mês, o que é feito usando a função sub para subtrair o número de meses desde o início do ano atual do número de meses desde o início do calendário gregoriano proléptico; se o número de meses desde o início do ano atual for maior que 9, a função adicionará 12 ao resultado, porque há 3 trimestres em um ano e cada trimestre tem 3 meses.

A função para verificar se é terça-feira é muito mais fácil de entender.


/// @dev Retorna o dia da semana a partir do timestamp Unix.

/// Segunda-feira: 1, Terça-feira: 2, ....., Domingo: 7.

function isTuesday(uint256 timestamp) internal pure returns (bool result, uint256 startOfDay) {

unchecked {

uint256 day = timestamp / 86400;

startOfDay = day * 86400;

result = ((day + 3) % 7) + 1 == 2;

          }

     }

}

Enter fullscreen mode Exit fullscreen mode
  • A função isTuesday da biblioteca é uma função que retorna se o timestamp Unix fornecido é uma terça-feira; a função recebe um único parâmetro, timestamp, que é o timestamp Unix a ser verificado; a função retorna um valor booleano, **result, que é true (verdadeiro) se o timestamp a for uma terça-feira e false (falso) caso contrário; a função também retorna um valor uint256, startOfDay, que é o timestamp Unix do início do dia em que o timestamp está.
  • A função isTuesday funciona primeiro dividindo o parâmetro timestamp por 86400, que é o número de segundos em um dia, o que dá o dia da semana do timestamp, a função então adiciona 3 ao dia da semana e, em seguida, pega o módulo de 7, o que dá o número de dias desde domingo, a função então adiciona 1 a esse número para obter o número de dias desde segunda-feira, se esse número for 2, então o timestamp é uma terça-feira.

Conclusão:

Uma das suposições mais interessantes que eu queria testar neste desafio era ver se era possível desfazer a posição em stake de Maia em um dia diferente de terça-feira. À medida que aprendi mais sobre como o Maia implementou uma boa abordagem para calcular datas e meses usando variáveis de blockchain, Assembly e Solidity, percebi que não era possível desfazer o stake em nenhum outro dia (até este momento).

Como desenvolvedores e auditores, aprendemos muito com esse desafio, aprendemos sobre as diferentes maneiras de calcular datas e meses na blockchain.

Obrigado pela leitura!

Referências:

Maia DAO Contest in Code4rena

Maia DAO Contest Repository

Solady DateTimeLib.sol

Maia DAO Modified Version of DateTimeLib.sol

Date Algorithms

Este artigo foi escrito por Sergio Mazariego e traduzido para o português por Rafael Ojeda

Você pode ler o artigo original aqui

Oldest comments (0)