Skip to content

Commit 0964c0f

Browse files
feat(blockchain): Implementa el contrato de créditos de biodiversidad
Introduce el contrato ERC-1155 para BiodiversityCredits con una arquitectura de verificador por proyecto. Incluye un conjunto completo de tests y refactoriza los contratos existentes a ERC-1155 para alinearse con los requisitos del frontend.
1 parent d00c539 commit 0964c0f

File tree

2 files changed

+298
-0
lines changed

2 files changed

+298
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.28;
3+
4+
/**
5+
* @title BiodiversityCredits_LatinHack_LatinHack
6+
* @author Equipo de desarrollo de E-co.lab
7+
* @notice Contrato diseñado para soportar múltiples proyectos, cada uno con su propio verificador.
8+
* Esta arquitectura permite alinear la experiencia del verificador (ej. biólogo marino, experto forestal)
9+
* con la naturaleza específica de cada proyecto de biodiversidad.
10+
*/
11+
contract BiodiversityCredits_LatinHack_LatinHack {
12+
13+
// --- Variables de Estado ---
14+
15+
address public admin;
16+
17+
enum Status { Pending, Approved, Rejected }
18+
19+
struct Project {
20+
address developer;
21+
address verifier; // El verificador específico para este proyecto
22+
string projectURI;
23+
bytes32 methodologyHash;
24+
Status status;
25+
}
26+
27+
struct CreditBatch {
28+
uint256 projectId; // Vínculo al proyecto que originó este lote de créditos
29+
uint256 timestamp;
30+
uint256 totalMinted;
31+
}
32+
33+
mapping(uint256 => Project) public projects;
34+
uint256 private _nextProjectId;
35+
36+
mapping(uint256 => CreditBatch) public creditBatchDetails;
37+
uint256 private _nextCreditId;
38+
39+
// Estructuras de datos ERC-1155
40+
mapping(uint256 => mapping(address => uint256)) private _balances;
41+
mapping(address => mapping(address => bool)) private _operatorApprovals;
42+
43+
// --- Eventos ---
44+
45+
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 amount);
46+
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
47+
event ProjectRegistered(uint256 indexed projectId, address indexed developer, address indexed verifier);
48+
event ProjectStatusUpdated(uint256 indexed projectId, Status newStatus);
49+
event CreditBatchCertified(uint256 indexed creditId, uint256 indexed projectId, address indexed verifier, address creditOwner, uint256 amount);
50+
event CreditsRetired(uint256 indexed creditId, address indexed retiredBy, uint256 amount);
51+
52+
// --- Modificadores ---
53+
54+
modifier onlyAdmin() {
55+
require(msg.sender == admin, "Llamador no es el admin");
56+
_;
57+
}
58+
59+
// --- Constructor ---
60+
61+
/**
62+
* @dev El constructor establece los roles iniciales de administración y verificación.
63+
* @param _initialAdmin La dirección que gestionará el contrato.
64+
*/
65+
66+
constructor(address _initialAdmin) {
67+
admin = _initialAdmin;
68+
}
69+
70+
// --- Gestión de Proyectos ---
71+
72+
/**
73+
* @notice INDICACIÓN CRÍTICA DE DISEÑO:
74+
* Para mitigar la centralización y el riesgo de un único punto de fallo, la dirección
75+
* '_verifier' DEBERÍA ser una cartera multi-firma (multisig) nativa de Polkadot (Substrate).
76+
* Esto asegura que la acuñación de nuevos créditos requiera el consenso de múltiples partes
77+
* independientes, aumentando la confianza y la robustez del sistema.
78+
*/
79+
80+
function registerProject(address _verifier, string memory _projectURI, bytes32 _methodologyHash) external returns (uint256) {
81+
require(_verifier != address(0), "El verificador no puede ser la direccion cero");
82+
uint256 projectId = _nextProjectId++;
83+
projects[projectId] = Project({
84+
developer: msg.sender,
85+
verifier: _verifier,
86+
projectURI: _projectURI,
87+
methodologyHash: _methodologyHash,
88+
status: Status.Pending
89+
});
90+
emit ProjectRegistered(projectId, msg.sender, _verifier);
91+
return projectId;
92+
}
93+
94+
function updateProjectStatus(uint256 _projectId, Status _newStatus) external onlyAdmin {
95+
require(projects[_projectId].developer != address(0), "El proyecto no existe");
96+
projects[_projectId].status = _newStatus;
97+
emit ProjectStatusUpdated(_projectId, _newStatus);
98+
}
99+
100+
// --- Lógica Principal del Negocio ---
101+
102+
function certifyAndMintBatch(uint256 _projectId, address _creditOwner, uint256 _amount) external {
103+
Project storage project = projects[_projectId];
104+
require(project.status == Status.Approved, "El proyecto no esta aprobado");
105+
require(msg.sender == project.verifier, "Llamador no es el verificador de este proyecto");
106+
107+
uint256 creditId = _nextCreditId++;
108+
creditBatchDetails[creditId] = CreditBatch({
109+
projectId: _projectId,
110+
timestamp: block.timestamp,
111+
totalMinted: _amount
112+
});
113+
114+
_mint(_creditOwner, creditId, _amount);
115+
emit CreditBatchCertified(creditId, _projectId, msg.sender, _creditOwner, _amount);
116+
}
117+
118+
function retireCredits(uint256 _creditId, uint256 _amount) external {
119+
burn(msg.sender, _creditId, _amount);
120+
emit CreditsRetired(_creditId, msg.sender, _amount);
121+
}
122+
123+
// --- Implementación Mínima de ERC-1155 ---
124+
125+
function balanceOf(address _account, uint256 _id) public view returns (uint256) {
126+
return _balances[_id][_account];
127+
}
128+
129+
function setApprovalForAll(address _operator, bool _approved) public {
130+
_operatorApprovals[msg.sender][_operator] = _approved;
131+
emit ApprovalForAll(msg.sender, _operator, _approved);
132+
}
133+
134+
function isApprovedForAll(address _account, address _operator) public view returns (bool) {
135+
return _operatorApprovals[_account][_operator];
136+
}
137+
138+
function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data) public {
139+
require(_from == msg.sender || isApprovedForAll(_from, msg.sender), "ERC1155: no autorizado para transferir");
140+
require(_to != address(0), "ERC1155: no se puede transferir a la direccion cero");
141+
require(_balances[_id][_from] >= _amount, "ERC1155: balance insuficiente");
142+
_balances[_id][_from] -= _amount;
143+
_balances[_id][_to] += _amount;
144+
emit TransferSingle(msg.sender, _from, _to, _id, _amount);
145+
}
146+
147+
function burn(address _from, uint256 _id, uint256 _amount) public {
148+
require(_from == msg.sender || isApprovedForAll(_from, msg.sender), "ERC1155: no autorizado para quemar");
149+
require(_balances[_id][_from] >= _amount, "ERC1155: balance insuficiente para quemar");
150+
_balances[_id][_from] -= _amount;
151+
emit TransferSingle(msg.sender, _from, address(0), _id, _amount);
152+
}
153+
154+
function _mint(address _to, uint256 _id, uint256 _amount) private {
155+
require(_to != address(0), "ERC1155: no se puede acunar a la direccion cero");
156+
_balances[_id][_to] += _amount;
157+
emit TransferSingle(msg.sender, address(0), _to, _id, _amount);
158+
}
159+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { expect } from "chai";
2+
import { ethers } from "hardhat";
3+
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
4+
import { BiodiversityCredits_LatinHack } from "../typechain-types";
5+
6+
// Describe el conjunto de pruebas para el contrato BiodiversityCredits_LatinHack con verificador por proyecto
7+
describe("BiodiversityCredits_LatinHack", function () {
8+
9+
let contract: BiodiversityCredits_LatinHack;
10+
let admin: HardhatEthersSigner;
11+
let developer: HardhatEthersSigner;
12+
let verifier1: HardhatEthersSigner;
13+
let verifier2: HardhatEthersSigner; // Un segundo verificador para probar la lógica de permisos
14+
let creditOwner: HardhatEthersSigner;
15+
let anotherUser: HardhatEthersSigner;
16+
17+
const projectURI = "ipfs://some-project-metadata";
18+
const methodologyHash = ethers.encodeBytes32String("METODOLOGIA_V1");
19+
const PROJECT_ID_0 = 0;
20+
const STATUS_PENDING = 0;
21+
const STATUS_APPROVED = 1;
22+
const STATUS_REJECTED = 2;
23+
24+
beforeEach(async function () {
25+
[admin, developer, verifier1, verifier2, creditOwner, anotherUser] = await ethers.getSigners();
26+
const Factory = await ethers.getContractFactory("BiodiversityCredits_LatinHack");
27+
contract = await Factory.deploy(admin.address);
28+
await contract.waitForDeployment();
29+
});
30+
31+
describe("Despliegue y Roles Iniciales", function () {
32+
it("Debería establecer el admin correctamente en el despliegue", async function () {
33+
expect(await contract.admin()).to.equal(admin.address);
34+
});
35+
});
36+
37+
describe("Ciclo de Vida del Proyecto", function () {
38+
it("Debería permitir a un desarrollador registrar un nuevo proyecto", async function () {
39+
await expect(contract.connect(developer).registerProject(verifier1.address, projectURI, methodologyHash))
40+
.to.emit(contract, "ProjectRegistered")
41+
.withArgs(PROJECT_ID_0, developer.address, verifier1.address);
42+
43+
const project = await contract.projects(PROJECT_ID_0);
44+
expect(project.developer).to.equal(developer.address);
45+
expect(project.verifier).to.equal(verifier1.address);
46+
expect(project.status).to.equal(STATUS_PENDING);
47+
});
48+
49+
it("Debería permitir al admin aprobar un proyecto", async function () {
50+
await contract.connect(developer).registerProject(verifier1.address, projectURI, methodologyHash);
51+
await expect(contract.connect(admin).updateProjectStatus(PROJECT_ID_0, STATUS_APPROVED))
52+
.to.emit(contract, "ProjectStatusUpdated")
53+
.withArgs(PROJECT_ID_0, STATUS_APPROVED);
54+
55+
const project = await contract.projects(PROJECT_ID_0);
56+
expect(project.status).to.equal(STATUS_APPROVED);
57+
});
58+
59+
it("Debería impedir que un no-admin actualice el estado de un proyecto", async function () {
60+
await contract.connect(developer).registerProject(verifier1.address, projectURI, methodologyHash);
61+
await expect(
62+
contract.connect(developer).updateProjectStatus(PROJECT_ID_0, STATUS_APPROVED)
63+
).to.be.revertedWith("Llamador no es el admin");
64+
});
65+
});
66+
67+
describe("certifyAndMintBatch (Acuñación)", function () {
68+
beforeEach(async function () {
69+
// Preparamos un proyecto aprobado para los tests de acuñación
70+
await contract.connect(developer).registerProject(verifier1.address, projectURI, methodologyHash);
71+
await contract.connect(admin).updateProjectStatus(PROJECT_ID_0, STATUS_APPROVED);
72+
});
73+
74+
it("Debería permitir al verificador del proyecto acuñar créditos", async function () {
75+
const amount = 100;
76+
const FIRST_CREDIT_ID = 0;
77+
await expect(contract.connect(verifier1).certifyAndMintBatch(PROJECT_ID_0, creditOwner.address, amount))
78+
.to.emit(contract, "CreditBatchCertified");
79+
80+
expect(await contract.balanceOf(creditOwner.address, FIRST_CREDIT_ID)).to.equal(amount);
81+
const batchDetails = await contract.creditBatchDetails(FIRST_CREDIT_ID);
82+
expect(batchDetails.projectId).to.equal(PROJECT_ID_0);
83+
});
84+
85+
it("Debería impedir que un verificador de OTRO proyecto acuñe créditos", async function () {
86+
const amount = 100;
87+
await expect(
88+
contract.connect(verifier2).certifyAndMintBatch(PROJECT_ID_0, creditOwner.address, amount)
89+
).to.be.revertedWith("Llamador no es el verificador de este proyecto");
90+
});
91+
92+
it("Debería impedir la acuñación para un proyecto que no está aprobado", async function () {
93+
const NEW_PROJECT_ID = 1;
94+
await contract.connect(developer).registerProject(verifier1.address, "uri2", methodologyHash); // Este proyecto está 'Pending'
95+
const amount = 100;
96+
await expect(
97+
contract.connect(verifier1).certifyAndMintBatch(NEW_PROJECT_ID, creditOwner.address, amount)
98+
).to.be.revertedWith("El proyecto no esta aprobado");
99+
});
100+
});
101+
102+
describe("Funcionalidad ERC-1155 (Transferencia, Quema, Aprobación)", function () {
103+
const amountMinted = 1000;
104+
const FIRST_CREDIT_ID = 0;
105+
106+
beforeEach(async function () {
107+
// Flujo completo para tener créditos con los que trabajar
108+
await contract.connect(developer).registerProject(verifier1.address, projectURI, methodologyHash);
109+
await contract.connect(admin).updateProjectStatus(PROJECT_ID_0, STATUS_APPROVED);
110+
await contract.connect(verifier1).certifyAndMintBatch(PROJECT_ID_0, creditOwner.address, amountMinted);
111+
});
112+
113+
it("Debería permitir al dueño de los créditos transferir una porción", async function () {
114+
const transferAmount = 300;
115+
await contract.connect(creditOwner).safeTransferFrom(creditOwner.address, anotherUser.address, FIRST_CREDIT_ID, transferAmount, "0x");
116+
117+
const ownerBalance = amountMinted - transferAmount;
118+
expect(await contract.balanceOf(creditOwner.address, FIRST_CREDIT_ID)).to.equal(ownerBalance);
119+
expect(await contract.balanceOf(anotherUser.address, FIRST_CREDIT_ID)).to.equal(transferAmount);
120+
});
121+
122+
it("Debería permitir a un operador aprobado transferir créditos", async function () {
123+
const transferAmount = 500;
124+
await contract.connect(creditOwner).setApprovalForAll(admin.address, true);
125+
await contract.connect(admin).safeTransferFrom(creditOwner.address, anotherUser.address, FIRST_CREDIT_ID, transferAmount, "0x");
126+
127+
const ownerBalance = amountMinted - transferAmount;
128+
expect(await contract.balanceOf(creditOwner.address, FIRST_CREDIT_ID)).to.equal(ownerBalance);
129+
});
130+
131+
it("Debería permitir al dueño de los créditos retirarlos (quemarlos)", async function () {
132+
const retireAmount = 250;
133+
await contract.connect(creditOwner).retireCredits(FIRST_CREDIT_ID, retireAmount);
134+
135+
const ownerBalance = amountMinted - retireAmount;
136+
expect(await contract.balanceOf(creditOwner.address, FIRST_CREDIT_ID)).to.equal(ownerBalance);
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)