ERC-20 com distribuição de renda proporcional (pull-based). Pausable, Ownable, KYC allowlist. OpenZeppelin 5.x auditado.
ERC-20 Standard
Transferível e divisível
Pull Dividends
Sem loops caros em gas
KYC Allowlist
Compliance regulatório
Pausable
Emergências via owner
USDC ou ETH
Escolha no constructor
Multisig Ready
Gnosis Safe como owner
Compile via Hardhat ou Remix IDE. OpenZeppelin 5.x — Solidity 0.8.20.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
/**
* @title ZonyRealEstateToken
* @notice ERC-20 com distribuição de renda (aluguel) proporcional.
* @dev Pull-based dividends usando "accumulated dividends per share".
*/
contract ZonyRealEstateToken is ERC20, ERC20Pausable, Ownable {
using Address for address payable;
uint8 private immutable _decimals;
bool public transfersRestricted;
mapping(address => bool) public allowlist;
// ──── DIVIDENDS ────
uint256 public constant ACC_PRECISION = 1e18;
uint256 public accDividendPerShare;
mapping(address => uint256) public dividendDebt;
mapping(address => uint256) public pendingDividends;
address public payoutToken; // address(0) = ETH
event DividendsDeposited(uint256 amount);
event Claimed(address indexed user, uint256 amount);
event AllowlistUpdated(address indexed user, bool allowed);
event TransfersRestricted(bool enabled);
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_,
address payoutToken_,
address owner_
) ERC20(name_, symbol_) Ownable(owner_) {
_decimals = decimals_;
payoutToken = payoutToken_;
}
function decimals() public view override returns (uint8) { return _decimals; }
// ──── KYC / ALLOWLIST ────
function setAllowlist(address user, bool allowed) external onlyOwner {
allowlist[user] = allowed;
emit AllowlistUpdated(user, allowed);
}
function setTransfersRestricted(bool enabled) external onlyOwner {
transfersRestricted = enabled;
emit TransfersRestricted(enabled);
}
// ──── MINT ────
function mint(address to, uint256 amount) external onlyOwner {
_updateAccount(to);
_mint(to, amount);
dividendDebt[to] = (balanceOf(to) * accDividendPerShare) / ACC_PRECISION;
}
// ──── PAUSE ────
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
// ──── TRANSFER HOOK ────
function _update(address from, address to, uint256 value)
internal override(ERC20, ERC20Pausable)
{
if (transfersRestricted) {
if (from != address(0)) require(allowlist[from], "FROM_NOT_ALLOWED");
if (to != address(0)) require(allowlist[to], "TO_NOT_ALLOWED");
}
if (from != address(0)) _updateAccount(from);
if (to != address(0)) _updateAccount(to);
super._update(from, to, value);
if (from != address(0))
dividendDebt[from] = (balanceOf(from) * accDividendPerShare) / ACC_PRECISION;
if (to != address(0))
dividendDebt[to] = (balanceOf(to) * accDividendPerShare) / ACC_PRECISION;
}
// ──── RENT DEPOSIT ────
receive() external payable { _deposit(msg.value); }
function depositETH() external payable onlyOwner { _deposit(msg.value); }
function depositToken(uint256 amount) external onlyOwner {
require(payoutToken != address(0), "PAYOUT_IS_ETH");
(bool ok, bytes memory data) = payoutToken.call(
abi.encodeWithSignature(
"transferFrom(address,address,uint256)",
msg.sender, address(this), amount
)
);
require(ok && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FROM_FAIL");
_deposit(amount);
}
function _deposit(uint256 amount) internal {
require(totalSupply() > 0, "NO_SUPPLY");
accDividendPerShare += (amount * ACC_PRECISION) / totalSupply();
emit DividendsDeposited(amount);
}
// ──── CLAIM ────
function claim() external {
_updateAccount(msg.sender);
uint256 amount = pendingDividends[msg.sender];
require(amount > 0, "NOTHING_TO_CLAIM");
pendingDividends[msg.sender] = 0;
if (payoutToken == address(0)) {
payable(msg.sender).sendValue(amount);
} else {
(bool ok, bytes memory data) = payoutToken.call(
abi.encodeWithSignature("transfer(address,uint256)", msg.sender, amount)
);
require(ok && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FAIL");
}
emit Claimed(msg.sender, amount);
}
// ──── VIEW ────
function claimable(address user) external view returns (uint256) {
uint256 bal = balanceOf(user);
uint256 accumulated = (bal * accDividendPerShare) / ACC_PRECISION;
uint256 debt = dividendDebt[user];
uint256 pending = pendingDividends[user];
if (accumulated < debt) return pending;
return pending + (accumulated - debt);
}
// ──── INTERNAL ────
function _updateAccount(address user) internal {
uint256 bal = balanceOf(user);
if (bal == 0) { dividendDebt[user] = 0; return; }
uint256 accumulated = (bal * accDividendPerShare) / ACC_PRECISION;
uint256 debt = dividendDebt[user];
if (accumulated > debt) pendingDividends[user] += (accumulated - debt);
}
}📋 Referência de Funções