Skip to content

Latest commit

 

History

History
 
 

45_Timelock

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
title tags
45. Time Lock
solidity
aplicação

WTF Solidity Introdução Simples: 45. Time Lock

Recentemente, tenho estudado solidity novamente para revisar os detalhes e escrever um "WTF Solidity Introdução Simples" para iniciantes (programadores experientes podem procurar outros tutoriais). Serão lançadas de 1 a 3 aulas por semana.

Twitter: @0xAA_Science

Comunidade: DiscordGrupo WeChatSite oficial wtf.academy

Todo o código e tutoriais estão disponíveis no GitHub: github.com/AmazingAng/WTF-Solidity


Nesta aula, vamos falar sobre time lock (bloqueio de tempo) e contratos de time lock. O código é uma simplificação do contrato Timelock do Compound Timelock.sol.

Time Lock

Time Lock

Time lock (bloqueio de tempo) é um mecanismo de bloqueio comum em cofres bancários e outros recipientes de alta segurança. É um tipo de temporizador projetado para evitar que um cofre ou depósito seja aberto antes de um determinado tempo, mesmo que a pessoa que está abrindo o cofre conheça a senha correta.

Na blockchain, o time lock é amplamente utilizado em DeFi e DAOs. É um trecho de código que pode bloquear certas funcionalidades de um smart contract por um determinado período de tempo. Isso pode melhorar significativamente a segurança de um smart contract. Por exemplo, se um hacker invadir uma carteira multi-assinatura do Uniswap e tentar retirar os fundos, mas o contrato do cofre tiver um time lock de 2 dias, o hacker terá que esperar 2 dias desde a criação da transação até a retirada dos fundos. Durante esse período, a equipe do projeto pode tomar medidas para lidar com a situação e os investidores podem vender seus tokens antecipadamente para minimizar as perdas.

Contrato de Time Lock

A seguir, vamos apresentar o contrato Timelock. A lógica do contrato não é complexa:

  • Ao criar o contrato Timelock, o projeto pode definir o período de bloqueio e definir a si mesmo como o administrador do contrato.

  • O time lock tem três principais funcionalidades:

    • Criar uma transação e adicioná-la à fila do time lock.
    • Executar uma transação após o período de bloqueio.
    • Cancelar uma ou mais transações na fila do time lock.
  • Geralmente, o contrato de time lock é definido como o administrador de contratos importantes, como o contrato do cofre, e é usado para operá-los.

  • O administrador do contrato de time lock geralmente é uma carteira multi-assinatura do projeto, garantindo a descentralização.

Eventos

O contrato Timelock possui quatro eventos.

  • QueueTransaction: evento disparado quando uma transação é criada e adicionada à fila do time lock.
  • ExecuteTransaction: evento disparado quando uma transação é executada após o período de bloqueio.
  • CancelTransaction: evento disparado quando uma transação é cancelada.
  • NewAdmin: evento disparado quando o endereço do administrador é alterado.
    // Eventos
    // Evento disparado quando uma transação é cancelada
    event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature,  bytes data, uint executeTime);
    // Evento disparado quando uma transação é executada
    event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature,  bytes data, uint executeTime);
    // Evento disparado quando uma transação é criada e adicionada à fila
    event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
    // Evento disparado quando o endereço do administrador é alterado
    event NewAdmin(address indexed newAdmin);

Variáveis de Estado

O contrato Timelock possui quatro variáveis de estado.

  • admin: endereço do administrador.
  • delay: período de bloqueio.
  • GRACE_PERIOD: tempo de expiração da transação. Se a transação estiver pronta para ser executada, mas não for executada dentro do GRACE_PERIOD, ela será considerada expirada.
  • queuedTransactions: mapeamento do hash da transação para um booleano, indicando se a transação está na fila do time lock.
    // Variáveis de Estado
    address public admin; // endereço do administrador
    uint public constant GRACE_PERIOD = 7 days; // tempo de expiração da transação, transações expiradas são canceladas
    uint public delay; // período de bloqueio (em segundos)
    mapping (bytes32 => bool) public queuedTransactions; // mapeamento do hash da transação para um booleano, indicando se a transação está na fila do time lock

