DESENVOLUPAMENT DE SMART CONTRACTS¶
Resultat d'Aprenentatge: Desenvolupa smart contracts
Criteris d'avaluació:
- a) Ha implementat smart contracts en Solidity, respectant la sintaxi i les bones pràctiques
- b) És capaç de desplegar smart contracts en una xarxa de prova i realitzar interaccions bàsiques
- c) Els smart contracts desenvolupats són segurs i compleixen les funcionalitats requerides
ÍNDEX DE CONTINGUTS¶
- Introducció als Smart Contracts
- Sintaxi Bàsica de Solidity
- Estructures de Dades i Tipus
- Funcions i Modificadors
- Herència i Interfaces
- Events i Error Handling
- Entorns de Desenvolupament
- Desplegament en Testnet
- Bones Pràctiques i Seguretat
- Exercicis Guiats
- Col·lecció d'Exercicis Proposats
1. INTRODUCCIÓ ALS SMART CONTRACTS¶
1.1 Què és un Smart Contract?¶
Un smart contract és un programa auto-executable que s'executa en una blockchain quan es compleixen condicions predefinides. A diferència dels contractes tradicionals, els smart contracts:
- Són descentralitzats: S'executen en tots els nodes de la xarxa
- Són immutables: Un cop desplegats, no es poden modificar
- Són transparents: El codi és visible i verificable per tothom
- Són deterministes: Sempre produeixen el mateix resultat amb les mateixes entrades
1.2 El Llenguatge Solidity¶
Solidity és el llenguatge de programació més utilitzat per desenvolupar smart contracts a Ethereum i altres blockchains compatibles amb EVM (Ethereum Virtual Machine).
Característiques principals: - Llenguatge d'alt nivell, orientat a objectes - Sintaxi similar a JavaScript - Estàticament tipat - Compilat a bytecode per a l'EVM
2. SINTAXI BÀSICA DE SOLIDITY¶
2.1 Estructura d'un Contracte¶
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract PrimerContract {
// Variables d'estat
string public nom;
uint256 public valor;
// Constructor
constructor(string memory _nom, uint256 _valor) {
nom = _nom;
valor = _valor;
}
// Funcions
function actualitzarValor(uint256 _nouValor) public {
valor = _nouValor;
}
function obtenirValor() public view returns (uint256) {
return valor;
}
}
Elements clau:
- SPDX-License-Identifier: Identificador de llicència (obligatori des de Solidity 0.6.8)
- pragma solidity: Versió del compilador
- contract: Paraula clau per definir un contracte
- Variables d'estat: Emmagatzemades permanentment a la blockchain
- Constructor: Funció especial que s'executa una vegada en el desplegament
2.2 Tipus de Dades¶
Tipus Valor¶
// Booleans
bool public esValid = true;
bool public esFals = false;
// Integers
int256 public enterNegatiu = -100;
uint256 public enterPositiu = 100; // unsigned
int8 public petitEnter = 5; // 8 bits
uint256 public granEnter = 1000000; // 256 bits (més comú)
// Adreces
address public propietari = 0x71C7656EC7ab88b098defB751B7401B5f6d8976F;
address payable public compte = payable(0x71C7656EC7ab88b098defB751B7401B5f6d8976F);
// Bytes
bytes1 public unByte = 0x42;
bytes32 public hash = keccak256(abi.encodePacked("dades"));
bytes public dadesDinamiques;
// Enums
enum Estat { Pendent, Procesant, Completat, Cancel·lat }
Estat public estatActual = Estat.Pendent;
Tipus Referència¶
// Strings
string public missatge = "Hola Blockchain";
// Arrays
uint256[] public numeros; // Array dinàmic
uint256[5] public cincNumeros; // Array fix
string[] public noms = ["Alice", "Bob"]; // Array amb valors
// Mappings (taules hash)
mapping(address => uint256) public saldos;
mapping(address => mapping(address => uint256)) public permisos;
// Structs
struct Persona {
string nom;
uint256 edat;
address wallet;
}
Persona public usuari1;
mapping(uint256 => Persona) public usuaris;
2.3 Operadors¶
contract Operadors {
function exemples() public pure returns (bool, uint256, bool) {
// Aritmètics
uint256 a = 10;
uint256 b = 3;
uint256 suma = a + b; // 13
uint256 resta = a - b; // 7
uint256 multiplicacio = a * b; // 30
uint256 divisio = a / b; // 3 (divisió entera)
uint256 modul = a % b; // 1
// Comparació
bool igual = (a == b); // false
bool diferent = (a != b); // true
bool major = (a > b); // true
// Lògics
bool c = true;
bool d = false;
bool and = c && d; // false
bool or = c || d; // true
bool not = !c; // false
return (and, suma, major);
}
}
3. FUNCIONS I MODIFICADORS¶
3.1 Declaració de Funcions¶
contract FuncionsExemple {
// Funció bàsica
function salutacio() public pure returns (string memory) {
return "Hola!";
}
// Funció amb paràmetres
function suma(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
// Funció que modifica estat
uint256 public comptador;
function incrementar() public {
comptador++;
}
// Múltiples valors de retorn
function obtenirDades() public pure returns (uint256, string memory, bool) {
return (42, "Resposta", true);
}
// Named returns
function calcular(uint256 x, uint256 y)
public
pure
returns (uint256 resultat, bool esPositiu)
{
resultat = x + y;
esPositiu = resultat > 0;
}
}
3.2 Visibilitat de Funcions¶
contract Visibilitat {
uint256 public valorPublic;
uint256 internal valorInternal;
uint256 private valorPrivate;
// Public: accessible des de dins i fora del contracte
function funcioPublica() public pure returns (string memory) {
return "Public";
}
// External: només accessible des de fora (més eficient per arrays grans)
function funcioExterna() external pure returns (string memory) {
return "External";
}
// Internal: només accessible des d'aquest contracte i derivats
function funcioInterna() internal pure returns (string memory) {
return "Internal";
}
// Private: només accessible des d'aquest contracte
function funcioPrivada() private pure returns (string memory) {
return "Private";
}
// Crida a funció interna
function cridarInterna() public pure returns (string memory) {
return funcioInterna();
}
}
3.3 Modificadors de Funció¶
Modificadors d'Estat (State Mutability)¶
contract ModificadorsEstat {
uint256 public valor = 100;
mapping(address => uint256) public saldos;
// view: pot llegir estat però no modificar-lo
function obtenirValor() public view returns (uint256) {
return valor;
}
// pure: no llegeix ni modifica estat
function calcular(uint256 a, uint256 b)
public
pure
returns (uint256)
{
return a + b;
}
// payable: pot rebre Ether
function dipositar() public payable {
require(msg.value > 0, "Ha d'enviar Ether");
saldos[msg.sender] += msg.value;
}
// Funció normal: pot modificar estat
function actualitzarValor(uint256 nouValor) public {
valor = nouValor;
}
}
Diferències clau: - view: Llegeix variables d'estat, no les modifica. No consumeix gas si es crida externament. - pure: No accedeix a l'estat. Només opera amb paràmetres. - payable: Permet rebre transaccions amb Ether.
Modificadors Personalitzats¶
contract ModificadorsPersonalitzats {
address public propietari;
bool public actiu = true;
uint256 public comptador;
modifier nomesPropietari() {
require(msg.sender == propietari, "No es el propietari");
_; // Executa la funció
}
modifier contracteActiu() {
require(actiu, "Contracte no actiu");
_;
}
modifier comptadorPositiu() {
_;
require(comptador > 0, "Comptador ha de ser positiu");
}
constructor() {
propietari = msg.sender;
}
function destruir() public nomesPropietari {
selfdestruct(payable(propietari));
}
function incrementar()
public
contracteActiu
comptadorPositiu
{
comptador++;
}
function desactivar() public nomesPropietari {
actiu = false;
}
}
3.4 Constructor i Funcions Especials¶
contract ContracteComplet {
address public propietari;
string public nom;
uint256 public dataCreacio;
// Constructor amb paràmetres
constructor(string memory _nom) {
propietari = msg.sender;
nom = _nom;
dataCreacio = block.timestamp;
}
// Fallback function (rep Ether sense dades)
fallback() external payable {
require(msg.sender == propietari, "Només propietari");
}
// Receive function (rep Ether amb dades buides)
receive() external payable {
// Accepta qualsevol enviament d'Ether
}
function obtenirSaldo() public view returns (uint256) {
return address(this).balance;
}
function retirar() public nomesPropietari {
payable(propietari).transfer(address(this).balance);
}
modifier nomesPropietari() {
require(msg.sender == propietari, "No es propietari");
_;
}
}
4. HERÈNCIA I INTERFÍCIES¶
4.1 Herència Simple i Múltiple¶
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Contracte base
contract Animal {
string public nom;
uint256 public edat;
constructor(string memory _nom, uint256 _edat) {
nom = _nom;
edat = _edat;
}
function ferSo() public virtual returns (string memory) {
return "...";
}
function getInfo() public view returns (string memory) {
return string(abi.encodePacked(nom, " te ", uint2str(edat), " anys"));
}
function uint2str(uint256 _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
uint256 j = _i;
uint256 len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint256 k = len;
while (_i != 0) {
k = k - 1;
uint8 temp = (48 + uint8(_i - _i % 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
}
// Herència simple
contract Gos is Animal {
constructor(string memory _nom, uint256 _edat)
Animal(_nom, _edat)
{}
function ferSo() public pure override returns (string memory) {
return "Bord!";
}
function perseguirPilota() public pure returns (string memory) {
return "Perseguint la pilota...";
}
}
// Herència múltiple
contract Mascota is Animal {
address public propietari;
constructor(string memory _nom, uint256 _edat, address _propietari)
Animal(_nom, _edat)
{
propietari = _propietari;
}
}
contract GosMascota is Gos, Mascota {
constructor(
string memory _nom,
uint256 _edat,
address _propietari
)
Animal(_nom, _edat)
Gos(_nom, _edat)
Mascota(_nom, _edat, _propietari)
{}
}
4.2 Classes Abstractes¶
abstract contract FiguraGeometrica {
function calcularArea() public virtual pure returns (uint256);
function calcularPerimetre() public virtual pure returns (uint256);
function descripcio() public pure returns (string memory) {
return "Aquesta es una figura geometrica";
}
}
contract Rectangle is FiguraGeometrica {
uint256 public ample;
uint256 public alt;
constructor(uint256 _ample, uint256 _alt) {
ample = _ample;
alt = _alt;
}
function calcularArea() public pure override returns (uint256) {
return ample * alt;
}
function calcularPerimetre() public pure override returns (uint256) {
return 2 * (ample + alt);
}
}
contract Cercle is FiguraGeometrica {
uint256 public radi;
constructor(uint256 _radi) {
radi = _radi;
}
function calcularArea() public pure override returns (uint256) {
return (radi * radi * 314) / 100; // Pi aproximat
}
function calcularPerimetre() public pure override returns (uint256) {
return (2 * radi * 314) / 100;
}
}
4.3 Interfaces¶
// Interface defineix funcions sense implementar-les
interface IToken {
function transfer(address recipient, uint256 amount)
external
returns (bool);
function balanceOf(address account)
external
view
returns (uint256);
function totalSupply()
external
view
returns (uint256);
}
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
// Contracte que implementa una interface
contract TokenSimple is IERC20 {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
uint256 _totalSupply
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
totalSupply = _totalSupply;
balanceOf[msg.sender] = _totalSupply;
}
function transfer(address to, uint256 amount) external returns (bool) {
require(balanceOf[msg.sender] >= amount, "Saldo insuficient");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount)
external
returns (bool)
{
require(allowance[from][msg.sender] >= amount, "Allowance insuficient");
require(balanceOf[from] >= amount, "Saldo insuficient");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
5. EVENTS I ERROR HANDLING¶
5.1 Events¶
Els events permeten registrar informació a la blockchain que es pot consultar des de fora del contracte. Són essencials per a DApps.
contract EventsExemple {
// Declaració d'events
event Transferencia(
address indexed from,
address indexed to,
uint256 amount,
uint256 timestamp
);
event PropietariCanviat(
address indexed anticPropietari,
address indexed nouPropietari
);
event Log(string message);
address public propietari;
mapping(address => uint256) public saldos;
constructor() {
propietari = msg.sender;
emit Log("Contracte desplegat");
}
function transferir(address to, uint256 amount) public {
require(saldos[msg.sender] >= amount, "Saldo insuficient");
saldos[msg.sender] -= amount;
saldos[to] += amount;
// Emitir event amb dades indexades per facilitar la cerca
emit Transferencia(
msg.sender,
to,
amount,
block.timestamp
);
}
function canviarPropietari(address nouPropietari) public {
require(msg.sender == propietari, "No autoritzat");
address antic = propietari;
propietari = nouPropietari;
emit PropietariCanviat(antic, nouPropietari);
}
function dipositar() public payable {
saldos[msg.sender] += msg.value;
emit Transferencia(address(0), msg.sender, msg.value, block.timestamp);
}
}
Bones pràctiques amb events:
- Utilitzar indexed per als paràmetres que es volen filtrar (màxim 3)
- Incluir timestamps per a traçabilitat
- Emetre events per a totes les operacions importants
5.2 Error Handling¶
Solidity proporciona tres mecanismes principals per gestionar errors: require, revert, i assert.
contract GestioErrors {
address public propietari;
uint256 public comptador;
mapping(address => uint256) public saldos;
constructor() {
propietari = msg.sender;
}
// Custom errors (més eficients en gas des de Solidity 0.8.4)
error SaldoInsuficient(uint256 disponible, uint256 requerit);
error NoAutoritzat(address sol·licitant);
error ValorInvalid(uint256 valor);
error ComptadorMaximAssolit(uint256 maxim);
modifier nomesPropietari() {
if (msg.sender != propietari) {
revert NoAutoritzat(msg.sender);
}
_;
}
// REQUIRE: validar condicions d'entrada
function dipositar(uint256 quantitat) public {
require(quantitat > 0, "La quantitat ha de ser major que 0");
require(quantitat <= 1000, "Quantitat maxima excedida");
saldos[msg.sender] += quantitat;
}
// Custom error amb require-style
function retirar(uint256 quantitat) public {
if (saldos[msg.sender] < quantitat) {
revert SaldoInsuficient({
disponible: saldos[msg.sender],
requerit: quantitat
});
}
saldos[msg.sender] -= quantitat;
}
// ASSERT: verificar invariants (mai haurien de fallar)
function incrementar() public {
uint256 anticComptador = comptador;
comptador++;
// Verificar que no hi ha hagut overflow
assert(comptador > anticComptador);
}
// REVERT: cancel·lar amb missatge personalitzat
function operacioComplexa(uint256 valor) public nomesPropietari {
if (valor == 0) {
revert ValorInvalid(0);
}
if (comptador >= 100) {
revert ComptadorMaximAssolit(100);
}
comptador += valor;
}
// Try-catch amb crides externes
function cridarContracteExtern(address target, bytes calldata data)
public
returns (bool success, bytes memory result)
{
(success, result) = target.call(data);
if (!success) {
// Manejar error
revert("Crida externa fallida");
}
}
function obtenirSaldo() public view returns (uint256) {
return saldos[msg.sender];
}
}
Diferències clau:
| Mecanisme | Ús | Gas | Exemple |
|---|---|---|---|
require |
Validar inputs i condicions | Reemborsa gas restant | require(x > 0, "x ha de ser > 0") |
revert |
Cancel·lar explícitament | Reemborsa gas restant | revert ErrorPersonalitzat() |
assert |
Verificar invariants | No reemborsa gas | assert(x >= 0) |
Custom Errors (recomanat des de Solidity 0.8.4): - Més eficients en gas que els strings - Poden incloure paràmetres - Tipats i verificables en compilació
6. ENTORNS DE DESENVOLUPAMENT¶
6.1 Remix IDE¶
Remix és un IDE basat en web per desenvolupar smart contracts.
Configuració inicial:
- Accedir a https://remix.ethereum.org
- Crear un nou fitxer
.sol - Escriure el contracte
- Compilar amb la versió adequada del compilador
- Desplegar a JavaScript VM, Injected Provider (MetaMask), o RPC personalitzat
Exemple pràctic amb Remix:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Calculadora {
uint256 public ultimResultat;
event OperacioRealitzada(
string operacio,
uint256 operand1,
uint256 operand2,
uint256 resultat
);
function sumar(uint256 a, uint256 b) public returns (uint256) {
uint256 resultat = a + b;
ultimResultat = resultat;
emit OperacioRealitzada("suma", a, b, resultat);
return resultat;
}
function restar(uint256 a, uint256 b)
public
returns (uint256)
{
// require(a >= b, "Resultat negatiu no permès"); Fixau-vos que falla pels accents.
require(a >= b, unicode"Resultat negatiu no permès");
uint256 resultat = a - b;
ultimResultat = resultat;
emit OperacioRealitzada("resta", a, b, resultat);
return resultat;
}
function multiplicar(uint256 a, uint256 b) public returns (uint256) {
uint256 resultat = a * b;
ultimResultat = resultat;
emit OperacioRealitzada("multiplicacio", a, b, resultat);
return resultat;
}
function dividir(uint256 a, uint256 b) public returns (uint256) {
require(b > 0, "Divisio per zero");
uint256 resultat = a / b;
ultimResultat = resultat;
emit OperacioRealitzada("divisio", a, b, resultat);
return resultat;
}
function obtenirUltimResultat() public view returns (uint256) {
return ultimResultat;
}
function resetear() public {
ultimResultat = 0;
}
}
6.2 Hardhat¶
Hardhat és un entorn de desenvolupament professional per Ethereum.
Instal·lació i configuració:
# Inicialitzar projecte
mkdir projecte-blockchain
cd projecte-blockchain
npm init -y
# Instal·lar Hardhat
npm install --save-dev hardhat
# Inicialitzar Hardhat
npx hardhat init
# Seleccionar: "Create a JavaScript project"
# Instal·lar dependències addicionals
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install @openzeppelin/contracts
Estructura de projecte:
projecte-blockchain/
├── contracts/
│ └── Token.sol
├── scripts/
│ └── deploy.js
├── test/
│ └── Token.test.js
├── hardhat.config.js
└── package.json
hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config();
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.20",
networks: {
hardhat: {
chainId: 31337
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL || "https://sepolia.infura.io/v3/YOUR_KEY",
accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
chainId: 11155111
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
Contracte d'exemple:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract TokenERC20 {
string public name = "Token Exemple";
string public symbol = "TEX";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 _initialSupply) {
totalSupply = _initialSupply * (10 ** uint256(decimals));
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value, "Saldo insuficient");
require(_to != address(0), "Adreça invalida");
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
function approve(address _spender, uint256 _value) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value)
public
returns (bool success)
{
require(_value <= balanceOf[_from], "Saldo insuficient");
require(_value <= allowance[_from][msg.sender], "Allowance insuficient");
require(_to != address(0), "Adreça invalida");
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
}
Script de desplegament (scripts/deploy.js):
const hre = require("hardhat");
async function main() {
const initialSupply = 1000000; // 1 milió de tokens
console.log("Desplegant TokenERC20...");
const Token = await hre.ethers.getContractFactory("TokenERC20");
const token = await Token.deploy(initialSupply);
await token.waitForDeployment();
const address = await token.getAddress();
console.log(`Token desplegat a: ${address}`);
const name = await token.name();
const symbol = await token.symbol();
const totalSupply = await token.totalSupply();
console.log(`Nom: ${name}`);
console.log(`Símbol: ${symbol}`);
console.log(`Total Supply: ${totalSupply.toString()}`);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Tests (test/Token.test.js):
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("TokenERC20", function () {
let token;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("TokenERC20");
token = await Token.deploy(1000000);
await token.waitForDeployment();
});
describe("Desplegament", function () {
it("Ha de tenir el nom correcte", async function () {
expect(await token.name()).to.equal("Token Exemple");
});
it("Ha de tenir el símbol correcte", async function () {
expect(await token.symbol()).to.equal("TEX");
});
it("Ha de assignar el supply inicial al propietari", async function () {
expect(await token.balanceOf(owner.address)).to.equal(
ethers.parseUnits("1000000", 18)
);
});
});
describe("Transferències", function () {
it("Ha de permetre transferir tokens", async function () {
await token.transfer(addr1.address, ethers.parseUnits("100", 18));
expect(await token.balanceOf(addr1.address)).to.equal(
ethers.parseUnits("100", 18)
);
});
it("Ha de fallar amb saldo insuficient", async function () {
await expect(
token.transfer(addr1.address, ethers.parseUnits("1000001", 18))
).to.be.revertedWith("Saldo insuficient");
});
});
});
Comandes útils:
# Compilar
npx hardhat compile
# Executar tests
npx hardhat test
# Desplegar a xarxa local
npx hardhat run scripts/deploy.js
# Desplegar a Sepolia
npx hardhat run scripts/deploy.js --network sepolia
# Node local
npx hardhat node
# Console interactiu
npx hardhat console
7. DESPLEGAMENT EN TESTNET¶
7.1 Configuració de Sepolia Testnet¶
Sepolia és una de les testnets principals d'Ethereum.
Preparatius:
-
Obtenir Sepolia ETH:
- Faucet: https://sepoliafaucet.com
- Alchemy Faucet: https://www.alchemy.com/faucets/ethereum-sepolia
- Infura Faucet: https://www.infura.io/faucet/sepolia
-
Configurar MetaMask:
- Network: Sepolia Testnet
- RPC URL: https://sepolia.infura.io/v3/YOUR_KEY
- Chain ID: 11155111
- Symbol: ETH
-
Variables d'entorn (.env):
SEPOLIA_RPC_URL="https://sepolia.infura.io/v3/YOUR_INFURA_KEY" PRIVATE_KEY="your_private_key_here" ETHERSCAN_API_KEY="your_etherscan_api_key"
7.2 Desplegament amb Hardhat¶
hardhat.config.js complet:
require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config();
module.exports = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {
chainId: 31337,
forking: {
url: process.env.MAINNET_RPC_URL,
blockNumber: 18000000
}
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL || "https://sepolia.infura.io/v3/",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 11155111,
gas: 2100000,
gasPrice: 8000000000
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
Script de desplegament amb verificació:
const hre = require("hardhat");
async function main() {
console.log("Iniciant desplegament a Sepolia...");
// Obtenir el signatari
const [deployer] = await hre.ethers.getSigners();
console.log("Desplegant amb el compte:", deployer.address);
// Verificar saldo
const balance = await hre.ethers.provider.getBalance(deployer.address);
console.log("Saldo del compte:", hre.ethers.formatEther(balance), "ETH");
// Desplegar contracte
const Token = await hre.ethers.getContractFactory("TokenERC20");
const token = await Token.deploy(1000000);
console.log("Esperant confirmació...");
await token.waitForDeployment();
const address = await token.getAddress();
console.log("Token desplegat a:", address);
// Esperar unes blocs per seguretat
console.log("Esperant confirmacions...");
await new Promise(r => setTimeout(r, 20000));
// Verificar contracte a Etherscan
console.log("Verificant contracte a Etherscan...");
try {
await hre.run("verify:verify", {
address: address,
constructorArguments: [1000000],
});
console.log("Contracte verificat correctament!");
} catch (error) {
console.log("Error verificant:", error.message);
}
// Mostrar informació
console.log("\n=== Informació del Contracte ===");
console.log("Adreça:", address);
console.log("Nom:", await token.name());
console.log("Símbol:", await token.symbol());
console.log("Total Supply:", (await token.totalSupply()).toString());
console.log("Propietari:", deployer.address);
console.log("==============================\n");
console.log("Explorer: https://sepolia.etherscan.io/address/" + address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("Error:", error);
process.exit(1);
});
Desplegar i verificar:
# Compilar
npx hardhat compile
# Desplegar a Sepolia
npx hardhat run scripts/deploy.js --network sepolia
# Verificar manualment si falla
npx hardhat verify --network sepolia <CONTRACT_ADDRESS> 1000000
7.3 Interacció amb el Contracte Desplegat¶
Script d'interacció:
const hre = require("hardhat");
const { ethers } = require("hardhat");
async function main() {
const contractAddress = "0xYourContractAddress";
// Obtenir el contracte
const Token = await ethers.getContractFactory("TokenERC20");
const token = Token.attach(contractAddress);
console.log("Interactuant amb:", contractAddress);
// Obtenir informació
const name = await token.name();
const symbol = await token.symbol();
const decimals = await token.decimals();
const totalSupply = await token.totalSupply();
console.log("\n=== Informació del Token ===");
console.log("Nom:", name);
console.log("Símbol:", symbol);
console.log("Decimals:", decimals);
console.log("Total Supply:", ethers.formatUnits(totalSupply, decimals));
// Obtenir signataris
const [owner, addr1] = await ethers.getSigners();
// Consultar saldo
const ownerBalance = await token.balanceOf(owner.address);
console.log("\nSaldo del propietari:", ethers.formatUnits(ownerBalance, decimals));
// Transferir tokens
console.log("\nRealitzant transferència...");
const tx = await token.transfer(addr1.address, ethers.parseUnits("100", decimals));
console.log("TX Hash:", tx.hash);
await tx.wait();
console.log("Transferència confirmada!");
// Verificar nous saldos
const newOwnerBalance = await token.balanceOf(owner.address);
const addr1Balance = await token.balanceOf(addr1.address);
console.log("\n=== Nous Saldos ===");
console.log("Propietari:", ethers.formatUnits(newOwnerBalance, decimals));
console.log("Addr1:", ethers.formatUnits(addr1Balance, decimals));
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
8. BONES PRÀCTIQUES I SEGURETAT¶
8.1 Principis de Seguretat¶
Segons les millors pràctiques actuals, els smart contracts han de seguir aquests principis:
- Principi del Mínim Privilegi: Només donar els permisos necessaris
- Checks-Effects-Interactions: Validar, actualitzar estat, després interaccions externes
- Fail-Safe: Mecanismes de pausa i emergència
- Auditoria contínua: Tests exhaustius i revisió de codi
8.2 Vulnerabilitats Comunes¶
Reentrancy Attack¶
// ❌ VULNERABLE
contract VaultVulnerable {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint256 balance = balances[msg.sender];
require(balance > 0);
// Vulnerable: crida externa abans d'actualitzar estat
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
balances[msg.sender] = 0; // Massa tard!
}
}
// ✅ SEGUR
contract VaultSecure {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint256 balance = balances[msg.sender];
require(balance > 0);
// Checks-Effects-Interactions
balances[msg.sender] = 0; // Actualitzar estat PRIMER
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
}
// Alternativament, utilitzar reentrancy guard
bool private locked;
modifier noReentrant() {
require(!locked, "No reentrant calls");
locked = true;
_;
locked = false;
}
function withdrawSafe() public noReentrant {
uint256 balance = balances[msg.sender];
require(balance > 0);
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
balances[msg.sender] = 0;
}
}
Integer Overflow/Underflow¶
// ❌ VULNERABLE (Solidity < 0.8.0)
// ✅ A Solidity >= 0.8.0, els overflow checks són automàtics
pragma solidity ^0.8.20;
contract SafeMath {
// A Solidity 0.8+, els overflow/underflow reverteixen automàticament
function sumar(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // Reverteix si hi ha overflow
}
function restar(uint256 a, uint256 b) public pure returns (uint256) {
return a - b; // Reverteix si hi ha underflow
}
}
Access Control¶
// ✅ Utilitzar OpenZeppelin Ownable
import "@openzeppelin/contracts/access/Ownable.sol";
contract AccessControlCorrect is Ownable {
mapping(address => bool) public authorized;
constructor() Ownable(msg.sender) {
authorized[msg.sender] = true;
}
function addAuthorized(address user) public onlyOwner {
authorized[user] = true;
}
function removeAuthorized(address user) public onlyOwner {
authorized[user] = false;
}
function funcioProtegida() public view {
require(authorized[msg.sender], "No autoritzat");
// Lògica protegida
}
}
8.3 Checklist de Seguretat¶
Abans de desplegar a mainnet, verificar:
✅ Desenvolupament: - [ ] Tests cobrint >95% del codi - [ ] Tests de seguretat específics - [ ] Utilitzar versions estables de Solidity - [ ] Custom errors en lloc de strings - [ ] Events per a totes les operacions importants
✅ Seguretat: - [ ] Auditoria de codi externa - [ ] Utilitzar OpenZeppelin per a estàndards - [ ] Implementar circuit breaker (pause) - [ ] Límits en quantitats i freqüències - [ ] Validar totes les entrades - [ ] Protegir contra reentrancy - [ ] Verificar access control
✅ Optimització: - [ ] Gas optimization - [ ] Utilitzar tipus adequats (uint256 vs uint8) - [ ] Minimitzar emmagatzematge on-chain - [ ] Utilitzar view/pure quan sigui possible
✅ Documentació: - [ ] NatSpec comments - [ ] README complet - [ ] Instruccions de desplegament - [ ] Variables d'entorn documentades
9. EXERCICIS GUIATS¶
EXERCICI GUIAT 1: Sistema de Votació¶
Objectiu: Crear un contracte de votació descentralitzat amb les següents característiques: - Registrar candidats - Permetre votar (un vot per adreça) - Comptar vots - Determinar guanyador
Pas a pas:
Pas 1: Estructura del Contracte¶
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SistemaVotacio {
// Structs
struct Candidat {
uint256 id;
string nom;
uint256 vots;
bool actiu;
}
// Variables d'estat
address public propietari;
uint256 public totalCandidats;
uint256 public totalVots;
bool public votacioIniciada;
bool public votacioFinalitzada;
uint256 public dataFi;
mapping(uint256 => Candidat) public candidats;
mapping(address => bool) public haVotat;
mapping(address => uint256) public votRealitzat;
// Events
event CandidatRegistrat(uint256 indexed candidatId, string nom);
event VotEmes(address indexed votant, uint256 indexed candidatId);
event VotacioIniciada(uint256 dataInici, uint256 dataFi);
event VotacioFinalitzada(uint256 guanyadorId, string guanyadorNom);
// Errors personalitzats
error NoPropietari();
error VotacioNoIniciada();
error VotacioFinalitzada();
error JaHaVotat();
error CandidatInvalid();
error CandidatNoActiu();
// Modifier
modifier nomesPropietari() {
if (msg.sender != propietari) {
revert NoPropietari();
}
_;
}
modifier votacioActiva() {
if (!votacioIniciada) {
revert VotacioNoIniciada();
}
if (votacioFinalitzada) {
revert VotacioFinalitzada();
}
if (block.timestamp > dataFi) {
revert VotacioFinalitzada();
}
_;
}
constructor() {
propietari = msg.sender;
}
Pas 2: Funcions de Gestió¶
// Afegir candidat
function afegirCandidat(string memory nom)
public
nomesPropietari
{
require(bytes(nom).length > 0, "Nom buit");
totalCandidats++;
candidats[totalCandidats] = Candidat({
id: totalCandidats,
nom: nom,
vots: 0,
actiu: true
});
emit CandidatRegistrat(totalCandidats, nom);
}
// Iniciar votació
function iniciarVotacio(uint256 duradaDies)
public
nomesPropietari
{
require(!votacioIniciada, "Ja iniciada");
require(duradaDies > 0, "Durada invalida");
require(totalCandidats >= 2, "Minim 2 candidats");
votacioIniciada = true;
dataFi = block.timestamp + (duradaDies * 1 days);
emit VotacioIniciada(block.timestamp, dataFi);
}
// Finalitzar votació
function finalitzarVotacio() public nomesPropietari {
require(votacioIniciada, "No iniciada");
require(!votacioFinalitzada, "Ja finalitzada");
require(block.timestamp > dataFi, "Temps no exhaurit");
votacioFinalitzada = true;
// Trobar guanyador
uint256 guanyadorId = 1;
uint256 maxVots = 0;
for (uint256 i = 1; i <= totalCandidats; i++) {
if (candidats[i].actiu && candidats[i].vots > maxVots) {
maxVots = candidats[i].vots;
guanyadorId = i;
}
}
emit VotacioFinalitzada(guanyadorId, candidats[guanyadorId].nom);
}
Pas 3: Funcions de Vot¶
// Votar
function votar(uint256 candidatId) public votacioActiva {
if (haVotat[msg.sender]) {
revert JaHaVotat();
}
if (candidatId == 0 || candidatId > totalCandidats) {
revert CandidatInvalid();
}
if (!candidats[candidatId].actiu) {
revert CandidatNoActiu();
}
haVotat[msg.sender] = true;
votRealitzat[msg.sender] = candidatId;
candidats[candidatId].vots++;
totalVots++;
emit VotEmes(msg.sender, candidatId);
}
// Consultar candidat
function obtenirCandidat(uint256 id)
public
view
returns (
uint256 candidatId,
string memory nom,
uint256 vots,
bool actiu
)
{
require(id > 0 && id <= totalCandidats, "Candidat invalid");
Candidat memory c = candidats[id];
return (c.id, c.nom, c.vots, c.actiu);
}
// Obtenir guanyador
function obtenirGuanyador()
public
view
returns (uint256 id, string memory nom, uint256 vots)
{
require(votacioFinalitzada, "Votacio no finalitzada");
uint256 guanyadorId = 1;
uint256 maxVots = 0;
for (uint256 i = 1; i <= totalCandidats; i++) {
if (candidats[i].actiu && candidats[i].vots > maxVots) {
maxVots = candidats[i].vots;
guanyadorId = i;
}
}
return (
guanyadorId,
candidats[guanyadorId].nom,
candidats[guanyadorId].vots
);
}
// Estadístiques
function obtenirEstadistiques()
public
view
returns (
uint256 totalCandidats_,
uint256 totalVots_,
bool activa,
uint256 tempsRestant
)
{
activa = votacioIniciada && !votacioFinalitzada && block.timestamp <= dataFi;
if (activa) {
tempsRestant = dataFi - block.timestamp;
} else {
tempsRestant = 0;
}
return (totalCandidats, totalVots, activa, tempsRestant);
}
}
Pas 4: Script de Desplegament i Test¶
// scripts/deployVotacio.js
const hre = require("hardhat");
async function main() {
console.log("Desplegant Sistema de Votacio...");
const Votacio = await hre.ethers.getContractFactory("SistemaVotacio");
const votacio = await Votacio.deploy();
await votacio.waitForDeployment();
const address = await votacio.getAddress();
console.log("Contracte desplegat a:", address);
// Afegir candidats
console.log("\nAfegint candidats...");
await votacio.afegirCandidat("Candidat A");
await votacio.afegirCandidat("Candidat B");
await votacio.afegirCandidat("Candidat C");
console.log("Total candidats:", await votacio.totalCandidats());
// Iniciar votació (7 dies)
console.log("\nIniciant votacio...");
await votacio.iniciarVotacio(7);
const stats = await votacio.obtenirEstadistiques();
console.log("Votacio activa:", stats.activa);
console.log("\n=== Informacio ===");
console.log("Adreça contracte:", address);
console.log("Candidats:", stats.totalCandidats_.toString());
console.log("Durada: 7 dies");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Tests:
// test/Votacio.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-network-helpers");
describe("SistemaVotacio", function () {
let votacio;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const Votacio = await ethers.getContractFactory("SistemaVotacio");
votacio = await Votacio.deploy();
await votacio.waitForDeployment();
// Afegir candidats
await votacio.afegirCandidat("Candidat A");
await votacio.afegirCandidat("Candidat B");
});
it("Ha de registrar candidats correctament", async function () {
expect(await votacio.totalCandidats()).to.equal(2);
const candidat1 = await votacio.obtenirCandidat(1);
expect(candidat1.nom).to.equal("Candidat A");
});
it("Ha de permetre votar", async function () {
await votacio.iniciarVotacio(7);
await votacio.connect(addr1).votar(1);
await votacio.connect(addr2).votar(2);
const candidat1 = await votacio.obtenirCandidat(1);
expect(candidat1.vots).to.equal(1);
});
it("No ha de permetre votar dues vegades", async function () {
await votacio.iniciarVotacio(7);
await votacio.connect(addr1).votar(1);
await expect(votacio.connect(addr1).votar(2))
.to.be.revertedWithCustomError(votacio, "JaHaVotat");
});
});
EXERCICI GUIAT 2: Marketplace NFT Simple¶
Objectiu: Crear un marketplace bàsic per comprar/vendre NFTs
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTMarketplace is ERC721, Ownable {
struct Listing {
uint256 tokenId;
address seller;
uint256 price;
bool active;
}
uint256 public nextTokenId;
uint256 public nextListingId;
uint256 public commissionPercent = 25; // 2.5%
mapping(uint256 => Listing) public listings;
mapping(uint256 => uint256) public tokenToListing;
event NFTMinted(address indexed owner, uint256 tokenId);
event NFTListed(uint256 indexed listingId, uint256 tokenId, uint256 price);
event NFTSold(uint256 indexed listingId, uint256 tokenId, address buyer);
event NFTCancelled(uint256 indexed listingId);
error PriceZero();
error NotOwner();
error NotApproved();
error ListingInactive();
constructor() ERC721("MarketplaceNFT", "MNFT") Ownable(msg.sender) {
nextTokenId = 1;
nextListingId = 1;
}
// Mint NFT
function mintNFT(string memory uri) public returns (uint256) {
uint256 tokenId = nextTokenId++;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
emit NFTMinted(msg.sender, tokenId);
return tokenId;
}
// Listar NFT
function listNFT(uint256 tokenId, uint256 price) public {
if (price == 0) revert PriceZero();
if (ownerOf(tokenId) != msg.sender) revert NotOwner();
// Aprovar marketplace
approve(address(this), tokenId);
uint256 listingId = nextListingId++;
listings[listingId] = Listing({
tokenId: tokenId,
seller: msg.sender,
price: price,
active: true
});
tokenToListing[tokenId] = listingId;
emit NFTListed(listingId, tokenId, price);
}
// Comprar NFT
function buyNFT(uint256 listingId) public payable {
Listing storage listing = listings[listingId];
if (!listing.active) revert ListingInactive();
if (msg.value != listing.price) revert PriceZero();
address seller = listing.seller;
uint256 tokenId = listing.tokenId;
// Calcular comissió
uint256 commission = (listing.price * commissionPercent) / 1000;
uint256 sellerAmount = listing.price - commission;
// Desactivar listing
listing.active = false;
tokenToListing[tokenId] = 0;
// Transferir NFT
_safeTransferFrom(seller, msg.sender, tokenId);
// Pagar al seller
payable(seller).transfer(sellerAmount);
// Pagar comissió (si hi ha)
if (commission > 0) {
payable(owner()).transfer(commission);
}
emit NFTSold(listingId, tokenId, msg.sender);
}
// Cancel·lar listing
function cancelListing(uint256 listingId) public {
Listing storage listing = listings[listingId];
if (listing.seller != msg.sender) revert NotOwner();
if (!listing.active) revert ListingInactive();
listing.active = false;
tokenToListing[listing.tokenId] = 0;
emit NFTCancelled(listingId);
}
// Actualitzar comissió
function setCommission(uint256 newCommission) public onlyOwner {
require(newCommission <= 100, "Commission too high"); // Max 10%
commissionPercent = newCommission;
}
// Get listing info
function getListing(uint256 listingId)
public
view
returns (
uint256 tokenId,
address seller,
uint256 price,
bool active
)
{
Listing memory listing = listings[listingId];
return (listing.tokenId, listing.seller, listing.price, listing.active);
}
}
EXERCICI GUIAT 3: Sistema de Crowdfunding¶
Objectiu: Crear una plataforma de crowdfunding amb metes i terminis
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Crowdfunding {
struct Campaign {
address creator;
string title;
string description;
uint256 goal;
uint256 raised;
uint256 deadline;
bool claimed;
bool completed;
}
struct Donation {
address donor;
uint256 amount;
uint256 timestamp;
}
uint256 public campaignCount;
mapping(uint256 => Campaign) public campaigns;
mapping(uint256 => Donation[]) public donations;
mapping(uint256 => mapping(address => uint256)) public donorIndex;
event CampaignCreated(uint256 indexed campaignId, address creator, uint256 goal);
event Donated(uint256 indexed campaignId, address donor, uint256 amount);
event GoalReached(uint256 indexed campaignId);
event FundsClaimed(uint256 indexed campaignId, address creator);
event Refunded(uint256 indexed campaignId, address donor, uint256 amount);
error GoalReached();
error CampaignEnded();
error GoalNotReached();
error AlreadyClaimed();
function createCampaign(
string memory title,
string memory description,
uint256 goal,
uint256 durationDays
) public returns (uint256) {
require(bytes(title).length > 0, "Title empty");
require(goal > 0, "Goal must be > 0");
require(durationDays > 0, "Duration must be > 0");
campaignCount++;
campaigns[campaignCount] = Campaign({
creator: msg.sender,
title: title,
description: description,
goal: goal,
raised: 0,
deadline: block.timestamp + (durationDays * 1 days),
claimed: false,
completed: false
});
emit CampaignCreated(campaignCount, msg.sender, goal);
return campaignCount;
}
function donate(uint256 campaignId) public payable {
Campaign storage campaign = campaigns[campaignId];
require(msg.value > 0, "Amount must be > 0");
require(block.timestamp < campaign.deadline, "Campaign ended");
require(!campaign.completed, "Goal reached");
campaign.raised += msg.value;
donations[campaignId].push(Donation({
donor: msg.sender,
amount: msg.value,
timestamp: block.timestamp
}));
emit Donated(campaignId, msg.sender, msg.value);
if (campaign.raised >= campaign.goal) {
campaign.completed = true;
emit GoalReached(campaignId);
}
}
function claimFunds(uint256 campaignId) public {
Campaign storage campaign = campaigns[campaignId];
require(msg.sender == campaign.creator, "Not creator");
require(campaign.completed, "Goal not reached");
require(!campaign.claimed, "Already claimed");
require(block.timestamp >= campaign.deadline, "Not ended");
campaign.claimed = true;
(bool success, ) = payable(campaign.creator).call{value: campaign.raised}("");
require(success, "Transfer failed");
emit FundsClaimed(campaignId, campaign.creator);
}
function refund(uint256 campaignId) public {
Campaign storage campaign = campaigns[campaignId];
require(block.timestamp > campaign.deadline, "Not ended");
require(!campaign.completed, "Goal reached");
uint256 index = donorIndex[campaignId][msg.sender];
require(index > 0, "No donation found");
Donation memory donation = donations[campaignId][index - 1];
require(donation.donor == msg.sender, "Not donor");
// Mark as refunded by setting amount to 0
donations[campaignId][index - 1].amount = 0;
(bool success, ) = payable(msg.sender).call{value: donation.amount}("");
require(success, "Refund failed");
emit Refunded(campaignId, msg.sender, donation.amount);
}
function getCampaign(uint256 id)
public
view
returns (
address creator,
string memory title,
string memory description,
uint256 goal,
uint256 raised,
uint256 deadline,
bool completed
)
{
Campaign memory c = campaigns[id];
return (
c.creator,
c.title,
c.description,
c.goal,
c.raised,
c.deadline,
c.completed
);
}
function getDonationsCount(uint256 campaignId) public view returns (uint256) {
return donations[campaignId].length;
}
}
10. COL·LECCIÓ D'EXERCICIS PROPOSATS¶
Nivell Bàsic¶
Exercici 1: Wallet Multi-Signature Simple - Crear un contracte que requereixi múltiples signatures per executar transaccions - Funcionalitats: - Afegir/eliminar propietaris - Proposar transaccions - Confirmar transaccions (mínim 2 de 3 signatures) - Executar transaccions confirmades
Exercici 2: Token amb Staking - Crear un token ERC20 personalitzat - Implementar sistema de staking: - Dipositar tokens - Retirar tokens - Calcular recompenses basades en temps - Taxa de recompensa configurable
Exercici 3: Subhasta Cega - Implementar una subhasta on les ofertes són encriptades - Funcionalitats: - Període de pujades (ofertes encriptades) - Període de revelació - Determinar guanyador - Retornar perdedors
Nivell Intermedi¶
Exercici 4: DAO Simple - Crear una organització autònoma descentralitzada - Funcionalitats: - Membres amb tokens de governança - Propostes de votació - Període de votació - Execució automàtica si s'aprova - Quòrum mínim
Exercici 5: lending Protocol - Implementar un protocol de préstecs - Funcionalitats: - Dipositar col·lateral - Sol·licitar préstecs (max 50% del col·lateral) - Calcular interessos - Liquidar posicions insolvents - Retirar col·lateral
Exercici 6: NFT amb Royalties - Crear un marketplace NFT amb royalties - Funcionalitats: - Mint amb royalties configurables - Venda secundària amb royalties automàtics - Subhastes - Ofertes - Historial de transaccions
Nivell Avançat¶
Exercici 7: DEX (Decentralized Exchange) - Implementar un exchange descentralitzat tipus Uniswap - Funcionalitats: - Pools de liquiditat - Swap de tokens - Provisors de liquiditat (LP tokens) - Càlcul de preus (constant product formula) - Fees per als LPs
Exercici 8: Bridge Cross-Chain Simulat - Crear un sistema de bridge entre dues xarxes simulades - Funcionalitats: - Bloquejar tokens a la xarxa origen - Generar prova de bloqueig - Mint a la xarxa destí - Burn per retornar - Validadors/multisig
Exercici 9: Insurance Protocol - Protocol d'assegurances descentralitzat - Funcionalitats: - Crear pòlisses - Pagar primes - Claims amb oracle simulat - Pool de risc compartit - Governança per aprovar claims
Exercicis de Seguretat¶
Exercici 10: Auditoria de Codi - Donat un contracte amb vulnerabilitats intencionades: - Identificar totes les vulnerabilitats - Explicar com explotar-les - Proposar solucions - Implementar fixes - Escriure tests que demostrin les vulnerabilitats
Exercici 11: Reentrancy Guard Implementació - Implementar des de zero un sistema de protecció contra reentrancy - Crear tests que demostrin l'atac i la protecció - Comparar gas costs amb i sense protecció
Projectes Integradors¶
Exercici 12: Plataforma de Crowdfunding Completa - Combinar crowdfunding amb NFTs com a recompenses - Frontend bàsic amb Web3.js - Tests exhaustius - Desplegament a Sepolia - Documentació completa
Exercici 13: Supply Chain amb NFTs - Traçabilitat de productes amb NFTs - Múltiples actors (fabricant, transportista, venedor) - Actualitzacions d'estat - Verificació de autenticitat
Exercici 14: Prediction Market - Mercat de prediccions descentralitzat - Crear esdeveniments - Apostar per resultats - Oracle per resoldre esdeveniments - Distribució de premis
RECURSOS ADDICIONALS¶
Eines Recomanades¶
-
Entorns de Desenvolupament:
- Remix IDE: https://remix.ethereum.org
- Hardhat: https://hardhat.org
- Foundry: https://book.getfoundry.sh
-
Testnets i Faucets:
- Sepolia Faucet: https://sepoliafaucet.com
- Alchemy Faucet: https://www.alchemy.com/faucets
-
Biblioteques:
- OpenZeppelin: https://openzeppelin.com/contracts
- Chainlink: https://chain.link
-
Seguretat:
- Slither: Anàlisi estàtica
- Mythril: Security analysis
- Echidna: Fuzzing
-
Documentació:
- Solidity Docs: https://docs.soliditylang.org
- Ethereum.org: https://ethereum.org/developers
Bones Pràctiques Finals¶
Sempre abans de desplegar:
- ✅ Tests exhaustius (unitaris, integració, seguretat)
- ✅ Auditoria de codi (interna i externa si és possible)
- ✅ Desplegament a testnet i testing complet
- ✅ Verificació del codi a block explorer
- ✅ Documentació completa
- ✅ Pla de resposta a incidents
- ✅ Mechanismes de pausa/emergència
- ✅ Monitorització post-desplegament
Aquesta unitat didàctica proporciona les bases sòlides per desenvolupar smart contracts professionals, segurs i eficients. La pràctica constant i l'estudi de casos reals són essencials per dominar aquest camp en constant evolució.
