- 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
315 lines
12 KiB
Solidity
315 lines
12 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 "../../reserve/IReserveSystem.sol";
|
|
import "./ICWReserveVerifier.sol";
|
|
|
|
interface ICWMultiTokenBridgeL1Accounting {
|
|
function supportedCanonicalToken(address token) external view returns (bool);
|
|
function paused(address token) external view returns (bool);
|
|
function lockedBalance(address token) external view returns (uint256);
|
|
function totalOutstanding(address token) external view returns (uint256);
|
|
function outstandingMinted(address token, uint64 destinationChainSelector) external view returns (uint256);
|
|
function maxOutstanding(address token, uint64 destinationChainSelector) external view returns (uint256);
|
|
}
|
|
|
|
interface IOwnableLike {
|
|
function owner() external view returns (address);
|
|
}
|
|
|
|
interface IStablecoinReserveVaultLike {
|
|
function compliantUSDT() external view returns (address);
|
|
function compliantUSDC() external view returns (address);
|
|
function getBackingRatio(address token) external view returns (
|
|
uint256 reserveBalance,
|
|
uint256 tokenSupply,
|
|
uint256 backingRatio
|
|
);
|
|
function checkReserveAdequacy() external view returns (bool usdtAdequate, bool usdcAdequate);
|
|
}
|
|
|
|
/**
|
|
* @title CWReserveVerifier
|
|
* @notice Verifies bridge escrow and canonical reserve backing before allowing new cW minting.
|
|
* @dev Intended for cUSDC/cUSDT -> cWUSDC/cWUSDT hard-peg flows on Chain 138.
|
|
*/
|
|
contract CWReserveVerifier is AccessControl, ICWReserveVerifier {
|
|
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
|
|
uint256 public constant HARD_THRESHOLD_BPS = 10000;
|
|
|
|
struct TokenConfig {
|
|
bool enabled;
|
|
address reserveAsset;
|
|
bool requireVaultBacking;
|
|
bool requireReserveSystemBalance;
|
|
bool requireTokenOwnerMatchVault;
|
|
}
|
|
|
|
struct VerificationStatus {
|
|
bool supportedCanonicalToken;
|
|
bool bridgePaused;
|
|
bool escrowSufficient;
|
|
bool chainCapSufficient;
|
|
bool reserveSystemAdequate;
|
|
bool vaultAdequate;
|
|
bool tokenOwnerMatchesVault;
|
|
bool passes;
|
|
uint256 lockedBalanceAmount;
|
|
uint256 totalOutstandingAmount;
|
|
uint256 chainOutstandingAmount;
|
|
uint256 maxOutstandingAmount;
|
|
uint256 canonicalTotalSupply;
|
|
uint256 reserveSystemBalance;
|
|
uint256 vaultReserveBalance;
|
|
uint256 vaultBackingRatio;
|
|
}
|
|
|
|
ICWMultiTokenBridgeL1Accounting public bridge;
|
|
IStablecoinReserveVaultLike public stablecoinReserveVault;
|
|
IReserveSystem public reserveSystem;
|
|
|
|
mapping(address => TokenConfig) public tokenConfigs;
|
|
|
|
event BridgeUpdated(address indexed newBridge);
|
|
event StablecoinReserveVaultUpdated(address indexed newVault);
|
|
event ReserveSystemUpdated(address indexed newReserveSystem);
|
|
event TokenConfigured(
|
|
address indexed canonicalToken,
|
|
address indexed reserveAsset,
|
|
bool requireVaultBacking,
|
|
bool requireReserveSystemBalance,
|
|
bool requireTokenOwnerMatchVault
|
|
);
|
|
event TokenDisabled(address indexed canonicalToken);
|
|
|
|
error ZeroAddress();
|
|
error TokenNotConfigured();
|
|
error VaultRequired();
|
|
error ReserveSystemRequired();
|
|
error UnsupportedCanonicalToken();
|
|
error BridgeTokenPaused();
|
|
error EscrowInvariantViolation();
|
|
error ChainOutstandingCapExceeded();
|
|
error TokenOwnerMismatch();
|
|
error VaultBackingInsufficient();
|
|
error ReserveSystemBackingInsufficient();
|
|
|
|
constructor(
|
|
address admin,
|
|
address bridge_,
|
|
address stablecoinReserveVault_,
|
|
address reserveSystem_
|
|
) {
|
|
if (admin == address(0) || bridge_ == address(0)) {
|
|
revert ZeroAddress();
|
|
}
|
|
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
_grantRole(OPERATOR_ROLE, admin);
|
|
|
|
bridge = ICWMultiTokenBridgeL1Accounting(bridge_);
|
|
stablecoinReserveVault = IStablecoinReserveVaultLike(stablecoinReserveVault_);
|
|
reserveSystem = IReserveSystem(reserveSystem_);
|
|
}
|
|
|
|
function setBridge(address bridge_) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
if (bridge_ == address(0)) {
|
|
revert ZeroAddress();
|
|
}
|
|
bridge = ICWMultiTokenBridgeL1Accounting(bridge_);
|
|
emit BridgeUpdated(bridge_);
|
|
}
|
|
|
|
function setStablecoinReserveVault(address stablecoinReserveVault_) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
stablecoinReserveVault = IStablecoinReserveVaultLike(stablecoinReserveVault_);
|
|
emit StablecoinReserveVaultUpdated(stablecoinReserveVault_);
|
|
}
|
|
|
|
function setReserveSystem(address reserveSystem_) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
reserveSystem = IReserveSystem(reserveSystem_);
|
|
emit ReserveSystemUpdated(reserveSystem_);
|
|
}
|
|
|
|
function configureToken(
|
|
address canonicalToken,
|
|
address reserveAsset,
|
|
bool requireVaultBacking,
|
|
bool requireReserveSystemBalance,
|
|
bool requireTokenOwnerMatchVault
|
|
) external onlyRole(OPERATOR_ROLE) {
|
|
if (canonicalToken == address(0)) {
|
|
revert ZeroAddress();
|
|
}
|
|
if ((requireVaultBacking || requireTokenOwnerMatchVault) && address(stablecoinReserveVault) == address(0)) {
|
|
revert VaultRequired();
|
|
}
|
|
if (requireReserveSystemBalance && address(reserveSystem) == address(0)) {
|
|
revert ReserveSystemRequired();
|
|
}
|
|
if (requireReserveSystemBalance && reserveAsset == address(0)) {
|
|
revert ZeroAddress();
|
|
}
|
|
|
|
tokenConfigs[canonicalToken] = TokenConfig({
|
|
enabled: true,
|
|
reserveAsset: reserveAsset,
|
|
requireVaultBacking: requireVaultBacking,
|
|
requireReserveSystemBalance: requireReserveSystemBalance,
|
|
requireTokenOwnerMatchVault: requireTokenOwnerMatchVault
|
|
});
|
|
|
|
emit TokenConfigured(
|
|
canonicalToken,
|
|
reserveAsset,
|
|
requireVaultBacking,
|
|
requireReserveSystemBalance,
|
|
requireTokenOwnerMatchVault
|
|
);
|
|
}
|
|
|
|
function disableToken(address canonicalToken) external onlyRole(OPERATOR_ROLE) {
|
|
delete tokenConfigs[canonicalToken];
|
|
emit TokenDisabled(canonicalToken);
|
|
}
|
|
|
|
function verifyLock(
|
|
address canonicalToken,
|
|
uint64 destinationChainSelector,
|
|
uint256
|
|
) external view override returns (bool verified) {
|
|
TokenConfig memory config = tokenConfigs[canonicalToken];
|
|
if (!config.enabled) {
|
|
revert TokenNotConfigured();
|
|
}
|
|
|
|
VerificationStatus memory status = _buildStatus(canonicalToken, destinationChainSelector, config);
|
|
|
|
if (!status.supportedCanonicalToken) {
|
|
revert UnsupportedCanonicalToken();
|
|
}
|
|
if (status.bridgePaused) {
|
|
revert BridgeTokenPaused();
|
|
}
|
|
if (!status.escrowSufficient) {
|
|
revert EscrowInvariantViolation();
|
|
}
|
|
if (!status.chainCapSufficient) {
|
|
revert ChainOutstandingCapExceeded();
|
|
}
|
|
if (config.requireTokenOwnerMatchVault && !status.tokenOwnerMatchesVault) {
|
|
revert TokenOwnerMismatch();
|
|
}
|
|
if (config.requireVaultBacking && !status.vaultAdequate) {
|
|
revert VaultBackingInsufficient();
|
|
}
|
|
if (config.requireReserveSystemBalance && !status.reserveSystemAdequate) {
|
|
revert ReserveSystemBackingInsufficient();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getVerificationStatus(
|
|
address canonicalToken,
|
|
uint64 destinationChainSelector
|
|
) external view returns (VerificationStatus memory status) {
|
|
TokenConfig memory config = tokenConfigs[canonicalToken];
|
|
if (!config.enabled) {
|
|
revert TokenNotConfigured();
|
|
}
|
|
return _buildStatus(canonicalToken, destinationChainSelector, config);
|
|
}
|
|
|
|
function _buildStatus(
|
|
address canonicalToken,
|
|
uint64 destinationChainSelector,
|
|
TokenConfig memory config
|
|
) internal view returns (VerificationStatus memory status) {
|
|
status.supportedCanonicalToken = bridge.supportedCanonicalToken(canonicalToken);
|
|
status.bridgePaused = bridge.paused(canonicalToken);
|
|
status.lockedBalanceAmount = bridge.lockedBalance(canonicalToken);
|
|
status.totalOutstandingAmount = bridge.totalOutstanding(canonicalToken);
|
|
status.chainOutstandingAmount = bridge.outstandingMinted(canonicalToken, destinationChainSelector);
|
|
status.maxOutstandingAmount = bridge.maxOutstanding(canonicalToken, destinationChainSelector);
|
|
status.escrowSufficient = status.lockedBalanceAmount >= status.totalOutstandingAmount;
|
|
status.chainCapSufficient = status.maxOutstandingAmount == 0
|
|
|| status.chainOutstandingAmount <= status.maxOutstandingAmount;
|
|
|
|
status.canonicalTotalSupply = IERC20(canonicalToken).totalSupply();
|
|
|
|
status.reserveSystemAdequate = !config.requireReserveSystemBalance;
|
|
if (config.requireReserveSystemBalance) {
|
|
status.reserveSystemBalance = reserveSystem.getReserveBalance(config.reserveAsset);
|
|
status.reserveSystemAdequate = status.reserveSystemBalance >= status.canonicalTotalSupply;
|
|
}
|
|
|
|
status.vaultAdequate = !config.requireVaultBacking;
|
|
status.tokenOwnerMatchesVault = !config.requireTokenOwnerMatchVault;
|
|
if (config.requireVaultBacking || config.requireTokenOwnerMatchVault) {
|
|
address vaultAddress = address(stablecoinReserveVault);
|
|
|
|
if (config.requireTokenOwnerMatchVault) {
|
|
try IOwnableLike(canonicalToken).owner() returns (address tokenOwner) {
|
|
status.tokenOwnerMatchesVault = tokenOwner == vaultAddress;
|
|
} catch {
|
|
status.tokenOwnerMatchesVault = false;
|
|
}
|
|
}
|
|
|
|
if (config.requireVaultBacking) {
|
|
bool tokenTrackedByVault;
|
|
bool tokenAdequate;
|
|
|
|
try stablecoinReserveVault.compliantUSDT() returns (address compliantUSDT) {
|
|
if (canonicalToken == compliantUSDT) {
|
|
tokenTrackedByVault = true;
|
|
try stablecoinReserveVault.checkReserveAdequacy() returns (bool usdtAdequate, bool) {
|
|
tokenAdequate = usdtAdequate;
|
|
} catch {}
|
|
}
|
|
} catch {}
|
|
|
|
if (!tokenTrackedByVault) {
|
|
try stablecoinReserveVault.compliantUSDC() returns (address compliantUSDC) {
|
|
if (canonicalToken == compliantUSDC) {
|
|
tokenTrackedByVault = true;
|
|
try stablecoinReserveVault.checkReserveAdequacy() returns (bool, bool usdcAdequate) {
|
|
tokenAdequate = usdcAdequate;
|
|
} catch {}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
if (tokenTrackedByVault) {
|
|
try stablecoinReserveVault.getBackingRatio(canonicalToken) returns (
|
|
uint256 reserveBalance,
|
|
uint256,
|
|
uint256 backingRatio
|
|
) {
|
|
status.vaultReserveBalance = reserveBalance;
|
|
status.vaultBackingRatio = backingRatio;
|
|
status.vaultAdequate = tokenAdequate
|
|
&& backingRatio >= HARD_THRESHOLD_BPS
|
|
&& reserveBalance >= status.canonicalTotalSupply;
|
|
} catch {
|
|
status.vaultAdequate = false;
|
|
}
|
|
} else {
|
|
status.vaultAdequate = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
status.passes =
|
|
status.supportedCanonicalToken
|
|
&& !status.bridgePaused
|
|
&& status.escrowSufficient
|
|
&& status.chainCapSufficient
|
|
&& status.tokenOwnerMatchesVault
|
|
&& status.vaultAdequate
|
|
&& status.reserveSystemAdequate;
|
|
}
|
|
}
|