- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault. - Token-aggregation service routes, planner, chain config, relay env templates. - Config snapshots and multi-chain deployment markdown updates. - gitignore services/btc-intake/dist/ (tsc output); do not track dist. Run forge build && forge test before deploy (large solc graph). Made-with: Cursor
330 lines
13 KiB
Solidity
330 lines
13 KiB
Solidity
// 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"
|
|
);
|
|
}
|
|
}
|