Nuestra intención con este artículo no es ser condescendientes con los desarrolladores que están comenzando su camino. Tras revisar el código de numerosos desarrolladores de Solidity, hemos visto que algunos errores ocurren con más frecuencia y los enumeramos aquí.
De ninguna manera es esta una lista exhaustiva de los errores que un desarrollador de Solidity puede cometer. Los desarrolladores intermedios e incluso los experimentados también pueden cometer estos errores.
Sin embargo, es más probable que estos errores se cometan al principio del proceso de aprendizaje, por lo que vale la pena enumerarlos.
1. División antes que multiplicación
En Solidity, la operación de división siempre debe ser la última operación porque la división redondea los números hacia abajo.
Por ejemplo, si queremos calcular que debemos pagar a alguien un interés del 33.33%, la forma incorrecta de hacerlo es:
interest = principal / 3_333 * 10_000;
Si el principal es menor a 3,333, el interest se redondeará hacia abajo a cero. En su lugar, el interest debería calcularse de la siguiente manera:
interest = principal * 10_000 / 3_333;
Aquí está la matemática detrás de cómo falla el redondeo en el primer ejemplo y funciona en el segundo:
**// Wrong way:**
If principal = 3000,
interest = principal / 3333 * 10000
interest = 3000 / 3333 * 10000
interest = 0 * 10000 (rounding down in division)
interest = 0
// **Correct Calculation:**
If principal = 3000,
interest = principal * 10000 / 3333
interest = 3000 * 10000 / 3333
interest = 30000000 / 3333 interest approx 9000
Detectando el problema con Slither
Slither es una herramienta de análisis estático de Trail of Bits que analiza el código base para buscar patrones de errores comunes.
Si creamos el siguiente contrato (defectuoso) interest.sol
contract Interest {
// 1 basis point is 0.01% or 1/10_000
function calculateInterest(uint256 principal, uint256 interestBasisPoints) public pure returns (uint256 interest){
interest = principal / 10_000 * interestBasisPoints;
}
}
Y en la terminal ejecutamos
slither interest.sol
obtenemos la siguiente advertencia:

En este caso, nos dice que dividimos antes de multiplicar, lo cual en general es algo que se debe evitar.
2. No seguir el patrón check-effects-interaction
En Solidity, seguir el patrón “check-effects-interaction” es crucial para prevenir ataques de reentrada (re-entrancy). Esto significa que llamar a otro contrato o enviar ETH a otra dirección debe ser la última operación en una función. No hacerlo puede dejar al contrato vulnerable a ataques maliciosos.
El siguiente contrato BadBank no sigue el patrón check-effects-interaction y, por lo tanto, puede ser vaciado de su ETH.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
// DO NOT USE
contract BadBank {
mapping(address => uint256) public balances;
constructor()
payable {
require(msg.value == 10 ether, "deposit 10 eth");
}
function deposit()
external
payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
(bool ok, ) = msg.sender.call{value: balances[msg.sender]}("");
require(ok, "transfer failed");
balances[msg.sender] = 0;
}
}
El siguiente contrato de ataque puede usarse para vaciar el banco:
contract BankDrainer {
function steal(BadBank bank) external payable {
require(msg.value == 1 ether, "send deposit 1 eth");
bank.deposit{value: 1 ether}();
bank.withdraw();
}
receive() external payable {
// msg.sender is the BadBank because the BadBank
// called `receive()` when it transfered either
while (msg.sender.balance >= 1 ether) {
BadBank(msg.sender).withdraw();
}
}
}
Puedes probar el código en Remix aquí. El siguiente video demuestra el hackeo.
La razón por la que este hackeo es posible es porque la función withdraw() de BadBank llama a la función receive() en BankDrainer antes de actualizar los balances. Enviar ether es equivalente a llamar a la función receive() o fallback() en otro contrato.
Por lo tanto, siempre llama a la función de otro contrato inteligente o envía el Ether al final. Esta categoría de ataque se llama re-entrancy (reentrada). Puedes aprender más sobre este ataque en nuestro artículo sobre re-entrancy.
Cuando ejecutamos Slither en el código anterior, Slither nos da dos advertencias:

La primera advertencia, que “envía eth a un usuario arbitrario”, es un falso positivo. Es cierto que cualquiera puede llamar a withdraw, pero la cantidad que pueden retirar está limitada a su balance (¡al menos inicialmente!).
Sin embargo, Slither detecta correctamente la vulnerabilidad de re-entrancy.
3. Usar transfer o send
Solidity tiene dos funciones convenientes, transfer() y send(), para enviar Ether desde el contrato a un destino. Sin embargo, no deberías usar estas funciones.
El blog de Consensys sobre por qué no deberías usar transfer o send es un clásico que todo desarrollador de Solidity debe leer en algún momento.
¿Por qué existen estas funciones?
Después del hackeo de The DAO, que dividió a Ethereum en Ethereum y Ethereum Classic, los desarrolladores estaban muy asustados de los ataques de re-entrancy. Para evitar este tipo de ataques, se introdujeron transfer() y send(), ya que limitan la cantidad de gas disponible para el destinatario. Esto previene la reentrada al privar al destinatario del gas necesario para ejecutar más código.
Escenario de ejemplo:
Puedes reemplazar el
(bool ok, ) = msg.sender.call{value: balances[msg.sender]}("");
require(ok, "transfer failed");
en el ejemplo anterior con payable(msg.sender).transfer(balances[msg.sender]); y verás que el banco ya no es vulnerable.
Sin embargo, esto romperá las integraciones cuando el contrato espere recibir suficiente gas para responder al Ether entrante. Por ejemplo, si el contrato de destino intenta acreditar el ETH al remitente, esto fallará porque no tiene suficiente gas para terminar la contabilidad.
Considera el siguiente ejemplo:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract GoodBank {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 balance = balances[msg.sender];
balances[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: balance}("");
require(ok, "transfer failed");
}
receive() external payable {
balances[msg.sender] += msg.value;
}
}
contract SendToBank {
address owner;
constructor() {
owner = msg.sender;
}
function depositInBank(
address bank
) external payable {
require(msg.sender == owner, "not owner");
// THIS LINE WILL FAIL
payable(bank).transfer(msg.value);
}
function withdrawBank(
address payable bank
) external {
require(msg.sender == owner, "not owner");
// this triggers the receive function
GoodBank(bank).withdraw();
// the receive function has completed
// and now this contract has a balance
// send it to the owner
(bool ok, ) = msg.sender.call{value: address(this).balance}("");
require(ok, "transfer failed");
}
// we need this to receive Ether from the bank
receive() external payable {
}
}
Puedes probar el código anterior aquí en Remix. Y aquí hay un video que demuestra la transferencia fallida.
La transacción falla porque receive() se queda sin gas al incrementar el balance del remitente.
Por lo tanto, no uses transfer ni send y no escribas código reentrante. La primera opción es reemplazar transfer o send por address(receiver).call{value: amountToSend}(""). Alternativamente, se puede usar la librería Address de OpenZeppelin para hacer lo mismo. Ambos métodos se muestran a continuación:
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
contract SendEthExample {
using Address for address payable;
// both these functions do the same thing. Note that OZ requires
// payable addresses, but a low-level call does not
function sendSomeEthV1(address receiver, uint256 amount) external payable {
payable(receiver).sendValue(amount);
}
function sendSomeEthV2(address receiver, uint256 amount) external payable {
(bool ok, ) = receiver.call{value: amount}("");
require(ok, "transfer failed");
}
}
Slither no proporciona ninguna advertencia sobre el uso de transfer o send, pero de todos modos deberías evitar usarlos.
4. Usar tx.origin en lugar de msg.sender
Solidity es un poco confuso en el sentido de que hay dos formas de determinar “quién me está llamando” desde la perspectiva del contrato: una es tx.origin y la otra es msg.sender.
tx.origin es la wallet que firmó la transacción. msg.sender es quien hace la llamada directamente. Si una wallet llama directamente a un contrato
wallet → contrato
entonces, desde la perspectiva del contrato, la wallet es tanto msg.sender como tx.origin.
Ahora considera si la wallet llama a un contrato intermedio que luego llama al contrato final:
wallet → contrato intermedio → contrato final
Desde la perspectiva del contrato final, la wallet es tx.origin y el contrato intermedio es msg.sender.
Usar tx.origin para identificar a quien llama abre una vulnerabilidad de seguridad. Supongamos que el usuario es víctima de phishing y es engañado para llamar a un contrato intermedio malicioso:
wallet → contrato intermedio malicioso → contrato final
En esta situación, el contrato intermedio malicioso obtiene todos los privilegios de la wallet, permitiéndole realizar cualquier acción que la wallet esté autorizada a hacer — como mover fondos.
Para aprender más sobre las diferencias entre msg.sender y tx.origin, consulta nuestro artículo sobre Cómo detectar si una dirección es un contrato inteligente.
Slither no proporciona ninguna advertencia con respecto a tx.origin.
5. No usar safeTransfer con ERC-20
El estándar ERC-20 solo establece que el token debe lanzar un error si el usuario intenta transferir más de lo que tiene en su balance. Sin embargo, si la transferencia falla por alguna otra razón, entonces el estándar no indica explícitamente qué debería suceder.
La firma de la función para el transfer de ERC-20 es:
function transfer(address _to, uint256 _value) public returns (bool success);
lo cual implica que el token ERC-20 debería devolver false si ocurre un fallo.
En la práctica, los tokens ERC-20 han sido implementados de maneras inconsistentes: algunos revierten al fallar, y otros no devuelven ningún booleano en absoluto (es decir, no respetan la firma de la función).
La librería SafeERC20 maneja ambos tipos de tokens ERC-20. Específicamente, hace una llamada de transfer a la dirección, y
- Si ocurre un revert,
SafeERC20propaga el revert hacia arriba. Esto maneja los tokens que revierten al fallar, pero que no necesariamente devuelven un booleano. - Si no hay un revert, verifica si se devolvió algún dato:
- si no se devolvieron datos y la dirección del token resulta ser una dirección vacía en lugar de un contrato inteligente, la librería revierte.
- si se devolvieron datos y el valor devuelto es falso, entonces
SafeERC20revierte.
- De lo contrario, la librería no revierte, señalizando una transferencia exitosa.
Aquí se muestra cómo se debe utilizar la librería SafeERC20, específicamente la librería SafeERC20 de OpenZeppelin:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "@openzeppelin/contracts@5.0.0/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts@5.0.0/token/ERC20/ERC20.sol";
contract SafeTransferDemo {
using SafeERC20 for IERC20;
function deposit(
IERC20 token,
uint256 amount)
external {
token.safeTransferFrom(msg.sender, address(this), amount);
}
// withdraw function not shown
}
contract MyToken is ERC20("MyToken", "MT") {
constructor() {
// mint the supply of 10_000 tokens
// to the deployer
_mint(msg.sender, 10_000 * 1e18);
}
}
6. Usar safeMath con Solidity 0.8.0 (o superior)
Antes de Solidity 0.8.0, las variables podían sufrir desbordamiento (overflow) si ocurría una operación matemática que resultara en un valor mayor del que la variable podía almacenar. En respuesta a esto, la librería SafeMath de OpenZeppelin se volvió popular. Así es como la librería prevenía el desbordamiento en la suma:
function add(uint256 x, uint256 y) internal pure returns (uint256) {
uint256 sum = x + y;
require(sum >= x || sum >= y, "overflow");
return sum;
}
La suma siempre debe ser mayor que x o y. Si ese no es el caso, ha ocurrido un desbordamiento y la función revierte.
En bases de código más antiguas, a menudo verás esta línea:
using SafeMath for uint256;
y las matemáticas realizándose de esta manera:
uint256 sum = x.add(y);
Sin embargo, no deberías hacer esto en Solidity 0.8.0 o superior porque el compilador añade una verificación de desbordamiento incorporada tras bambalinas. Por lo tanto, usar la librería SafeMath para operaciones aritméticas básicas hace que el código sea menos legible e ineficiente, sin ninguna ganancia de seguridad adicional.
7. Olvidar el control de acceso
Usemos un ejemplo mínimo. ¿Puedes identificar el problema?
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
contract NFTSale is ERC721("MyTok", "MT") {
uint256 public price;
uint256 public currentId;
function setPrice(
uint256 price_
) public {
price = price_;
}
function buyNFT() external payable {
require(msg.value == price, "wrong price");
currentId++;
_mint(msg.sender, currentId);
}
}
Cualquiera puede llamar a setPrice() y establecerlo en cero antes de llamar a buyNFT().
Siempre que escribas una función que sea public o external, pregúntate si debería haber una restricción sobre quién puede llamar a la función. Aquí hay una variación sutil del problema anterior:
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
contract NFTSale is ERC721("MyTok", "MT") {
uint256 public price;
address owner;
uint256 public currentId;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "onlyOwner");
_;
}
function setPrice(
uint256 price_
) public onlyOwner {
price = price_;
}
function buyNFT() external payable {
require(msg.value == price, "wrong price");
currentId++;
_mint(msg.sender, currentId);
}
}
Aquí, el desarrollador ha añadido un modificador onlyOwner, que concede acceso solo al usuario designado. En los ejemplos anteriores, el modificador de control de acceso asegura que solo el propietario del contrato puede establecer el precio, como se ve en la función setPrice.
8. Operaciones costosas en un bucle
Los arrays que pueden crecer sin límite son problemáticos porque los costos de transacción para iterar sobre ellos pueden llegar a ser extremadamente altos.
El siguiente contrato recibe donaciones en Ether y añade a los donantes a un array. Más tarde, el propietario llamará a distributeNFTs() y acuñará un NFT para todos los donantes. Sin embargo, si hay muchos donantes, podría volverse demasiado costoso para el propietario completar la donación.
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@5.0.0/access/Ownable.sol";
contract GiveNFTToDonors is ERC721("MyTok", "MT"), Ownable(msg.sender) {
address[] donors;
uint256 currentId;
receive() external payable {
require(msg.value >= 0.1 ether, "donation too small");
donors.push(msg.sender);
}
function distributeNFTs() external onlyOwner {
for (uint256 i = 0; i < donors.length; i++) {
currentId++;
_mint(msg.sender, currentId);
}
}
}
La función distributeNFTs() intentará iterar sobre todo el array de donantes. Sin embargo, si la lista de donantes en el array es grande, este bucle resultará en un costo de gas muy alto, haciendo que la transacción sea inviable. Slither te dará una advertencia sobre esta situación similar a la siguiente:

La solución a esto se conoce como “pull over push” (tirar en lugar de empujar). En lugar de enviar a cada uno de los receptores su NFT, haces que ellos llamen a una función que transfiera el NFT a la dirección, si esa dirección llama a la función.
9. Falta de comprobaciones de cordura (sanity checks) en las entradas de las funciones
Siempre que escribas una función pública, anota explícitamente los valores que esperas que se pasen a los argumentos de la función y asegúrate de que las declaraciones require los impongan. Por ejemplo, las personas no deberían poder retirar más de lo que tienen en su balance. Las personas no deberían poder retirar activos que no depositaron.
Considera los siguientes ejemplos:
contract LendingProtocol is Ownable {
function offerLoan(
uint256 amount,
uint256 interest,
uint256 duration)
external {}
function setProtocolFee(
uint256 feeInBasisPoints)
external
onlyOwner {}
}
El diseñador debería pensar en qué parámetros son razonables aquí. Una tasa de interés superior al 1000% es irrazonable. Una duración que es extremadamente corta, como 1 hora, también es irrazonable.
De manera similar, la función setProtocolFee debería tener un límite superior sensato sobre qué tarifa puede establecer el propietario, o los usuarios podrían sorprenderse cuando las tarifas para usar el protocolo suban a un nivel irrazonable de repente.
Para implementar las comprobaciones de cordura, simplemente añadimos declaraciones require que limiten el rango aceptable de las entradas.
Al diseñar una función pública siempre considera qué rango de parámetros tiene sentido para los argumentos de la función.
10. Código faltante
Algunos errores en Solidity ocurren debido a código faltante en lugar de código defectuoso. El siguiente contrato de acuñación de NFTs permite al propietario especificar quién está autorizado a acuñar el NFT y cuánto. (Esta no es una forma eficiente en gas de hacerlo, pero queremos centrarnos en el principio en cuestión).
Aquí está el código, ¿puedes identificar qué falta?
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "@openzeppelin/contracts@5.0.0/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@5.0.0/access/Ownable2Step.sol";
contract MissingCode is ERC721("MissingCode", "MC"), Ownable(msg.sender) {
uint256 id;
mapping(address => uint256) public amountAllowedToMint;
function mint(
uint256 amount
) external {
require(amount < amountAllowedToMint[msg.sender],
"not enough allocation");
for (uint256 i = 0; i < amount; i++) {
id++;
_mint(msg.sender, id);
}
}
function setAmountAllowedToMint(
address[] calldata minters,
uint256[] calldata amounts
) external onlyOwner {
require(minters.length == amounts.length,
"length mismatch");
for (uint256 i = 0; i < minters.length; i++) {
amountAllowedToMint[minters[i]] = amounts[i];
}
}
}
El problema es que la cantidad que un comprador acuña no se deduce de amountAllowedToMint, por lo que el “límite” no se aplica realmente. Una dirección en el mapping podría llamar a mint() tantas veces como quiera.
Debería haber una línea adicional amountAllowedToMint[msg.sender] -= amount después de la función _mint().
11. No fijar el pragma de Solidity
Cuando lees el código de librerías de Solidity, a menudo verás algo como
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
en la parte superior. Debido a esto, los desarrolladores más nuevos tienden a copiar ciegamente este patrón.
Sin embargo, establecer la versión de Solidity con ^0.8.0 solo es apropiado para librerías. El autor que distribuye la librería no conoce la versión exacta con la que un programador posterior la compilará, por lo que solo establece una versión mínima.
Como desarrollador que despliega la aplicación, tú sabes qué versión del compilador estás usando para compilar el código. Por lo tanto, deberías fijar la versión a la exacta que usaste para que sea más claro para otros que auditen el código qué versión del compilador de Solidity utilizaste. Por ejemplo, en lugar de poner pragma solidity ^0.8.0 escribe la versión exacta pragma solidity 0.8.26. Esto aclarará las cosas para otras personas que auditen el código con la versión especificada.
12. No seguir la guía de estilo
Hemos documentado la guía de estilo de Solidity en una publicación de blog separada.
Aquí están los puntos más destacados:
- el constructor es la primera función
- luego
fallback()yreceive()(si el contrato las tiene) - luego las funciones
external, funcionespublic, funcionesinternaly funcionespure - dentro de cada grupo
- las funciones
payablevan primero - seguidas de las funciones que no son
payableniview - y las funciones
viewvan al final
- las funciones
13. Falta de registros (logs) o registros indexados incorrectamente
En Ethereum, no existe un método nativo para listar todas las transacciones enviadas a un contrato inteligente específico, excepto buscando esta información en exploradores de bloques. Sin embargo, esto se puede lograr haciendo que el contrato emita eventos.
Aquí hay algunas reglas generales sobre los eventos:
- Cualquier función que pueda cambiar una variable de almacenamiento debería emitir un evento.
- El evento debe contener suficiente información para que alguien que audite los registros pueda determinar qué valor tomó la variable de almacenamiento en ese momento.
- Cualquier parámetro de tipo
addressen el evento debería tener el modificadorindexedpara que sea fácil desglosar la actividad de una wallet en particular. - Las funciones
viewypureno deben contener eventos porque no cambian el estado.
Puedes leer más sobre esto en nuestro artículo sobre eventos en Solidity y Ethereum.
En general, si cambias una variable de almacenamiento o mueves Ether hacia dentro y fuera del contrato, deberías emitir un evento.
14. No escribir pruebas unitarias (unit tests)
¿Cómo sabes que el contrato funciona en todos los escenarios posibles que enfrentará, a menos que realmente haya sido probado?
Desde nuestro punto de vista, es un poco sorprendente que los contratos inteligentes se desplieguen sin pruebas unitarias. Esto no debería ser el caso.
Consulta nuestro tutorial sobre pruebas unitarias en Solidity aquí.
15. Redondear en la dirección incorrecta
Si divides 100/3 obtendrás 33 a pesar de que la respuesta “correcta” es 33.33333 porque Solidity no soporta números de punto flotante. En ese caso, 0.3333 de la unidad que estés midiendo ha desaparecido, porque estás obligado a “redondear hacia abajo” cuando se usa la división. Aquí está la regla de oro de la división:
Siempre redondea para que el usuario pierda o el protocolo gane.
Por ejemplo, si estás calculando cuánto necesita pagar un usuario por algo, entonces la división causará que la estimación sea menor de lo que debería ser. En el ejemplo anterior, el usuario obtiene un descuento de 0.3333.
Situación 1: Calcular cuánto paga el protocolo
Si estamos calculando 100/3 para determinar cuánto le paga el contrato inteligente al usuario, entonces el contrato inteligente le pagará de menos al usuario. Esta es la forma correcta de hacerlo. El usuario no podrá extraer valor y desangrar al protocolo.
Situación 2: Calcular cuánto paga el usuario
Por otro lado, si estamos calculando 100/3 para determinar cuánto debería pagar el usuario al contrato inteligente, entonces tenemos un problema, porque el usuario paga 0.333 menos de lo que debería. ¡Si el usuario es capaz de vender ese activo con una ganancia de 0.333, entonces puede repetir el proceso hasta vaciar el protocolo!
Lo correcto en esta circunstancia es sumar uno a la división para recuperar lo que perdimos en los decimales. Es decir, deberíamos calcular cuánto paga el usuario como 100/3 + 1, de modo que el usuario tenga que pagar 34 por un activo que vale 33.333. La pequeña cantidad de valor que pierden evitará que le roben al contrato inteligente.
Aprende más sobre cómo manejar fracciones correctamente en nuestro artículo sobre matemáticas de punto fijo (fixed-point math).
16. No ejecutar un formateador
No hay necesidad de reinventar la rueda al formatear el código de Solidity. Puedes usar forge fmt en Foundry o usar la herramienta solfmt. Hará que tu código sea más fácil de leer para el revisor.
El siguiente código es innecesariamente difícil de leer:
contract GoodBank {
mapping(address=>uint256) public balances;
function withdraw () external {
uint256 balance=balances[msg.sender];
balances[msg.sender] = 0;
(bool ok,) =msg.sender.call{value: balance}("");
require(ok,"transfer failed");
}
receive() external payable {
balances[msg.sender]+=msg.value;
}
}
Debería pasarse por un formateador para que el espaciado sea más uniforme:
contract GoodBank {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 balance = balances[msg.sender];
balances[msg.sender] = 0;
(bool ok,) = msg.sender.call{value: balance}("");
require(ok, "transfer failed");
}
receive() external payable {
balances[msg.sender] += msg.value;
}
}
17. Usar _msgSender() en contratos que no soportan metatransacciones
Los nuevos desarrolladores de Solidity a menudo se confunden por el uso frecuente de _msgSender() en los contratos de OpenZeppelin. Por ejemplo, aquí está la librería ERC-20 de OpenZeppelin usando _msgSender():