Modificadores

O contrato Timelock possui dois modificadores.

  • onlyOwner(): o contrato só pode ser executado pelo administrador.
  • onlyTimelock(): o contrato só pode ser executado pelo contrato de time lock.
    // Modificador onlyOwner
    modifier onlyOwner() {
        require(msg.sender == admin, "Timelock: Caller not admin");
        _;
    }

    // Modificador onlyTimelock
    modifier onlyTimelock() {
        require(msg.sender == address(this), "Timelock: Caller not Timelock");
        _;
    }

Funções

O contrato Timelock possui sete funções.

  • Construtor: inicializa o período de bloqueio (em segundos) e o endereço do administrador.

  • queueTransaction(): cria uma transação e a adiciona à fila do time lock. Os parâmetros são complexos porque descrevem uma transação completa:

    • target: endereço do contrato de destino.
    • value: quantidade de ETH a ser enviada.
    • signature: assinatura da função a ser chamada.
    • data: dados da chamada da transação.
    • executeTime: timestamp da blockchain para a execução da transação.

    Ao chamar essa função, é necessário garantir que o tempo de execução da transação executeTime seja maior que o timestamp atual da blockchain mais o período de bloqueio delay. O hash dos parâmetros é usado como identificador exclusivo da transação, calculado pela função getTxHash(). A transação adicionada à fila é atualizada na variável queuedTransactions e o evento QueueTransaction é emitido.

  • executeTransaction(): executa uma transação. Os parâmetros são os mesmos da função queueTransaction(). A transação a ser executada deve estar na fila do time lock, ter atingido o tempo de execução e não ter expirado. A função utiliza a função de baixo nível call do Solidity, que foi explicada na aula 22.

  • cancelTransaction(): cancela uma transação. Os parâmetros são os mesmos da função queueTransaction(). A transação a ser cancelada deve estar na fila e é atualizada na variável queuedTransactions e o evento CancelTransaction é emitido.

  • changeAdmin(): altera o endereço do administrador, só pode ser chamada pelo contrato de time lock.

  • getBlockTimestamp(): obtém o timestamp atual da blockchain.

  • getTxHash(): retorna o identificador da transação, que é o hash dos parâmetros da transação.

    /**
     * @dev Construtor, inicializa o período de bloqueio (em segundos) e o endereço do administrador.
     */
    constructor(uint delay_) {
        delay = delay_;
        admin = msg.sender;
    }

    /**
     * @dev Altera o endereço do administrador, só pode ser chamada pelo contrato de time lock.
     */
    function changeAdmin(address newAdmin) public onlyTimelock {
        admin = newAdmin;

        emit NewAdmin(newAdmin);
    }

    /**
     * @dev Cria uma transação e a adiciona à fila do time lock.
     * @param target: endereço do contrato de destino.
     * @param value: quantidade de ETH a ser enviada.
     * @param signature: assinatura da função a ser chamada.
     * @param data: dados da chamada da transação.
     * @param executeTime: timestamp da blockchain para a execução da transação.
     *
     * Requer: executeTime seja maior que o timestamp atual da blockchain mais o período de bloqueio.
     */
    function queueTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner returns (bytes32) {
        // Requer: o tempo de execução da transação seja maior que o período de bloqueio
        require(executeTime >= getBlockTimestamp() + delay, "Timelock::queueTransaction: Estimated execution block must satisfy delay.");
        // Calcula o identificador exclusivo da transação: hash dos parâmetros
        bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
        // Adiciona a transação à fila
        queuedTransactions[txHash] = true;

        emit QueueTransaction(txHash, target, value, signature, data, executeTime);
        return txHash;
    }

    /**
     * @dev Cancela uma transação específica.
     *
     * Requer: a transação esteja na fila do time lock.
     */
    function cancelTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner{
        // Calcula o identificador exclusivo da transação: hash dos parâmetros
        bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
        // Requer: a transação esteja na fila do time lock
        require(queuedTransactions[txHash], "Timelock::cancelTransaction: Transaction hasn't been queued.");
        // Remove a transação da fila
        queuedTransactions[txHash] = false;

        emit CancelTransaction(txHash, target, value, signature, data, executeTime);
    }

    /**
     * @dev Executa uma transação específica.
     *
     * Requer:
     * 1. A transação esteja na fila do time lock.
     * 2. Tenha atingido o tempo de execução.
     * 3. Não tenha expirado.
     */
    function executeTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public payable onlyOwner returns (bytes memory) {
        bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
        // Requer: a transação esteja na fila do time lock
        require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued.");
        // Requer: tenha atingido o tempo de execução
        require(getBlockTimestamp() >= executeTime, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.");
        // Requer: não tenha expirado
       require(getBlockTimestamp() <= executeTime + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale.");
        // Remove a transação da fila
        queuedTransactions[txHash] = false;

        // Obtém os dados da chamada
        bytes memory callData;
        if (bytes(signature).length == 0) {
            callData = data;
        } else {
            callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
        }
        // Executa a transação usando call
        (bool success, bytes memory returnData) = target.call{value: value}(callData);
        require(success, "Timelock::executeTransaction: Transaction execution reverted.");

        emit ExecuteTransaction(txHash, target, value, signature, data, executeTime);

        return returnData;
    }

    /**
     * @dev Obtém o timestamp atual da blockchain.
     */
    function getBlockTimestamp() public view returns (uint) {
        return block.timestamp;
    }

    /**
     * @dev Retorna o identificador da transação, que é o hash dos parâmetros da transação.
     */
    function getTxHash(
        address target,
        uint value,
        string memory signature,
        bytes memory data,
        uint executeTime
    ) public pure returns (bytes32) {
        return keccak256(abi.encode(target, value, signature, data, executeTime));
    }

Demonstração no Remix

1. Implante o contrato Timelock com um período de bloqueio de 120 segundos.

Demonstração no Remix

2. Chame diretamente a função changeAdmin() e você receberá um erro.

Demonstração no Remix

3. Construa uma transação para alterar o administrador.

Para construir a transação, você precisa preencher os seguintes parâmetros: address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime

  • target: como estamos chamando uma função do próprio contrato Timelock, preencha com o endereço do contrato.
  • value: não é necessário enviar ETH, então preencha com 0.
  • signature: a assinatura da função changeAdmin() é "changeAdmin(address)".
  • data: preencha com os parâmetros da função, que é o endereço do novo administrador. No entanto, você precisa preencher o endereço com 32 bytes de dados para atender ao padrão de codificação ABI do Ethereum. Você pode usar o site hashex para codificar os parâmetros. Exemplo:
    Endereço antes da codificação: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
    Endereço após a codificação: 0x000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2
  • executeTime: obtenha o timestamp atual da blockchain chamando a função getBlockTimestamp() e adicione 150 segundos. Demonstração no Remix

4. Chame a função queueTransaction() para adicionar a transação à fila do time lock.

Demonstração no Remix

5. Chame a função executeTransaction() durante o período de bloqueio e a chamada falhará.

Demonstração no Remix

6. Chame a função executeTransaction() após o período de bloqueio e a chamada será bem-sucedida.

Demonstração no Remix

7. Verifique o novo endereço do admin.

Demonstração no Remix

Conclusão

O time lock pode bloquear certas funcionalidades de um smart contract por um determinado período de tempo, reduzindo significativamente as chances de um projeto fazer um "rug pull" e aumentando a segurança contra ataques de hackers. É amplamente utilizado em DeFi e DAOs, incluindo Uniswap e Compound. Os projetos em que você investe utilizam time lock?