// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; interface ICompliantTokenOwnerControl { function owner() external view returns (address); function pause() external; function unpause() external; function transferOwnership(address newOwner) external; function mint(address to, uint256 amount) external; function burn(uint256 amount) external; function totalSupply() external view returns (uint256); } /** * @title StablecoinReserveVault * @notice 1:1 backing mechanism for CompliantUSDT and CompliantUSDC with official tokens * @dev Locks official USDT/USDC, mints cUSDT/cUSDC 1:1. Can be deployed on Ethereum Mainnet * or connected via cross-chain bridge to Chain 138 for minting. * * IMPORTANT: This contract should be deployed on Ethereum Mainnet where official USDT/USDC exist. * For Chain 138 deployment, tokens would be bridged/locked via cross-chain infrastructure. */ contract StablecoinReserveVault is AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant RESERVE_OPERATOR_ROLE = keccak256("RESERVE_OPERATOR_ROLE"); bytes32 public constant REDEMPTION_OPERATOR_ROLE = keccak256("REDEMPTION_OPERATOR_ROLE"); // Official token addresses on Ethereum Mainnet // These can be overridden in constructor for different networks address public immutable officialUSDT; address public immutable officialUSDC; // Compliant token contracts (on Chain 138 or same network) ICompliantTokenOwnerControl public immutable compliantUSDT; ICompliantTokenOwnerControl public immutable compliantUSDC; // Reserve tracking uint256 public usdtReserveBalance; uint256 public usdcReserveBalance; // Total minted (for verification) uint256 public totalCUSDTMinted; uint256 public totalCUSDCMinted; // Pause mechanism bool public paused; event ReserveDeposited(address indexed token, uint256 amount, address indexed depositor); event ReserveWithdrawn(address indexed token, uint256 amount, address indexed recipient); event CompliantTokensMinted(address indexed token, uint256 amount, address indexed recipient); event CompliantTokensBurned(address indexed token, uint256 amount, address indexed redeemer); event CompliantTokenPaused(address indexed token, address indexed operator); event CompliantTokenUnpaused(address indexed token, address indexed operator); event CompliantTokenOwnershipTransferred(address indexed token, address indexed newOwner); event Paused(address indexed account); event Unpaused(address indexed account); modifier whenNotPaused() { require(!paused, "StablecoinReserveVault: paused"); _; } /** * @notice Constructor * @param admin Admin address (will receive DEFAULT_ADMIN_ROLE) * @param officialUSDT_ Official USDT token address (on Ethereum Mainnet: 0xdAC17F958D2ee523a2206206994597C13D831ec7) * @param officialUSDC_ Official USDC token address (on Ethereum Mainnet: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) * @param compliantUSDT_ CompliantUSDT contract address * @param compliantUSDC_ CompliantUSDC contract address */ constructor( address admin, address officialUSDT_, address officialUSDC_, address compliantUSDT_, address compliantUSDC_ ) { require(admin != address(0), "StablecoinReserveVault: zero admin"); require(officialUSDT_ != address(0), "StablecoinReserveVault: zero USDT"); require(officialUSDC_ != address(0), "StablecoinReserveVault: zero USDC"); require(compliantUSDT_ != address(0), "StablecoinReserveVault: zero cUSDT"); require(compliantUSDC_ != address(0), "StablecoinReserveVault: zero cUSDC"); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(RESERVE_OPERATOR_ROLE, admin); _grantRole(REDEMPTION_OPERATOR_ROLE, admin); officialUSDT = officialUSDT_; officialUSDC = officialUSDC_; compliantUSDT = ICompliantTokenOwnerControl(compliantUSDT_); compliantUSDC = ICompliantTokenOwnerControl(compliantUSDC_); } /** * @notice Deposit official USDT and mint cUSDT 1:1 * @dev Transfers USDT from caller, mints cUSDT to caller * @param amount Amount of USDT to deposit (6 decimals) */ function depositUSDT(uint256 amount) external whenNotPaused nonReentrant { require(amount > 0, "StablecoinReserveVault: zero amount"); // Transfer official USDT from caller IERC20(officialUSDT).safeTransferFrom(msg.sender, address(this), amount); // Update reserve usdtReserveBalance += amount; totalCUSDTMinted += amount; // Mint cUSDT to caller compliantUSDT.mint(msg.sender, amount); emit ReserveDeposited(officialUSDT, amount, msg.sender); emit CompliantTokensMinted(address(compliantUSDT), amount, msg.sender); } /** * @notice Seed official USDT reserves without minting new cUSDT * @dev Used to retrofit backing for pre-existing canonical supply */ function seedUSDTReserve(uint256 amount) external onlyRole(RESERVE_OPERATOR_ROLE) nonReentrant { require(amount > 0, "StablecoinReserveVault: zero amount"); IERC20(officialUSDT).safeTransferFrom(msg.sender, address(this), amount); usdtReserveBalance += amount; emit ReserveDeposited(officialUSDT, amount, msg.sender); } /** * @notice Deposit official USDC and mint cUSDC 1:1 * @dev Transfers USDC from caller, mints cUSDC to caller * @param amount Amount of USDC to deposit (6 decimals) */ function depositUSDC(uint256 amount) external whenNotPaused nonReentrant { require(amount > 0, "StablecoinReserveVault: zero amount"); // Transfer official USDC from caller IERC20(officialUSDC).safeTransferFrom(msg.sender, address(this), amount); // Update reserve usdcReserveBalance += amount; totalCUSDCMinted += amount; // Mint cUSDC to caller compliantUSDC.mint(msg.sender, amount); emit ReserveDeposited(officialUSDC, amount, msg.sender); emit CompliantTokensMinted(address(compliantUSDC), amount, msg.sender); } /** * @notice Seed official USDC reserves without minting new cUSDC * @dev Used to retrofit backing for pre-existing canonical supply */ function seedUSDCReserve(uint256 amount) external onlyRole(RESERVE_OPERATOR_ROLE) nonReentrant { require(amount > 0, "StablecoinReserveVault: zero amount"); IERC20(officialUSDC).safeTransferFrom(msg.sender, address(this), amount); usdcReserveBalance += amount; emit ReserveDeposited(officialUSDC, amount, msg.sender); } /** * @notice Redeem cUSDT for official USDT 1:1 * @dev Burns cUSDT from caller, transfers USDT to caller * @param amount Amount of cUSDT to redeem (6 decimals) */ function redeemUSDT(uint256 amount) external whenNotPaused nonReentrant { require(amount > 0, "StablecoinReserveVault: zero amount"); require(usdtReserveBalance >= amount, "StablecoinReserveVault: insufficient reserve"); // Pull cUSDT from the redeemer, then burn from the vault balance as token owner. IERC20(address(compliantUSDT)).safeTransferFrom(msg.sender, address(this), amount); compliantUSDT.burn(amount); // Update reserve usdtReserveBalance -= amount; totalCUSDTMinted -= amount; // Transfer official USDT to caller IERC20(officialUSDT).safeTransfer(msg.sender, amount); emit CompliantTokensBurned(address(compliantUSDT), amount, msg.sender); emit ReserveWithdrawn(officialUSDT, amount, msg.sender); } /** * @notice Redeem cUSDC for official USDC 1:1 * @dev Burns cUSDC from caller, transfers USDC to caller * @param amount Amount of cUSDC to redeem (6 decimals) */ function redeemUSDC(uint256 amount) external whenNotPaused nonReentrant { require(amount > 0, "StablecoinReserveVault: zero amount"); require(usdcReserveBalance >= amount, "StablecoinReserveVault: insufficient reserve"); // Pull cUSDC from the redeemer, then burn from the vault balance as token owner. IERC20(address(compliantUSDC)).safeTransferFrom(msg.sender, address(this), amount); compliantUSDC.burn(amount); // Update reserve usdcReserveBalance -= amount; totalCUSDCMinted -= amount; // Transfer official USDC to caller IERC20(officialUSDC).safeTransfer(msg.sender, amount); emit CompliantTokensBurned(address(compliantUSDC), amount, msg.sender); emit ReserveWithdrawn(officialUSDC, amount, msg.sender); } /** * @notice Get reserve backing ratio * @param token Address of compliant token (cUSDT or cUSDC) * @return reserveBalance Current reserve balance * @return tokenSupply Current token supply * @return backingRatio Backing ratio (10000 = 100%) */ function getBackingRatio(address token) external view returns ( uint256 reserveBalance, uint256 tokenSupply, uint256 backingRatio ) { if (token == address(compliantUSDT)) { reserveBalance = usdtReserveBalance; tokenSupply = compliantUSDT.totalSupply(); } else if (token == address(compliantUSDC)) { reserveBalance = usdcReserveBalance; tokenSupply = compliantUSDC.totalSupply(); } else { revert("StablecoinReserveVault: unsupported token"); } backingRatio = tokenSupply > 0 ? (reserveBalance * 10000) / tokenSupply : 0; } /** * @notice Check if reserves are adequate * @return usdtAdequate True if USDT reserves are adequate * @return usdcAdequate True if USDC reserves are adequate */ function checkReserveAdequacy() external view returns (bool usdtAdequate, bool usdcAdequate) { uint256 cUSDTSupply = compliantUSDT.totalSupply(); uint256 cUSDCSupply = compliantUSDC.totalSupply(); usdtAdequate = usdtReserveBalance >= cUSDTSupply; usdcAdequate = usdcReserveBalance >= cUSDCSupply; } /** * @notice Pause all operations */ function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { paused = true; emit Paused(msg.sender); } /** * @notice Unpause all operations */ function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { paused = false; emit Unpaused(msg.sender); } /** * @notice Pause one of the compliant tokens currently owned by the vault */ function pauseCompliantToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { _requireSupportedCompliantToken(token); ICompliantTokenOwnerControl(token).pause(); emit CompliantTokenPaused(token, msg.sender); } /** * @notice Unpause one of the compliant tokens currently owned by the vault */ function unpauseCompliantToken(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { _requireSupportedCompliantToken(token); ICompliantTokenOwnerControl(token).unpause(); emit CompliantTokenUnpaused(token, msg.sender); } /** * @notice Move compliant token ownership away from the vault if governance needs to recover control */ function transferCompliantTokenOwnership( address token, address newOwner ) external onlyRole(DEFAULT_ADMIN_ROLE) { require(newOwner != address(0), "StablecoinReserveVault: zero new owner"); _requireSupportedCompliantToken(token); ICompliantTokenOwnerControl(token).transferOwnership(newOwner); emit CompliantTokenOwnershipTransferred(token, newOwner); } /** * @notice Emergency withdrawal (admin only, after pause) * @dev Can be used to recover funds in emergency situations */ function emergencyWithdraw(address token, uint256 amount, address recipient) external onlyRole(DEFAULT_ADMIN_ROLE) whenPaused { require(recipient != address(0), "StablecoinReserveVault: zero recipient"); IERC20(token).safeTransfer(recipient, amount); } modifier whenPaused() { require(paused, "StablecoinReserveVault: not paused"); _; } function _requireSupportedCompliantToken(address token) internal view { require( token == address(compliantUSDT) || token == address(compliantUSDC), "StablecoinReserveVault: unsupported compliant token" ); } }