Witaj, Peter. Skoro kliknąłeś w ukryty link, oznacza to, że nie wystarczają Ci hand-wavy opisy tokenomiki dla zwykłych użytkowników. Wolisz zobaczyć, jak to będzie wyglądać pod maską. Zgadzam się z Tobą w pełni — czytając zwykły tekst i tak trzeba rozumieć, jak działa kod, więc lepiej od razu pokazać draft.
Poniżej znajdziesz roboczy zarys kontraktu PapaCoin (PPC) w Solidity z technicznymi komentarzami. Główny cel jest jeden: brak funkcji mint() po fazie generacji (Genesis).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title PapaCoin (PPC)
* @dev Peter, zwróć uwagę, że dziedziczymy tylko po ERC20 i Ownable.
* Nie korzystamy z ERC20Mintable, ponieważ całkowita podaż jest
* wybijana tylko raz, w momencie publikacji kontraktu (lub poprzez
* dokładnie zaplanowany i zamknięty kontrakt dystrybucyjny).
*/
contract PapaCoin is ERC20, Ownable {
// 21,370,000,000 PPC (21.37 mld) to całkowity, niezmienny hard cap
uint256 public constant MAX_SUPPLY = 21370000000 * 10 ** 18;
/**
* @dev Konstruktor wywołany tylko raz podczas deploymentu.
* Mincimy od od razu całą pulę do portfeli lub kontraktów skarbcowych
* z przeznaczeniem na snapshot (Airdrop) + pule płynności (DEX).
*/
constructor() ERC20("PapaCoin", "PPC") Ownable(msg.sender) {
// Wszystkie tokeny trafiają natychmiast do deployera (lub do
// smart contractu zajmującego się dystrybucją airdropów/snapshotu).
_mint(msg.sender, MAX_SUPPLY);
}
}Peter, kluczowe jest to, czego w tym kontrakcie nie ma. Często projekty zostawiają furtkę dla właściciela w taki sposób:
// TAKIEGO KODU U NAS NIE ZNAJDZIESZ:
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}Nasz kontrakt korzysta ze standardowej implementacji z OpenZeppelin. Ponieważ w naszym kontrakcie PapaCoin nie zdefiniowaliśmy żadnej publicznej ani zewnętrznej funkcji wywołującej wewnętrzne _mint(), EVM fizycznie uniemożliwia stworzenie nowych tokenów. Nikt, nawet adres widniejący jako owner(), nie ma technicznej możliwości zwiększenia puli. totalSupply() z OpenZeppelin pozostanie niezmienne (lub może jedynie maleć, jeśli zaimplementujemy burning).
Skoro wszystkie tokeny zostały wybite w momencie powstania kontraktu, jak trafią do użytkowników z bazy danych PapaHunt? Przygotujemy oddzielny kontrakt typu Merkle Airdrop, który będzie przetrzymywał zablokowaną pulę przeznaczoną dla łowców.
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
contract PapaClaim {
IERC20 public immutable papaCoin;
bytes32 public immutable merkleRoot; // Hash zawierający stan sald offline
mapping(address => bool) public hasClaimed;
constructor(address _papaCoin, bytes32 _merkleRoot) {
papaCoin = IERC20(_papaCoin);
merkleRoot = _merkleRoot;
}
/**
* @dev Każdy gracz z PapaHunt weryfikuje swoje historyczne
* saldo za pomocą kryptograficznego dowodu Merkle'a.
*/
function claim(uint256 amount, bytes32[] calldata merkleProof) external {
require(!hasClaimed[msg.sender], "Juz odebrano!");
// Weryfikacja: tylko zapisane w bazie off-chain salda przejdą ten test
bytes32 node = keccak256(abi.encodePacked(msg.sender, amount));
require(MerkleProof.verify(merkleProof, merkleRoot, node), "Bledny dowod!");
hasClaimed[msg.sender] = true;
// Transfer wygenerowanych na samym poczatku tokenów
require(papaCoin.transfer(msg.sender, amount), "Transfer failed");
}
}Mam nadzieję, Peter, że ten kod wyjaśnia całą architekturę i zamyka temat „skąd się biorą monety”. Cały proces opiera się o sprawdzoną kryptografię (Merkle Trees) i publiczne niezmienne kontrakty. Użytkownik ufa matematyce, a nie adminowi.