Skip to content

Instantly share code, notes, and snippets.

@bekovrafik
Created February 6, 2026 05:37
Show Gist options
  • Select an option

  • Save bekovrafik/d58222453759312c05791a9f4e3d5392 to your computer and use it in GitHub Desktop.

Select an option

Save bekovrafik/d58222453759312c05791a9f4e3d5392 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.20+commit.a1b79de6.js&optimize=undefined&runs=200&gist=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title PropertyToken
* @dev Represents a fractionalized property with Escrow, Refunds, and Dividend capabilities.
* FORCED TO 6 DECIMALS to match USDC.
*/
contract PropertyToken is ERC20, Ownable, ReentrancyGuard {
enum PropertyTier { Rental, Growth, Stay }
enum Status { FUNDRAISING, ACTIVE, REFUND_MODE }
PropertyTier public tier;
Status public currentStatus;
uint256 public pricePerToken;
uint256 public targetRaiseAmount;
uint256 public totalRaised;
string public legalDocHash;
IERC20 public paymentToken;
// --- DIVIDEND TRACKING ---
uint256 public totalDividendsDistributed;
mapping(address => uint256) public lastClaimedDividend;
// We track "Dividends Per Token" (DPT) to allow fair claims
uint256 public dividendsPerTokenCumulative;
uint256 public constant PRECISION = 1e18; // Precision for math
event Invested(address indexed investor, uint256 tokenAmount, uint256 cost, string legalDocHashSigned);
event FundraisingFinalized(uint256 totalRaised, uint256 timestamp);
event RefundEnabled(uint256 timestamp);
event Refunded(address indexed investor, uint256 tokenAmount, uint256 refundAmount);
// --- NEW EVENTS ---
event DividendDeposited(uint256 amount, uint256 timestamp);
event DividendClaimed(address indexed investor, uint256 amount);
event DocHashUpdated(string oldHash, string newHash);
constructor(
string memory name,
string memory symbol,
uint256 _targetSupply,
uint256 _price,
PropertyTier _tier,
string memory _docHash,
address _paymentToken,
address admin
) ERC20(name, symbol) Ownable(admin) {
tier = _tier;
pricePerToken = _price;
legalDocHash = _docHash;
paymentToken = IERC20(_paymentToken);
targetRaiseAmount = _targetSupply * _price;
currentStatus = Status.FUNDRAISING;
}
/**
* @dev Overrides the default 18 decimals to match USDC (6 decimals).
*/
function decimals() public view virtual override returns (uint8) {
return 6;
}
// --- INVESTMENT LOGIC ---
function invest(uint256 amount, string memory legalDocHashSigned) external nonReentrant {
require(currentStatus == Status.FUNDRAISING, "Fundraising not active");
uint256 cost = amount * pricePerToken;
require(totalRaised + cost <= targetRaiseAmount, "Exceeds target raise");
// Before minting, claim any pending dividends to reset the user's tracker
if (totalSupply() > 0 && currentStatus == Status.ACTIVE) {
_claimDividend(msg.sender);
}
require(paymentToken.transferFrom(msg.sender, address(this), cost), "Payment failed");
_mint(msg.sender, amount * 10**decimals());
totalRaised += cost;
// Reset dividend tracker for new tokens
lastClaimedDividend[msg.sender] = dividendsPerTokenCumulative;
emit Invested(msg.sender, amount, cost, legalDocHashSigned);
}
// --- REVENUE DISTRIBUTION ---
/**
* @notice Admin deposits USDC Rent into the contract.
*/
function depositRent(uint256 amount) external onlyOwner nonReentrant {
require(currentStatus == Status.ACTIVE, "Property not active");
require(totalSupply() > 0, "No tokens");
require(paymentToken.transferFrom(msg.sender, address(this), amount), "Transfer failed");
// Calculate Share Per Token
uint256 amountPerToken = (amount * PRECISION) / totalSupply();
dividendsPerTokenCumulative += amountPerToken;
totalDividendsDistributed += amount;
emit DividendDeposited(amount, block.timestamp);
}
/**
* @notice User calls this to withdraw their share.
*/
function claimRent() external nonReentrant {
_claimDividend(msg.sender);
}
function _claimDividend(address user) internal {
uint256 owed = getClaimableRent(user);
if (owed > 0) {
lastClaimedDividend[user] = dividendsPerTokenCumulative;
require(paymentToken.transfer(user, owed), "Transfer failed");
emit DividendClaimed(user, owed);
} else {
lastClaimedDividend[user] = dividendsPerTokenCumulative;
}
}
function getClaimableRent(address user) public view returns (uint256) {
uint256 userBalance = balanceOf(user);
if (userBalance == 0) return 0;
uint256 difference = dividendsPerTokenCumulative - lastClaimedDividend[user];
return (userBalance * difference) / PRECISION;
}
// --- STANDARD ADMIN FUNCTIONS ---
function finalizeFundraising() external onlyOwner nonReentrant {
require(currentStatus == Status.FUNDRAISING, "Not fundraising");
currentStatus = Status.ACTIVE;
uint256 balance = paymentToken.balanceOf(address(this));
require(paymentToken.transfer(owner(), balance), "Withdraw failed");
emit FundraisingFinalized(totalRaised, block.timestamp);
}
function enableRefunds() external onlyOwner {
require(currentStatus == Status.FUNDRAISING, "Too late");
currentStatus = Status.REFUND_MODE;
emit RefundEnabled(block.timestamp);
}
function refund() external nonReentrant {
require(currentStatus == Status.REFUND_MODE, "Refunds not active");
uint256 userBalance = balanceOf(msg.sender);
require(userBalance > 0, "No tokens");
// Calculate Refund: (Tokens * Price) / 10^6
uint256 refundAmt = (userBalance * pricePerToken) / (10**decimals());
_burn(msg.sender, userBalance);
require(paymentToken.transfer(msg.sender, refundAmt), "Transfer failed");
emit Refunded(msg.sender, userBalance, refundAmt);
}
function updateLegalDocHash(string memory newHash) external onlyOwner {
string memory old = legalDocHash;
legalDocHash = newHash;
emit DocHashUpdated(old, newHash);
}
}
/**
* @title PropertyFactory
* @dev Deploys the new Crowdfunding-ready PropertyTokens.
*/
contract PropertyFactory is Ownable {
address[] public allProperties;
IERC20 public stablecoin;
event PropertyDeployed(
address indexed propertyAddress,
string name,
uint256 price,
uint8 tier,
uint256 targetRaise
);
constructor(address _stablecoinAddress) Ownable(msg.sender) {
stablecoin = IERC20(_stablecoinAddress);
}
function deployProperty(
string memory name,
string memory symbol,
uint256 supply,
uint256 price,
uint8 tierIndex,
string memory docHash
) external onlyOwner returns (address) {
require(tierIndex <= 2, "Invalid property tier");
PropertyToken newProperty = new PropertyToken(
name,
symbol,
supply,
price,
PropertyToken.PropertyTier(tierIndex),
docHash,
address(stablecoin),
owner()
);
address propertyAddr = address(newProperty);
allProperties.push(propertyAddr);
uint256 target = supply * price;
emit PropertyDeployed(propertyAddr, name, price, tierIndex, target);
return propertyAddr;
}
function getProperties() external view returns (address[] memory) {
return allProperties;
}
function setStablecoin(address _newStablecoin) external onlyOwner {
stablecoin = IERC20(_newStablecoin);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment