Desafío
Clase 14 de 15 • Curso de Introducción a Seguridad de Smart Contracts
Contenido del curso
Clase 14 de 15 • Curso de Introducción a Seguridad de Smart Contracts
Contenido del curso
José Manuel Piña Rodríguez
Adolfo Sebastián Jara Gavilanes
Leandro Vitale
Sebastian Perez
José Manuel Piña Rodríguez
Walter López Calle
Jonathan Muñoz
Juan Carlos Vásquez
guillermo sanchez
Fernando Luis Sproviero
Sebastian Perez
Evaluados Team
Miguel Martínez Gonzaléz
Martín Alexis Samán Arata
Kevin Fiorentino
Leandro Ariel Labiano Ramo
Nicolás García Puerta
Contrato sin desafio para usar como plantilla:
//SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Desafio { uint private pin; mapping(address => uint) balances; constructor(uint ownerPin) { pin = ownerPin; } function min(uint ownerPin, uint amount) public { require(pin == ownerPin, "El pin no es correcto"); balances[msg.sender] += amount; } function depositar() public payable{ balances[msg.sender] += msg.value; } function retirar() public { require(balances[msg.sender] > 0); msg.sender.call{value:balances[msg.sender]}(""); balances[msg.sender = 0]; } }
Muchas gracias
Como puedo saber que valor de gas colocar? Que sea razonable.
Hola Leandro, es una gran pregunta ya que no existe un criterio definitivo ya que el gas price puede variar y una misma cantidad de gas puede ser mucho o poco dependiendo de este valor. Una forma podría ser setear el valor en una variable y que ese valor lo puedas ajustar en base al precio del gas de ese momento como para que sea suficiente para lo que desees ejecutar. Otra opción es hacerlo desde el código de Solidity observando la variable tx.gasprice y calculando el valor en el momento. Ahora, ¿Cuánto gas necesita? Bueno, eso puedes verlo en Remix cuando ejecutas una función o bien utilizar web3js (https://web3js.readthedocs.io/en/v1.2.11/web3-eth.html#estimategas), de esta forma podrás saber cuánto gas demanda ejecutar una operación y luego calcular la variabilidad del gas price.
Desafio, implementando OpenZeppelin:
//SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/Ownable.sol"; contract Desafio is Ownable { mapping(address => uint) balances; function min(uint amount) public onlyOwner { balances[msg.sender] += amount; } function depositar() public payable{ balances[msg.sender] += msg.value; } function retirar() public { require(balances[msg.sender] > 0); uint amount = balances[msg.sender]; balances[msg.sender] = 0; (bool result, ) = msg.sender.call{value:amount,gas:600000}(""); if(!result) revert(); } }
Agregué una variable estado para suspender la ejecución de las funciones del contrato.
// SPDX-License-Identifier: MIT pragma solidity >= 0.8.7; contract Desafio{ address private _ownerPin; bool private estado; mapping(address => uint) balances; constructor (){ _ownerPin = msg.sender; estado=true; } function mint(address ownerPin, uint amount) public { require(_ownerPin == ownerPin, "No es el pin correcto"); require (estado, "Estado no permite ejecutar la opcion"); balances[msg.sender] += amount; } function depositar() public payable { require (estado, "Estado no permite ejecutar la opcion"); balances[msg.sender] += msg.value; } function retirar() public { require (estado, "Estado no permite ejecutar la opcion"); require (balances[msg.sender] > 0, "No tienes suficiente saldo"); uint monto = balances[msg.sender]; balances[msg.sender] = 0; (bool res, ) = msg.sender.call{value: monto, gas: 1000}(""); if (!res) revert(); } function cambiarEstado() public { require (_ownerPin == msg.sender, "No puede cambiar el estado"); estado = !estado; } }
mapping(address => uint ) public balances;
function retirar() public { uint amount = balances[msg.sender]; require(amount > 0, "Saldo insuficiente");
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transferencia fallida"); }
Hola aca en el 2025 dejo mi Desafio y espero sus comentarios si les parece.
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
contract Desafio{
//uint private pin; // Esto es un error, no es seguro al macenar un pin porque expone su valor.
address public immutable owner; // en otro caso podemos hacer por doble acceso usando un owner
mapping(address => uint256 ) private _balances;
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
event Mint(address indexed user, uint256 amount);
constructor() {
require(msg.sender != address(0), "Cero address");
owner = msg.sender;
}
function mint(uint256 amount) public {
require(msg.sender == owner, "El usuario no es correcto");
_balances[msg.sender] += amount;
emit Mint(msg.sender, amount);
}
function depositar() public payable {
_balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function retirar() public {
uint256 monto = _balances[msg.sender];
require(monto > 0, "Insufficient balance");
_balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: monto}("");
require(success, "Transfer failed");
emit Withdraw(msg.sender, monto);
}
}
Entiendo que es un ejemplo pero, ¿cuál sería el sentido de la función mint(amount)?
En el mismo mapa de balances estaríamos mezclando montos minteados con montos reales de ETH de la función depositar().
Se puede dar este caso:
deposit con un valor de 10 eth.mint con cualquier valor.retirar no le va a funcionar y no podrá recuperar sus 10 eth.También se puede dar este caso:
deposit con un valor de 1 eth.mint con un valor de 1000000000000000000.retirar y le va a robar 1 eth a Bob.Hola Fernando, como bien dices es un ejercicio de ejemplo y no busca ser un caso "real" si no una práctica de los temas vistos, es por eso que se mezclan los tokens generados (o minteados con "mint") y los que se obtienen a través de la transferencia de ethers (los que se generan en "deposit"). Sin embargo, en el desafío los retiros y depósitos se hacen sobre msg.sender, por lo tanto un owner no podría robar los fondos de otro usuario.
Tenes otro problema sin solucionar:
En el mint, al no ser payable, permitis incrementos de balance que al no haber recibido ether, generan balances que no estan calzados con los respectivos ether depositados.
Eso genera que haya mas balance total que ether total depositado y que el metodo de retiro se convierta en un juego de la silla, donde el ultimo en querer retirar no recibirá nada.
Esta es mi respuesta al desafío. Algunas consideraciones que tuve:
//SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; error WrongPin(); error CeroBalance(); error FailCall(bytes response); contract Challenge { bytes32 private immutable i_pin; mapping(address => uint) private s_balances; constructor(string memory ownerPin) { i_pin = keccak256(abi.encodePacked(ownerPin)); } function deposit(string calldata ownerPin) external payable{ if(i_pin != keccak256(abi.encodePacked(ownerPin))) revert WrongPin(); s_balances[msg.sender] += msg.value; } function withdraw() external { if(s_balances[msg.sender] <= 0) revert CeroBalance(); s_balances[msg.sender] = 0; (bool success, bytes memory response) = msg.sender.call{value:s_balances[msg.sender]}(""); if (!success) revert FailCall(response); } }
Buen curso!
SPOILER Veo dos cosas en el Contrato antes de que el profesor de su solución: Que la variable privada almacena datos sensibles y fácilmente puede averiguarse ese valor. Y que el contrato es propenso a un Reetrancy Simple y robar todos los fondos.
Muy lindo el curso. Saludos.
Hola Sebastián. Tienes un error en la parte en la que recomiendas manejar un rango de versiones del compilador para el smart contract.
Contracts should be deployed with the same compiler version and flags that they have been tested the most with. Locking the pragma helps ensure that contracts do not accidentally get deployed using, for example, the latest compiler which may have higher risks of undiscovered bugs.
https://swcregistry.io/docs/SWC-103
Saludos