
Solidity Security
Esta publicación pretende ser una introducción relativamente profunda y actualizada que detalla los errores pasados cometidos por desarrolladores de Solidity, con el fin de evitar que los futuros desarrolladores repitan la historia.
Una de las características de los contratos inteligentes de Ethereum es la capacidad de llamar y utilizar código de otros contratos externos. Los contratos también suelen manejar ether y, como tal, a menudo envían ether a diversas direcciones de usuarios externos. La operación de llamar a contratos externos, o enviar ether a una dirección, requiere que el contrato realice una llamada externa. Estas llamadas externas pueden ser secuestradas por atacantes, quienes fuerzan al contrato a ejecutar código adicional (es decir, a través de una función fallback) , incluidas llamadas de vuelta a sí mismo. Así, la ejecución del código "re-entra" en el contrato. Ataques de este tipo se utilizaron en el infame hackeo de The DAO.
Para más información sobre ataques de reentrancia, consulte Ataque de Reentrancia en Contratos Inteligentes y Consensus - Mejores Prácticas para Contratos Inteligentes de Ethereum.
Este ataque puede ocurrir cuando un contrato envía ether a una dirección desconocida. Un atacante puede construir cuidadosamente un contrato en una dirección externa que contenga código malicioso en la función fallback. Por lo tanto, cuando un contrato envía ether a esta dirección, invocará el código malicioso. Típicamente, el código malicioso ejecuta una función en el contrato vulnerable, realizando operaciones no esperadas por el desarrollador. El nombre "reentrancia" proviene del hecho de que el contrato malicioso externo llama de vuelta a una función en el contrato vulnerable y "re-entra" en la ejecución del código en una ubicación arbitraria del contrato vulnerable.
Para aclarar esto, considere el siguiente contrato vulnerable, que actúa como una bóveda de Ethereum que permite a los depositantes retirar solo 1 ether por semana.
EtherStore.sol:```solidity contract EtherStore {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
}
}
Este contrato tiene dos funciones públicas. `depositFunds()` y `withdrawFunds()`. La función `depositFunds()` simplemente incrementa los saldos de los remitentes. La función `withdrawFunds()` permite al remitente especificar la cantidad de wei a retirar. Solo tendrá éxito si la cantidad solicitada para retirar es menor que 1 ether y no se ha producido un retiro en la última semana. ¿O lo hace?...
La vulnerabilidad está en la línea \[17\], donde enviamos al usuario la cantidad de ether solicitada. Consideremos a un atacante malicioso que crea el siguiente contrato,
Attack.sol:```solidity
import "EtherStore.sol";
contract Attack {
EtherStore public etherStore;
// initialise the etherStore variable with the contract address
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
function pwnEtherStore() public payable {
// attack to the nearest ether
require(msg.value >= 1 ether);
// send eth to the depositFunds() function
etherStore.depositFunds.value(1 ether)();
// start the magic
etherStore.withdrawFunds(1 ether);
}
function collectEther() public {
msg.sender.transfer(this.balance);
}
// fallback function - where the magic happens
function () payable {
if (etherStore.balance > 1 ether) {
etherStore.withdrawFunds(1 ether);
}
}
}
Veamos cómo este contrato malicioso puede explotar nuestro contrato EtherStore. El atacante crearía el contrato anterior (digamos en la dirección 0x0...123) con la dirección del contrato EtherStore como parámetro del constructor. Esto inicializará y apuntará la variable pública etherStore al contrato que deseamos atacar.
El atacante llamará entonces a la función pwnEtherStore(), con cierta cantidad de ether (mayor o igual a 1), digamos 1 ether para este ejemplo. En este ejemplo asumimos que varios otros usuarios han depositado ether en este contrato, de modo que su saldo actual es 10 ether. Entonces ocurrirá lo siguiente:
Attack.sol - Línea [15] - Se llamará a la función depositFunds() del contrato EtherStore con un msg.value de 1 ether (y mucho gas). El remitente (msg.sender) será nuestro contrato malicioso (0x0...123). Por lo tanto, balances[0x0..123] = 1 ether.
Attack.sol - Línea [17] - El contrato malicioso llamará entonces a la función withdrawFunds() del contrato EtherStore con un parámetro de 1 ether. Esto pasará todos los requisitos (Líneas [12]-[16] del contrato EtherStore) ya que no hemos hecho retiros anteriores.
EtherStore.sol - Línea [17] - El contrato enviará entonces 1 ether de vuelta al contrato malicioso.
El resultado final es que el atacante ha retirado todo (excepto 1) el ether del contrato EtherStore, instantáneamente con una sola transacción.
Existen varias técnicas comunes que ayudan a evitar posibles vulnerabilidades de reentrancia en contratos inteligentes. La primera es (siempre que sea posible) usar la función incorporada transfer() al enviar ether a contratos externos. La función transfer solo envía 2300 gas con la llamada externa, lo cual no es suficiente para que la dirección/contrato de destino llame a otro contrato (es decir, reingrese al contrato que envía).
La segunda técnica es asegurarse de que toda la lógica que cambia variables de estado ocurra antes de que el ether se envíe fuera del contrato (o cualquier llamada externa). En el ejemplo EtherStore, las líneas [18] y [19] de EtherStore.sol deberían colocarse antes de la línea [17]. Es una buena práctica ubicar cualquier código que realice llamadas externas a direcciones desconocidas como la última operación en una función localizada o pieza de ejecución de código. Esto se conoce como el patrón checks-effects-interactions.
Una tercera técnica es introducir un mutex. Es decir, agregar una variable de estado que bloquee el contrato durante la ejecución del código, evitando llamadas de reentrancia.
Aplicando todas estas técnicas (las tres son innecesarias, pero se hace con fines demostrativos) a EtherStore.sol, se obtiene el contrato libre de reentrancia:```solidity
contract EtherStore {
// initialise the mutex
bool reEntrancyMutex = false;
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(!reEntrancyMutex);
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
// set the reEntrancy mutex before the external call
reEntrancyMutex = true;
msg.sender.transfer(_weiToWithdraw);
// release the mutex after the external call
reEntrancyMutex = false;
}
}
<h3 id="re-example">Ejemplo del mundo real: The DAO</h3>
[The DAO](https://en.wikipedia.org/wiki/The_DAO_(organization)) (Organización Autónoma Descentralizada) fue uno de los grandes hackeos ocurridos en el desarrollo inicial de Ethereum. En ese momento, el contrato contenía más de 150 millones de dólares estadounidenses. La reentrancia jugó un papel importante en el ataque que finalmente condujo a la bifurcación dura que creó Ethereum Classic (ETC). Para un buen análisis del exploit de The DAO, consulta [la publicación de Phil Daian](http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/).
<h2 id="ouflow"><span id="SP-2">2. Desbordamiento y subdesbordamiento aritméticos</span></h2>
La Máquina Virtual de Ethereum (EVM) especifica tipos de datos de tamaño fijo para los enteros. Esto significa que una variable entera solo puede representar un cierto rango de números. Un `uint8`, por ejemplo, solo puede almacenar números en el rango \[0,255\]. Intentar almacenar `256` en un `uint8` dará como resultado `0`. Si no se tiene cuidado, las variables en Solidity pueden ser explotadas si la entrada del usuario no se verifica y se realizan cálculos que producen números fuera del rango del tipo de datos que los almacena.
Para más información sobre desbordamientos y subdesbordamientos aritméticos, consulta [How to Secure Your Smart Contracts](https://medium.com/loom-network/how-to-secure-your-smart-contracts-6-solidity-vulnerabilities-and-how-to-avoid-them-part-1-c33048d4d17d), [Ethereum Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/known_attacks/#integer-overflow-and-underflow) y [Ethereum, Solidity and integer overflows: programming blockchains like 1970](https://randomoracle.wordpress.com/2018/04/27/ethereum-solidity-and-integer-overflows-programming-blockchains-like-1970/)
<h3 id="ou-vuln">La vulnerabilidad</h3>
Un desbordamiento o subdesbordamiento ocurre cuando se realiza una operación que requiere que una variable de tamaño fijo almacene un número (o dato) que está fuera del rango del tipo de datos de la variable.
Por ejemplo, restar `1` a una variable `uint8` (entero sin signo de 8 bits, es decir, solo positivo) que almacena `0` como valor, dará como resultado el número `255`. Esto es un subdesbordamiento. Hemos asignado un número por debajo del rango del `uint8`, el resultado *da la vuelta* y produce el número más grande que un `uint8` puede almacenar. De manera similar, sumar `2^8=256` a un `uint8` dejará la variable sin cambios, ya que hemos dado la vuelta a toda la longitud del `uint` (para los matemáticos, esto es similar a sumar $2\pi$ al ángulo de una función trigonométrica, $\sin(x) = \sin(x+2\pi)$). Sumar números mayores que el rango del tipo de datos se llama desbordamiento. Para mayor claridad, sumar `257` a un `uint8` que actualmente tiene un valor de cero dará como resultado el número `1`. A veces es instructivo pensar que las variables de tipo fijo son cíclicas: volvemos a empezar desde cero si sumamos números por encima del mayor número almacenable, y viceversa con el cero (donde empezamos a contar hacia abajo desde el número más grande cuanto más restamos de 0).
Este tipo de salvedades numéricas permite a los atacantes hacer un mal uso del código y crear flujos lógicos inesperados. Por ejemplo, considera el siguiente contrato de bloqueo temporal.
TimeLock.sol:```solidity
contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() public payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = now + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
uint transferValue = balances[msg.sender];
balances[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Este contrato está diseñado para actuar como una bóveda de tiempo, donde los usuarios pueden depositar ether en el contrato y quedará bloqueado allí durante al menos una semana. El usuario puede extender el tiempo de espera a más de 1 semana si así lo desea, pero una vez depositado, el usuario puede estar seguro de que su ether está bloqueado de forma segura durante al menos una semana. ¿O pueden?...
En el caso de que un usuario se vea obligado a entregar su clave privada (piensa en una situación de rehenes), un contrato como este puede ser útil para garantizar que el ether sea inobtenible en períodos cortos de tiempo. Si un usuario hubiera bloqueado 100 ether en este contrato y hubiera entregado sus claves a un atacante, este podría usar un desbordamiento para recibir el ether, independientemente del lockTime.
El atacante podría determinar el lockTime actual para la dirección de la que ahora poseen la clave (es una variable pública). Llamemos a esto userLockTime. Luego podrían llamar a la función increaseLockTime y pasar como argumento el número 2^256 - userLockTime. Este número se sumaría al userLockTime actual y causaría un desbordamiento, restableciendo lockTime[msg.sender] a 0. El atacante podría entonces simplemente llamar a la función withdraw para obtener su recompensa.
Veamos otro ejemplo, este de los Ethernaut Challanges.
ALERTA DE SPOILER: Si aún no has hecho los desafíos de Ethernaut, esto da una solución a uno de los niveles.```solidity pragma solidity ^0.4.18;
contract Token {
mapping(address => uint) balances; uint public totalSupply;
function Token(uint _initialSupply) { balances[msg.sender] = totalSupply = _initialSupply; }
function transfer(address _to, uint _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; }
function balanceOf(address _owner) public constant returns (uint balance) { return balances[_owner]; } }
Este es un contrato de token simple que emplea una función `transfer()`, que permite a los participantes mover sus tokens. ¿Puedes ver el error en este contrato?
El fallo está en la función `transfer()`. La sentencia require en la línea \[13\] puede ser omitida mediante un subdesbordamiento. Considere un usuario que no tiene saldo. Podría llamar a la función `transfer()` con cualquier `_value` distinto de cero y superar la sentencia require de la línea \[13\]. Esto se debe a que `balances[msg.sender]` es cero (y un `uint256`), por lo que restar cualquier cantidad positiva (excepto `2^256`) dará como resultado un número positivo debido al subdesbordamiento que describimos anteriormente. Esto también es cierto para la línea \[14\], donde nuestro saldo será acreditado con un número positivo. Así, en este ejemplo, hemos obtenido tokens gratuitos debido a una vulnerabilidad de subdesbordamiento.
<h3 id="ou-prevention">Técnicas de Prevención</h3>
La técnica (actualmente) convencional para protegerse contra vulnerabilidades de subdesbordamiento/desbordamiento es usar o construir bibliotecas matemáticas que reemplacen los operadores matemáticos estándar; suma, resta y multiplicación (la división queda excluida porque no causa subdesbordamientos/desbordamientos y la EVM revierte al dividir por 0).
[OppenZepplin](https://github.com/OpenZeppelin/zeppelin-solidity) ha hecho un gran trabajo construyendo y auditando bibliotecas seguras que pueden ser aprovechadas por la comunidad de Ethereum. En particular, su [Safe Math Library](https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/math/SafeMath.sol) es una referencia o biblioteca a usar para evitar vulnerabilidades de subdesbordamiento/desbordamiento.
Para demostrar cómo se usan estas bibliotecas en Solidity, corrijamos el contrato `TimeLock`, usando la biblioteca `SafeMath` de Open Zepplin. El contrato sin desbordamientos quedaría así:```solidity
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
contract TimeLock {
using SafeMath for uint; // use the library for uint type
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;
function deposit() public payable {
balances[msg.sender] = balances[msg.sender].add(msg.value);
lockTime[msg.sender] = now.add(1 weeks);
}
function increaseLockTime(uint256 _secondsToIncrease) public {
lockTime[msg.sender] = lockTime[msg.sender].add(_secondsToIncrease);
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
uint transferValue = balances[msg.sender];
balances[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Notice that all standard math operations have been replaced by the those defined in the SafeMath library. The TimeLock contract no longer performs any operation which is capable of doing an under/over flow.
Un grupo de 4chan decidió que era una gran idea construir un esquema ponzi en Ethereum, escrito en Solidity. Lo llamaron Proof of Weak Hands Coin (PoWHC). Desafortunadamente, parece que los autores del contrato nunca habían visto over/under flows antes y, en consecuencia, 866 ether fueron liberados de su contrato. Una buena descripción general de cómo ocurre el underflow (que no es muy diferente al desafío de Ethernaut mencionado anteriormente) se encuentra en el post de Eric Banisadar.
Algunos desarrolladores también implementaron una función batchTransfer() en algunos contratos de tokens ERC20. La implementación contenía un overflow. Este post lo explica; sin embargo, creo que el título es engañoso, ya que no tiene nada que ver con el estándar ERC20, sino que algunos contratos de tokens ERC20 tienen una función batchTransfer() vulnerable implementada.
Normalmente, cuando se envía ether a un contrato, debe ejecutarse la función fallback u otra función definida en el contrato. Hay dos excepciones a esto, en las que el ether puede existir en un contrato sin que se haya ejecutado ningún código. Los contratos que dependen de la ejecución de código para cada ether enviado al contrato pueden ser vulnerables a ataques en los que se envía ether por la fuerza a un contrato.
Para leer más sobre esto, consulta Cómo asegurar tus contratos inteligentes: 6 y Patrones de seguridad de Solidity - forzando ether a un contrato .
Una técnica común de programación defensiva que es útil para garantizar transiciones de estado correctas o validar operaciones es la verificación de invariantes. Esta técnica consiste en definir un conjunto de invariantes (métricas o parámetros que no deberían cambiar) y comprobar que estos invariantes permanecen sin cambios después de una (o muchas) operación(es). Normalmente es un buen diseño, siempre que los invariantes verificados sean de hecho invariantes. Un ejemplo de invariante es el totalSupply de un token ERC20 de emisión fija. Como ninguna función debería modificar este invariante, se podría añadir una comprobación a la función transfer() que garantice que el totalSupply permanece sin modificar para asegurar que la función funciona como se espera.
En particular, hay un invariante aparente que puede resultar tentador usar
pero que, de hecho, puede ser manipulado por usuarios externos (independientemente de las reglas establecidas
en el contrato inteligente) .Este es el ether actual almacenado en el
contrato. A menudo, cuando los desarrolladores aprenden Solidity por primera vez, tienen la
idea errónea de que un contrato solo puede aceptar u obtener ether mediante funciones
payable. Esta idea errónea puede llevar a contratos que tienen suposiciones falsas
sobre el balance de ether dentro de ellos, lo que puede conducir a una serie de
vulnerabilidades. La prueba fehaciente de esta vulnerabilidad es el (incorrecto) uso
de this.balance. Como veremos, los usos incorrectos de this.balance pueden llevar a
vulnerabilidades graves de este tipo.
Hay dos formas en las que se puede enviar ether (por la fuerza) a un contrato sin usar una función payable ni ejecutar ningún código en el contrato. Se enumeran a continuación.
Cualquier contrato puede implementar la función selfdestruct(address), que elimina todo el bytecode de la dirección del contrato y envía todo el ether almacenado allí a la dirección especificada por parámetro. Si esta dirección especificada también es un contrato, no se llama a ninguna función (incluida la fallback). Por lo tanto, la función selfdestruct() puede usarse para enviar ether por la fuerza a cualquier contrato, independientemente de cualquier código que pueda existir en el contrato. Esto incluye contratos sin funciones payable. Esto significa que cualquier atacante puede crear un contrato con una función selfdestruct(), enviarle ether, llamar a selfdestruct(target) y forzar que se envíe ether a un contrato target. Martin Swende tiene un excelente post en su blog que describe algunas peculiaridades del opcode self-destruct (Quirk #2) junto con una descripción de cómo los nodos cliente verificaban invariantes incorrectos, lo que podría haber llevado a una aniquilación bastante catastrófica de los clientes.
La segunda forma en que un contrato puede obtener ether sin usar una función selfdestruct() ni llamar a ninguna función payable es precargar la dirección del contrato con ether. Las direcciones de los contratos son deterministas; de hecho, la dirección se calcula a partir del hash keccak256 (a veces sinónimo de SHA3) de la dirección que crea el contrato y del nonce de la transacción que lo crea. En concreto, tiene la forma: address = sha3(rlp.encode([account_address,transaction_nonce])) (consulta Keyless Ether para ver algunos casos de uso divertidos de esto). Esto significa que cualquiera puede calcular cuál será la dirección de un contrato antes de que se cree y, por tanto, enviar ether a esa dirección. Cuando el contrato se crea, tendrá un balance de ether distinto de cero.
Exploremos algunos escollos que pueden surgir con el conocimiento anterior.
Considera el contrato excesivamente simple,
EtherGame.sol:```solidity contract EtherGame {
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
mapping(address => uint) redeemableEther;
// users pay 0.5 ether. At specific milestones, credit their accounts
function play() public payable {
require(msg.value == 0.5 ether); // each play is 0.5 ether
uint currentBalance = this.balance + msg.value;
// ensure no players after the game as finished
require(currentBalance <= finalMileStone);
// if at a milestone credit the players account
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
return;
}
function claimReward() public {
// ensure the game is complete
require(this.balance == finalMileStone);
// ensure there is a reward to give
require(redeemableEther[msg.sender] > 0);
uint transferValue = redeemableEther[msg.sender];
redeemableEther[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Este contrato representa un juego simple (que naturalmente invocaría [condiciones de carrera](#race-conditions)) en el que los jugadores envían cuantos de `0.5 ether` al contrato con la esperanza de ser el jugador que alcanza uno de los tres hitos primero. Los hitos están denominados en ether. El primero en alcanzar el hito puede reclamar una parte del ether cuando el juego haya terminado. El juego termina cuando se alcanza el hito final (`10 ether`) y los usuarios pueden reclamar sus recompensas.
Los problemas con el contrato `EtherGame` provienen del mal uso de `this.balance` en las líneas \[14\] (y por asociación \[16\]) y \[32\]. Un atacante travieso podría enviar por la fuerza una pequeña cantidad de ether, digamos `0.1 ether`, mediante la función `selfdestruct()` (discutida anteriormente) para impedir que cualquier jugador futuro alcance un hito. Como todos los jugadores legítimos solo pueden enviar incrementos de `0.5 ether`, `this.balance` ya no sería un número semientero, ya que también tendría la contribución de `0.1 ether`. Esto impide que todas las condiciones if de las líneas \[18\], \[21\] y \[24\] sean verdaderas.
Peor aún, un atacante vengativo que haya perdido un hito podría enviar por la fuerza `10 ether` (o una cantidad equivalente de ether que empuje el balance del contrato por encima del `finalMileStone`), lo que bloquearía todas las recompensas en el contrato para siempre. Esto se debe a que la función `claimReward()` siempre revertirá, debido al require en la línea \[32\] (es decir, `this.balance` es mayor que `finalMileStone`).
<h3 id="ether-prevention">Técnicas de Prevención</h3>
Esta vulnerabilidad suele surgir del mal uso de `this.balance`. La lógica del contrato, cuando sea posible, debería evitar depender de valores exactos del balance del contrato porque puede ser manipulada artificialmente. Si se aplica lógica basada en `this.balance`, asegúrese de tener en cuenta balances inesperados.
Si se requieren valores exactos del ether depositado, se debe utilizar una variable propia que se incremente en las funciones payable, para rastrear de forma segura el ether depositado. Esta variable no se verá influenciada por el ether forzado enviado mediante una llamada a `selfdestruct()`.
Con esto en mente, una versión corregida del contrato `EtherGame` podría verse así:```solidity
contract EtherGame {
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
uint public depositedWei;
mapping (address => uint) redeemableEther;
function play() public payable {
require(msg.value == 0.5 ether);
uint currentBalance = depositedWei + msg.value;
// ensure no players after the game as finished
require(currentBalance <= finalMileStone);
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
depositedWei += msg.value;
return;
}
function claimReward() public {
// ensure the game is complete
require(depositedWei == finalMileStone);
// ensure there is a reward to give
require(redeemableEther[msg.sender] > 0);
uint transferValue = redeemableEther[msg.sender];
redeemableEther[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Aquí, acabamos de crear una nueva variable, depositedWei, que lleva la cuenta del ether depositado conocido, y es sobre esta variable sobre la que aplicamos nuestros requisitos y pruebas. Observa que ya no tenemos ninguna referencia a this.balance.
Aún no he encontrado un ejemplo de esto que haya sido explotado en la naturaleza. Sin embargo, se dieron algunos ejemplos de contratos explotables en el Underhanded Solidity Contest.
Los opcodes CALL y DELEGATECALL son útiles para permitir a los desarrolladores de Ethereum modularizar su código. Las llamadas de mensaje externas estándar a contratos son manejadas por el opcode CALL, mediante el cual el código se ejecuta en el contexto del contrato/función externo. El opcode DELEGATECALL es idéntico a la llamada de mensaje estándar, excepto que el código ejecutado en la dirección objetivo se ejecuta en el contexto del contrato que realiza la llamada, junto con el hecho de que msg.sender y msg.value permanecen sin cambios. Esta característica permite la implementación de bibliotecas mediante las cuales los desarrolladores pueden crear código reutilizable para futuros contratos.
Aunque las diferencias entre estos dos opcodes son simples e intuitivas, el uso de DELEGATECALL puede conducir a una ejecución de código inesperada.
Para más información, consulta Pregunta en Ethereum Stack Exchange, Documentación de Solidity y Cómo proteger tus contratos inteligentes: 6.
La naturaleza de DELEGATECALL de preservar el contexto ha demostrado que construir bibliotecas personalizadas sin vulnerabilidades no es tan fácil como uno podría pensar. El código de las propias bibliotecas puede ser seguro y estar libre de vulnerabilidades, pero cuando se ejecuta en el contexto de otra aplicación pueden surgir nuevas vulnerabilidades. Veamos un ejemplo bastante complejo de esto, utilizando números de Fibonacci.
Considera la siguiente biblioteca que puede generar la secuencia de Fibonacci y secuencias de forma similar.
FibonacciLib.sol[^1]```solidity
// library contract - calculates fibonacci-like numbers;
contract FibonacciLib {
// initializing the standard fibonacci sequence;
uint public start;
uint public calculatedFibNumber;
// modify the zeroth number in the sequence
function setStart(uint _start) public {
start = _start;
}
function setFibonacci(uint n) public {
calculatedFibNumber = fibonacci(n);
}
function fibonacci(uint n) internal returns (uint) {
if (n == 0) return start;
else if (n == 1) return start + 1;
else return fibonacci(n - 1) + fibonacci(n - 2);
}
}
Esta librería proporciona una función que puede generar el *n*-ésimo número de Fibonacci en la secuencia. Permite a los usuarios cambiar el número inicial de la secuencia (`start`) y calcular los *n*-ésimos números similares a Fibonacci en esta nueva secuencia.
Consideremos ahora un contrato que utiliza esta librería.
`FibonacciBalance.sol`:```solidity
contract FibonacciBalance {
address public fibonacciLibrary;
// the current fibonacci number to withdraw
uint public calculatedFibNumber;
// the starting fibonacci sequence number
uint public start = 3;
uint public withdrawalCounter;
// the fibonancci function selector
bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)"));
// constructor - loads the contract with ether
constructor(address _fibonacciLibrary) public payable {
fibonacciLibrary = _fibonacciLibrary;
}
function withdraw() {
withdrawalCounter += 1;
// calculate the fibonacci number for the current withdrawal user
// this sets calculatedFibNumber
require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
msg.sender.transfer(calculatedFibNumber * 1 ether);
}
// allow users to call fibonacci library functions
function() public {
require(fibonacciLibrary.delegatecall(msg.data));
}
}
Este contrato permite a un participante retirar ether del contrato, siendo la cantidad de ether igual al número de Fibonacci correspondiente al orden de retiro del participante; es decir, el primer participante recibe 1 ether, el segundo también recibe 1, el tercero recibe 2, el cuarto recibe 3, el quinto 5 y así sucesivamente (hasta que el saldo del contrato sea menor que el número de Fibonacci que se está retirando).
Hay una serie de elementos en este contrato que pueden requerir alguna explicación. En primer lugar, hay una variable de aspecto interesante, fibSig. Esta contiene los primeros 4 bytes del hash Keccak (SHA-3) de la cadena "setFibonacci(uint256)". Esto se conoce como function selector y se coloca en calldata para especificar qué función de un contrato inteligente será llamada. Se utiliza en la función delegatecall en la línea [21] para especificar que deseamos ejecutar la función setFibonacci(uint256). El segundo argumento en delegatecall es el parámetro que estamos pasando a la función. En segundo lugar, asumimos que la dirección de la biblioteca FibonacciLib está correctamente referenciada en el constructor (la sección External Contract Referencing analiza algunas vulnerabilidades potenciales relacionadas con este tipo de inicialización de referencias a contratos).
¿Puedes detectar algún error en este contrato? Si lo pones en Remix, lo llenas con ether y llamas a withdraw(), probablemente revertirá.
Habrás notado que la variable de estado start se utiliza tanto en la biblioteca como en el contrato principal que realiza la llamada. En el contrato de la biblioteca, start se usa para especificar el comienzo de la secuencia de Fibonacci y se establece en 0, mientras que en el contrato FibonacciBalance se establece en 3. También habrás notado que la función de respaldo en el contrato FibonacciBalance permite que todas las llamadas se pasen a la biblioteca, lo que también permite que se llame a la función setStart() de la biblioteca. Recordando que se preserva el estado del contrato, puede parecer que esta función permitiría cambiar el estado de la variable start en el contrato local FibonnacciBalance. Si fuera así, esto permitiría retirar más ether, ya que el calculatedFibNumber resultante depende de la variable start (como se ve en el contrato de la biblioteca). De hecho, la función setStart() no modifica (y no puede modificar) la variable en el contrato . La vulnerabilidad subyacente en este contrato es significativamente peor que simplemente modificar la variable .
Antes de discutir el problema real, haremos un breve desvío para entender cómo se almacenan realmente las variables de estado (variables de storage) en los contratos. Las variables de estado o storage (variables que persisten entre transacciones individuales) se colocan en slots secuencialmente según se introducen en el contrato. (Hay algunas complejidades aquí, y animo al lector a leer Layout of State Variables in Storage para una comprensión más profunda).
Como ejemplo, veamos el contrato de la biblioteca. Tiene dos variables de estado, start y calculatedFibNumber. La primera variable es start, por lo que se almacena en el storage del contrato en slot[0] (es decir, el primer slot). La segunda variable, calculatedFibNumber, se coloca en el siguiente slot de storage disponible, slot[1]. Si observamos la función setStart(), esta toma una entrada y establece start a lo que sea esa entrada. Por lo tanto, esta función está estableciendo slot[0] a cualquier entrada que proporcionemos en la función setStart(). De manera similar, la función setFibonacci() establece calculatedFibNumber al resultado de fibonacci(n). Nuevamente, esto es simplemente establecer el storage al valor de .
Ahora veamos el contrato FibonacciBalance. El storage slot[0] ahora corresponde a la dirección fibonacciLibrary y slot[1] corresponde a calculatedFibNumber. Es en esta asignación incorrecta donde ocurre la vulnerabilidad. delegatecall preserva el contexto del contrato. Esto significa que el código que se ejecuta mediante delegatecall actuará sobre el estado (es decir, el storage) del contrato que realiza la llamada.
Ahora observa que en withdraw() en la línea [21] ejecutamos fibonacciLibrary.delegatecall(fibSig,withdrawalCounter). Esto llama a la función setFibonacci(), que como discutimos, modifica el storage slot[1], que en nuestro contexto actual es calculatedFibNumber. Esto es lo esperado (es decir, después de la ejecución, calculatedFibNumber se ajusta). Sin embargo, recuerda que la variable start en el contrato FibonacciLib está ubicada en el storage slot[0], que es la dirección fibonacciLibrary en el contrato actual. Esto significa que la función fibonacci() dará un resultado inesperado. Esto se debe a que hace referencia a start (slot[0]), que en el contexto de llamada actual es la dirección (que a menudo será bastante grande, cuando se interpreta como ). Por lo tanto, es probable que la función revierta, ya que no contendrá la cantidad de ether , que es lo que devolverá .
Peor aún, el contrato FibonacciBalance permite a los usuarios llamar a todas las funciones de fibonacciLibrary a través de la función de respaldo en la línea [26]. Como discutimos anteriormente, esto incluye la función setStart(). Discutimos que esta función permite a cualquiera modificar o establecer el storage slot[0]. En este caso, el storage slot[0] es la dirección fibonacciLibrary. Por lo tanto, un atacante podría crear un contrato malicioso (un ejemplo se muestra a continuación), convertir la dirección a uint (esto se puede hacer fácilmente en Python usando int('<address>',16)) y luego llamar a setStart(<attack_contract_address_as_uint>). Esto cambiará fibonacciLibrary a la dirección del contrato atacante. Entonces, cuando un usuario llame a withdraw() o a la función de respaldo, se ejecutará el contrato malicioso (que puede robar el saldo completo del contrato) porque hemos modificado la dirección real de fibonacciLibrary. Un ejemplo de tal contrato de ataque sería,```solidity
contract Attack {
uint storageSlot0; // corresponds to fibonacciLibrary
uint storageSlot1; // corresponds to calculatedFibNumber
// fallback - this will run if a specified function is not found
function() public {
storageSlot1 = 0; // we set calculatedFibNumber to 0, so that if withdraw
// is called we don't send out any ether.
<attacker_address>.transfer(this.balance); // we take all the ether
}
}
Nótese que este contrato de ataque modifica el `calculatedFibNumber` al cambiar el slot de almacenamiento `slot[1]`. En principio, un atacante podría modificar cualquier otro slot de almacenamiento que elija para llevar a cabo todo tipo de ataques contra este contrato. Animo a todos los lectores a introducir estos contratos en [Remix](https://remix.ethereum.org) y experimentar con diferentes contratos de ataque y cambios de estado a través de estas funciones `delegatecall`.
También es importante notar que cuando decimos que `delegatecall` preserva el estado, no nos referimos a los nombres de las variables del contrato, sino a los slots de almacenamiento reales a los que apuntan esos nombres. Como puedes ver en este ejemplo, un simple error puede permitir que un atacante secuestre todo el contrato y su ether.
<h3 id="dc-prevention">Técnicas preventivas</h3>
Solidity proporciona la palabra clave `library` para implementar contratos de biblioteca (consulta la [documentación de Solidity](http://solidity.readthedocs.io/en/latest/contracts.html?highlight=library#libraries) para más detalles). Esto garantiza que el contrato de biblioteca no tenga estado y no sea autodestructible. Obligar a que las bibliotecas no tengan estado mitiga las complejidades del contexto de almacenamiento demostradas en esta sección. Las bibliotecas sin estado también previenen ataques en los que los atacantes modifican directamente el estado de la biblioteca para afectar a los contratos que dependen del código de esta.
Como regla general, al usar `DELEGATECALL` presta mucha atención al posible contexto de llamada tanto del contrato de biblioteca como del contrato que llama y, siempre que sea posible, construye bibliotecas sin estado.
<h3 id="dc-example">Ejemplo real: Parity Multisig Wallet (segundo hackeo)</h3>
El segundo hackeo de Parity Multisig Wallet es un ejemplo de cómo el contexto de un código de biblioteca bien escrito puede ser explotado si se ejecuta en un contexto no previsto. Hay varias buenas explicaciones de este hackeo, como esta visión general: [Parity MultiSig Hacked. Again](https://medium.com/chain-cloud-company-blog/parity-multisig-hack-again-b46771eaa838) de Anthony Akentiev, esta [pregunta de Stack Exchange](https://ethereum.stackexchange.com/questions/30128/explanation-of-parity-library-suicide/30130) y [An In-Depth Look at the Parity Multisig Bug](http://hackingdistributed.com/2017/07/22/deep-dive-parity-bug/).
Para añadir a estas referencias, exploremos los contratos que fueron explotados. El contrato de biblioteca y el de wallet se pueden encontrar en el github de parity [aquí](https://github.com/paritytech/parity/blob/b640df8fbb964da7538eef268dffc125b081a82f/js/src/contracts/snippets/enhanced-wallet.sol).
Veamos los aspectos relevantes de este contrato. Aquí hay dos contratos de interés: el contrato de biblioteca y el contrato de wallet.
El contrato de biblioteca,```solidity
contract WalletLibrary is WalletEvents {
...
// throw unless the contract is not yet initialized.
modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
...
}
y el contrato de la wallet,```solidity contract Wallet is WalletEvents {
...
// METHODS
// gets called when no other function matches function() payable { // just being sent some cash? if (msg.value > 0) Deposit(msg.sender, msg.value); else if (msg.data.length > 0) _walletLibrary.delegatecall(msg.data); }
...
// FIELDS address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; }
Nótese que el contrato `Wallet` esencialmente pasa todas las llamadas al contrato `WalletLibrary` mediante una llamada delegate. La constante `_walletLibrary` en este fragmento de código actúa como un marcador de posición para el contrato `WalletLibrary` realmente desplegado (que estaba en `0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4`).
La operación prevista de estos contratos era tener un contrato `Wallet` simple y de bajo costo de despliegue cuya base de código y funcionalidad principal residiera en el contrato `WalletLibrary`. Desafortunadamente, el contrato `WalletLibrary` es en sí mismo un contrato y mantiene su propio estado. ¿Puedes ver por qué esto podría ser un problema?
Es posible enviar llamadas al propio contrato `WalletLibrary`. Específicamente, el contrato `WalletLibrary` podía ser inicializado y pasar a tener propietario. Un usuario hizo esto llamando a la función `initWallet()` en el contrato `WalletLibrary`, convirtiéndose en propietario del contrato de la librería. El mismo usuario llamó posteriormente a la función `kill()`. Debido a que el usuario era propietario del contrato de la librería, el modificador pasó y el contrato de la librería se autodestruyó. Como todos los contratos `Wallet` existentes hacen referencia a este contrato de librería y no contienen ningún método para cambiar dicha referencia, toda su funcionalidad, incluida la capacidad de retirar ether, se pierde junto con el contrato `WalletLibrary`. Más directamente, todo el ether en todas las carteras multifirma de Parity de este tipo se pierde al instante o queda permanentemente irrecuperable.
<h2 id="visibility"><span id="SP-5">5. Visibilidades por defecto</span></h2>
Las funciones en Solidity tienen especificadores de visibilidad que determinan cómo se permite que sean llamadas. La visibilidad determina si una función puede ser llamada externamente por usuarios, por otros contratos derivados, solo internamente o solo externamente. Hay cuatro especificadores de visibilidad, que se describen en detalle en la [Documentación de Solidity](http://solidity.readthedocs.io/en/latest/contracts.html?highlight=library#visibility-and-getters). Las funciones tienen `public` por defecto, lo que permite a los usuarios llamarlas externamente. El uso incorrecto de los especificadores de visibilidad puede conducir a algunas vulnerabilidades devastadoras en los contratos inteligentes, como se discutirá en esta sección.
<h3 id="visibility-vuln">La vulnerabilidad</h3>
La visibilidad por defecto de las funciones es `public`. Por lo tanto, las funciones que no especifican ninguna visibilidad podrán ser llamadas por usuarios externos. El problema surge cuando los desarrolladores ignoran por error los especificadores de visibilidad en funciones que deberían ser `private` (o solo llamables dentro del propio contrato).
Exploremos rápidamente un ejemplo trivial.```solidity
contract HashForEther {
function withdrawWinnings() {
// Winner if the last 8 hex characters of the address are 0.
require(uint32(msg.sender) == 0);
_sendWinnings();
}
function _sendWinnings() {
msg.sender.transfer(this.balance);
}
}
Este contrato simple está diseñado para actuar como un juego de recompensa por adivinar direcciones. Para ganar el saldo del contrato, un usuario debe generar una dirección de Ethereum cuyos últimos 8 caracteres hexadecimales sean 0. Una vez obtenida, puede llamar a la función WithdrawWinnings() para obtener su recompensa.
Desafortunadamente, la visibilidad de las funciones no ha sido especificada. En particular, la función _sendWinnings() es public y, por lo tanto, cualquier dirección puede llamar a esta función para robar la recompensa.
Es una buena práctica especificar siempre la visibilidad de todas las funciones en un contrato, incluso si son intencionalmente public. Las versiones recientes de Solidity ahora mostrarán advertencias durante la compilación para las funciones que no tienen una visibilidad explícita establecida, para ayudar a fomentar esta práctica.
En el primer hackeo de multi-sig de Parity, se robaron aproximadamente $31M en Ether de principalmente tres carteras. Un buen resumen de exactamente cómo se hizo esto lo proporciona Haseeb Qureshi en esta publicación.
Esencialmente, la cartera multi-sig (que se puede encontrar aquí) se construye a partir de un contrato base Wallet que llama a un contrato de biblioteca que contiene la funcionalidad principal (como se describió en Ejemplo del mundo real: Parity Multisig (segundo hackeo)). El contrato de biblioteca contiene el código para inicializar la cartera, como se puede ver en el siguiente fragmento```solidity
contract WalletLibrary is WalletEvents {
...
// METHODS
...
// constructor is given number of sigs required to do protected "onlymanyowners" transactions // as well as the selection of addresses capable of confirming them. function initMultiowned(address[] _owners, uint _required) { m_numOwners = _owners.length + 1; m_owners[1] = uint(msg.sender); m_ownerIndex[uint(msg.sender)] = 1; for (uint i = 0; i < _owners.length; ++i) { m_owners[2 + i] = uint(_owners[i]); m_ownerIndex[uint(_owners[i])] = 2 + i; } m_required = _required; }
...
// constructor - just pass on the owner array to the multiowned and // the limit to daylimit function initWallet(address[] _owners, uint _required, uint _daylimit) { initDaylimit(_daylimit); initMultiowned(_owners, _required); } }
Observe que ninguna de las funciones tiene una visibilidad explícitamente especificada. Ambas funciones son `public` por defecto. La función `initWallet()` se llama en el constructor de las wallets y establece los propietarios de la wallet multi-firma, como se puede ver en la función `initMultiowned()`. Debido a que estas funciones quedaron accidentalmente como `public`, un atacante pudo llamarlas en los contratos desplegados, restableciendo la propiedad a la dirección del atacante. Al ser el propietario, el atacante vació las wallets de todo su ether, por un valor de \$31M.
<h2 id="entropy"><span id="SP-6">6. Ilusión de entropía</span></h2>
Todas las transacciones en la blockchain de Ethereum son operaciones de transición de estado deterministas. Esto significa que cada transacción modifica el estado global del ecosistema Ethereum y lo hace de forma calculable, sin incertidumbre. En última instancia, esto significa que dentro del ecosistema blockchain no existe ninguna fuente de entropía o aleatoriedad. No hay una función `rand()` en Solidity. Lograr entropía (aleatoriedad) descentralizada es un problema bien conocido y se han propuesto muchas ideas para abordarlo (ver, por ejemplo, [RandDAO](https://github.com/randao/randao) o el uso de una cadena de hashes como describe Vitalik en este [post](https://vitalik.ca/files/randomness.html)).
<h3 id="entropy-vuln">La vulnerabilidad</h3>
Algunos de los primeros contratos construidos en la plataforma Ethereum se basaban en juegos de azar. Fundamentalmente, el juego requiere incertidumbre (algo sobre lo que apostar), lo que hace que construir un sistema de juego en la blockchain (un sistema determinista) sea bastante difícil. Está claro que la incertidumbre debe provenir de una fuente externa a la blockchain. Esto es posible para apuestas entre pares (ver, por ejemplo, la [técnica de commit-reveal](https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract)); sin embargo, es significativamente más difícil si se quiere implementar un contrato que actúe como *la casa* (como en el blackjack o la ruleta). Un error común es utilizar variables de bloques futuros, como hashes, marcas de tiempo, número de bloque o límite de gas. El problema con estas es que están controladas por el minero que extrae el bloque y, por tanto, no son verdaderamente aleatorias. Considere, por ejemplo, un contrato inteligente de ruleta con lógica que devuelve un número negro si el hash del siguiente bloque termina en un número par. Un minero (o un pool de minería) podría apostar \$1M al negro. Si resuelve el siguiente bloque y descubre que el hash termina en un número impar, no publicaría su bloque y minaría otro hasta encontrar una solución cuyo hash de bloque sea un número par (asumiendo que la recompensa del bloque y las comisiones sean menores que \$1M). Usar variables pasadas o presentes puede ser aún más devastador, como demuestra Martin Swende en su excelente [entrada de blog](http://martin.swende.se/blog/Breaking_the_house.html). Además, usar únicamente variables de bloque significa que el número pseudoaleatorio será el mismo para todas las transacciones de un bloque, por lo que un atacante puede multiplicar sus ganancias realizando muchas transacciones dentro de un bloque (si existe una apuesta máxima).
<h3 id="entropy-prevention">Técnicas de prevención</h3>
La fuente de entropía (aleatoriedad) debe ser externa a la blockchain. Esto se puede lograr entre pares con sistemas como [commit-reveal](https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract), o cambiando el modelo de confianza a un grupo de participantes (como en [RandDAO](https://github.com/randao/randao)). También se puede hacer mediante una entidad centralizada, que actúe como oráculo de aleatoriedad. Las variables de bloque (en general, hay algunas excepciones) no deberían usarse como fuente de entropía, ya que los mineros pueden manipularlas.
<h3 id="entropy-example">Ejemplo del mundo real: Contratos PRNG</h3>
Arseny Reutov escribió una [entrada de blog](https://blog.positive.com/predicting-random-numbers-in-ethereum-smart-contracts-e5358c6b8620) después de analizar 3649 contratos inteligentes activos que utilizaban algún tipo de generador de números pseudoaleatorios (PRNG) y encontró 43 contratos que podían ser explotados.
<h2 id="contract-reference"><span id="SP-7">7. Referencia a contratos externos</span></h2>
Uno de los beneficios del *computador global* de Ethereum es la capacidad de reutilizar código e interactuar con contratos ya desplegados en la red. Como resultado, una gran cantidad de contratos hacen referencia a contratos externos y, en su funcionamiento general, utilizan llamadas a mensajes externos para interactuar con dichos contratos. Estas llamadas a mensajes externos pueden ocultar las intenciones de actores maliciosos de maneras no obvias, como veremos a continuación.
<h3 id="cr-vuln">La vulnerabilidad</h3>
En Solidity, cualquier dirección puede ser convertida a un contrato, independientemente de si el código en esa dirección representa el tipo de contrato que se está convirtiendo. Esto puede ser engañoso, especialmente cuando el autor del contrato intenta ocultar código malicioso. Ilustremos esto con un ejemplo:
Considere un fragmento de código que implementa de forma rudimentaria el cifrado [Rot13](https://github.com/al1ex/soliditysecurity/blob/HEAD/www.wikipedia.com/rot13).
`Rot13Encryption.sol`:```solidity
//encryption contract
contract Rot13Encryption {
event Result(string convertedString);
//rot13 encrypt a string
function rot13Encrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
//inline assembly to modify the string
assembly {
char := byte(0,char) // get the first byte
if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping.
{ char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z.
if iszero(eq(char, 0x20)) // ignore spaces
{mstore8(add(add(text,0x20), mul(i,1)), add(char,13))} // add 13 to char.
}
}
emit Result(text);
}
// rot13 decrypt a string
function rot13Decrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
assembly {
char := byte(0,char)
if and(gt(char,0x60), lt(char,0x6E))
{ char:= add(0x7B, sub(char,0x61)) }
if iszero(eq(char, 0x20))
{mstore8(add(add(text,0x20), mul(i,1)), sub(char,13))}
}
}
emit Result(text);
}
}
Este código simplemente toma una cadena (letras a-z, sin validación) y la cifra desplazando cada carácter 13 lugares a la derecha (envolviendo alrededor de 'z'); es decir, 'a' se desplaza a 'n' y 'x' se desplaza a 'k'. El ensamblador aquí no es importante, así que no te preocupes si no tiene sentido en esta etapa.
Considera el siguiente contrato que utiliza este código para su cifrado,```solidity import "Rot13Encryption.sol";
// encrypt your top secret info contract EncryptionContract { // library for encryption Rot13Encryption encryptionLibrary;
// constructor - initialise the library
constructor(Rot13Encryption _encryptionLibrary) {
encryptionLibrary = _encryptionLibrary;
}
function encryptPrivateData(string privateInfo) {
// potentially do some operations here
encryptionLibrary.rot13Encrypt(privateInfo);
}
}
El problema con este contrato es que la dirección de `encryptionLibrary` no es pública ni constante. Por lo tanto, el desplegador del contrato podría haber proporcionado una dirección en el constructor que apunte a este contrato:```solidity
//encryption contract
contract Rot26Encryption {
event Result(string convertedString);
//rot13 encrypt a string
function rot13Encrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
//inline assembly to modify the string
assembly {
char := byte(0,char) // get the first byte
if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping.
{ char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z.
if iszero(eq(char, 0x20)) // ignore spaces
{mstore8(add(add(text,0x20), mul(i,1)), add(char,26))} // add 13 to char.
}
}
emit Result(text);
}
// rot13 decrypt a string
function rot13Decrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
assembly {
char := byte(0,char)
if and(gt(char,0x60), lt(char,0x6E))
{ char:= add(0x7B, sub(char,0x61)) }
if iszero(eq(char, 0x20))
{mstore8(add(add(text,0x20), mul(i,1)), sub(char,26))}
}
}
emit Result(text);
}
}
que implementa el cifrado rot26 (desplaza cada carácter 26 posiciones, ¿lo captas? :p). De nuevo, no hay necesidad de entender el ensamblaje en este contrato. El desplegador también podría haber vinculado el siguiente contrato:```solidity contract Print{ event Print(string text);
function rot13Encrypt(string text) public {
emit Print(text);
}
}
Si la dirección de cualquiera de estos contratos se proporcionara en el constructor, la función `encryptPrivateData()` simplemente produciría un evento que imprime los datos privados sin cifrar. Aunque en este ejemplo se estableció un contrato tipo biblioteca en el constructor, a menudo ocurre que un usuario privilegiado (como un `owner`) puede cambiar las direcciones de los contratos de biblioteca. Si un contrato enlazado no contiene la función que se está llamando, se ejecutará la función de respaldo (fallback). Por ejemplo, con la línea `encryptionLibrary.rot13Encrypt()`, si el contrato especificado por `encryptionLibrary` fuera:```solidity
contract Blank {
event Print(string text);
function () {
emit Print("Here");
//put malicious code here and it will run
}
}
then an event with the text "Here" would be emitted. Thus if users can alter contract libraries, they can in principle get users to unknowingly run arbitrary code.
Note: Don't use encryption contracts such as these, as the input parameters to smart contracts are visible on the blockchain. Also the Rot cipher is not a recommended encryption technique :p
Como se demostró anteriormente, los contratos sin vulnerabilidades pueden (en algunos casos) desplegarse de manera que se comporten maliciosamente. Un auditor podría verificar públicamente un contrato y hacer que su propietario lo despliegue de forma maliciosa, lo que daría como resultado un contrato auditado públicamente que tiene vulnerabilidades o intenciones maliciosas.
Existen varias técnicas que previenen estos escenarios.
Una técnica es utilizar la palabra clave new para crear contratos. En el ejemplo anterior, el constructor podría escribirse así:```solidity
constructor() {
encryptionLibrary = new Rot13Encryption();
}
De esta manera, una instancia del contrato referenciado se crea en el momento del despliegue y el desplegador no puede reemplazar el contrato `Rot13Encryption` por ningún otro sin modificar el smart contract.
Otra solución es codificar de forma fija las direcciones de los contratos externos si se conocen.
En general, el código que llama a contratos externos siempre debe examinarse con atención. Como desarrollador, al definir contratos externos, puede ser una buena idea hacer públicas las direcciones de los contratos (que no es el caso en el ejemplo de honeypot que se muestra a continuación) para permitir a los usuarios examinar fácilmente qué código está referenciando el contrato. Por el contrario, si un contrato tiene una variable privada con la dirección del contrato, puede ser una señal de que alguien se está comportando de forma maliciosa (como se muestra en el ejemplo del mundo real). Si un usuario privilegiado (o cualquier usuario) es capaz de cambiar una dirección de contrato que se utiliza para llamar a funciones externas, puede ser importante (en un contexto de sistema descentralizado) implementar un mecanismo de bloqueo temporal o de votación para permitir a los usuarios ver qué código se está cambiando o dar a los participantes la oportunidad de optar por entrar o salir con la nueva dirección del contrato.
<h3 id="cr-example">Ejemplo del mundo real: Honeypot de reentrancia</h3>
Se han publicado varios honeypots recientes en la red principal (mainnet). Estos contratos intentan engañar a los hackers de Ethereum que tratan de explotar los contratos, pero que a su vez terminan perdiendo ether en el contrato que esperaban explotar. Un ejemplo emplea el ataque anterior reemplazando un contrato esperado por uno malicioso en el constructor. El código se puede encontrar [aquí](https://etherscan.io/address/0x95d34980095380851902ccd9a1fb4c813c2cb639#code):```solidity
pragma solidity ^0.4.19;
contract Private_Bank
{
mapping (address => uint) public balances;
uint public MinDeposit = 1 ether;
Log TransferLog;
function Private_Bank(address _log)
{
TransferLog = Log(_log);
}
function Deposit()
public
payable
{
if(msg.value >= MinDeposit)
{
balances[msg.sender]+=msg.value;
TransferLog.AddMessage(msg.sender,msg.value,"Deposit");
}
}
function CashOut(uint _am)
{
if(_am<=balances[msg.sender])
{
if(msg.sender.call.value(_am)())
{
balances[msg.sender]-=_am;
TransferLog.AddMessage(msg.sender,_am,"CashOut");
}
}
}
function() public payable{}
}
contract Log
{
struct Message
{
address Sender;
string Data;
uint Val;
uint Time;
}
Message[] public History;
Message LastMsg;
function AddMessage(address _adr,uint _val,string _data)
public
{
LastMsg.Sender = _adr;
LastMsg.Time = now;
LastMsg.Val = _val;
LastMsg.Data = _data;
History.push(LastMsg);
}
}
Este post de un usuario de reddit explica cómo perdió 1 ether con este contrato al intentar explotar el bug de reentrancia que esperaba que estuviera presente en el contrato.
Este ataque no se realiza específicamente sobre los contratos Solidity en sí, sino sobre aplicaciones de terceros que pueden interactuar con ellos. Añado este ataque por completitud y para ser conscientes de cómo se pueden manipular los parámetros en los contratos.
Para más información, consulte The ERC20 Short Address Attack Explained, ICO Smart contract Vulnerability: Short Address Attack o este post de reddit.
Al pasar parámetros a un contrato inteligente, los parámetros se codifican según la especificación ABI. Es posible enviar parámetros codificados que sean más cortos que la longitud esperada del parámetro (por ejemplo, enviar una dirección que solo tenga 38 caracteres hex (19 bytes) en lugar de los 40 caracteres hex estándar (20 bytes)). En tal escenario, la EVM rellenará con 0's al final de los parámetros codificados para completar la longitud esperada.
Esto se convierte en un problema cuando las aplicaciones de terceros no validan las entradas. El ejemplo más claro es un exchange que no verifica la dirección de un token ERC20 cuando un usuario solicita un retiro. Este ejemplo se cubre con más detalle en el post de Peter Venesses, The ERC20 Short Address Attack Explained mencionado anteriormente.
Considere, la interfaz estándar de la función transfer de ERC20, observando el orden de los parámetros,```solidity function transfer(address to, uint tokens) public returns (bool success);
Ahora consideremos un exchange que posee una gran cantidad de un token (digamos `REP`) y un usuario que desea retirar su parte de 100 tokens. El usuario enviaría su dirección, `0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead`, y el número de tokens, `100`. El exchange codificaría estos parámetros en el orden especificado por la función `transfer()`, es decir, `address` y luego `tokens`. El resultado codificado sería `a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000` `000000000000000000000000000000000056bc75e2d63100000`. Los primeros cuatro bytes (`a9059cbb`) son el [selector de firma/función](https://solidity.readthedocs.io/en/latest/abi-spec.html#function-selector) de `transfer()`, los siguientes 32 bytes son la dirección, seguidos de los últimos 32 bytes que representan el número `uint256` de tokens. Obsérvese que el hex `56bc75e2d63100000` al final corresponde a 100 tokens (con 18 decimales, según lo especificado por el contrato del token `REP`).
Vale, ahora veamos qué ocurre si enviáramos una dirección a la que le falta 1 byte (2 dígitos hexadecimales). Concretamente, digamos que un atacante envía `0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde` como dirección (faltan los dos últimos dígitos) y los mismos `100` tokens para retirar. Si el exchange no valida esta entrada, se codificaría como `a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeadde00000000000000` `00000000000000000000000000000000056bc75e2d6310000000`. La diferencia es sutil. Nótese que se ha añadido `00` al final de la codificación para compensar la dirección corta que se envió. Cuando esto se envía al contrato inteligente, los parámetros `address` se leerán como `0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde00` y el valor se leerá como `56bc75e2d6310000000` (nótese los dos `0` adicionales). Este valor ahora es `25600` tokens (el valor se ha multiplicado por `256`). En este ejemplo, si el exchange tuviera esa cantidad de tokens, el usuario retiraría `25600` tokens (mientras que el exchange cree que el usuario solo retira `100`) a la dirección modificada. Obviamente, el atacante no poseerá la dirección modificada en este ejemplo, pero si el atacante generara cualquier dirección que terminara en `0` (lo que se puede forzar fácilmente por fuerza bruta) y usara esa dirección generada, podría robar fácilmente tokens del exchange desprevenido.
<h3 id="short-prev">Técnicas de prevención</h3>
Supongo que es obvio decir que validar todas las entradas antes de enviarlas a la blockchain evitará este tipo de ataques. También cabe señalar que el orden de los parámetros juega un papel importante aquí. Dado que el relleno solo ocurre al final, un orden cuidadoso de los parámetros en el contrato inteligente puede mitigar potencialmente algunas formas de este ataque.
<h3 id="short-example">Ejemplo del mundo real: Desconocido</h3>
No conozco ningún ataque publicitado de este tipo en la naturaleza.
<h2 id="unchecked-calls"><span id="SP-9">9. Valores de retorno de CALL no verificados</span></h2>
Hay varias formas de realizar llamadas externas en Solidity. El envío de ether a cuentas externas se realiza comúnmente mediante el método `transfer()`. Sin embargo, la función `send()` también se puede usar y, para llamadas externas más versátiles, el opcode `CALL` se puede emplear directamente en Solidity. Las funciones `call()` y `send()` devuelven un booleano que indica si la llamada tuvo éxito o falló. Por lo tanto, estas funciones tienen una advertencia simple: la transacción que ejecuta estas funciones no revertirá si la llamada externa (inicializada por `call()` o `send()`) falla; en cambio, `call()` o `send()` simplemente devolverán `false`. Un error común surge cuando no se verifica el valor de retorno; en lugar de ello, el desarrollador espera que ocurra una reversión.
Para más lectura, consulte [DASP Top 10](http://www.dasp.co/#item-4) y [Scanning Live Ethereum Contracts for the "Unchecked-Send" Bug](http://hackingdistributed.com/2016/06/16/scanning-live-ethereum-contracts-for-bugs/).
<h3 id="unchecked-calls-vuln">La vulnerabilidad</h3>
Considere el siguiente ejemplo:```solidity
contract Lotto {
bool public payedOut = false;
address public winner;
uint public winAmount;
// ... extra functionality here
function sendToWinner() public {
require(!payedOut);
winner.send(winAmount);
payedOut = true;
}
function withdrawLeftOver() public {
require(payedOut);
msg.sender.send(this.balance);
}
}
Este contrato representa un contrato similar a Lotto, donde un winner recibe winAmount de ether, lo que normalmente deja un pequeño sobrante que cualquiera puede retirar.
El error existe en la línea [11] donde se usa send() sin comprobar la respuesta. En este ejemplo trivial, un winner cuya transacción falla (ya sea por quedarse sin gas o por ser un contrato que lanza una excepción intencionalmente en la función fallback) permite que payedOut se establezca a true (independientemente de si el ether se envió o no). En este caso, el público puede retirar las ganancias del winner mediante la función withdrawLeftOver().
Siempre que sea posible, use la función transfer() en lugar de send(), ya que transfer() hará revert si la transacción externa revierte. Si se requiere send(), asegúrese siempre de comprobar el valor de retorno.
Una recomendación aún más robusta es adoptar un patrón de retiro. En esta solución, cada usuario tiene la responsabilidad de llamar a una función aislada (es decir, una función withdraw) que maneja el envío de ether fuera del contrato y, por lo tanto, lidia de forma independiente con las consecuencias de las transacciones de envío fallidas. La idea es aislar lógicamente la funcionalidad de envío externo del resto de la base de código y colocar la carga de una posible transacción fallida en el usuario final que llama a la función withdraw.
Etherpot era una lotería de contratos inteligentes, no muy diferente del contrato de ejemplo mencionado anteriormente. El código Solidity de Etherpot se puede encontrar aquí: lotto.sol. El principal defecto de este contrato se debía a un uso incorrecto de los hashes de bloque (solo los últimos 256 hashes de bloque son utilizables; consulte la publicación de Aakil Fernandes sobre cómo Etherpot no logró implementar esto correctamente). Sin embargo, este contrato también sufría de un valor de llamada no comprobado. Observe la función cash() en la línea [80] de lotto.sol:```solidity
...
function cash(uint roundIndex, uint subpotIndex){
var subpotsCount = getSubpotsCount(roundIndex);
if(subpotIndex>=subpotsCount)
return;
var decisionBlockNumber = getDecisionBlockNumber(roundIndex,subpotIndex);
if(decisionBlockNumber>block.number)
return;
if(rounds[roundIndex].isCashed[subpotIndex])
return;
//Subpots can only be cashed once. This is to prevent double payouts
var winner = calculateWinner(roundIndex,subpotIndex);
var subpot = getSubpot(roundIndex);
winner.send(subpot);
rounds[roundIndex].isCashed[subpotIndex] = true;
//Mark the round as cashed
} ...
Observa que en la línea \[21\] no se comprueba el valor de retorno de la función send, y la línea siguiente establece un booleano que indica que se han enviado los fondos al ganador. Este bug puede permitir un estado en el que el ganador no recibe su ether, pero el estado del contrato puede indicar que al ganador ya se le ha pagado.
Una versión más grave de este bug ocurrió en el [King of the Ether](https://www.kingoftheether.com/thrones/kingoftheether/index.html). Se ha escrito un excelente [post-mortem](https://www.kingoftheether.com/postmortem.html) de este contrato que detalla cómo un `send()` fallido sin comprobar podía utilizarse para atacar el contrato.
<h2 id="race-conditions"><span id="SP-10">10. Condiciones de carrera / Front Running</span></h2>
La combinación de llamadas externas a otros contratos y la naturaleza multiusuario de la cadena de bloques subyacente da lugar a una variedad de posibles trampas de Solidity en las que los usuarios *compiten* por la ejecución del código para obtener estados inesperados. [Re-Entrancy](#reentrancy) es un ejemplo de dicha condición de carrera. En esta sección hablaremos de forma más general sobre los distintos tipos de condiciones de carrera que pueden ocurrir en la cadena de bloques de Ethereum. Existen varios buenos artículos sobre este tema, algunos son: [Ethereum Wiki - Safety](https://github.com/ethereum/wiki/wiki/Safety#race-conditions), [DASP - Front-Running](http://www.dasp.co/#item-7) y [Consensus - Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/known_attacks/#race-conditions).
<h3 id="race-conditions-vuln">La Vulnerabilidad</h3>
Como ocurre con la mayoría de las cadenas de bloques, los nodos de Ethereum agrupan las transacciones en un pool y las forman en bloques. Las transacciones solo se consideran válidas una vez que un minero ha resuelto un mecanismo de consenso (actualmente [ETHASH](https://github.com/ethereum/wiki/wiki/Ethash) PoW para Ethereum). El minero que resuelve el bloque también elige qué transacciones del pool se incluirán en el bloque; esto se ordena normalmente por el `gasPrice` de una transacción. Aquí reside un posible vector de ataque. Un atacante puede observar el pool de transacciones en busca de transacciones que puedan contener soluciones a problemas, modificar o revocar los permisos del atacante, o cambiar un estado en un contrato que le resulte indeseable. El atacante puede entonces obtener los datos de esa transacción y crear una transacción propia con un `gasPrice` más alto para que su transacción se incluya en un bloque antes que la original.
Veamos cómo podría funcionar esto con un ejemplo sencillo. Considera el contrato `FindThisHash.sol`:```solidity
contract FindThisHash {
bytes32 constant public hash = 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a;
constructor() public payable {} // load with ether
function solve(string solution) public {
// If you can find the pre image of the hash, receive 1000 ether
require(hash == sha3(solution));
msg.sender.transfer(1000 ether);
}
}
Imagine este contrato contiene 1000 ether. El usuario que pueda encontrar la preimagen del hash sha3 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a puede enviar la solución y recuperar los 1000 ether. Digamos que un usuario descubre que la solución es Ethereum!. Llama a solve() con Ethereum! como parámetro. Desafortunadamente, un atacante ha sido lo bastante astuto como para vigilar el pool de transacciones en busca de cualquiera que envíe una solución. Este ve la solución, comprueba su validez y, a continuación, envía una transacción equivalente con un gasPrice mucho más alto que el de la transacción original. El minero que resuelva el bloque probablemente dará preferencia al atacante debido al mayor gasPrice y aceptará su transacción antes que la del solucionador original. El atacante se llevará los 1000 ether y el usuario que resolvió el problema no obtendrá nada (no queda ether en el contrato).
Un problema más realista se presenta en el diseño de la futura implementación de Casper. Los contratos de prueba de participación (proof of stake) de Casper invocan condiciones de slashing en las que los usuarios que detectan que los validadores votan doble o se comportan mal reciben incentivos para enviar una prueba de que lo han hecho. El validador será castigado y el usuario recompensado. En tal escenario, se espera que los mineros y los usuarios se adelanten a todas estas presentaciones de pruebas, y este problema debe abordarse antes del lanzamiento final.
Hay dos clases de usuarios que pueden realizar este tipo de ataques de front-running. Los usuarios (que modifican el gasPrice de sus transacciones) y los propios mineros (que pueden reordenar las transacciones de un bloque como mejor les parezca). Un contrato vulnerable a la primera clase (usuarios) está en una situación significativamente peor que uno vulnerable a la segunda (mineros), ya que los mineros solo pueden realizar el ataque cuando resuelven un bloque, algo poco probable para un minero individual que apunte a un bloque específico. Aquí enumeraré algunas medidas de mitigación en relación con la clase de atacantes que pueden prevenir.
Un método que se puede emplear es crear lógica en el contrato que establezca un límite superior para el gasPrice. Esto evita que los usuarios aumenten el gasPrice y obtengan un orden de transacción preferente más allá del límite superior. Esta medida preventiva solo mitiga la primera clase de atacantes (usuarios arbitrarios). Los mineros en este escenario aún pueden atacar el contrato, ya que pueden ordenar las transacciones de su bloque como quieran, independientemente del precio del gas.
Un método más robusto es utilizar un esquema de commit-reveal siempre que sea posible. Este esquema dicta que los usuarios envíen transacciones con información oculta (normalmente un hash). Después de que la transacción se haya incluido en un bloque, el usuario envía una transacción que revela los datos que se enviaron (la fase de revelación). Este método evita que tanto mineros como usuarios se adelanten a las transacciones, ya que no pueden determinar el contenido de la transacción. Sin embargo, este método no puede ocultar el valor de la transacción (que en algunos casos es la información valiosa que necesita ocultarse). El contrato inteligente de ENS permitía a los usuarios enviar transacciones cuyos datos comprometidos incluían la cantidad de ether que estaban dispuestos a gastar. Los usuarios podían entonces enviar transacciones de valor arbitrario. Durante la fase de revelación, se reembolsaba a los usuarios la diferencia entre la cantidad enviada en la transacción y la cantidad que estaban dispuestos a gastar.
Una sugerencia adicional de Lorenz, Phil, Ari y Florian es usar Submarine Sends. Una implementación eficiente de esta idea requiere el opcode CREATE2, que actualmente no ha sido adoptado, pero parece probable en próximos hard forks.
El estándar ERC20 es bastante conocido para construir tokens en Ethereum. Este estándar tiene una potencial vulnerabilidad de front-running que surge debido a la función approve(). Una buena explicación de esta vulnerabilidad se puede encontrar aquí.
El estándar especifica la función approve() como:```solidity
function approve(address _spender, uint256 _value) returns (bool success)
Esta función permite a un usuario autorizar a otros usuarios a transferir tokens en su nombre. La vulnerabilidad de frontrunning se presenta en el escenario en el que una usuaria, Alice, *aprueba* a su amigo `Bob` para gastar `100 tokens`. Alice decide más tarde que quiere revocar la aprobación de `Bob` para gastar `100 tokens`, por lo que crea una transacción que establece la asignación de `Bob` en `50 tokens`. `Bob`, que ha estado observando atentamente la cadena, ve esta transacción y construye una transacción propia gastando los `100 tokens`. Pone un `gasPrice` más alto en su transacción que el de `Alice` y consigue que su transacción se priorice sobre la de ella. Algunas implementaciones de `approve()` permitirían a `Bob` transferir sus `100 tokens` y, cuando la transacción de `Alice` se confirme, restablecerían la aprobación de `Bob` a `50 tokens`, dando en efecto a `Bob` acceso a `150 tokens`. Las estrategias de mitigación de este ataque se proporcionan [aquí](https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit) en el documento enlazado anteriormente.
Otro ejemplo prominente del mundo real es [Bancor](https://www.bancor.network/). Ivan Bogatty y su equipo documentaron un ataque rentable contra la implementación inicial de Bancor. Su [entrada de blog](https://hackernoon.com/front-running-bancor-in-150-lines-of-python-with-ethereum-api-d5e2bfd0d798) y su [charla en Devon 3](https://www.youtube.com/watch?v=RL2nE3huNiI) analizan en detalle cómo se hizo esto. En esencia, los precios de los tokens se determinan en función del valor de la transacción; los usuarios pueden observar el pool de transacciones en busca de transacciones de Bancor y adelantarse a ellas para beneficiarse de las diferencias de precios. El equipo de Bancor ha abordado este ataque.
<h2 id="dos"><span id="SP-11">11. Denegación de Servicio (DOS)</span></h2>
Esta categoría es muy amplia, pero fundamentalmente consiste en ataques en los que los usuarios pueden dejar el contrato inoperable durante un pequeño período de tiempo o, en algunos casos, de forma permanente. Esto puede atrapar ether en estos contratos para siempre, como fue el caso del [segundo hackeo de Parity MultiSig](#dc-example)
<h3 id="dos-vuln">La Vulnerabilidad</h3>
Existen diversas formas en que un contrato puede volverse inoperable. Aquí solo resaltaré algunos patrones de codificación de Solidity con matices de Blockchain potencialmente menos obvios que pueden llevar a los atacantes a realizar ataques de DOS.
**1. Llamadas externas sin estipendios de gas** - Puede darse el caso de que desees
hacer una llamada externa a un contrato desconocido y continuar procesando la
transacción independientemente de si la llamada falla o no. Normalmente esto se
logra mediante el uso del opcode `CALL`, que no revierte la transacción
si la llamada falla (consulta [Valores de retorno de CALL no verificados](#unchecked-calls) para más detalles y ejemplos).
Consideremos un ejemplo sencillo en el que tenemos un contrato de cartera que va
liberando ether lentamente cuando se llama a la función `withdraw()`. Un `partner` puede
añadir su dirección y gastar gas para llamar a withdraw, otorgando tanto al
`partner` como al `owner` el 1% del saldo total del contrato.```solidity
contract TrickleWallet {
address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = 0xA9E;
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
require(partner == '0x0' || msg.sender == partner);
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance/100;
// perform a call without checking return
// the recipient can revert, the owner will still get their share
partner.call.value(amountToSend)();
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] += amountToSend;
}
// allow deposit of funds
function() payable {}
// convenience function
function contractBalance() view returns (uint) {
return address(this).balance;
}
}
Observa que en la línea [17] realizamos una llamada externa enviando el 1% del
saldo del contrato a una cuenta especificada por el usuario. La razón por la que se usa el opcode CALL es para asegurar que
el propietario reciba el pago, incluso si la llamada externa revierte. El problema es que
la transacción enviará todo su gas (en realidad, solo se envía la mayor parte del gas de la transacción, se deja un poco para terminar de procesar la llamada) a la llamada externa. Si el usuario fuera malintencionado, podría crear un contrato que consuma todo el gas y obligar a que todas las transacciones a withdraw() fallen debido a que se agota el gas.
Por ejemplo, considera el siguiente contrato malicioso que consume todo el gas,```solidity contract ConsumeAllGas { function () payable { // an assert consumes all transaction gas, unlike a //revert which returns the remaining gas assert(1==2); } }
Si un socio que se retira decidiera que no le agrada el propietario del contrato.
Podría establecer la dirección del socio a este contrato y bloquear todos los fondos en
el contrato `TrickleWallet` para siempre.
Para prevenir tales vectores de ataque DOS, asegúrese de que se especifique un estipendio de gas en una
llamada externa, para limitar la cantidad de gas que esa transacción puede usar. En nuestro
ejemplo, podríamos remediar este ataque cambiando la línea \[17\] a:```solidity
partner.call.gas(50000).value(amountToSend)();
Esta modificación permite que solo se gasten 50,000 de gas en la transacción
externa. El owner puede fijar un precio del gas mayor que este, para que su
transacción se complete, sin importar cuánto gas use la transacción externa.
2. Iterar sobre mappings o arrays manipulados externamente - En mis aventuras he visto varias formas de este tipo de patrón. Normalmente aparece en escenarios donde un owner desea distribuir tokens entre sus inversores, y lo hace con una función tipo distribute() como se puede ver en el contrato de ejemplo:```solidity
contract DistributeTokens {
address public owner; // gets set somewhere
address[] investors; // array of investors
uint[] investorTokens; // the amount of tokens each investor gets
// ... extra functionality, including transfertoken()
function invest() public payable {
investors.push(msg.sender);
investorTokens.push(msg.value * 5); // 5 times the wei sent
}
function distribute() public {
require(msg.sender == owner); // only owner
for(uint i = 0; i < investors.length; i++) {
// here transferToken(to,amount) transfers "amount" of tokens to the address "to"
transferToken(investors[i],investorTokens[i]);
}
}
}
Tenga en cuenta que el bucle en este contrato se ejecuta sobre un array que puede inflarse artificialmente. Un atacante puede crear muchas cuentas de usuario haciendo que el array `investor` sea grande. En principio, esto puede hacerse de tal manera que el gas necesario para ejecutar el bucle for supere el límite de gas del bloque, haciendo esencialmente que la función `distribute()` sea inoperable.
**3. Operaciones del propietario** - Otro patrón común es cuando los propietarios tienen privilegios específicos en los contratos y deben realizar alguna tarea para que el contrato pase al siguiente estado. Un ejemplo sería un contrato ICO que requiere que el propietario finalice (`finalize()`) el contrato, lo que luego permite que los tokens sean transferibles, es decir,``` solidity
bool public isFinalized = false;
address public owner; // gets set somewhere
function finalize() public {
require(msg.sender == owner);
isFinalized == true;
}
// ... extra ICO functionality
// overloaded transfer function
function transfer(address _to, uint _value) returns (bool) {
require(isFinalized);
super.transfer(_to,_value)
}
...
En tales casos, si un usuario privilegiado pierde sus claves privadas o queda inactivo, todo el contrato de tokens queda inoperable. En este caso, si owner no puede llamar a finalize(), no se puede transferir ningún token; es decir, todo el funcionamiento del ecosistema de tokens depende de una única dirección.
4. Progreso de estado basado en llamadas externas - Los contratos a veces se escriben de tal manera que para progresar a un nuevo estado se requiere enviar ether a una dirección, o esperar alguna entrada de una fuente externa. Estos patrones pueden llevar a ataques de DOS, cuando la llamada externa falla o se ve impedida por razones externas. En el ejemplo de enviar ether, un usuario puede crear un contrato que no acepta ether. Si un contrato requiere que se retire ether (considere un contrato de bloqueo temporal que requiere que todo el ether sea retirado antes de poder usarse de nuevo) para progresar a un nuevo estado, el contrato nunca alcanzará el nuevo estado, ya que el ether nunca puede ser enviado al contrato del usuario que no acepta ether.
En el primer ejemplo, los contratos no deberían iterar sobre estructuras de datos que puedan ser manipuladas artificialmente por usuarios externos. Se recomienda un patrón de retiro, mediante el cual cada uno de los inversores llama a una función de retiro para reclamar tokens de forma independiente.
En el segundo ejemplo, se requería que un usuario privilegiado cambiara el estado del contrato. En tales ejemplos (siempre que sea posible) se puede utilizar un mecanismo a prueba de fallos en caso de que owner quede incapacitado. Una solución podría ser configurar a owner como un contrato multisig. Otra solución es usar un timelock, donde el require de la línea [13] podría incluir un mecanismo basado en tiempo, como require(msg.sender == owner || now > unlockTime), que permite a cualquier usuario finalizar después de un período de tiempo, especificado por unlockTime. Este tipo de técnica de mitigación también se puede usar en el tercer ejemplo. Si se requieren llamadas externas para progresar a un nuevo estado, hay que considerar su posible fallo y potencialmente añadir una progresión de estado basada en tiempo en caso de que la llamada deseada nunca llegue.
Nota: Por supuesto, existen alternativas centralizadas a estas sugerencias, donde se puede añadir un maintenanceUser que pueda aparecer y solucionar problemas con vectores de ataque basados en DOS si es necesario. Normalmente, este tipo de contratos conlleva problemas de confianza sobre el poder de dicha entidad, pero esa no es una conversación para esta sección.
GovernMental era un antiguo esquema Ponzi que acumuló una cantidad bastante grande de ether. De hecho, en un momento llegó a acumular 1100 ether. Desafortunadamente, era susceptible a las vulnerabilidades de DOS mencionadas en esta sección. Esta publicación de Reddit describe cómo el contrato requería la eliminación de un mapping grande para poder retirar el ether. La eliminación de este mapping tenía un coste de gas que superaba el límite de gas del bloque en ese momento, por lo que no era posible retirar los 1100 ether. La dirección del contrato es 0xF45717552f12Ef7cb65e95476F217Ea008167Ae3 y se puede ver en la transacción 0x0d80d67202bd9cb6773df8dd2020e7190a1b0793e8ec4fc105257e8128f0506b que los 1100 ether se obtuvieron finalmente con una transacción que usó 2.5M de gas (después de que el límite de gas del bloque permitiera dicha transacción).
Las marcas de tiempo de bloque se han utilizado históricamente para una variedad de aplicaciones, como entropía para números aleatorios (consulte la sección Ilusión de entropía para más detalles), bloqueo de fondos durante períodos de tiempo y diversas declaraciones condicionales de cambio de estado que dependen del tiempo. Los mineros tienen la capacidad de ajustar ligeramente las marcas de tiempo, lo que puede resultar bastante peligroso si las marcas de tiempo de bloque se usan incorrectamente en contratos inteligentes.
Algunas referencias útiles para esto son: La documentación de Solidity, esta pregunta de Stack Exchange.
block.timestamp o su alias now puede ser manipulado por los mineros si tienen algún incentivo para hacerlo. Construyamos un juego sencillo que sería vulnerable a la explotación por parte de los mineros,
roulette.sol:```solidity
contract Roulette {
uint public pastBlockTime; // Forces one bet per block
constructor() public payable {} // initially fund contract
// fallback function used to make a bet
function () public payable {
require(msg.value == 10 ether); // must send 10 ether to play
require(now != pastBlockTime); // only 1 transaction per block
pastBlockTime = now;
if(now % 15 == 0) { // winner
msg.sender.transfer(this.balance);
}
}
}
This contract behaves like a simple lottery. One transaction per block can bet `10 ether` for a chance to win the balance of the contract. The assumption here is that, `block.timestamp` is uniformly distributed about the last two digits. If that were the case, there would be a 1/15 chance of winning this lottery.
However, as we know, miners can adjust the timestamp, should they need to. In this particular case, if enough ether pooled in the contract, a miner who solves a block is incentivised to choose a timestamp such that `block.timestamp` or `now` modulo 15 is `0`. In doing so they may win the ether locked in this contract along with the block reward. As there is only one person allowed to bet per block, this is also vulnerable to [front-running](#race-conditions) attacks.
In practice, block timestamps are monotonically increasing and so miners cannot choose arbitrary block timestamps (they must be larger than their predecessors). They are also limited to setting blocktimes not too far in the future as these blocks will likely be rejected by the network (nodes will not validate blocks whose timestamps are in the future).
<h3 id="block-timestamp-prev">Técnicas Preventivas</h3>
Los timestamps de bloque no deberían usarse para entropía o para generar números aleatorios - es decir, no deberían ser el factor decisivo (ya sea directa o indirectamente a través de alguna derivación) para ganar un juego o cambiar un estado importante (si se asume que son aleatorios).
A veces se requiere lógica sensible al tiempo; es decir, desbloquear contratos (timelocking), completar una ICO después de unas semanas o imponer fechas de caducidad. A veces se recomienda usar `block.number` (ver la [documentación de Solidity](http://solidity.readthedocs.io/en/latest/units-and-global-variables.html#block-and-transaction-properties)) y un tiempo de bloque promedio para estimar tiempos; es decir, `1 week` con un tiempo de bloque de `10 second` equivale aproximadamente a `60480 blocks`. Por lo tanto, especificar un número de bloque en el que cambiar el estado de un contrato puede ser más seguro, ya que los mineros no pueden manipular el número de bloque con tanta facilidad. El contrato [BAT ICO](https://etherscan.io/address/0x0d8775f648430679a709e98d2b0cb6250d2887ef#code) empleó esta estrategia.
Esto puede ser innecesario si los contratos no están particularmente preocupados por las manipulaciones del timestamp de bloque por parte de los mineros, pero es algo a tener en cuenta al desarrollar contratos.
<h3 id="block-timestamp-example">Ejemplo del mundo real: GovernMental </h3>
[GovernMental](http://governmental.github.io/GovernMental/) era un antiguo esquema Ponzi que acumuló una cantidad bastante grande de ether. También era vulnerable a un ataque basado en timestamp. El contrato pagaba al jugador que fuera el último en unirse (durante al menos un minuto) en una ronda. Por lo tanto, un minero que fuera jugador podía ajustar el timestamp (a un momento futuro, para que pareciera que había transcurrido un minuto) para hacer parecer que el jugador había sido el último en unirse durante más de un minuto (aunque esto no sea cierto en la realidad). Se pueden encontrar más detalles al respecto en el [artículo sobre la Historia de las Vulnerabilidades de Seguridad de Ethereum](https://applicature.com/blog/history-of-ethereum-security-vulnerabilities-hacks-and-their-fixes) de Tanya Bahrynovska.
<h2 id="constructors"><span id="SP-13">13. Constructores con Cuidado</span></h2>
Los constructores son funciones especiales que a menudo realizan tareas críticas y privilegiadas al inicializar contratos. Antes de solidity `v0.4.22`, los constructores se definían como funciones que tenían el mismo nombre que el contrato que los contenía. Por lo tanto, cuando se cambia el nombre de un contrato durante el desarrollo, si no se cambia el nombre del constructor, este se convierte en una función normal e invocable. Como puedes imaginar, esto puede (y ha) llevar a algunos hacks de contratos interesantes.
Para lectura adicional, sugiero al lector intentar los [Desafíos Ethernaught](https://github.com/OpenZeppelin/ethernaut) (en particular el nivel Fallout).
<h3 id="constructors-vuln">La Vulnerabilidad</h3>
Si el nombre del contrato se modifica, o hay un error tipográfico en el nombre del constructor que hace que ya no coincida con el nombre del contrato, el constructor se comportará como una función normal. Esto puede tener consecuencias graves, especialmente si el constructor está realizando operaciones privilegiadas. Considera el siguiente contrato```solidity
contract OwnerWallet {
address public owner;
//constructor
function ownerWallet(address _owner) public {
owner = _owner;
}
// fallback. Collect ether.
function () payable {}
function withdraw() public {
require(msg.sender == owner);
msg.sender.transfer(this.balance);
}
}
Este contrato recolecta ether y solo permite al propietario retirar todo el ether llamando a la función withdraw(). El problema surge debido a que el constructor no tiene exactamente el mismo nombre que el contrato. Específicamente, ownerWallet no es lo mismo que OwnerWallet. Por lo tanto, cualquier usuario puede llamar a la función ownerWallet(), establecerse como propietario y luego tomar todo el ether del contrato llamando a withdraw().
Este problema se ha abordado principalmente en el compilador de Solidity en la versión 0.4.22. Esta versión introdujo una palabra clave constructor que especifica el constructor, en lugar de requerir que el nombre de la función coincida con el nombre del contrato. Se recomienda usar esta palabra clave para especificar los constructores y así prevenir los problemas de nomenclatura mencionados anteriormente.
Rubixi (código del contrato) fue otro esquema piramidal que presentaba este tipo de vulnerabilidad. Originalmente se llamaba DynamicPyramid, pero el nombre del contrato se cambió antes del despliegue a Rubixi. El nombre del constructor no se cambió, lo que permitía a cualquier usuario convertirse en el creator. Se puede encontrar una discusión interesante relacionada con este error en este Hilo de Bitcoin. En última instancia, permitía a los usuarios luchar por el estado de creator para reclamar las comisiones del esquema piramidal. Se pueden encontrar más detalles sobre este error en particular aquí.
La EVM almacena datos ya sea como storage o como memory. Comprender exactamente cómo se hace esto y los tipos predeterminados de las variables locales de las funciones es muy recomendable al desarrollar contratos. Esto se debe a que es posible producir contratos vulnerables al inicializar variables de forma inapropiada.
Para leer más sobre storage y memory en la EVM, consulta la Documentación de Solidity: Ubicación de Datos, Documentación de Solidity: Disposición de las Variables de Estado en Storage, Documentación de Solidity: Disposición en Memoria.
Esta sección se basa en la excelente publicación de Stefan Beyer. Se pueden encontrar más lecturas sobre este tema a partir de la inspiración de Sefan, que es este hilo de reddit.
Las variables locales dentro de las funciones tienen como valor predeterminado storage o memory según su tipo. Las variables locales storage no inicializadas pueden apuntar a otras variables de storage inesperadas en el contrato, lo que conduce a vulnerabilidades intencionales (es decir, el desarrollador las coloca ahí intencionalmente para atacar más tarde) o no intencionales.
Consideremos el siguiente contrato de registro de nombres relativamente simple:```solidity // A Locked Name Registrar contract NameRegistrar {
bool public unlocked = false; // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name;
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}
Este simple registrador de nombres tiene una sola función. Cuando el contrato está `unlocked`, permite que cualquiera registre un nombre (como un hash `bytes32`) y mapee ese nombre a una dirección. Desafortunadamente, este registrador está inicialmente bloqueado y el `require` en la línea \[23\] impide que `register()` agregue registros de nombres. Sin embargo, hay una vulnerabilidad en este contrato que permite el registro de nombres independientemente de la variable `unlocked`.
Para discutir esta vulnerabilidad, primero necesitamos entender cómo funciona el almacenamiento en Solidity. Como visión general de alto nivel (sin ningún detalle técnico apropiado — sugiero leer la documentación de Solidity para una revisión adecuada), las variables de estado se almacenan secuencialmente en *slots* a medida que aparecen en el contrato (pueden agruparse, pero no en este ejemplo, así que no nos preocuparemos por eso). Por lo tanto, `unlocked` existe en el `slot 0`, `registeredNameRecord` existe en el `slot 1` y `resolve` en el `slot 2`, etc. Cada uno de estos slots tiene un tamaño de 32 bytes (hay complejidades adicionales con los mappings que ignoramos por ahora). El booleano `unlocked` se verá como `0x000...0` (64 `0`s, excluyendo el `0x`) para `false` o `0x000...1`(63 `0`s) para `true`. Como puedes ver, hay un desperdicio significativo de almacenamiento en este ejemplo en particular.
La siguiente información que necesitamos es que Solidity asigna por defecto los tipos de datos complejos, como `structs`, a `storage` cuando se inicializan como variables locales. Por lo tanto, `newRecord` en la línea \[16\] se asigna por defecto a `storage`. La vulnerabilidad se debe al hecho de que `newRecord` no está inicializado. Debido a que por defecto es `storage`, se convierte en un puntero a storage y, como no está inicializado, apunta al slot `0` (es decir, donde se almacena `unlocked`). Observa que en las líneas \[17\] y \[18\] establecemos `nameRecord.name` a `_name` y `nameRecord.mappedAddress` a `_mappedAddress`; esto, en efecto, cambia la ubicación de almacenamiento del slot 0 y del slot 1, lo que modifica tanto `unlocked` como el slot de almacenamiento asociado con `registeredNameRecord`.
Esto significa que `unlocked` puede modificarse directamente, simplemente mediante el parámetro `bytes32 _name` de la función `register()`. Por lo tanto, si el último byte de `_name` es distinto de cero, modificará el último byte del `slot 0` de almacenamiento y cambiará directamente `unlocked` a `true`. Tales valores de `_name` superarán el `require()` en la línea \[23\] ya que estamos estableciendo `unlocked` a `true`. Pruébalo en Remix. Observa que la función pasará si usas un `_name` de la forma: `0x0000000000000000000000000000000000000000000000000000000000000001`
<h3 id="storage-prev">Técnicas de Prevención</h3>
El compilador de Solidity genera advertencias para las variables de storage no inicializadas, por lo que los desarrolladores deben prestar mucha atención a estas advertencias al construir contratos inteligentes. La versión actual de mist (0.10) no permite compilar estos contratos. Es una buena práctica usar explícitamente las palabras clave `memory` o `storage` al tratar con tipos complejos para asegurar que se comporten como se espera. A partir de la versión `0.5.0` de Solidity, el uso de `memory` y `storage` es obligatorio.
<h3 id="storage-example">Ejemplos del Mundo Real: Honey Pots: OpenAddressLottery y CryptoRoulette</h3>
Se desplegó un honey pot llamado OpenAddressLottery ([código del contrato](https://etherscan.io/address/0x741f1923974464efd0aa70e77800ba5d9ed18902#code)) que utilizaba esta peculiaridad de variable de storage no inicializada para recolectar ether de algunos supuestos hackers. El contrato es bastante complejo, así que dejaré la discusión a este [hilo de reddit](https://www.reddit.com/r/ethdev/comments/7wp363/how_does_this_honeypot_work_it_seems_like_a/) donde el ataque está explicado con bastante claridad.
Otro honey pot, CryptoRoulette ([código del contrato](https://etherscan.io/address/0x8685631276cfcf17a973d92f6dc11645e5158c0c#code)) también utiliza este truco para intentar recolectar algo de ether. Si no puedes descubrir cómo funciona el ataque, consulta [Un análisis de un par de contratos honey pot de Ethereum](https://medium.com/@jsanjuas/an-analysis-of-a-couple-ethereum-honeypot-contracts-5c07c95b0a8d) para una visión general de este contrato y otros.
<h2 id="precision"><span id="SP-15">15. Puntos Flotantes y Precisión</span></h2>
Al momento de escribir esto (Solidity v0.4.24), los números de punto fijo o de punto flotante no son compatibles. Esto significa que las representaciones de punto flotante deben hacerse con los tipos enteros en Solidity. Esto puede provocar errores/vulnerabilidades si no se implementa correctamente.
Para más información, consulta [Técnicas y Consejos de Seguridad en Contratos de Ethereum - Redondeo con División de Enteros](https://github.com/ethereum/wiki/wiki/Safety#beware-rounding-with-integer-division),
<h3 id="precision-vuln">La Vulnerabilidad</h3>
Como no existe un tipo de punto fijo en Solidity, los desarrolladores deben implementar el suyo propio utilizando los tipos de datos enteros estándar. Hay una serie de trampas en las que los desarrolladores pueden caer durante este proceso. Intentaré resaltar algunas de ellas en esta sección.
Comencemos con un ejemplo de código (ignoremos cualquier problema de overflow/underflow por simplicidad).```solidity
contract FunWithNumbers {
uint constant public tokensPerEth = 10;
uint constant public weiPerEth = 1e18;
mapping(address => uint) public balances;
function buyTokens() public payable {
uint tokens = msg.value/weiPerEth*tokensPerEth; // convert wei to eth, then multiply by token rate
balances[msg.sender] += tokens;
}
function sellTokens(uint tokens) public {
require(balances[msg.sender] >= tokens);
uint eth = tokens/tokensPerEth;
balances[msg.sender] -= tokens;
msg.sender.transfer(eth*weiPerEth); //
}
}
Este sencillo contrato de compra/venta de tokens tiene algunos problemas evidentes en la compra y venta de tokens. Aunque los cálculos matemáticos para comprar y vender tokens son correctos, la falta de números de punto flotante dará resultados erróneos. Por ejemplo, al comprar tokens en la línea [7], si el valor es menor que 1 ether, la división inicial dará como resultado 0, dejando la multiplicación final en 0 (es decir, 200 wei dividido por 1e18 weiPerEth es igual a 0). Del mismo modo, al vender tokens, cualquier cantidad de tokens inferior a 10 también dará como resultado 0 ether. De hecho, el redondeo aquí siempre es hacia abajo, por lo que vender 29 tokens dará como resultado 2 ether.
El problema con este contrato es que la precisión es solo hasta el ether más cercano (es decir, 1e18 wei). Esto puede volverse complicado cuando se trabaja con decimals en tokens ERC20 cuando se necesitan mayores precisiones.
Mantener la precisión adecuada en tus contratos inteligentes es muy importante, especialmente cuando se manejan ratios y tasas que reflejan decisiones económicas.
Debes asegurarte de que cualquier ratio o tasa que estés utilizando permita numeradores grandes en las fracciones. Por ejemplo, usamos la tasa tokensPerEth en nuestro ejemplo. Habría sido mejor usar weiPerTokens, que sería un número grande. Para resolver la cantidad de tokens podríamos hacer msg.value/weiPerTokens. Esto daría un resultado más preciso.
Otra táctica a tener en cuenta es ser consciente del orden de las operaciones. En el ejemplo anterior, el cálculo para comprar tokens era msg.value/weiPerEth*tokenPerEth. Nótese que la división ocurre antes de la multiplicación. Este ejemplo habría logrado una mayor precisión si el cálculo realizara primero la multiplicación y luego la división, es decir, msg.value*tokenPerEth/weiPerEth.
Finalmente, al definir precisión arbitraria para números, puede ser una buena idea convertir las variables a una precisión mayor, realizar todas las operaciones matemáticas y, finalmente, cuando sea necesario, volver a convertirlas a la precisión de salida. Normalmente se utilizan uint256 (ya que son óptimos para el consumo de gas), que proporcionan aproximadamente 60 órdenes de magnitud en su rango, algunos de los cuales pueden dedicarse a la precisión de las operaciones matemáticas. Puede darse el caso de que sea mejor mantener todas las variables en alta precisión en solidity y convertirlas de nuevo a precisiones más bajas en aplicaciones externas (así es esencialmente como funciona la variable decimals en los contratos de ERC20 Token). Para ver ejemplos de cómo se puede hacer esto y las bibliotecas para hacerlo, recomiendo mirar Maker DAO DSMath. Utilizan una nomenclatura curiosa, WADs y RAYs, pero el concepto es útil.
No pude encontrar un buen ejemplo donde el redondeo haya causado un problema grave en un contrato, pero estoy seguro de que hay muchos por ahí. Siéntete libre de actualizar esto si tienes uno bueno en mente.
Ante la falta de un buen ejemplo, quiero llamar tu atención sobre Ethstick, principalmente porque me gusta la nomenclatura genial dentro del contrato. Este contrato no usa ninguna precisión extendida, sin embargo, trabaja con wei. Por lo tanto, este contrato tendrá problemas de redondeo, pero solo a nivel de precisión de wei. Tiene algunos defectos más graves, pero estos se relacionan con la dificultad de obtener entropía en la blockchain (ver Ilusión de entropía). Para una discusión adicional sobre el contrato Ethstick, te remito a otra publicación de Peter Venesses, Ethereum Contracts Are Going to be Candy For Hackers.
Solidity tiene una variable global, tx.origin, que recorre toda la pila de llamadas y devuelve la dirección de la cuenta que originalmente envió la llamada (o transacción). Usar esta variable para la autenticación en contratos inteligentes deja al contrato vulnerable a un ataque similar al phishing.
Para lectura adicional, consulta Pregunta en Stack Exchange, Blog de Peter Venesses y Solidity - Ataques Tx.Origin.
Los contratos que autorizan a los usuarios mediante la variable tx.origin suelen ser vulnerables a ataques de phishing que pueden engañar a los usuarios para que realicen acciones autenticadas en el contrato vulnerable.
Considera el contrato simple,```solidity contract Phishable { address public owner;
constructor (address _owner) {
owner = _owner;
}
function () public payable {} // collect ether
function withdrawAll(address _recipient) public {
require(tx.origin == owner);
_recipient.transfer(this.balance);
}
}
Notice that on line \[11\] this contract authorises the `withdrawAll()` function using `tx.origin`. This contract allows for an attacker to create an attacking contract of the form,```solidity
import "Phishable.sol";
contract AttackContract {
Phishable phishableContract;
address attacker; // The attackers address to receive funds.
constructor (Phishable _phishableContract, address _attackerAddress) {
phishableContract = _phishableContract;
attacker = _attackerAddress;
}
function () payable {
phishableContract.withdrawAll(attacker);
}
}
Para utilizar este contrato, un atacante lo desplegaría y luego convencería al propietario del contrato Phishable de que envíe a este contrato una cierta cantidad de ether. El atacante podría disfrazar este contrato como su propia dirección privada y manipular socialmente a la víctima para que envíe algún tipo de transacción a la dirección. La víctima, a menos que sea cuidadosa, puede no notar que hay código en la dirección del atacante, o el atacante puede hacerlo pasar por una cartera multifirma o alguna cartera de almacenamiento avanzada (recuerda
que el código fuente de los contratos públicos no está disponible por defecto).
En cualquier caso, si la víctima envía una transacción (con suficiente gas) a la dirección del AttackContract, se invocará la función fallback, que a su vez llama a la función withdrawAll() del contrato Phishable, con el parámetro attacker. Esto resultará en la retirada de todos los fondos del contrato Phishable a la dirección del attacker. Esto se debe a que la dirección que primero inicializó la llamada fue la víctima (es decir, el owner del contrato Phishable). Por lo tanto, tx.origin será igual a owner y el require en la línea [11] del contrato Phishable se cumplirá.
tx.origin no debería utilizarse para la autorización en contratos inteligentes. Esto no quiere decir que la variable tx.origin nunca deba usarse. Sí tiene algunos casos de uso legítimos en contratos inteligentes. Por ejemplo, si uno quisiera impedir que contratos externos llamen al contrato actual, podría implementar un require de la forma require(tx.origin == msg.sender). Esto evita que se utilicen contratos intermedios para llamar al contrato actual, limitando el contrato a direcciones normales sin código.
No conozco ningún exploit divulgado de esta forma en la naturaleza.
Tengo la intención de poblar esta sección con varias curiosidades interesantes que la comunidad vaya descubriendo. Se conservan en este blog porque pueden ayudar en el desarrollo de contratos inteligentes si se utilizaran estas curiosidades en la práctica.
Las direcciones de los contratos son deterministas, lo que significa que pueden calcularse antes de crear realmente la dirección. Este es el caso de las direcciones que crean contratos y también de los contratos que generan otros contratos. De hecho, la dirección de un contrato creado se determina mediante:
keccak256(rlp.encode([<account_address>, <transaction_nonce>])
En esencia, la dirección de un contrato es simplemente el hash keccak256 de la cuenta que lo creó concatenado con el nonce de transacción de la cuenta[^2]. Lo mismo ocurre con los contratos, excepto que los nonces de los contratos comienzan en 1, mientras que los nonces de transacción de las direcciones comienzan en 0.
Esto significa que, dada una dirección de Ethereum, podemos calcular todas las posibles direcciones de contrato que esta dirección puede generar. Por ejemplo, si la dirección 0x123000...000 creara un contrato en su transacción número 100, crearía la dirección de contrato keccak256(rlp.encode[0x123...000, 100]), lo que daría como resultado la dirección de contrato 0xed4cafc88a13f5d58a163e61591b9385b6fe6d1a.
Attack.sol - Línea [25] - El ether enviado al contrato malicioso ejecutará entonces la función fallback.
Attack.sol - Línea [26] - El balance total del contrato EtherStore era 10 ether y ahora es 9 ether, por lo que esta declaración if se cumple.
Attack.sol - Línea [27] - La función fallback llama entonces a la función withdrawFunds() del EtherStore nuevamente y "re-entra" en el contrato EtherStore.
EtherStore.sol - Línea [11] - En esta segunda llamada a withdrawFunds(), nuestro balance sigue siendo 1 ether ya que la línea [18] aún no se ha ejecutado. Por lo tanto, todavía tenemos balances[0x0..123] = 1 ether. Este es también el caso de la variable lastWithdrawTime. Nuevamente, pasamos todos los requisitos.
EtherStore.sol - Línea [17] - Retiramos otro 1 ether.
Los pasos 4-8 se repetirán - hasta que EtherStore.balance >= 1 según lo dictado por la línea [26] en Attack.sol.
Attack.sol - Línea [26] - Una vez que quede menos de 1 (o menos) ether en el contrato EtherStore, esta declaración if fallará. Esto permitirá que se ejecuten las líneas [18] y [19] del contrato EtherStore (para cada llamada a la función withdrawFunds()).
EtherStore.sol - Líneas [18] y [19] - Los mappings balances y lastWithdrawTime se establecerán y la ejecución terminará.
startFibonacciBalancestartslot[1]fibonacci(n)fibonacciLibraryuintwithdraw()uint(fibonacciLibrary)calculatedFibNumber