Prefácio
Esta é a seção final da série de 5 partes sobre como resolver falhas de projeto recorrentes por meio de padrões de projeto convencionais e reutilizáveis. Por meio disso, vamos dissecar em Security, um conjunto de padrões que introduzem medidas de segurança para mitigar danos e garantir uma execução confiável do contrato.
Verificações-Efeitos-Interação
Problema
Quando um contrato chama outro contrato, ele entrega o controle a esse outro contrato. O contrato chamado pode, por sua vez, reinserir o contrato pelo qual foi chamado e tentar
manipular seu estado ou sequestrar o fluxo de controle por meio de código malicioso.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
// ESTE CONTRATO CONTÉM UM BUG - NÃO USE
contract Fund {
/// Mapeamento @dev das partes ether do contrato.
mapping(address => uint256) shares;
/// Saque a sua parte.
function withdraw() public {
// o código do chamador é executado e pode reinserir a retirada novamente
(bool success,) = msg.sender.call{value: shares[msg.sender]}("");
// INSEGURO - os compartilhamentos do usuário devem ser redefinidos antes da chamada externa
if (success)
shares[msg.sender] = 0;
}
}
A transferência de Ether sempre pode incluir a execução de código, portanto, o destinatário pode ser um contrato que chama de volta retirar. Isso permitiria obter vários reembolsos e, basicamente, recuperar todo o Ether do contrato.
Solução
O padrão verificação-efeitos-interação é fundamental para funções de codificação e descreve como o código de função deve ser estruturado para evitar efeitos colaterais e comportamento de execução indesejado.
O padrão verificação-efeitos-interação garante que todos os caminhos de código através de um contrato completem todas as verificações necessárias dos parâmetros fornecidos antes de modificar o estado do contrato (Checks), só então ele faz qualquer alteração no estado (Effects), pode fazer chamadas para funções em outros contratos depois que todas as alterações de estado planejadas sejam gravadas no armazenamento (interações).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Fund {
/// Mapeamento @dev das partes ether do contrato.
mapping(address => uint256) shares;
/// Saque sua parte.
function withdraw() public {
uint256 share = shares[msg.sender];
// 1. Verificações
require(share > 0);
// 2. Efeitos
shares[msg.sender] = 0;
// 3. Interação
payable(msg.sender).transfer(share);
}
}
O ataque de reentrância é especialmente prejudicial quando usa address.call de baixo nível, que encaminha todo o gás restante por padrão, dando ao contrato chamado mais espaço para ações potencialmente maliciosas. Portanto, o uso de address.call de baixo nível deve ser evitado sempre que possível.
Para envio de fundos address.send() e address.transfer() devem ser preferidos, essas funções minimizam o risco de reentrância por meio de encaminhamento de gás limitado (o contrato chamado recebe apenas um estipêndio de 2.300 gás, que atualmente é suficiente apenas para registrar um evento).
Parada de Emergência (Disjuntor)
Problema
Uma vez que um contrato implantado é executado de forma autônoma na rede Ethereum, não há opção de interromper sua execução em caso de um grande bug ou problema de segurança.
Solução
Uma contramedida e uma resposta rápida a ataques desconhecidos são paradas de emergência ou disjuntores. Eles interrompem a execução de um contrato ou de suas partes quando certas condições são atendidas.
Um cenário recomendado seria que, assim que um bug fosse detectado, todas as funções críticas seriam interrompidas, deixando apenas a possibilidade de retirar fundos.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Pausable {
bool private paused;
event Paused(address account);
event Unpaused(address account);
error EnforcedPause();
error ExpectedPause();
modifier whenNotPaused() {
if (paused) {
revert EnforcedPause();
}
_;
}
modifier whenPaused() {
if (!paused) {
revert ExpectedPause();
}
_;
}
constructor() {
paused = false;
}
function _pause() internal virtual whenNotPaused {
paused = true;
emit Paused(msg.sender);
}
function _unpause() internal virtual whenPaused {
paused = false;
emit Unpaused(msg.sender);
}
}
Implemente a parada de emergência no contrato de Stake.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Pausable.sol";
contract Staking is Pausable, Ownable {
/** declarar variáveis de estado */
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
function deposit() external payable whenNotPaused {
// algum código
}
function withdraw() external whenNotPaused {
// algum código
}
function emergencyWithdraw() external whenPaused {
// algum código
}
}
Speedbump (Redutor de Velocidade)
Problema
A execução simultânea de tarefas delicadas por um grande número de partes pode acarretar a ruína de um contrato.
Solução
Tarefas sensíveis ao contrato são desaceleradas de propósito, portanto, quando ocorrem ações maliciosas, o dano é restrito e há mais tempo disponível para neutralizar.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SpeedBump {
struct Withdrawal {
uint256 amount;
uint256 requestedAt;
}
uint256 constant WAIT_PERIOD = 7 days;
mapping (address => uint256) private balances;
mapping (address => Withdrawal) private withdrawals;
// O endereço de cada usuário pode depositar apenas 1 vez até ser totalmente retirado
function deposit() public payable {
bool hasDeposited = withdrawals[msg.sender].amount > 0;
if(!hasDeposited)
balances[msg.sender] += msg.value;
}
function requestWithdrawal() public {
if (balances[msg.sender] > 0) {
uint256 amountToWithdraw = balances[msg.sender];
balances[msg.sender] = 0;
withdrawals[msg.sender] = Withdrawal({
amount: amountToWithdraw,
requestedAt: block.timestamp
});
}
}
// Somente retirado totalmente quando o WAIT_PERIOD expirou
function withdraw() public {
if(withdrawals[msg.sender].amount > 0 &&
block.timestamp > withdrawals[msg.sender].requestedAt + WAIT_PERIOD)
{
uint256 amount = withdrawals[msg.sender].amount;
withdrawals[msg.sender].amount = 0;
payable(msg.sender).transfer(amount);
}
}
}
Você pode fazer referência ao contrato TimelockController implementado pelo OpenZeppelin para a versão do contrato baseado em produção.Um timelock é um contrato inteligente que atrasa as chamadas de função de outro contrato inteligente após um período de tempo predeterminado. Os prazos são usados principalmente no contexto da governança para adicionar um atraso nas ações administrativas e geralmente são considerados um forte indicador de que um projeto é legítimo e demonstra o compromisso com o projeto por parte dos proprietários do projeto.
Rate Limit (Taxa Limite)
Problema
Uma solicitação apressada em uma determinada tarefa não é desejada e pode prejudicar a correta execução operacional de um contrato.
Solução
Um limite de taxa regula a frequência com que uma função pode ser chamada consecutivamente dentro de um intervalo de tempo especificado.
Um cenário de uso para contratos inteligentes pode ser fundamentado em considerações operacionais, a fim de controlar o impacto do comportamento (coletivo) do usuário.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract RateLimit {
uint enabledAt = block.timestamp;
modifier enabledEvery(uint256 t) {
if (block.timestamp >= enabledAt) {
enabledAt = block.timestamp + t;
_;
}
}
function withdraw() public enabledEvery(1 minutes) {
// algum código
}
}
O exemplo acima demonstra a limitação da taxa de execução de retirada de um contrato para evitar uma rápida drenagem de fundos.
mutex
Problema
Os ataques de reentrância podem manipular o estado de um contrato e sequestrar o fluxo de controle.
Solução
Um mutex (de exclusão mútua) é conhecido como um mecanismo de sincronização na ciência da computação para restringir o acesso simultâneo a um recurso. Após o surgimento de cenários de ataque de reentrância, esse padrão encontrou sua aplicação em contratos inteligentes para proteger contra chamadas de função recursivas de contratos externos.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
error ReentrancyGuardReentrantCall();
constructor() {
_status = _NOT_ENTERED;
}
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// Na primeira chamada para nonReentrant, _status será _NOT_ENTERED
if (_status == _ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Quaisquer chamadas para nonReentrant após este ponto falharão
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// Ao armazenar novamente o valor original, um reembolso é acionado (consulte
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
}
contract Mutex is ReentrancyGuard {
/** declara variáveis de estado */
// f é protegido por um mutex, portanto chamadas reentrantes
// de dentro de msg.sender.call não pode chamar f novamente
function f() external nonReentrant {
// algum código
}
}
Limite de Saldo
Problema
Sempre existe o risco de um contrato ser comprometido devido a bugs no código ou ainda problemas de segurança desconhecidos na plataforma do contrato.
Solução
Geralmente, é uma boa ideia gerenciar a quantia de dinheiro em risco ao codificar contratos inteligentes. Isso pode ser alcançado limitando o saldo total mantido em um contrato.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract LimitBalance {
uint256 public limit;
modifier limitedPayable() {
require(address(this).balance < limit);
_;
}
constructor(uint256 value) {
limit = value;
}
function deposit() external payable limitedPayable {
// algum código
}
}
O padrão monitora o saldo do contrato e rejeita pagamentos enviados ao longo de uma chamada de função após exceder uma cota _limite _predefinido.
Deve-se notar que esta abordagem não pode impedir a admissão de Ether enviado à força, por exemplo, como beneficiário de uma chamada selfdestruct(endereço), ou como recipient (destinatário) de recompensas de deveres de validador.
Conclusão
Descrevi o grupo de padrões de segurança em detalhes e forneci um código como exemplo para melhor ilustrar. Eu recomendo que você use pelo menos um desses padrões em seu próximo projeto do Solidity para testar sua compreensão deste tópico.
Lembre-se de que, mesmo que seu código de contrato inteligente esteja livre de bugs, mesmo que você cumpra estritamente os padrões que mencionei, o compilador ou a própria plataforma pode ter um bug. Uma lista de alguns bugs relevantes à segurança conhecidos publicamente do compilador pode ser encontrada aqui e considerações de segurança esplêndidas podem ser encontradas aqui. Eu sugiro que dê uma olhada nesses documentos, encontre mais artigos e blogs para melhorar a segurança e é uma prática recomendada sempre pedir às pessoas que revisem seu código.
Siga-me no Linkedin para ficar conectado
https://www.linkedin.com/in/ninh-kim-927571149/
Este artigo foi escrito por BrianKim e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.
Top comments (0)