A menos que estés construyendo un contrato que soporte transacciones sin gas o metatransacciones, usa el msg.sender normal en lugar de _msgSender().
_msgSender() es una función creada por el contrato Context.sol de OpenZeppelin:

Esto solo se usa en contratos que soportan metatransacciones.
Una metatransacción o transacción sin gas es aquella en la que un retransmisor (relayer) envía la transacción en nombre de un usuario y paga el gas por ellos. Debido a que la transacción provino de un relayer, msg.sender no será el remitente “original”. Los contratos inteligentes que usan metatransacciones codifican el “verdadero” msg.sender en otra parte de la transacción y designan al “verdadero” msg.sender sobrescribiendo la función _msgSender().
Si no estás haciendo ninguna de esas cosas, no hay razón para usar _msgSender(). Usa msg.sender en su lugar.
18. Subir accidentalmente claves de API o claves privadas a Github
Aunque no hemos visto que esto ocurra con mucha frecuencia, las pocas veces que sucede resulta en resultados extremadamente catastróficos. Si pones claves de API o claves privadas en un archivo .env, siempre añade el archivo .env al archivo .gitignore.
19. No tener en cuenta el frontrunning, el slippage o el retraso entre la firma y la ejecución de la transacción
El frontrunning es un problema contraintuitivo en los contratos de Solidity porque sus equivalentes rara vez ocurren en la programación web2.
Ejemplo 1: Cambiar el precio mientras la transacción de compra está pendiente
Considera el siguiente contrato, que permite a un vendedor de un NFT intercambiarlo con un comprador por USDC en una sola transacción. Teóricamente, esto tiene el beneficio de que ninguna de las partes tiene que enviar su token primero y confiar en que la contraparte enviará el suyo.
Sin embargo, tiene una vulnerabilidad de frontrunning. El vendedor puede cambiar el precio del intercambio mientras la transacción de intercambio está pendiente.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "@openzeppelin/contracts@5.0.0/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
contract BadSwapERC20ForNFT is Ownable(msg.sender) {
using SafeERC20 for IERC20;
uint256 price;
IERC20 token;
IERC721 nft;
address public seller;
constructor(IERC721 nft_, IERC20 token_) {
nft = nft_;
token = token_;
seller = msg.sender;
}
function setPrice(uint256 price_) external {
require(msg.sender == seller, "only seller");
price = price_;
}
// the buyer calls this function
function atomicSwap(uint256 nftId) external
// requires both the seller and buyer
// to approve their tokens first
token.safeTransferFrom(msg.sender, owner(), price);
nft.transferFrom(owner(), msg.sender, nftId);
}
}
Siempre que a un usuario se le estén transfiriendo tokens desde su cuenta, se le debe exigir siempre que pase datos que especifiquen la cantidad máxima que está dispuesto a enviar, de modo que el vendedor no pueda cambiar el precio mientras la transacción de compra esté pendiente.
Ejemplo 2: NFT que sube de precio con cada compra
Las siguientes ventas de NFT están programadas para aumentar el precio en un 5% con cada compra. Tiene un problema similar al anterior. El precio en el momento en que el comprador firma la transacción podría no ser el mismo precio cuando la transacción se confirma. Si 10 compradores envían una transacción de compra al mismo tiempo, entonces 9 de ellos van a pagar un precio más alto del que esperaban.
Cuando un contrato calcula cuántos tokens transferir de un usuario, el usuario debería especificar un límite del máximo que permitirá que se transfiera desde su cuenta.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "@openzeppelin/contracts@5.0.0/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts@5.0.0/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@5.0.0/access/Ownable2Step.sol";
contract BadNFTSale is ERC721("BadNFT", "BNFT"), Ownable(msg.sender) {
using SafeERC20 for IERC20;
uint256 price = 100e6; // USDC / USDT have 6 decimals
IERC20 immutable token;
uint256 id;
constructor(IERC20 token_) {
token = token_;
}
function buyNFT() external {
token.safeTransferFrom(msg.sender, owner(), price);
price = price * 105 / 100;
id++;
_mint(msg.sender, id);
}
}
Hay un problema aún más sutil: ¡el propietario podría cambiar el token mientras la transacción de un comprador todavía está pendiente! Ahora bien, es poco probable que el comprador haya aprobado el contrato para el nuevo token, por lo que el transferFrom probablemente fallará. Pero en un contrato más complejo que, de manera realista, podría tener múltiples aprobaciones, esto sería un problema a tener en cuenta.
20. Funciones que no tienen en cuenta a los usuarios que realizan la misma transacción varias veces
Los contratos inteligentes necesitan tener en cuenta la posibilidad de que un usuario realice la misma transacción más de una vez. Considera el siguiente ejemplo:
contract DepositAndWithdraw {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] = msg.value;
}
function withdraw(
uint256 amount
) external {
require(
amount <= balances[msg.sender],
"insufficient balance"
);
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}
}
Si se llama a deposit dos veces, entonces el primer balance será sobrescrito por la segunda transacción y ese dinero se perderá. Por ejemplo, si el usuario llama a deposit() con un valor de 1 ETH, y luego llama a deposit() nuevamente con un valor de 2 ETH, entonces el balance de esa dirección será de 2 ETH a pesar de que depositaron 3 ETH. La corrección es incrementar el balance, es decir, balances[msg.sender] += msg.value;.