Salta el contingut
Logo esquerra

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

  1. Introducció als Smart Contracts
  2. Sintaxi Bàsica de Solidity
  3. Estructures de Dades i Tipus
  4. Funcions i Modificadors
  5. Herència i Interfaces
  6. Events i Error Handling
  7. Entorns de Desenvolupament
  8. Desplegament en Testnet
  9. Bones Pràctiques i Seguretat
  10. Exercicis Guiats
  11. 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:

  1. SPDX-License-Identifier: Identificador de llicència (obligatori des de Solidity 0.6.8)
  2. pragma solidity: Versió del compilador
  3. contract: Paraula clau per definir un contracte
  4. Variables d'estat: Emmagatzemades permanentment a la blockchain
  5. 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:

  1. Accedir a https://remix.ethereum.org
  2. Crear un nou fitxer .sol
  3. Escriure el contracte
  4. Compilar amb la versió adequada del compilador
  5. 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:

  1. Obtenir Sepolia ETH:

    • Faucet: https://sepoliafaucet.com
    • Alchemy Faucet: https://www.alchemy.com/faucets/ethereum-sepolia
    • Infura Faucet: https://www.infura.io/faucet/sepolia
  2. Configurar MetaMask:

    • Network: Sepolia Testnet
    • RPC URL: https://sepolia.infura.io/v3/YOUR_KEY
    • Chain ID: 11155111
    • Symbol: ETH
  3. 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:

  1. Principi del Mínim Privilegi: Només donar els permisos necessaris
  2. Checks-Effects-Interactions: Validar, actualitzar estat, després interaccions externes
  3. Fail-Safe: Mecanismes de pausa i emergència
  4. 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

  1. Entorns de Desenvolupament:

    • Remix IDE: https://remix.ethereum.org
    • Hardhat: https://hardhat.org
    • Foundry: https://book.getfoundry.sh
  2. Testnets i Faucets:

    • Sepolia Faucet: https://sepoliafaucet.com
    • Alchemy Faucet: https://www.alchemy.com/faucets
  3. Biblioteques:

    • OpenZeppelin: https://openzeppelin.com/contracts
    • Chainlink: https://chain.link
  4. Seguretat:

    • Slither: Anàlisi estàtica
    • Mythril: Security analysis
    • Echidna: Fuzzing
  5. Documentació:

    • Solidity Docs: https://docs.soliditylang.org
    • Ethereum.org: https://ethereum.org/developers

Bones Pràctiques Finals

Sempre abans de desplegar:

  1. ✅ Tests exhaustius (unitaris, integració, seguretat)
  2. ✅ Auditoria de codi (interna i externa si és possible)
  3. ✅ Desplegament a testnet i testing complet
  4. ✅ Verificació del codi a block explorer
  5. ✅ Documentació completa
  6. ✅ Pla de resposta a incidents
  7. ✅ Mechanismes de pausa/emergència
  8. ✅ 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ó.