- 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
190 lines
7.6 KiB
Solidity
190 lines
7.6 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import "@openzeppelin/contracts/utils/Pausable.sol";
|
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
|
|
interface ICWMintBurnMetadata is IERC20Metadata {
|
|
function mint(address to, uint256 amount) external;
|
|
function burnFrom(address from, uint256 amount) external;
|
|
}
|
|
|
|
/**
|
|
* @title USDWPublicWrapVault
|
|
* @notice Locks native public-chain USDW and mints the shared cWUSDW mirror 1:1 after decimals normalization.
|
|
* @dev The same cWUSDW supply can be minted by both this vault and the GRU bridge. This contract deliberately
|
|
* does not expose an admin path to withdraw the underlying USDW reserve because that requires a fuller
|
|
* liability model across wrap-originated and bridge-originated cWUSDW supply.
|
|
*/
|
|
contract USDWPublicWrapVault is AccessControl, Pausable, ReentrancyGuard {
|
|
using SafeERC20 for IERC20;
|
|
|
|
bytes32 public constant RESERVE_OPERATOR_ROLE = keccak256("RESERVE_OPERATOR_ROLE");
|
|
bytes32 public constant EMERGENCY_ADMIN_ROLE = keccak256("EMERGENCY_ADMIN_ROLE");
|
|
|
|
IERC20 public immutable nativeUsdw;
|
|
ICWMintBurnMetadata public immutable wrappedUsdw;
|
|
uint8 public immutable nativeDecimals;
|
|
uint8 public immutable wrappedDecimals;
|
|
|
|
event LiquiditySeeded(address indexed operator, uint256 nativeAmount, uint256 wrappedEquivalent);
|
|
event Wrapped(
|
|
address indexed caller,
|
|
address indexed recipient,
|
|
uint256 nativeAmount,
|
|
uint256 wrappedAmount
|
|
);
|
|
event Unwrapped(
|
|
address indexed caller,
|
|
address indexed recipient,
|
|
uint256 wrappedAmount,
|
|
uint256 nativeAmount
|
|
);
|
|
event NonUnderlyingTokenRecovered(address indexed token, address indexed recipient, uint256 amount);
|
|
|
|
error InsufficientUnderlyingLiquidity(uint256 requested, uint256 available);
|
|
error NonCanonicalAmount(uint256 amount);
|
|
error UnderlyingTokenProtected();
|
|
error UnsupportedDecimals(uint8 nativeDecimals, uint8 wrappedDecimals);
|
|
error ZeroAddress();
|
|
error ZeroAmount();
|
|
error ZeroRecipient();
|
|
|
|
constructor(address admin, address nativeUsdw_, address wrappedUsdw_) {
|
|
if (admin == address(0) || nativeUsdw_ == address(0) || wrappedUsdw_ == address(0)) revert ZeroAddress();
|
|
|
|
nativeUsdw = IERC20(nativeUsdw_);
|
|
wrappedUsdw = ICWMintBurnMetadata(wrappedUsdw_);
|
|
nativeDecimals = IERC20Metadata(nativeUsdw_).decimals();
|
|
wrappedDecimals = IERC20Metadata(wrappedUsdw_).decimals();
|
|
|
|
uint8 diff = nativeDecimals > wrappedDecimals
|
|
? nativeDecimals - wrappedDecimals
|
|
: wrappedDecimals - nativeDecimals;
|
|
if (diff > 77) revert UnsupportedDecimals(nativeDecimals, wrappedDecimals);
|
|
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
_grantRole(RESERVE_OPERATOR_ROLE, admin);
|
|
_grantRole(EMERGENCY_ADMIN_ROLE, admin);
|
|
}
|
|
|
|
function seedLiquidity(uint256 nativeAmount) external onlyRole(RESERVE_OPERATOR_ROLE) nonReentrant {
|
|
if (nativeAmount == 0) revert ZeroAmount();
|
|
|
|
nativeUsdw.safeTransferFrom(msg.sender, address(this), nativeAmount);
|
|
emit LiquiditySeeded(msg.sender, nativeAmount, _toWrappedFloor(nativeAmount));
|
|
}
|
|
|
|
function wrap(uint256 nativeAmount, address recipient) external whenNotPaused nonReentrant returns (uint256 wrappedAmount) {
|
|
if (nativeAmount == 0) revert ZeroAmount();
|
|
if (recipient == address(0)) revert ZeroRecipient();
|
|
|
|
wrappedAmount = _toWrappedExact(nativeAmount);
|
|
nativeUsdw.safeTransferFrom(msg.sender, address(this), nativeAmount);
|
|
wrappedUsdw.mint(recipient, wrappedAmount);
|
|
|
|
emit Wrapped(msg.sender, recipient, nativeAmount, wrappedAmount);
|
|
}
|
|
|
|
function unwrap(uint256 wrappedAmount, address recipient) external whenNotPaused nonReentrant returns (uint256 nativeAmount) {
|
|
if (wrappedAmount == 0) revert ZeroAmount();
|
|
if (recipient == address(0)) revert ZeroRecipient();
|
|
|
|
nativeAmount = _toNativeExact(wrappedAmount);
|
|
uint256 available = nativeUsdw.balanceOf(address(this));
|
|
if (available < nativeAmount) revert InsufficientUnderlyingLiquidity(nativeAmount, available);
|
|
|
|
wrappedUsdw.burnFrom(msg.sender, wrappedAmount);
|
|
nativeUsdw.safeTransfer(recipient, nativeAmount);
|
|
|
|
emit Unwrapped(msg.sender, recipient, wrappedAmount, nativeAmount);
|
|
}
|
|
|
|
function pause() external onlyRole(EMERGENCY_ADMIN_ROLE) {
|
|
_pause();
|
|
}
|
|
|
|
function unpause() external onlyRole(EMERGENCY_ADMIN_ROLE) {
|
|
_unpause();
|
|
}
|
|
|
|
function recoverNonUnderlyingToken(
|
|
address token,
|
|
address recipient,
|
|
uint256 amount
|
|
) external onlyRole(EMERGENCY_ADMIN_ROLE) nonReentrant {
|
|
if (token == address(nativeUsdw)) revert UnderlyingTokenProtected();
|
|
if (recipient == address(0)) revert ZeroRecipient();
|
|
IERC20(token).safeTransfer(recipient, amount);
|
|
emit NonUnderlyingTokenRecovered(token, recipient, amount);
|
|
}
|
|
|
|
function availableUnderlyingLiquidity() external view returns (uint256) {
|
|
return nativeUsdw.balanceOf(address(this));
|
|
}
|
|
|
|
function availableWrappedLiquidity() external view returns (uint256) {
|
|
return _toWrappedFloor(nativeUsdw.balanceOf(address(this)));
|
|
}
|
|
|
|
function wrappedSupply() external view returns (uint256) {
|
|
return wrappedUsdw.totalSupply();
|
|
}
|
|
|
|
function liquidityCoverageBps() external view returns (uint256) {
|
|
uint256 supply = wrappedUsdw.totalSupply();
|
|
if (supply == 0) {
|
|
return 0;
|
|
}
|
|
return (_toWrappedFloor(nativeUsdw.balanceOf(address(this))) * 10_000) / supply;
|
|
}
|
|
|
|
function previewWrap(uint256 nativeAmount) external view returns (uint256) {
|
|
if (nativeAmount == 0) revert ZeroAmount();
|
|
return _toWrappedExact(nativeAmount);
|
|
}
|
|
|
|
function previewUnwrap(uint256 wrappedAmount) external view returns (uint256) {
|
|
if (wrappedAmount == 0) revert ZeroAmount();
|
|
return _toNativeExact(wrappedAmount);
|
|
}
|
|
|
|
function _toWrappedExact(uint256 nativeAmount) internal view returns (uint256) {
|
|
if (nativeDecimals == wrappedDecimals) {
|
|
return nativeAmount;
|
|
}
|
|
if (nativeDecimals > wrappedDecimals) {
|
|
uint256 divisor = 10 ** (nativeDecimals - wrappedDecimals);
|
|
if (nativeAmount % divisor != 0) revert NonCanonicalAmount(nativeAmount);
|
|
return nativeAmount / divisor;
|
|
}
|
|
return nativeAmount * (10 ** (wrappedDecimals - nativeDecimals));
|
|
}
|
|
|
|
function _toNativeExact(uint256 wrappedAmount) internal view returns (uint256) {
|
|
if (nativeDecimals == wrappedDecimals) {
|
|
return wrappedAmount;
|
|
}
|
|
if (wrappedDecimals > nativeDecimals) {
|
|
uint256 divisor = 10 ** (wrappedDecimals - nativeDecimals);
|
|
if (wrappedAmount % divisor != 0) revert NonCanonicalAmount(wrappedAmount);
|
|
return wrappedAmount / divisor;
|
|
}
|
|
return wrappedAmount * (10 ** (nativeDecimals - wrappedDecimals));
|
|
}
|
|
|
|
function _toWrappedFloor(uint256 nativeAmount) internal view returns (uint256) {
|
|
if (nativeDecimals == wrappedDecimals) {
|
|
return nativeAmount;
|
|
}
|
|
if (nativeDecimals > wrappedDecimals) {
|
|
return nativeAmount / (10 ** (nativeDecimals - wrappedDecimals));
|
|
}
|
|
return nativeAmount * (10 ** (wrappedDecimals - nativeDecimals));
|
|
}
|
|
}
|