Files
smom-dbis-138/contracts/bridge/integration/USDWPublicWrapVault.sol
defiQUG 76aa419320 feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- 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
2026-04-07 23:40:52 -07:00

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));
}
}