Las Organizaciones Autónomas Descentralizadas (DAOs) son una piedra angular del ecosistema descentralizado. Las DAOs son organizaciones gobernadas por contratos inteligentes y cuyos procesos de toma de decisiones se ejecutan de manera descentralizada. En este artículo, profundizaremos en conceptos avanzados de Solidity para construir una DAO desde cero.
Crédito | Kevin Laminto
Visión General de la Estructura de la DAO
Nuestra DAO consistirá de los siguientes componentes:
- Un contrato inteligente que representa a la propia DAO.
- Un token que representa el poder de voto y la propiedad dentro de la DAO.
- Un mecanismo de propuesta para presentar y votar sobre propuestas.
- Un tesoro para gestionar fondos y ejecutar propuestas que han sido aprobadas.
Prerrequisitos
Para seguir este tutorial, debes tener un conocimiento básico de Solidity, Ethereum y el entorno de desarrollo de Truffle.
Paso 1: Creación del Token de la DAO
Primero, vamos a crear un nuevo token ERC20 para nuestra DAO. Este token se utilizará para representar el poder de voto dentro de la organización.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract DAOToken is ERC20 {
constructor(uint256 initialSupply) ERC20("DAO Token", "DAO") {
_mint(msg.sender, initialSupply);
}
}
En este contrato, estamos utilizando la biblioteca OpenZeppelin para crear un nuevo token ERC20. El constructor toma el suministro inicial como un parámetro y acuña los tokens para el implementador.
Paso 2: Construyendo el Contrato DAO
A continuación, construyamos el contrato DAO principal. Crea un nuevo archivo llamado DAO.sol
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./DAOToken.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract DAO is Ownable {
using EnumerableSet for EnumerableSet.AddressSet;
// El contrato del token de la DAO
DAOToken public daoToken;
// Cantidad mínima de tokens requeridos para crear una propuesta
uint256 public constant MIN_PROPOSAL_THRESHOLD = 1000 * 10**18;
// Cantidad mínima de tokens requeridos para votar en una propuesta
uint256 public constant MIN_VOTING_THRESHOLD = 100 * 10**18;
// Estructura de Propuesta
struct Proposal {
uint256 id;
address proposer;
string description;
uint256 amount;
address payable recipient;
uint256 startTime;
uint256 endTime;
uint256 yesVotes;
uint256 noVotes;
EnumerableSet.AddressSet voters;
bool executed;
}
// Lista de todas las propuestas
Proposal[] public proposals;
// Mapeo para verificar si una dirección tiene una propuesta activa
mapping(address => bool) public activeProposals;
// Evento para una nueva propuesta
event NewProposal(uint256 indexed proposalId, address indexed proposer, string description);
// Evento para la ejecución de una propuesta
event ProposalExecuted(uint256 indexed proposalId, address indexed proposer, address indexed recipient, uint256 amount);
constructor(DAOToken _daoToken) {
daoToken = _daoToken;
}
// Función para crear una nueva propuesta
function createProposal(string memory _description, uint256 _amount, address payable _recipient) external {
require(daoToken.balanceOf(msg.sender) >= MIN_PROPOSAL_THRESHOLD, "Tokens insuficientes para crear propuesta");
require(!activeProposals[msg.sender], "Ya tienes una propuesta activa");
Proposal memory newProposal = Proposal({
id: proposals.length,
proposer: msg.sender,
description: _description,
amount: _amount,
recipient: _recipient,
startTime: block.timestamp,
endTime: block.timestamp + 7 days,
yesVotes: 0,
noVotes: 0,
voters: new EnumerableSet.AddressSet(),
executed: false
});
proposals.push(newProposal);
activeProposals[msg.sender] = true;
emit NewProposal(newProposal.id, msg.sender, _description);
}
// Función para votar en una propuesta
function vote(uint256 _proposalId, bool _support) external {
require(daoToken.balanceOf(msg.sender) >= MIN_VOTING_THRESHOLD, "Tokens insuficientes para votar");
Proposal storage proposal = proposals[_proposalId];
require(block.timestamp >= proposal.startTime && block.timestamp <= proposal.endTime, "Periodo de votacion invalido");
require(!proposal.voters.contains(msg.sender), "Ya has votado en esta propuesta");
uint256 voterWeight = daoToken.balanceOf(msg.sender);
if (_support) {
proposal.yesVotes += voterWeight;
} else {
proposal.noVotes += voterWeight;
}
proposal.voters.add(msg.sender);
}
// Función para ejecutar una propuesta
function executeProposal(uint256 _proposalId) external {
Proposal storage proposal = proposals[_proposalId];
require(!proposal.executed, "La propuesta ya ha sido ejecutada");
require(block.timestamp > proposal.endTime, "El periodo de votacion aun esta en curso");
require(proposal.yesVotes > proposal.noVotes, "La propuesta no ha alcanzado el apoyo de la mayoria");
proposal.recipient.transfer(proposal.amount);
proposal.executed = true;
activeProposals[proposal.proposer] = false;
emit ProposalExecuted(_proposalId, proposal.proposer, proposal.recipient, proposal.amount);
}
// Función para retirar fondos de la DAO
function withdraw(uint256 _amount) external onlyOwner {
payable(owner()).transfer(_amount);
}
// Función de fallback para aceptar Ether
receive() external payable {}
}
En este contrato, definimos la estructura principal de la DAO:
- Se importa el contrato del token de la DAO y se guarda como una variable.
- Se define una estructura de propuesta con campos necesarios como el id de la propuesta, el proponente, la descripción, la cantidad, el destinatario y los detalles de la votación.
- Un array almacena todas las propuestas, y un mapeo lleva un registro de las propuestas activas.
- Se crean funciones para manejar la creación de propuestas, la votación y la ejecución.
Paso 3: Desplegar y Probar la DAO
Ahora que hemos construido el contrato de la DAO, vamos a desplegarlo y probar su funcionalidad. Primero, creemos un archivo de migración para nuestros contratos.
const DAOToken = artifacts.require("DAOToken");
const DAO = artifacts.require("DAO");
module.exports = async function (deployer) {
// Despliega el contrato DAOToken con un suministro inicial de 1,000,000 de tokens
await deployer.deploy(DAOToken, "1000000" + "0".repeat(18));
const daoTokenInstance = await DAOToken.deployed();
// Despliega el contrato DAO con una referencia al contrato DAOToken
await deployer.deploy(DAO, daoTokenInstance.address);
const daoInstance = await DAO.deployed();
};
Ahora agregamos algunas pruebas para asegurarnos de que nuestro contrato DAO funcione como se esperaba.
const { assert } = require("chai");
const { expectRevert, time } = require("@openzeppelin/test-helpers");
const DAOToken = artifacts.require("DAOToken");
const DAO = artifacts.require("DAO");
contract("DAO", ([deployer, user1, user2, recipient]) => {
beforeEach(async () => {
// Crear nuevos tokens y contratos DAO
this.daoToken = await DAOToken.new("1000000" + "0".repeat(18), { from: deployer });
this.dao = await DAO.new(this.daoToken.address, { from: deployer });
});
it("debería crear una propuesta y votar en ella", async () => {
// Transferir tokens a user1
await this.daoToken.transfer(user1, "1100" + "0".repeat(18), { from: deployer });
// User1 crea una propuesta
await this.dao.createProposal("Financiar el proyecto X", "100" + "0".repeat(18), recipient, {
from: user1,
});
// Verificar los detalles de la propuesta
const proposal = await this.dao.proposals(0);
assert.equal(proposal.id, "0");
assert.equal(proposal.proposer, user1);
assert.equal(proposal.description, "Financiar el proyecto X");
assert.equal(proposal.amount, "100" + "0".repeat(18));
assert.equal(proposal.recipient, recipient);
// User1 vota en la propuesta
await this.dao.vote(0, true, { from: user1 });
// Verificar los resultados de la votación
assert.equal((await this.dao.proposals(0)).yesVotes, "1100" + "0".repeat(18));
assert.equal((await this.dao.proposals(0)).noVotes, "0");
// Avanzar el tiempo hasta después del período de votación
await time.increase(time.duration.days(8));
// Ejecutar la propuesta
await this.dao.executeProposal(0, { from: user1 });
// Verificar que la propuesta ha sido ejecutada
assert.equal((await this.dao.proposals(0)).executed, true);
});
it("no debería permitir que un usuario con tokens insuficientes cree una propuesta", async () => {
// Intentar crear una propuesta con tokens insuficientes
await expectRevert(
this.dao.createProposal("Financiar el proyecto X", "100" + "0".repeat(18), recipient, { from: user2 }),
"Tokens insuficientes para crear la propuesta"
);
});
// Añadir más pruebas según sea necesario
});
En este archivo de prueba, cubrimos dos pruebas clave:
Crear una propuesta y votar sobre ella.
Asegurar que un usuario con tokens insuficientes no pueda crear una propuesta.
Paso 4: Interactuando con la DAO
Puedes usar una interfaz web para interactuar con tu DAO desplegada.
Los componentes clave de la interfaz web podrían incluir:
- Un tablero que muestra el saldo de tokens de la DAO y la lista de propuestas.
- Un formulario para crear una nueva propuesta.
- Una interfaz de votación para votar sobre propuestas existentes.
- Un botón para ejecutar propuestas aprobadas.
Conclusión
El potencial de la web 3.0 y las aplicaciones descentralizadas es vasto y ofrece emocionantes oportunidades para que los desarrolladores den forma al futuro de internet. Al aprovechar el poder de la tecnología blockchain, podemos crear aplicaciones más seguras, transparentes y resilientes que empoderan a los usuarios y promueven la descentralización.
Como desarrollador, tienes la oportunidad de contribuir al crecimiento del ecosistema descentralizado y construir soluciones innovadoras para abordar problemas del mundo real. Te animo a explorar las numerosas posibilidades que ofrecen las tecnologías web3, desde finanzas descentralizadas (DeFi) y tokens no fungibles (NFTs) hasta almacenamiento descentralizado y gestión de identidad.
Artículo original publicado por Faucet Consumer. Traducción de Paulinho Giovannini.
Discussion (0)