// SPDX-License-Identifier: MIT pragma solidity 0.8.36; // uCurrencies V2: permissionless factory for UCASH-pegged currencies. // // Upgrade of the dormant 2018 Ethereum factory (0x7CD4...3729, solc 0.4.x): // - same external surface (createToken / buyTokens / redeemTokens / convertTokens, // tokenList / tokenRatios / getTokenListLength / created / isEIP20 / // UCASHAddress / ratioD / getRealTokenRatio) with identical ratio convention: // a stored ratio is "child smallest-units per 1e8 UCASH base units" and is // derived from the creator-supplied UCASHRatio (denominated in 1e8 = 1 UCASH) // as (UCASHRatio * 10^decimals) / 1e8. // - fixed vs V1: checked 0.8 math (ratio overflow reverts), ratio 0 rejected // (V1 registered permanently unusable tokens), zero-output guards on // buy/redeem/convert (V1 silently burned input), unique name+symbol, // UCASH transfer/transferFrom bool returns checked, events, reentrancy guard. // - children are EIP-1167 minimal proxies of one UCurrency implementation // (~45-byte clones vs V1's ~3M-gas full-token deploys), mint/burn gated to // the factory, standard ERC20 events with address(0) counterparties. // - the UCASH pulled in by buys stays pooled in the factory as the shared // collateral reserve (V1 model, preserved). // No admin keys, no owner, no pauser: permissionless like V1. The only // per-chain difference is the UCASH address passed to the constructor. interface IERC20 { function transfer(address to, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); } contract UCurrency { // ---- ERC20 metadata (set once by the factory at clone initialization) ---- string public name; string public symbol; uint8 public decimals; uint256 public cap; // ---- ERC20 state ---- uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; // ---- factory gate (immutable lives in the implementation bytecode, which // every clone executes via delegatecall) ---- address public immutable factory; error NotFactory(); error AlreadyInitialized(); error CapExceeded(); error InvalidAddress(); error ZeroAmount(); error InsufficientBalance(); error InsufficientAllowance(); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); constructor(address factory_) { factory = factory_; } function initialize(string calldata name_, string calldata symbol_, uint8 decimals_, uint256 cap_) external { if (msg.sender != factory) revert NotFactory(); if (bytes(name).length != 0) revert AlreadyInitialized(); name = name_; symbol = symbol_; decimals = decimals_; cap = cap_; } function mint(address to, uint256 amount) external { if (msg.sender != factory) revert NotFactory(); if (to == address(0)) revert InvalidAddress(); if (totalSupply + amount > cap) revert CapExceeded(); balanceOf[to] += amount; totalSupply += amount; emit Transfer(address(0), to, amount); } function burn(address from, uint256 amount) external { if (msg.sender != factory) revert NotFactory(); uint256 b = balanceOf[from]; if (amount > b) revert InsufficientBalance(); balanceOf[from] = b - amount; totalSupply -= amount; emit Transfer(from, address(0), amount); } function transfer(address to, uint256 value) external returns (bool) { if (to == address(0)) revert InvalidAddress(); uint256 b = balanceOf[msg.sender]; if (value > b) revert InsufficientBalance(); balanceOf[msg.sender] = b - value; balanceOf[to] += value; emit Transfer(msg.sender, to, value); return true; } function approve(address spender, uint256 value) external returns (bool) { if (spender == address(0)) revert InvalidAddress(); allowance[msg.sender][spender] = value; emit Approval(msg.sender, spender, value); return true; } function transferFrom(address from, address to, uint256 value) external returns (bool) { if (to == address(0) || from == address(0)) revert InvalidAddress(); uint256 b = balanceOf[from]; if (value > b) revert InsufficientBalance(); uint256 a = allowance[from][msg.sender]; if (a != type(uint256).max) { if (value > a) revert InsufficientAllowance(); allowance[from][msg.sender] = a - value; } balanceOf[from] = b - value; balanceOf[to] += value; emit Transfer(from, to, value); return true; } /// @notice V1 parity alias: the 2018 child tokens exposed the cap as `limit()`. function limit() external view returns (uint256) { return cap; } } contract UCurrencyFactory { uint256 public constant ratioD = 1e8; IERC20 private immutable _ucash; address public immutable implementation; bytes32 public immutable cloneBytecodeHash; // ---- registry ---- address[] public tokenList; mapping(address => uint256) public tokenRatios; mapping(address => address) public created; // token -> creator mapping(address => bool) public isEIP20; // V1 parity name mapping(string => bool) public nameTaken; mapping(string => bool) public symbolTaken; event TokenCreated( address indexed token, address indexed creator, string name, string symbol, uint8 decimals, uint256 limit, uint256 ratio ); event Bought(address indexed token, address indexed buyer, uint256 ucashIn, uint256 tokensOut); event Redeemed(address indexed token, address indexed seller, uint256 tokensIn, uint256 ucashOut); event Converted(address indexed user, address indexed fromToken, address indexed toToken, uint256 amountIn, uint256 amountOut); error ZeroRatio(); error BadDecimals(); error BadLimit(); error EmptyMetadata(); error Taken(); error Unregistered(); error SameToken(); error DustAmount(); error UCASHTransferFailed(); // ---- reentrancy guard ---- uint256 private constant _NOT_ENTERED = 1; uint256 private constant _ENTERED = 2; uint256 private _status = _NOT_ENTERED; modifier nonReentrant() { require(_status != _ENTERED, "reentrant"); _status = _ENTERED; _; _status = _NOT_ENTERED; } constructor(address ucash_) { _ucash = IERC20(ucash_); implementation = address(new UCurrency(address(this))); // keccak256 of the 35-byte EIP-1167 proxy runtime code for `implementation` cloneBytecodeHash = keccak256( abi.encodePacked(hex"363d3d373d3d3d363d73", bytes20(implementation), hex"5af43d82803e903d91602b57fd5bf3") ); } // ---- V1 parity getters ---- function UCASHAddress() external view returns (address) { return address(_ucash); } function getTokenListLength() external view returns (uint256) { return tokenList.length; } function getRealTokenRatio(uint256 UCASHRatio, uint256 decimals_) external pure returns (uint256) { return (UCASHRatio * (10 ** decimals_)) / ratioD; } /// @notice Cheap authenticity check: extcodehash against the impl-derived constant. function isFactoryToken(address token) external view returns (bool) { return token.codehash == cloneBytecodeHash; } // ---- creation ---- function createToken( uint256 limit_, uint8 decimals_, string calldata name_, string calldata symbol_, uint256 UCASHRatio ) external nonReentrant returns (address token) { if (UCASHRatio == 0) revert ZeroRatio(); if (decimals_ > 18) revert BadDecimals(); if (limit_ == 0 || limit_ > 21_000_000_000 * (10 ** decimals_)) revert BadLimit(); if (bytes(name_).length == 0 || bytes(symbol_).length == 0) revert EmptyMetadata(); if (nameTaken[name_] || symbolTaken[symbol_]) revert Taken(); uint256 ratio = (UCASHRatio * (10 ** decimals_)) / ratioD; if (ratio == 0) revert ZeroRatio(); // e.g. UCASHRatio=1 with decimals=0: would floor to an unusable 0 token = _clone(implementation); UCurrency(token).initialize(name_, symbol_, decimals_, limit_); tokenRatios[token] = ratio; created[token] = msg.sender; isEIP20[token] = true; nameTaken[name_] = true; symbolTaken[symbol_] = true; tokenList.push(token); emit TokenCreated(token, msg.sender, name_, symbol_, decimals_, limit_, ratio); } // ---- mint / redeem / convert ---- function buyTokens(address token, uint256 amount) external nonReentrant { _buy(token, amount); } function redeemTokens(address token, uint256 amount) external nonReentrant { _redeem(token, amount); } function convertTokens(address from, address to, uint256 amount) external nonReentrant { if (from == to) revert SameToken(); if (from == address(_ucash)) { _buy(to, amount); return; } if (to == address(_ucash)) { _redeem(from, amount); return; } uint256 fromRatio = tokenRatios[from]; uint256 toRatio = tokenRatios[to]; if (fromRatio == 0 || toRatio == 0) revert Unregistered(); UCurrency(from).burn(msg.sender, amount); uint256 ucashEquiv = (ratioD * amount) / fromRatio; uint256 out = (toRatio * ucashEquiv) / ratioD; if (out == 0) revert DustAmount(); UCurrency(to).mint(msg.sender, out); emit Converted(msg.sender, from, to, amount, out); } // ---- internals (unguarded; the public entrypoints hold the guard) ---- function _buy(address token, uint256 amount) internal { uint256 ratio = tokenRatios[token]; if (ratio == 0) revert Unregistered(); if (!_ucash.transferFrom(msg.sender, address(this), amount)) revert UCASHTransferFailed(); uint256 out = (ratio * amount) / ratioD; if (out == 0) revert DustAmount(); UCurrency(token).mint(msg.sender, out); emit Bought(token, msg.sender, amount, out); } function _redeem(address token, uint256 amount) internal { uint256 ratio = tokenRatios[token]; if (ratio == 0) revert Unregistered(); UCurrency(token).burn(msg.sender, amount); uint256 out = (ratioD * amount) / ratio; if (out == 0) revert DustAmount(); if (!_ucash.transfer(msg.sender, out)) revert UCASHTransferFailed(); emit Redeemed(token, msg.sender, amount, out); } // ---- EIP-1167 minimal proxy ---- function _clone(address impl) internal returns (address proxy) { assembly ("memory-safe") { let ptr := mload(0x40) // 0x3d602d80600a3d3981f3 | 363d3d373d3d3d363d73 5af43d82803e903d91602b57fd5bf3 // note the shl(96, impl): mstore writes a left-padded 32-byte word, so the // 160-bit address must be shifted to sit exactly at byte offset 0x14 mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) mstore(add(ptr, 0x14), shl(96, impl)) mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) proxy := create(0, ptr, 0x37) } if (proxy == address(0)) revert(string("clone failed")); } }