Production-hardened ERC-20 stablecoin + governance contracts. OpenZeppelin base, audited patterns, Sepolia-deployed.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title SmartUSD (\$SUSD)
* @notice AI-powered algorithmic stablecoin — production v3.1.0
* @dev OpenZeppelin ERC20 + AccessControl + ReentrancyGuard + Pausable
* Dual-token model: collateral (USDC) → $SUSD + SGOV
*/
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
interface ISGov {
function mintReward(address to, uint256 amount) external;
function burnFrom(address from, uint256 amount) external;
}
contract SmartUSD is ERC20, AccessControl, ReentrancyGuard, Pausable {
using SafeERC20 for IERC20;
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant CONTROLLER_ROLE = keccak256("CONTROLLER_ROLE");
// ── State ──────────────────────────────────────────────────────────
IERC20 public immutable collateralToken; // USDC (6 decimals)
ISGov public sgovToken;
uint256 public collateralRatio = 115_00; // 115.00% in bps (÷100)
uint256 public mintFee = 10; // 0.10% in bps
uint256 public burnFee = 30; // 0.30% in bps
uint256 public mintCap = 50_000_000e18; // 50M SUSD hard cap
uint256 public singleMintMax = 1_000_000e18; // per-tx guard
uint256 public totalCollateral;
// SGOV reward rate: 1% of locked collateral (in bps)
uint256 public sgovRewardRate = 100; // 1.00%
// ── Events ─────────────────────────────────────────────────────────
event Minted(address indexed user, uint256 susdAmount, uint256 collateral, uint256 sgovRewarded, uint256 fee);
event Burned(address indexed user, uint256 susdAmount, uint256 usdcReturned, uint256 sgovBurned, uint256 fee);
event CollateralRatioUpdated(uint256 oldCR, uint256 newCR);
event MintFeeUpdated(uint256 newFee);
event BurnFeeUpdated(uint256 newFee);
event SGovTokenSet(address indexed sgov);
event MintCapUpdated(uint256 newCap);
// ── Constructor ────────────────────────────────────────────────────
constructor(address _collateralToken, address admin) ERC20("Smart USD", "SUSD") {
require(_collateralToken != address(0), "SUSD: zero collateral");
require(admin != address(0), "SUSD: zero admin");
collateralToken = IERC20(_collateralToken);
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(MINTER_ROLE, admin);
_grantRole(PAUSER_ROLE, admin);
_grantRole(CONTROLLER_ROLE, admin);
}
// ── Admin ──────────────────────────────────────────────────────────
function setSGovToken(address _sgov) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(_sgov != address(0), "SUSD: zero sgov");
sgovToken = ISGov(_sgov);
emit SGovTokenSet(_sgov);
}
function setCollateralRatio(uint256 newCR) external onlyRole(CONTROLLER_ROLE) {
require(newCR >= 105_00 && newCR <= 200_00, "SUSD: CR out of range");
emit CollateralRatioUpdated(collateralRatio, newCR);
collateralRatio = newCR;
}
function setMintFee(uint256 newFee) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(newFee <= 200, "SUSD: fee > 2%");
mintFee = newFee;
emit MintFeeUpdated(newFee);
}
function setBurnFee(uint256 newFee) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(newFee <= 200, "SUSD: fee > 2%");
burnFee = newFee;
emit BurnFeeUpdated(newFee);
}
function setMintCap(uint256 newCap) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(newCap >= totalSupply(), "SUSD: cap below supply");
mintCap = newCap;
emit MintCapUpdated(newCap);
}
function pause() external onlyRole(PAUSER_ROLE) { _pause(); }
function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); }
// ── Core: Mint ─────────────────────────────────────────────────────
/**
* @notice Deposit USDC collateral → receive $SUSD + SGOV reward
* @param usdcAmount Amount of USDC to deposit (6-decimal)
*/
function mint(uint256 usdcAmount) external nonReentrant whenNotPaused {
require(usdcAmount > 0, "SUSD: zero amount");
// Normalise USDC (6 dec) to 18 dec
uint256 susdAmount = usdcAmount * 1e12;
require(susdAmount <= singleMintMax, "SUSD: exceeds single-tx cap");
require(totalSupply() + susdAmount <= mintCap, "SUSD: exceeds protocol cap");
// Collateral required (115% of $SUSD face value, back in USDC 6-dec)
uint256 collateralRequired = (usdcAmount * collateralRatio) / 100_00;
// Pull collateral
collateralToken.safeTransferFrom(msg.sender, address(this), collateralRequired);
totalCollateral += collateralRequired;
// Protocol fee (deducted from SUSD received)
uint256 fee = (susdAmount * mintFee) / 10_000;
uint256 netSusd = susdAmount - fee;
// Mint $SUSD to user
_mint(msg.sender, netSusd);
// Mint SGOV reward (1% of locked collateral)
uint256 sgovReward = 0;
if (address(sgovToken) != address(0)) {
sgovReward = (collateralRequired * 1e12 * sgovRewardRate) / 10_000;
sgovToken.mintReward(msg.sender, sgovReward);
}
emit Minted(msg.sender, netSusd, collateralRequired, sgovReward, fee);
}
// ── Core: Burn ─────────────────────────────────────────────────────
/**
* @notice Burn $SUSD → redeem USDC collateral
* @param susdAmount Amount of $SUSD to burn (18-decimal)
*/
function burn(uint256 susdAmount) external nonReentrant whenNotPaused {
require(susdAmount > 0, "SUSD: zero amount");
require(balanceOf(msg.sender) >= susdAmount, "SUSD: insufficient balance");
// Burn $SUSD from user
_burn(msg.sender, susdAmount);
// USDC to return (face value ÷ CR, minus redemption fee), back to 6 dec
uint256 usdcGross = (susdAmount / 1e12) * 100_00 / collateralRatio;
uint256 fee = (usdcGross * burnFee) / 10_000;
uint256 usdcNet = usdcGross - fee;
require(totalCollateral >= usdcGross, "SUSD: insufficient collateral");
totalCollateral -= usdcGross;
// Burn SGOV if held (1% of susdAmount)
uint256 sgovBurned = 0;
if (address(sgovToken) != address(0)) {
sgovBurned = (susdAmount * sgovRewardRate) / 10_000;
// Best-effort burn — don't revert if user has none
try sgovToken.burnFrom(msg.sender, sgovBurned) {} catch { sgovBurned = 0; }
}
collateralToken.safeTransfer(msg.sender, usdcNet);
emit Burned(msg.sender, susdAmount, usdcNet, sgovBurned, fee);
}
// ── View ───────────────────────────────────────────────────────────
function collateralBalance() external view returns (uint256) {
return collateralToken.balanceOf(address(this));
}
function healthRatio() external view returns (uint256) {
uint256 supply = totalSupply();
if (supply == 0) return type(uint256).max;
// Returns value * 100 (e.g., 11500 = 115.00%)
return (totalCollateral * 1e12 * 100_00) / supply;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title SGov
* @notice SMART USD Governance & Volatility Absorber Token — v3.1.0
* @dev ERC20 + staking + yield distribution + governance power
*/
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SGov is ERC20, AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
// ── Staking State ──────────────────────────────────────────────────
struct StakeInfo {
uint256 amount;
uint256 rewardDebt; // snapshot of rewardPerToken at stake time
uint256 pendingYield; // accrued but not yet claimed
uint256 stakedAt;
}
mapping(address => StakeInfo) public stakes;
uint256 public totalStaked;
uint256 public rewardPerTokenStored; // scaled by 1e18
uint256 public lastRewardTimestamp;
uint256 public rewardRate; // wei per second distributed to stakers
uint256 private constant PRECISION = 1e18;
// ── Supply Cap ─────────────────────────────────────────────────────
uint256 public constant MAX_SUPPLY = 100_000_000e18; // 100M SGOV
// ── Events ─────────────────────────────────────────────────────────
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount);
event YieldClaimed(address indexed user, uint256 amount);
event RewardRateUpdated(uint256 newRate);
constructor(address admin) ERC20("Smart Governance", "SGOV") {
require(admin != address(0), "SGOV: zero admin");
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(MINTER_ROLE, admin);
_grantRole(BURNER_ROLE, admin);
lastRewardTimestamp = block.timestamp;
}
// ── Minting / Burning ──────────────────────────────────────────────
function mintReward(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
require(totalSupply() + amount <= MAX_SUPPLY, "SGOV: supply cap");
_mint(to, amount);
}
function burnFrom(address from, uint256 amount) external onlyRole(BURNER_ROLE) {
_burn(from, amount);
}
// ── Reward Accounting ──────────────────────────────────────────────
function _updateReward(address user) internal {
rewardPerTokenStored = _rewardPerToken();
lastRewardTimestamp = block.timestamp;
if (user != address(0)) {
stakes[user].pendingYield += _earned(user);
stakes[user].rewardDebt = rewardPerTokenStored;
}
}
function _rewardPerToken() internal view returns (uint256) {
if (totalStaked == 0) return rewardPerTokenStored;
return rewardPerTokenStored
+ ((block.timestamp - lastRewardTimestamp) * rewardRate * PRECISION)
/ totalStaked;
}
function _earned(address user) internal view returns (uint256) {
return (stakes[user].amount
* (_rewardPerToken() - stakes[user].rewardDebt))
/ PRECISION;
}
// ── Stake ──────────────────────────────────────────────────────────
function stake(uint256 amount) external nonReentrant {
require(amount > 0, "SGOV: zero amount");
require(balanceOf(msg.sender) >= amount, "SGOV: insufficient balance");
_updateReward(msg.sender);
_transfer(msg.sender, address(this), amount);
stakes[msg.sender].amount += amount;
stakes[msg.sender].stakedAt = block.timestamp;
totalStaked += amount;
emit Staked(msg.sender, amount);
}
// ── Unstake ────────────────────────────────────────────────────────
function unstake(uint256 amount) external nonReentrant {
require(amount > 0, "SGOV: zero amount");
require(stakes[msg.sender].amount >= amount, "SGOV: insufficient staked");
_updateReward(msg.sender);
stakes[msg.sender].amount -= amount;
totalStaked -= amount;
_transfer(address(this), msg.sender, amount);
emit Unstaked(msg.sender, amount);
}
// ── Claim Yield ────────────────────────────────────────────────────
function claimYield() external nonReentrant {
_updateReward(msg.sender);
uint256 yield = stakes[msg.sender].pendingYield;
require(yield > 0, "SGOV: no yield");
stakes[msg.sender].pendingYield = 0;
// Yield is minted as new SGOV (protocol revenue share)
require(totalSupply() + yield <= MAX_SUPPLY, "SGOV: supply cap");
_mint(msg.sender, yield);
emit YieldClaimed(msg.sender, yield);
}
// ── View ───────────────────────────────────────────────────────────
function stakedBalance(address user) external view returns (uint256) {
return stakes[user].amount;
}
function pendingYield(address user) external view returns (uint256) {
return stakes[user].pendingYield + _earned(user);
}
function votingPower(address user) external view returns (uint256) {
// Voting power = liquid + staked balance
return balanceOf(user) + stakes[user].amount;
}
// ── Admin ──────────────────────────────────────────────────────────
function setRewardRate(uint256 newRate) external onlyRole(DEFAULT_ADMIN_ROLE) {
_updateReward(address(0));
rewardRate = newRate;
emit RewardRateUpdated(newRate);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title SmartUSD Testnet Airdrop Faucet
* @notice One-time faucet: 500 SUSD + 50 SGOV + 1000 MockUSDC per wallet
* @dev Deploy on Sepolia only. Disable before mainnet launch.
*/
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface IMintable {
function mintReward(address to, uint256 amount) external;
function mint(address to, uint256 amount) external; // MockUSDC
}
contract SmartUSDAirdrop is Ownable, ReentrancyGuard {
IMintable public susdToken;
IMintable public sgovToken;
IMintable public mockUSDC;
uint256 public susdAmount = 500e18; // 500 SUSD
uint256 public sgovAmount = 50e18; // 50 SGOV
uint256 public usdcAmount = 1_000e6; // 1000 USDC (6 dec)
mapping(address => bool) public claimed;
uint256 public totalClaimed;
bool public active = true;
// ── Events ─────────────────────────────────────────────────────────
event AirdropClaimed(address indexed user, uint256 susd, uint256 sgov, uint256 usdc);
event AirdropDeactivated();
constructor(
address _susd,
address _sgov,
address _usdc,
address _owner
) Ownable(_owner) {
require(_susd != address(0) && _sgov != address(0) && _usdc != address(0), "Zero address");
susdToken = IMintable(_susd);
sgovToken = IMintable(_sgov);
mockUSDC = IMintable(_usdc);
}
// ── Claim ──────────────────────────────────────────────────────────
function claim() external nonReentrant {
require(active, "Airdrop: not active");
require(!claimed[msg.sender], "Airdrop: already claimed");
claimed[msg.sender] = true;
totalClaimed++;
// Mint all three tokens to caller
susdToken.mintReward(msg.sender, susdAmount);
sgovToken.mintReward(msg.sender, sgovAmount);
mockUSDC.mint(msg.sender, usdcAmount);
emit AirdropClaimed(msg.sender, susdAmount, sgovAmount, usdcAmount);
}
// ── View ───────────────────────────────────────────────────────────
function isEligible(address user) external view returns (bool) {
return active && !claimed[user];
}
// ── Admin ──────────────────────────────────────────────────────────
function setAmounts(uint256 _susd, uint256 _sgov, uint256 _usdc) external onlyOwner {
susdAmount = _susd;
sgovAmount = _sgov;
usdcAmount = _usdc;
}
function deactivate() external onlyOwner {
active = false;
emit AirdropDeactivated();
}
}