// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; interface IERC20 { function decimals() external view returns (uint8); function balanceOf(address) external view returns (uint256); function transfer(address,uint256) external returns (bool); function transferFrom(address,address,uint256) external returns (bool); } interface IPonsV2FeeEscrow { function balanceOf(address recipient) external view returns (uint256); function balanceOfToken(address recipient,address token) external view returns (uint256); function claim() external; function claimToken(address token) external; } contract StrataStableAsset { string public name; string public symbol; uint8 public immutable decimals; uint256 public totalSupply; address public immutable controller; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; event Transfer(address indexed from,address indexed to,uint256 value); event Approval(address indexed owner,address indexed spender,uint256 value); modifier onlyController(){ require(msg.sender == controller, "CONTROLLER"); _; } constructor(string memory n,string memory s,address c,uint8 d){ require(c != address(0), "ZERO_CONTROLLER"); name=n; symbol=s; controller=c; decimals=d; } function approve(address spender,uint256 value) external returns(bool){ allowance[msg.sender][spender]=value; emit Approval(msg.sender,spender,value); return true; } function transfer(address to,uint256 value) external returns(bool){ _move(msg.sender,to,value); return true; } function transferFrom(address from,address to,uint256 value) external returns(bool){ uint256 a=allowance[from][msg.sender]; require(a>=value,"ALLOWANCE"); if(a!=type(uint256).max) allowance[from][msg.sender]=a-value; _move(from,to,value); return true; } function mint(address to,uint256 value) external onlyController { require(to != address(0), "ZERO_TO"); totalSupply += value; balanceOf[to] += value; emit Transfer(address(0),to,value); } function burnFrom(address from,uint256 value) external onlyController { require(balanceOf[from]>=value,"BALANCE"); balanceOf[from]-=value; totalSupply-=value; emit Transfer(from,address(0),value); } function _move(address from,address to,uint256 value) internal { require(to!=address(0),"ZERO_TO"); require(balanceOf[from]>=value,"BALANCE"); balanceOf[from]-=value; balanceOf[to]+=value; emit Transfer(from,to,value); } } /// @notice USDG-backed project vault for Robinhood Chain. /// @dev Stable minting requires a matching USDG deposit. Pons creator fees paid in USDG /// accumulate as excess collateral and can never be withdrawn by an admin. contract StrataProjectVault { IERC20 public immutable usdg; address public immutable creator; IPonsV2FeeEscrow public immutable ponsFeeEscrow; StrataStableAsset public immutable stable; uint256 private unlocked = 1; event BackingDeposited(address indexed from,uint256 amount); event PonsFeesClaimed(address indexed asset,uint256 amount); event StableMinted(address indexed account,uint256 amount); event StableRedeemed(address indexed account,uint256 stableAmount,uint256 usdgAmount); modifier nonReentrant(){ require(unlocked==1,"LOCKED"); unlocked=2; _; unlocked=1; } constructor(address usdg_,address feeEscrow_,address creator_,string memory stableName,string memory stableSymbol){ require(usdg_!=address(0) && feeEscrow_!=address(0) && creator_!=address(0),"ZERO"); usdg=IERC20(usdg_); ponsFeeEscrow=IPonsV2FeeEscrow(feeEscrow_); creator=creator_; uint8 backingDecimals=IERC20(usdg_).decimals(); stable=new StrataStableAsset(stableName,stableSymbol,address(this),backingDecimals); } function reserve() public view returns(uint256){ return usdg.balanceOf(address(this)); } function stableSupply() public view returns(uint256){ return stable.totalSupply(); } function excessBacking() public view returns(uint256){ uint256 r=reserve(); uint256 s=stable.totalSupply(); return r>s ? r-s : 0; } function collateralRatioBps() public view returns(uint256){ uint256 s=stable.totalSupply(); return s==0 ? type(uint256).max : reserve()*10_000/s; } function claimablePonsUSDG() external view returns(uint256){ return ponsFeeEscrow.balanceOfToken(address(this),address(usdg)); } function claimablePonsNative() external view returns(uint256){ return ponsFeeEscrow.balanceOf(address(this)); } /// @notice Deposit USDG without minting. Useful for donations / explicit extra backing. function depositBacking(uint256 amount) external nonReentrant { require(amount>0,"ZERO_AMOUNT"); require(usdg.transferFrom(msg.sender,address(this),amount),"TRANSFER"); emit BackingDeposited(msg.sender,amount); } /// @notice Anyone may trigger the claim; payout can only arrive at this vault. function claimPonsUSDG() external nonReentrant { uint256 beforeBal=reserve(); ponsFeeEscrow.claimToken(address(usdg)); emit PonsFeesClaimed(address(usdg),reserve()-beforeBal); } /// @notice Native Pons payouts are claimable but are not counted as USDG backing. /// STRATA V1 launch UI therefore requires USDG as the Pons quote asset. function claimPonsNative() external nonReentrant { uint256 beforeBal=address(this).balance; ponsFeeEscrow.claim(); emit PonsFeesClaimed(address(0),address(this).balance-beforeBal); } /// @notice 1 USDG deposited mints 1 project stable unit (same decimals as USDG). function depositAndMint(uint256 amount) external nonReentrant { require(amount>0,"ZERO_AMOUNT"); require(usdg.transferFrom(msg.sender,address(this),amount),"TRANSFER"); stable.mint(msg.sender,amount); emit BackingDeposited(msg.sender,amount); emit StableMinted(msg.sender,amount); } /// @notice Burn stable and redeem USDG 1:1. Fee-derived excess collateral remains in vault. function redeem(uint256 amount) external nonReentrant { require(amount>0,"ZERO_AMOUNT"); stable.burnFrom(msg.sender,amount); require(usdg.transfer(msg.sender,amount),"TRANSFER"); emit StableRedeemed(msg.sender,amount,amount); } receive() external payable {} } contract StrataProtocolFactory { address public immutable usdg; address public immutable ponsFeeEscrow; mapping(bytes32 => address) public vaultByProjectKey; address[] public allVaults; event ProjectCreated(bytes32 indexed projectKey,address indexed creator,address vault,address stable,bytes32 userSalt); constructor(address usdg_,address ponsFeeEscrow_){ require(usdg_!=address(0) && ponsFeeEscrow_!=address(0),"ZERO"); usdg=usdg_; ponsFeeEscrow=ponsFeeEscrow_; } function projectKey(address creator,bytes32 userSalt) public pure returns(bytes32){ return keccak256(abi.encode(creator,userSalt)); } function createProject(bytes32 userSalt,string calldata stableName,string calldata stableSymbol) external returns(address vault,address stable){ bytes32 key=projectKey(msg.sender,userSalt); require(vaultByProjectKey[key]==address(0),"PROJECT_EXISTS"); StrataProjectVault v=new StrataProjectVault{salt:key}(usdg,ponsFeeEscrow,msg.sender,stableName,stableSymbol); vault=address(v); stable=address(v.stable()); vaultByProjectKey[key]=vault; allVaults.push(vault); emit ProjectCreated(key,msg.sender,vault,stable,userSalt); } function predictProjectAddress(address creator,bytes32 userSalt,string calldata stableName,string calldata stableSymbol) external view returns(address predicted){ bytes32 key=projectKey(creator,userSalt); bytes memory init=abi.encodePacked(type(StrataProjectVault).creationCode,abi.encode(usdg,ponsFeeEscrow,creator,stableName,stableSymbol)); bytes32 h=keccak256(abi.encodePacked(bytes1(0xff),address(this),key,keccak256(init))); predicted=address(uint160(uint256(h))); } function projectCount() external view returns(uint256){ return allVaults.length; } }