WIP: Zedxion bridge, GRU monetary policy gate, LiIndex flash vault, cw-settlement

This commit is contained in:
defiQUG
2026-06-02 06:09:36 -07:00
parent db517eca80
commit e1560a880b
17 changed files with 1694 additions and 12 deletions

View File

@@ -4,7 +4,9 @@ pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../ccip/IRouterClient.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/contracts/interfaces/IAny2EVMMessageReceiver.sol";
interface ICWMintBurnToken is IERC20 {
function mint(address to, uint256 amount) external;
@@ -15,14 +17,16 @@ interface ICWMintBurnToken is IERC20 {
* @title CWMultiTokenBridgeL2
* @notice Destination/public-chain bridge for minting and burning cW* assets without prefunding.
* @dev Supports multiple canonical -> mirrored token pairs behind one bridge address.
* Outbound CCIP uses Chainlink Client.EVM2AnyMessage encoding (required by public routers).
*/
contract CWMultiTokenBridgeL2 {
contract CWMultiTokenBridgeL2 is IAny2EVMMessageReceiver {
using SafeERC20 for IERC20;
IRouterClient public sendRouter;
address public receiveRouter;
address public feeToken;
address public admin;
uint256 public ccipGasLimit;
struct DestinationConfig {
address receiverBridge;
@@ -64,6 +68,7 @@ contract CWMultiTokenBridgeL2 {
event SendRouterUpdated(address indexed newRouter);
event ReceiveRouterUpdated(address indexed newRouter);
event FeeTokenUpdated(address indexed newFeeToken);
event CcipGasLimitUpdated(uint256 newGasLimit);
event AdminChanged(address indexed newAdmin);
modifier onlyAdmin() {
@@ -83,6 +88,7 @@ contract CWMultiTokenBridgeL2 {
receiveRouter = _receiveRouter;
feeToken = _feeToken;
admin = msg.sender;
ccipGasLimit = 300_000;
}
function configureTokenPair(address canonicalToken, address mirroredToken) external onlyAdmin {
@@ -148,6 +154,12 @@ contract CWMultiTokenBridgeL2 {
emit FeeTokenUpdated(newFeeToken);
}
function setCcipGasLimit(uint256 newGasLimit) external onlyAdmin {
require(newGasLimit > 0, "CWMultiTokenBridgeL2: zero gas limit");
ccipGasLimit = newGasLimit;
emit CcipGasLimitUpdated(newGasLimit);
}
function changeAdmin(address newAdmin) external onlyAdmin {
require(newAdmin != address(0), "CWMultiTokenBridgeL2: zero admin");
admin = newAdmin;
@@ -164,7 +176,10 @@ contract CWMultiTokenBridgeL2 {
require(canonicalToken != address(0), "CWMultiTokenBridgeL2: token not configured");
DestinationConfig memory dest = destinations[destinationChainSelector];
require(dest.enabled, "CWMultiTokenBridgeL2: destination disabled");
return sendRouter.getFee(destinationChainSelector, _buildMessage(dest.receiverBridge, canonicalToken, recipient, amount));
return sendRouter.getFee(
destinationChainSelector,
_buildMessage(dest.receiverBridge, canonicalToken, recipient, amount)
);
}
function circulatingSupply(address mirroredToken) external view returns (uint256) {
@@ -173,7 +188,7 @@ contract CWMultiTokenBridgeL2 {
return totalMinted > totalBurned ? totalMinted - totalBurned : 0;
}
function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external onlyReceiveRouter {
function ccipReceive(Client.Any2EVMMessage calldata message) external onlyReceiveRouter {
require(!processed[message.messageId], "CWMultiTokenBridgeL2: replayed");
(address canonicalToken, address recipient, uint256 amount) =
@@ -229,13 +244,13 @@ contract CWMultiTokenBridgeL2 {
IERC20(mirroredToken).totalSupply()
);
IRouterClient.EVM2AnyMessage memory message =
Client.EVM2AnyMessage memory message =
_buildMessage(dest.receiverBridge, canonicalToken, recipient, amount);
uint256 fee = sendRouter.getFee(destinationChainSelector, message);
_collectAndApproveFee(fee);
(messageId, ) = feeToken == address(0)
messageId = feeToken == address(0)
? sendRouter.ccipSend{value: fee}(destinationChainSelector, message)
: sendRouter.ccipSend(destinationChainSelector, message);
@@ -260,13 +275,13 @@ contract CWMultiTokenBridgeL2 {
address canonicalToken,
address recipient,
uint256 amount
) internal view returns (IRouterClient.EVM2AnyMessage memory message) {
message = IRouterClient.EVM2AnyMessage({
) internal view returns (Client.EVM2AnyMessage memory message) {
message = Client.EVM2AnyMessage({
receiver: abi.encode(receiverBridge),
data: abi.encode(canonicalToken, recipient, amount),
tokenAmounts: new IRouterClient.TokenAmount[](0),
tokenAmounts: new Client.EVMTokenAmount[](0),
feeToken: feeToken,
extraArgs: ""
extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: ccipGasLimit}))
});
}

View File

@@ -0,0 +1,217 @@
// 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/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./interfaces/IZedxionTransport.sol";
interface ICWMintBurnToken is IERC20 {
function mint(address to, uint256 amount) external;
function burnFrom(address from, uint256 amount) external;
}
/**
* @title ZedxionCustomBridge
* @notice GRU custom transport for Chain 138 ↔ ZEDXION (83872). No CCIP.
* @dev Deploy at same address on 138 and 83872 via CREATE2 (salt keccak256("ZedxionCustomBridge")).
* On 138: lock canonical c* and emit LockForZedxion.
* On 83872: relayer calls releaseOnZedxion to mint cW* or transfer escrowed assets.
* Reverse: lock on 83872 (LockFor138), relayer completes releaseOn138 on 138.
*/
contract ZedxionCustomBridge is IZedxionTransport, AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
uint256 public constant ZEDXION_CHAIN_ID = 83872;
uint256 public constant HUB_CHAIN_ID = 138;
mapping(bytes32 => LockRecord) public locks;
mapping(bytes32 => bool) public releasedOnDestination;
mapping(address => address) public canonicalToMirrored;
mapping(address => address) public mirroredToCanonical;
mapping(address => bool) public mintableMirroredToken;
mapping(address => uint256) public nonces;
bool private _hasRelayer;
struct LockRecord {
address sender;
address token;
uint256 amount;
address recipient;
uint256 createdAt;
bool released;
}
event LockForZedxion(
bytes32 indexed requestId,
address indexed sender,
address indexed token,
uint256 amount,
address recipient,
uint256 sourceChainId
);
event UnlockOnZedxion(
bytes32 indexed requestId,
address indexed recipient,
address indexed token,
uint256 amount
);
event LockFor138(
bytes32 indexed requestId,
address indexed sender,
address indexed token,
uint256 amount,
address recipient,
uint256 sourceChainId
);
event UnlockOn138(
bytes32 indexed requestId,
address indexed recipient,
address indexed token,
uint256 amount
);
event TokenPairConfigured(address indexed canonicalToken, address indexed mirroredToken);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(RELAYER_ROLE, admin);
_hasRelayer = true;
}
function zedxionChainId() external pure override returns (uint256) {
return ZEDXION_CHAIN_ID;
}
function configureTokenPair(address canonicalToken, address mirroredToken) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(canonicalToken != address(0) && mirroredToken != address(0), "zero token");
canonicalToMirrored[canonicalToken] = mirroredToken;
mirroredToCanonical[mirroredToken] = canonicalToken;
emit TokenPairConfigured(canonicalToken, mirroredToken);
}
function setMintableMirroredToken(address mirroredToken, bool enabled) external onlyRole(DEFAULT_ADMIN_ROLE) {
mintableMirroredToken[mirroredToken] = enabled;
}
function bridgeToZedxion(
address token,
uint256 amount,
address recipientOn83872
) external payable override nonReentrant returns (bytes32 requestId) {
require(block.chainid == HUB_CHAIN_ID, "ZedxionCustomBridge: hub only");
return _lock(token, amount, recipientOn83872, ZEDXION_CHAIN_ID);
}
function bridgeFromZedxion(
address tokenOn83872,
uint256 amount,
address recipientOn138
) external payable override nonReentrant returns (bytes32 requestId) {
require(block.chainid == ZEDXION_CHAIN_ID, "ZedxionCustomBridge: zedxion only");
return _lock(tokenOn83872, amount, recipientOn138, HUB_CHAIN_ID);
}
function _lock(
address token,
uint256 amount,
address recipient,
uint256 destinationChainId
) internal returns (bytes32 requestId) {
require(recipient != address(0), "zero recipient");
require(amount > 0, "zero amount");
requestId = keccak256(abi.encodePacked(
msg.sender,
token,
amount,
recipient,
nonces[msg.sender]++,
block.chainid,
block.timestamp
));
if (token == address(0)) {
require(msg.value >= amount, "insufficient value");
} else {
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
}
locks[requestId] = LockRecord({
sender: msg.sender,
token: token,
amount: amount,
recipient: recipient,
createdAt: block.timestamp,
released: false
});
if (destinationChainId == ZEDXION_CHAIN_ID) {
emit LockForZedxion(requestId, msg.sender, token, amount, recipient, block.chainid);
emit ZedxionBridgeInitiated(requestId, msg.sender, token, amount, recipient, ZEDXION_CHAIN_ID);
} else {
emit LockFor138(requestId, msg.sender, token, amount, recipient, block.chainid);
emit ZedxionBridgeInitiated(requestId, msg.sender, token, amount, recipient, HUB_CHAIN_ID);
}
}
/**
* @notice On 83872: mint cW* or release escrow after 138 lock. On 138: release canonical c* after 83872 burn/lock.
*/
function releaseOnZedxion(
bytes32 requestId,
address token,
uint256 amount,
address recipient
) external onlyRole(RELAYER_ROLE) nonReentrant {
require(block.chainid == ZEDXION_CHAIN_ID, "ZedxionCustomBridge: zedxion only");
_release(requestId, token, amount, recipient, true);
}
function releaseOn138(
bytes32 requestId,
address token,
uint256 amount,
address recipient
) external onlyRole(RELAYER_ROLE) nonReentrant {
require(block.chainid == HUB_CHAIN_ID, "ZedxionCustomBridge: hub only");
_release(requestId, token, amount, recipient, false);
}
function _release(
bytes32 requestId,
address token,
uint256 amount,
address recipient,
bool onZedxion
) internal {
require(!releasedOnDestination[requestId], "already released");
require(recipient != address(0), "zero recipient");
require(amount > 0, "zero amount");
releasedOnDestination[requestId] = true;
if (token == address(0)) {
(bool sent,) = payable(recipient).call{value: amount}("");
require(sent, "transfer failed");
} else if (mintableMirroredToken[token]) {
ICWMintBurnToken(token).mint(recipient, amount);
} else {
IERC20(token).safeTransfer(recipient, amount);
}
emit ZedxionBridgeCompleted(requestId, bytes32(0));
if (onZedxion) {
emit UnlockOnZedxion(requestId, recipient, token, amount);
} else {
emit UnlockOn138(requestId, recipient, token, amount);
}
}
receive() external payable {}
}

View File

@@ -0,0 +1,158 @@
// 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/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "../../interfaces/IChainAdapter.sol";
import "../../interfaces/IZedxionTransport.sol";
/**
* @title ZedxionAdapter
* @notice Bridge adapter for ZEDXION Smart Chain (ZED20), chainId 83872.
* @dev CCIP does not list 83872. Wire ZedxionCustomBridge via setZedxionTransport().
*/
contract ZedxionAdapter is IChainAdapter, AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant BRIDGE_OPERATOR_ROLE = keccak256("BRIDGE_OPERATOR_ROLE");
uint256 public constant ZEDXION_CHAIN_ID = 83872;
IZedxionTransport public zedxionTransport;
bool public isActive;
uint256 public bridgeFee;
mapping(bytes32 => BridgeRequest) public bridgeRequests;
mapping(address => uint256) public nonces;
event ZedxionAdapterBridge(
bytes32 indexed requestId,
address indexed sender,
address indexed token,
uint256 amount,
address recipient
);
event ZedxionBridgeConfirmed(bytes32 indexed requestId, bytes32 indexed destinationTxHash);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(BRIDGE_OPERATOR_ROLE, admin);
isActive = true;
bridgeFee = 0;
}
function setZedxionTransport(address transport) external onlyRole(DEFAULT_ADMIN_ROLE) {
zedxionTransport = IZedxionTransport(transport);
}
function setActive(bool active) external onlyRole(DEFAULT_ADMIN_ROLE) {
isActive = active;
}
function setBridgeFee(uint256 fee) external onlyRole(DEFAULT_ADMIN_ROLE) {
bridgeFee = fee;
}
function getChainType() external pure override returns (string memory) {
return "EVM";
}
function getChainIdentifier() external pure override returns (uint256 chainId, string memory identifier) {
return (ZEDXION_CHAIN_ID, "ZEDXION");
}
function validateDestination(bytes calldata destination) external pure override returns (bool) {
if (destination.length != 20) return false;
return address(bytes20(destination)) != address(0);
}
function bridge(
address token,
uint256 amount,
bytes calldata destination,
bytes calldata /* recipient */
) external payable override nonReentrant returns (bytes32 requestId) {
require(isActive, "ZedxionAdapter: inactive");
require(amount > 0, "ZedxionAdapter: zero amount");
require(address(zedxionTransport) != address(0), "ZedxionAdapter: transport not set");
require(this.validateDestination(destination), "ZedxionAdapter: invalid destination");
address recipientAddr = address(bytes20(destination));
requestId = keccak256(abi.encodePacked(
msg.sender,
token,
amount,
recipientAddr,
nonces[msg.sender]++,
block.timestamp
));
if (token == address(0)) {
require(msg.value >= amount, "ZedxionAdapter: insufficient value");
} else {
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
IERC20(token).forceApprove(address(zedxionTransport), amount);
}
bridgeRequests[requestId] = BridgeRequest({
sender: msg.sender,
token: token,
amount: amount,
destinationData: destination,
requestId: requestId,
status: BridgeStatus.Locked,
createdAt: block.timestamp,
completedAt: 0
});
zedxionTransport.bridgeToZedxion{value: msg.value}(token, amount, recipientAddr);
emit ZedxionAdapterBridge(requestId, msg.sender, token, amount, recipientAddr);
return requestId;
}
function getBridgeStatus(bytes32 requestId) external view override returns (BridgeRequest memory) {
return bridgeRequests[requestId];
}
function cancelBridge(bytes32 requestId) external override returns (bool) {
BridgeRequest storage request = bridgeRequests[requestId];
require(
request.status == BridgeStatus.Pending || request.status == BridgeStatus.Locked,
"ZedxionAdapter: cannot cancel"
);
require(msg.sender == request.sender, "ZedxionAdapter: not sender");
if (request.token == address(0)) {
payable(request.sender).transfer(request.amount);
} else {
IERC20(request.token).safeTransfer(request.sender, request.amount);
}
request.status = BridgeStatus.Cancelled;
return true;
}
function estimateFee(
address /* token */,
uint256 /* amount */,
bytes calldata /* destination */
) external view override returns (uint256) {
return bridgeFee;
}
function confirmBridge(bytes32 requestId, bytes32 destinationTxHash)
external
onlyRole(BRIDGE_OPERATOR_ROLE)
{
BridgeRequest storage request = bridgeRequests[requestId];
require(request.status == BridgeStatus.Locked, "ZedxionAdapter: invalid status");
request.status = BridgeStatus.Confirmed;
request.completedAt = block.timestamp;
emit ZedxionBridgeConfirmed(requestId, destinationTxHash);
}
}

View File

@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title IZedxionTransport
* @notice Transport layer for Chain 138 ↔ ZEDXION (83872) when CCIP is unavailable.
* @dev Production implementations may wrap Zedx official bridge contracts
* (https://bridge.zedxion.com/) or GRU CWMultiTokenBridge L1/L2 on 83872.
*/
interface IZedxionTransport {
event ZedxionBridgeInitiated(
bytes32 indexed requestId,
address indexed sender,
address indexed token,
uint256 amount,
address recipient,
uint256 destinationChainId
);
event ZedxionBridgeCompleted(bytes32 indexed requestId, bytes32 indexed destinationTxHash);
function zedxionChainId() external view returns (uint256);
function bridgeToZedxion(
address token,
uint256 amount,
address recipientOn83872
) external payable returns (bytes32 requestId);
function bridgeFromZedxion(
address tokenOn83872,
uint256 amount,
address recipientOn138
) external payable returns (bytes32 requestId);
}

View File

@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title MonetaryFormulas
* @notice Classical + GRU monetary formulas (canonical library — promoted from archive).
* @dev Formulas MUST match vault COMPLIANCE_REQUIREMENTS.md and GRU_Formulas.md crosswalk.
*
* Classical:
* - M = C + D; M = MB × m
* - V = PQ / M
* - m = 1 / r; m = (1 + c) / (r + c)
*
* GRU layer fan-out (policy): 1 M00 = 5 M0 = 25 M1 face units
*/
library MonetaryFormulas {
uint256 internal constant BP = 10_000;
uint256 internal constant WAD = 1e18;
/// @notice M = C + D
function moneySupplyCD(uint256 currency, uint256 deposits) internal pure returns (uint256) {
return currency + deposits;
}
/// @notice M = MB × m (m in 18 decimals)
function moneySupplyFromBase(uint256 monetaryBase, uint256 moneyMultiplierWad) internal pure returns (uint256) {
return (monetaryBase * moneyMultiplierWad) / WAD;
}
/// @notice V = PQ / M
function velocityPQOverM(uint256 priceLevel, uint256 quantity, uint256 moneySupply) internal pure returns (uint256) {
if (moneySupply == 0) return 0;
return (priceLevel * quantity) / moneySupply;
}
/// @notice m = 1 / r (r in basis points)
function simpleMultiplier(uint256 reserveRatioBps) internal pure returns (uint256) {
require(reserveRatioBps > 0 && reserveRatioBps <= BP, "MonetaryFormulas: r");
return (WAD * BP) / reserveRatioBps;
}
/// @notice m = (1 + c) / (r + c)
function multiplierWithCurrency(uint256 reserveRatioBps, uint256 currencyRatioBps) internal pure returns (uint256) {
require(reserveRatioBps > 0 && reserveRatioBps <= BP, "MonetaryFormulas: r");
require(currencyRatioBps <= BP, "MonetaryFormulas: c");
uint256 num = WAD * (BP + currencyRatioBps);
uint256 den = reserveRatioBps + currencyRatioBps;
require(den > 0, "MonetaryFormulas: den");
return num / den;
}
/// @notice Coverage ratio in bps: reserveValue / circulatingValue
function coverageRatioBps(uint256 reserveValue, uint256 circulatingValue) internal pure returns (uint256) {
if (circulatingValue == 0) return type(uint256).max;
return (reserveValue * BP) / circulatingValue;
}
/// @notice Effective GRU layer multiplier M00→M1 (25 face units per M00)
function gruM00ToM1Fanout() internal pure returns (uint256) {
return 25;
}
/// @notice 7:10 atomic issuance factor as WAD (10/7)
function gruAtomicIssuanceFactorWad() internal pure returns (uint256) {
return (10 * WAD) / 7;
}
/// @notice v_cov = v_adj × (coverageBps / 12000)
function coverageWeightedVelocity(uint256 vAdjWad, uint256 coverageBps) internal pure returns (uint256) {
return (vAdjWad * coverageBps) / 12_000;
}
}

View File

@@ -0,0 +1,93 @@
// 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/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "../bridge/integration/ICWReserveVerifier.sol";
interface ICWBurnable {
function burn(uint256 amount) external;
}
/**
* @title CWBuybackExecutor
* @notice Routes fee revenue into cW buybacks; burns purchased tokens or locks in treasury.
*/
contract CWBuybackExecutor is AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
ICWReserveVerifier public reserveVerifier;
address public treasuryLock;
bool public burnOnBuyback = true;
event ReserveVerifierUpdated(address indexed verifier);
event TreasuryLockUpdated(address indexed treasuryLock);
event BurnModeUpdated(bool burnOnBuyback);
event BuybackExecuted(address indexed paymentToken, address indexed cwToken, uint256 paid, uint256 received, bool burned);
error ZeroAddress();
error ReserveCheckFailed();
constructor(address admin, address reserveVerifier_, address treasuryLock_) {
if (admin == address(0)) {
revert ZeroAddress();
}
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(EXECUTOR_ROLE, admin);
reserveVerifier = ICWReserveVerifier(reserveVerifier_);
treasuryLock = treasuryLock_;
}
function setReserveVerifier(address verifier_) external onlyRole(DEFAULT_ADMIN_ROLE) {
reserveVerifier = ICWReserveVerifier(verifier_);
emit ReserveVerifierUpdated(verifier_);
}
function setTreasuryLock(address treasuryLock_) external onlyRole(DEFAULT_ADMIN_ROLE) {
treasuryLock = treasuryLock_;
emit TreasuryLockUpdated(treasuryLock_);
}
function setBurnOnBuyback(bool enabled) external onlyRole(DEFAULT_ADMIN_ROLE) {
burnOnBuyback = enabled;
emit BurnModeUpdated(enabled);
}
/**
* @notice Operator delivers pre-purchased cW tokens; contract burns or locks them after reserve check.
*/
function executeBuyback(
address canonicalToken,
uint64 destinationChainSelector,
address cwToken,
address paymentToken,
uint256 paymentAmount,
uint256 cwAmount
) external onlyRole(EXECUTOR_ROLE) nonReentrant {
if (address(reserveVerifier) != address(0)) {
if (!reserveVerifier.verifyLock(canonicalToken, destinationChainSelector, 0)) {
revert ReserveCheckFailed();
}
}
if (paymentAmount > 0 && paymentToken != address(0)) {
IERC20(paymentToken).safeTransferFrom(msg.sender, address(this), paymentAmount);
}
IERC20(cwToken).safeTransferFrom(msg.sender, address(this), cwAmount);
bool burned = false;
if (burnOnBuyback) {
ICWBurnable(cwToken).burn(cwAmount);
burned = true;
} else if (treasuryLock != address(0)) {
IERC20(cwToken).safeTransfer(treasuryLock, cwAmount);
}
emit BuybackExecuted(paymentToken, cwToken, paymentAmount, cwAmount, burned);
}
}

View File

@@ -0,0 +1,162 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "../reserve/IReserveSystem.sol";
interface ICWNavBridgeL1 {
function lockedBalance(address token) external view returns (uint256);
function totalOutstanding(address token) external view returns (uint256);
}
/**
* @title CWNavOracle
* @notice Publishes reserve-backed NAV for canonical c* / global cW supply pairs.
* @dev Global cW supply is aggregated off-chain across mesh chains and pushed by OPERATOR_ROLE.
* On-chain locked/outstanding reads come from CWMultiTokenBridgeL1.
*/
contract CWNavOracle is AccessControl {
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
uint256 public constant BPS = 10_000;
uint256 public constant NAV_SCALE = 1e18;
struct TokenConfig {
bool enabled;
address reserveAsset;
}
struct NavSnapshot {
uint256 totalLockedAssets;
uint256 totalReserveAssets;
uint256 totalCwSupply;
uint256 backingRatioBps;
uint256 navPerShare;
uint256 chain138CollateralBps;
uint256 treasuryReservesBps;
uint256 protocolFeesBps;
bytes32 attestationHash;
uint256 updatedAt;
}
ICWNavBridgeL1 public bridge;
IReserveSystem public reserveSystem;
mapping(address => TokenConfig) public tokenConfigs;
mapping(address => uint256) public globalCwSupply;
mapping(address => NavSnapshot) public latestSnapshot;
event BridgeUpdated(address indexed newBridge);
event ReserveSystemUpdated(address indexed newReserveSystem);
event TokenConfigured(address indexed canonicalToken, address indexed reserveAsset);
event TokenDisabled(address indexed canonicalToken);
event NavPublished(address indexed canonicalToken, uint256 backingRatioBps, uint256 navPerShare, bytes32 attestationHash);
error ZeroAddress();
error TokenNotConfigured();
constructor(address admin, address bridge_, address reserveSystem_) {
if (admin == address(0) || bridge_ == address(0)) {
revert ZeroAddress();
}
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(OPERATOR_ROLE, admin);
bridge = ICWNavBridgeL1(bridge_);
reserveSystem = IReserveSystem(reserveSystem_);
}
function setBridge(address bridge_) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (bridge_ == address(0)) {
revert ZeroAddress();
}
bridge = ICWNavBridgeL1(bridge_);
emit BridgeUpdated(bridge_);
}
function setReserveSystem(address reserveSystem_) external onlyRole(DEFAULT_ADMIN_ROLE) {
reserveSystem = IReserveSystem(reserveSystem_);
emit ReserveSystemUpdated(reserveSystem_);
}
function configureToken(address canonicalToken, address reserveAsset) external onlyRole(OPERATOR_ROLE) {
if (canonicalToken == address(0)) {
revert ZeroAddress();
}
tokenConfigs[canonicalToken] = TokenConfig({enabled: true, reserveAsset: reserveAsset});
emit TokenConfigured(canonicalToken, reserveAsset);
}
function disableToken(address canonicalToken) external onlyRole(OPERATOR_ROLE) {
delete tokenConfigs[canonicalToken];
emit TokenDisabled(canonicalToken);
}
function publishNav(
address canonicalToken,
uint256 globalCwSupplyAmount,
uint256 chain138CollateralBps,
uint256 treasuryReservesBps,
uint256 protocolFeesBps,
bytes32 attestationHash
) external onlyRole(OPERATOR_ROLE) {
TokenConfig memory config = tokenConfigs[canonicalToken];
if (!config.enabled) {
revert TokenNotConfigured();
}
require(chain138CollateralBps + treasuryReservesBps + protocolFeesBps == BPS, "CWNavOracle: bad decomposition");
globalCwSupply[canonicalToken] = globalCwSupplyAmount;
uint256 locked = bridge.lockedBalance(canonicalToken);
uint256 reserveBalance = address(reserveSystem) == address(0) || config.reserveAsset == address(0)
? 0
: reserveSystem.getReserveBalance(config.reserveAsset);
uint256 totalAssets = locked + reserveBalance;
uint256 backingRatioBps = globalCwSupplyAmount == 0 ? 0 : (totalAssets * BPS) / globalCwSupplyAmount;
uint256 navPerShareValue = globalCwSupplyAmount == 0 ? 0 : (totalAssets * NAV_SCALE) / globalCwSupplyAmount;
NavSnapshot memory snap = NavSnapshot({
totalLockedAssets: locked,
totalReserveAssets: reserveBalance,
totalCwSupply: globalCwSupplyAmount,
backingRatioBps: backingRatioBps,
navPerShare: navPerShareValue,
chain138CollateralBps: chain138CollateralBps,
treasuryReservesBps: treasuryReservesBps,
protocolFeesBps: protocolFeesBps,
attestationHash: attestationHash,
updatedAt: block.timestamp
});
latestSnapshot[canonicalToken] = snap;
emit NavPublished(canonicalToken, backingRatioBps, navPerShareValue, attestationHash);
}
function totalLockedAssets(address canonicalToken) external view returns (uint256) {
return bridge.lockedBalance(canonicalToken);
}
function totalReserveAssets(address canonicalToken) external view returns (uint256) {
TokenConfig memory config = tokenConfigs[canonicalToken];
if (!config.enabled || address(reserveSystem) == address(0) || config.reserveAsset == address(0)) {
return 0;
}
return reserveSystem.getReserveBalance(config.reserveAsset);
}
function totalCWUSupply(address canonicalToken) external view returns (uint256) {
return globalCwSupply[canonicalToken];
}
function backingRatio(address canonicalToken) external view returns (uint256 backingRatioBps) {
return latestSnapshot[canonicalToken].backingRatioBps;
}
function navPerShare(address canonicalToken) external view returns (uint256) {
return latestSnapshot[canonicalToken].navPerShare;
}
function getNavSnapshot(address canonicalToken) external view returns (NavSnapshot memory) {
return latestSnapshot[canonicalToken];
}
}

View File

@@ -0,0 +1,57 @@
// 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/utils/SafeERC20.sol";
/**
* @title CWProtocolTreasury
* @notice Holds protocol-owned liquidity proceeds and fee routing targets for POL flywheel.
*/
contract CWProtocolTreasury is AccessControl {
using SafeERC20 for IERC20;
bytes32 public constant FEE_ROUTER_ROLE = keccak256("FEE_ROUTER_ROLE");
bytes32 public constant POL_OPERATOR_ROLE = keccak256("POL_OPERATOR_ROLE");
address public buybackExecutor;
event BuybackExecutorUpdated(address indexed executor);
event FeeRouted(address indexed token, uint256 amount, address indexed destination);
event PolPositionRegistered(bytes32 indexed positionId, address indexed venue, address pair, uint256 lpBalance);
error ZeroAddress();
mapping(bytes32 => bool) public polPositionRegistered;
constructor(address admin, address buybackExecutor_) {
if (admin == address(0)) {
revert ZeroAddress();
}
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(FEE_ROUTER_ROLE, admin);
_grantRole(POL_OPERATOR_ROLE, admin);
buybackExecutor = buybackExecutor_;
}
function setBuybackExecutor(address executor_) external onlyRole(DEFAULT_ADMIN_ROLE) {
buybackExecutor = executor_;
emit BuybackExecutorUpdated(executor_);
}
function routeFees(address token, uint256 amount, address destination) external onlyRole(FEE_ROUTER_ROLE) {
IERC20(token).safeTransfer(destination, amount);
emit FeeRouted(token, amount, destination);
}
function registerPolPosition(
bytes32 positionId,
address venue,
address pair,
uint256 lpBalance
) external onlyRole(POL_OPERATOR_ROLE) {
polPositionRegistered[positionId] = true;
emit PolPositionRegistered(positionId, venue, pair, lpBalance);
}
}

View File

@@ -0,0 +1,189 @@
// 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/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title CWRedemptionQueue
* @notice Tiered redemption for hub c* collateral released from cW burn flows.
* @dev Instant <= instantTierMax, standard <= standardTierMax (24h delay), above => extended queue.
*/
contract CWRedemptionQueue is AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant BRIDGE_ROLE = keccak256("BRIDGE_ROLE");
bytes32 public constant PROCESSOR_ROLE = keccak256("PROCESSOR_ROLE");
enum Tier {
Instant,
Standard,
Extended
}
enum Status {
Pending,
Ready,
Claimed,
Cancelled
}
struct Request {
address beneficiary;
address token;
uint256 amount;
Tier tier;
Status status;
uint64 readyAt;
}
uint256 public instantTierMax = 5_000 * 1e6;
uint256 public standardTierMax = 50_000 * 1e6;
uint256 public standardDelay = 24 hours;
uint256 public extendedDelay = 72 hours;
uint256 public nextRequestId = 1;
mapping(uint256 => Request) public requests;
event TierLimitsUpdated(uint256 instantMax, uint256 standardMax, uint256 standardDelaySec, uint256 extendedDelaySec);
event RedemptionEnqueued(uint256 indexed requestId, address indexed beneficiary, address indexed token, uint256 amount, Tier tier);
event RedemptionReady(uint256 indexed requestId);
event RedemptionClaimed(uint256 indexed requestId, address indexed beneficiary, uint256 amount);
error ZeroAddress();
error InvalidAmount();
error NotReady();
error InvalidStatus();
constructor(address admin) {
if (admin == address(0)) {
revert ZeroAddress();
}
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(PROCESSOR_ROLE, admin);
}
function setTierLimits(
uint256 instantMax,
uint256 standardMax,
uint256 standardDelaySec,
uint256 extendedDelaySec
) external onlyRole(DEFAULT_ADMIN_ROLE) {
instantTierMax = instantMax;
standardTierMax = standardMax;
standardDelay = standardDelaySec;
extendedDelay = extendedDelaySec;
emit TierLimitsUpdated(instantMax, standardMax, standardDelaySec, extendedDelaySec);
}
function enqueueRedemption(
address beneficiary,
address token,
uint256 amount
) external onlyRole(BRIDGE_ROLE) nonReentrant returns (uint256 requestId) {
if (beneficiary == address(0) || token == address(0) || amount == 0) {
revert InvalidAmount();
}
Tier tier = _tierForAmount(amount);
uint64 readyAt = tier == Tier.Instant ? uint64(block.timestamp) : uint64(block.timestamp + _delayForTier(tier));
Status status = tier == Tier.Instant ? Status.Ready : Status.Pending;
requestId = nextRequestId++;
requests[requestId] = Request({
beneficiary: beneficiary,
token: token,
amount: amount,
tier: tier,
status: status,
readyAt: readyAt
});
emit RedemptionEnqueued(requestId, beneficiary, token, amount, tier);
}
function fundAndEnqueue(
address beneficiary,
address token,
uint256 amount
) external nonReentrant returns (uint256 requestId) {
if (beneficiary == address(0) || token == address(0) || amount == 0) {
revert InvalidAmount();
}
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
Tier tier = _tierForAmount(amount);
uint64 readyAt = tier == Tier.Instant ? uint64(block.timestamp) : uint64(block.timestamp + _delayForTier(tier));
Status status = tier == Tier.Instant ? Status.Ready : Status.Pending;
requestId = nextRequestId++;
requests[requestId] = Request({
beneficiary: beneficiary,
token: token,
amount: amount,
tier: tier,
status: status,
readyAt: readyAt
});
emit RedemptionEnqueued(requestId, beneficiary, token, amount, tier);
}
function markReady(uint256 requestId) external onlyRole(PROCESSOR_ROLE) {
Request storage req = requests[requestId];
if (req.status != Status.Pending) {
revert InvalidStatus();
}
req.status = Status.Ready;
emit RedemptionReady(requestId);
}
function claim(uint256 requestId) external nonReentrant {
Request storage req = requests[requestId];
if (req.status != Status.Ready) {
revert InvalidStatus();
}
if (block.timestamp < req.readyAt) {
revert NotReady();
}
if (msg.sender != req.beneficiary && !hasRole(PROCESSOR_ROLE, msg.sender)) {
revert("CWRedemptionQueue: not beneficiary");
}
req.status = Status.Claimed;
IERC20(req.token).safeTransfer(req.beneficiary, req.amount);
emit RedemptionClaimed(requestId, req.beneficiary, req.amount);
}
function processDueRequests(uint256[] calldata requestIds) external onlyRole(PROCESSOR_ROLE) {
for (uint256 i = 0; i < requestIds.length; i++) {
Request storage req = requests[requestIds[i]];
if (req.status == Status.Pending && block.timestamp >= req.readyAt) {
req.status = Status.Ready;
emit RedemptionReady(requestIds[i]);
}
}
}
function _tierForAmount(uint256 amount) internal view returns (Tier) {
if (amount <= instantTierMax) {
return Tier.Instant;
}
if (amount <= standardTierMax) {
return Tier.Standard;
}
return Tier.Extended;
}
function _delayForTier(Tier tier) internal view returns (uint256) {
if (tier == Tier.Standard) {
return standardDelay;
}
if (tier == Tier.Extended) {
return extendedDelay;
}
return 0;
}
}

View File

@@ -0,0 +1,66 @@
// 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/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title CWStabilityFund
* @notice Insurance / surplus buffer for bridge, oracle, and temporary depeg events.
*/
contract CWStabilityFund is AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE");
bytes32 public constant DEPOSITOR_ROLE = keccak256("DEPOSITOR_ROLE");
mapping(address => bool) public supportedAsset;
event AssetSupported(address indexed asset, bool supported);
event Deposited(address indexed asset, address indexed from, uint256 amount);
event EmergencyWithdrawn(address indexed asset, address indexed to, uint256 amount, bytes32 reason);
error ZeroAddress();
error UnsupportedAsset();
constructor(address admin) {
if (admin == address(0)) {
revert ZeroAddress();
}
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(EMERGENCY_ROLE, admin);
_grantRole(DEPOSITOR_ROLE, admin);
}
function setSupportedAsset(address asset, bool supported) external onlyRole(DEFAULT_ADMIN_ROLE) {
supportedAsset[asset] = supported;
emit AssetSupported(asset, supported);
}
function deposit(address asset, uint256 amount) external onlyRole(DEPOSITOR_ROLE) nonReentrant {
if (!supportedAsset[asset]) {
revert UnsupportedAsset();
}
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
emit Deposited(asset, msg.sender, amount);
}
function emergencyWithdraw(
address asset,
address to,
uint256 amount,
bytes32 reason
) external onlyRole(EMERGENCY_ROLE) nonReentrant {
if (to == address(0)) {
revert ZeroAddress();
}
IERC20(asset).safeTransfer(to, amount);
emit EmergencyWithdrawn(asset, to, amount, reason);
}
function balanceOf(address asset) external view returns (uint256) {
return IERC20(asset).balanceOf(address(this));
}
}

View File

@@ -0,0 +1,74 @@
// 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/utils/SafeERC20.sol";
/**
* @title CWVotingEscrow
* @notice Minimal vote-escrow (veCW) for emission-direction governance.
*/
contract CWVotingEscrow is AccessControl {
using SafeERC20 for IERC20;
IERC20 public immutable escrowToken;
uint256 public constant MAX_LOCK_DURATION = 4 * 365 days;
struct Lock {
uint256 amount;
uint256 unlockAt;
uint256 votingPower;
}
mapping(address => Lock[]) public locks;
event Locked(address indexed account, uint256 amount, uint256 unlockAt, uint256 votingPower);
event Withdrawn(address indexed account, uint256 amount);
error ZeroAddress();
error InvalidLock();
error NothingToWithdraw();
constructor(address admin, address escrowToken_) {
if (admin == address(0) || escrowToken_ == address(0)) {
revert ZeroAddress();
}
_grantRole(DEFAULT_ADMIN_ROLE, admin);
escrowToken = IERC20(escrowToken_);
}
function createLock(uint256 amount, uint256 lockDuration) external {
if (amount == 0 || lockDuration == 0 || lockDuration > MAX_LOCK_DURATION) {
revert InvalidLock();
}
escrowToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 unlockAt = block.timestamp + lockDuration;
uint256 votingPower = (amount * lockDuration) / MAX_LOCK_DURATION;
locks[msg.sender].push(Lock({amount: amount, unlockAt: unlockAt, votingPower: votingPower}));
emit Locked(msg.sender, amount, unlockAt, votingPower);
}
function withdraw(uint256 lockIndex) external {
Lock storage entry = locks[msg.sender][lockIndex];
if (entry.amount == 0 || block.timestamp < entry.unlockAt) {
revert NothingToWithdraw();
}
uint256 amount = entry.amount;
entry.amount = 0;
entry.votingPower = 0;
escrowToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
function votingPowerOf(address account) external view returns (uint256 total) {
Lock[] storage userLocks = locks[account];
for (uint256 i = 0; i < userLocks.length; i++) {
if (userLocks[i].amount > 0 && block.timestamp < userLocks[i].unlockAt) {
total += userLocks[i].votingPower;
}
}
}
}

View File

@@ -0,0 +1,193 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {
IAavePoolLike,
IAaveFlashLoanReceiver
} from "./AaveQuotePushFlashReceiver.sol";
interface IUniswapV2Router02Minimal {
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
function removeLiquidity(
address tokenA,
address tokenB,
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external returns (uint256 amountA, uint256 amountB);
}
/**
* @title AaveUniV2CwStableRebalanceFlashReceiver
* @notice Same-block Aave V3 USDC flash loan to quote-side rebalance a skewed cW/stable UniV2 pair,
* remove owned LP, sell cW* back into the pair for USDC, and repay Aave.
*
* @dev LP tokens must be on this contract before `runRebalanceRemove` (transfer or `pullLpFrom`).
*/
contract AaveUniV2CwStableRebalanceFlashReceiver is IAaveFlashLoanReceiver, Ownable {
using SafeERC20 for IERC20;
address public immutable pool;
struct RebalanceRemoveParams {
address router;
address pair;
address cwToken;
address stableToken;
uint256 lpAmount;
uint256 rebalanceStableIn;
uint256 minCwFromRebalance;
uint256 minStableFromRemove;
uint256 minCwFromRemove;
uint256 cwToSellForRepay;
uint256 minStableFromRepaySwap;
address recipient;
}
error UntrustedPool();
error UntrustedInitiator();
error BadParams();
error InsufficientToRepay();
error NothingToSweep();
event RebalanceRemoveExecuted(
address indexed pair,
address indexed stableToken,
uint256 borrowedAmount,
uint256 premium,
uint256 lpRemoved,
uint256 stableSurplus
);
event TokenSwept(address indexed token, address indexed to, uint256 amount);
constructor(address pool_, address initialOwner) Ownable(initialOwner) {
if (pool_ == address(0) || initialOwner == address(0)) revert BadParams();
pool = pool_;
}
function pullLpFrom(address pair, address from, uint256 amount) external onlyOwner {
if (pair == address(0) || from == address(0) || amount == 0) revert BadParams();
IERC20(pair).safeTransferFrom(from, address(this), amount);
}
function runRebalanceRemove(address stableToken, uint256 amount, RebalanceRemoveParams calldata params)
external
onlyOwner
{
if (stableToken == address(0) || amount == 0) revert BadParams();
if (params.lpAmount == 0 || params.rebalanceStableIn == 0 || params.recipient == address(0)) {
revert BadParams();
}
if (IERC20(params.pair).balanceOf(address(this)) < params.lpAmount) revert BadParams();
address[] memory assets = new address[](1);
uint256[] memory amts = new uint256[](1);
uint256[] memory modes = new uint256[](1);
assets[0] = stableToken;
amts[0] = amount;
modes[0] = 0;
IAavePoolLike(pool).flashLoan(
address(this), assets, amts, modes, address(this), abi.encode(address(this), params), 0
);
}
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external override returns (bool) {
if (msg.sender != pool) revert UntrustedPool();
if (assets.length != 1 || amounts.length != 1 || premiums.length != 1) revert BadParams();
_executeRebalanceRemove(assets[0], amounts[0], premiums[0], initiator, params);
return true;
}
function sweepToken(address token, address to, uint256 amount) external onlyOwner {
if (token == address(0) || to == address(0) || amount == 0) revert BadParams();
IERC20(token).safeTransfer(to, amount);
emit TokenSwept(token, to, amount);
}
function _executeRebalanceRemove(
address stableToken,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) internal {
(address expectedInitiator, RebalanceRemoveParams memory p) =
abi.decode(params, (address, RebalanceRemoveParams));
if (initiator != expectedInitiator) revert UntrustedInitiator();
if (
p.router == address(0) || p.pair == address(0) || p.cwToken == address(0)
|| p.stableToken != stableToken
) revert BadParams();
if (p.rebalanceStableIn != amount) revert BadParams();
uint256 deadline = block.timestamp;
address[] memory stableToCw = _path(p.stableToken, p.cwToken);
address[] memory cwToStable = _path(p.cwToken, p.stableToken);
IERC20(p.stableToken).forceApprove(p.router, p.rebalanceStableIn);
IUniswapV2Router02Minimal(p.router).swapExactTokensForTokens(
p.rebalanceStableIn, p.minCwFromRebalance, stableToCw, address(this), deadline
);
IERC20(p.pair).forceApprove(p.router, p.lpAmount);
(address tokenA, address tokenB) = _sortTokens(p.cwToken, p.stableToken);
(uint256 minA, uint256 minB) = p.cwToken < p.stableToken
? (p.minCwFromRemove, p.minStableFromRemove)
: (p.minStableFromRemove, p.minCwFromRemove);
IUniswapV2Router02Minimal(p.router).removeLiquidity(
tokenA, tokenB, p.lpAmount, minA, minB, address(this), deadline
);
uint256 repayNeed = amount + premium;
uint256 stableBal = IERC20(p.stableToken).balanceOf(address(this));
if (stableBal < repayNeed && p.cwToSellForRepay > 0) {
IERC20(p.cwToken).forceApprove(p.router, p.cwToSellForRepay);
IUniswapV2Router02Minimal(p.router).swapExactTokensForTokens(
p.cwToSellForRepay, p.minStableFromRepaySwap, cwToStable, address(this), deadline
);
stableBal = IERC20(p.stableToken).balanceOf(address(this));
}
if (stableBal < repayNeed) revert InsufficientToRepay();
uint256 surplus = stableBal - repayNeed;
IERC20(p.stableToken).forceApprove(pool, repayNeed);
if (surplus > 0) {
IERC20(p.stableToken).safeTransfer(p.recipient, surplus);
}
uint256 cwLeft = IERC20(p.cwToken).balanceOf(address(this));
if (cwLeft > 0) {
IERC20(p.cwToken).safeTransfer(p.recipient, cwLeft);
}
emit RebalanceRemoveExecuted(p.pair, p.stableToken, amount, premium, p.lpAmount, surplus);
}
function _path(address tokenIn, address tokenOut) internal pure returns (address[] memory path) {
path = new address[](2);
path[0] = tokenIn;
path[1] = tokenOut;
}
function _sortTokens(address tokenA, address tokenB) internal pure returns (address, address) {
return tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
}
}

View File

@@ -414,7 +414,7 @@ contract UniversalAssetRegistry is
jurisdiction,
50,
1,
type(uint256).max / 2
100_000_000 * 10 ** 6
);
}
@@ -462,7 +462,10 @@ contract UniversalAssetRegistry is
asset.volatilityScore = volatilityScore;
asset.minBridgeAmount = minBridgeAmount;
asset.maxBridgeAmount = maxBridgeAmount;
asset.dailyVolumeLimit = maxBridgeAmount * 10;
unchecked {
asset.dailyVolumeLimit =
maxBridgeAmount > type(uint256).max / 10 ? type(uint256).max : maxBridgeAmount * 10;
}
asset.requiresGovernance = _requiresGovernance(assetType, complianceLevel);
asset.isActive = true;
asset.registeredAt = block.timestamp;

View File

@@ -0,0 +1,105 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "../compliance/libraries/MonetaryFormulas.sol";
import "./IGruMonetaryPolicyGate.sol";
/**
* @title GruMonetaryPolicyGate
* @notice Enforces GRU Monetary Policy guardrails before Li* / M00 mint.
* @dev Operator updates metrics from `run-gru-monetary-metrics.mjs`; RWAToken mint calls canMint.
*/
contract GruMonetaryPolicyGate is IGruMonetaryPolicyGate, AccessControl {
bytes32 public constant METRICS_PUBLISHER_ROLE = keccak256("METRICS_PUBLISHER_ROLE");
bytes32 public constant GATE_ADMIN_ROLE = keccak256("GATE_ADMIN_ROLE");
uint256 public immutable coverageMinBps;
uint256 public immutable coverageAlertBps;
uint256 public immutable maxM1ToM00Utilization;
MetricsSnapshot private _metrics;
mapping(address => bool) public override tokenGateEnabled;
constructor(
address admin,
uint256 coverageMinBps_,
uint256 coverageAlertBps_,
uint256 maxM1ToM00Utilization_
) {
require(admin != address(0), "GruMonetaryPolicyGate: admin");
require(coverageMinBps_ >= coverageAlertBps_, "GruMonetaryPolicyGate: coverage");
require(maxM1ToM00Utilization_ > 0, "GruMonetaryPolicyGate: util");
coverageMinBps = coverageMinBps_;
coverageAlertBps = coverageAlertBps_;
maxM1ToM00Utilization = maxM1ToM00Utilization_;
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(GATE_ADMIN_ROLE, admin);
_grantRole(METRICS_PUBLISHER_ROLE, admin);
}
function metrics() external view override returns (MetricsSnapshot memory) {
return _metrics;
}
function setTokenGate(address token, bool enabled) external onlyRole(GATE_ADMIN_ROLE) {
tokenGateEnabled[token] = enabled;
emit PolicyGateSet(token, enabled);
}
function updateMetrics(
uint256 coverageRatioBps,
uint256 m1ToM00Utilization,
VelocityZone zone,
bool issuancePaused
) external onlyRole(METRICS_PUBLISHER_ROLE) {
_metrics = MetricsSnapshot({
coverageRatioBps: coverageRatioBps,
m1ToM00Utilization: m1ToM00Utilization,
velocityZone: zone,
metricsUpdatedAt: block.timestamp,
issuancePaused: issuancePaused
});
emit MetricsUpdated(coverageRatioBps, m1ToM00Utilization, zone, issuancePaused);
}
function canMint(address token, uint256) external view override returns (bool) {
(bool ok,) = canMintReason(token, 0);
return ok;
}
function canMintReason(address token, uint256)
public
view
override
returns (bool ok, bytes32 reason)
{
if (!tokenGateEnabled[token]) {
return (true, bytes32(0));
}
if (_metrics.issuancePaused) {
return (false, keccak256("ISSUANCE_PAUSED"));
}
if (_metrics.velocityZone == VelocityZone.Black || _metrics.velocityZone == VelocityZone.Red) {
return (false, keccak256("VELOCITY_ZONE_BLOCK"));
}
if (_metrics.coverageRatioBps < coverageAlertBps) {
return (false, keccak256("COVERAGE_BELOW_ALERT"));
}
if (_metrics.m1ToM00Utilization > maxM1ToM00Utilization) {
return (false, keccak256("M1_M00_UTILIZATION_EXCEEDED"));
}
if (_metrics.coverageRatioBps < coverageMinBps && _metrics.velocityZone >= VelocityZone.AmberHigh) {
return (false, keccak256("COVERAGE_VELOCITY_COMBO"));
}
token;
return (true, bytes32(0));
}
/// @notice Pure helper mirroring off-chain metrics job (coverage bps).
function computeCoverageBps(uint256 reserveValue, uint256 circulatingValue) external pure returns (uint256) {
return MonetaryFormulas.coverageRatioBps(reserveValue, circulatingValue);
}
}

View File

@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @notice On-chain GRU Monetary Policy mint gate (coverage, velocity zone, utilization).
*/
interface IGruMonetaryPolicyGate {
enum VelocityZone {
Green,
AmberLow,
AmberHigh,
Red,
Black
}
struct MetricsSnapshot {
uint256 coverageRatioBps;
uint256 m1ToM00Utilization; // whole number ratio numerator (e.g. 25 = 25:1)
VelocityZone velocityZone;
uint256 metricsUpdatedAt;
bool issuancePaused;
}
event MetricsUpdated(uint256 coverageRatioBps, uint256 m1ToM00Utilization, VelocityZone zone, bool issuancePaused);
event PolicyGateSet(address indexed token, bool enabled);
function metrics() external view returns (MetricsSnapshot memory);
function tokenGateEnabled(address token) external view returns (bool);
function setTokenGate(address token, bool enabled) external;
function updateMetrics(
uint256 coverageRatioBps,
uint256 m1ToM00Utilization,
VelocityZone zone,
bool issuancePaused
) external;
function canMint(address token, uint256 amount) external view returns (bool);
function canMintReason(address token, uint256 amount) external view returns (bool ok, bytes32 reason);
}

View File

@@ -0,0 +1,184 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
import "./IRWAToken.sol";
/**
* @title LiIndexFlashVault
* @notice IERC3156 flash lender for M00 Li* index tokens held in OMNL institutional vault.
* @dev Flash borrow permitted only when utilization LTV is below GRU Monetary Policy ceiling
* (default 80% = 20% operational buffer). Enforces index freshness and MPAP parity band.
*/
contract LiIndexFlashVault is IERC3156FlashLender, AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant VAULT_MANAGER_ROLE = keccak256("VAULT_MANAGER_ROLE");
bytes32 private constant _FLASH_RETURN = keccak256("ERC3156FlashBorrower.onFlashLoan");
/// @notice Max utilization LTV before flash disabled (8000 = 80%, retains 20% buffer).
uint256 public maxUtilizationLtvBps;
/// @notice Max single flash as share of vault balance (5000 = 50%).
uint256 public maxFlashShareOfVaultBps;
/// @notice Flash fee in basis points (≤ 9 bps per policy).
uint256 public flashFeeBps;
/// @notice MPAP parity band — block flash if index drift vs XAU anchor exceeds this.
uint256 public mpapParityBandBps;
/// @notice Max indexValue age (seconds) for flash eligibility.
uint256 public maxIndexAgeSeconds;
/// @notice XAU/USD anchor for parity drift check (8 decimals, e.g. 2400_00000000).
uint256 public xauUsdAnchor8;
mapping(address => bool) public supportedToken;
mapping(address => uint256) public vaultBalance;
mapping(address => uint256) public outstandingFlash;
event TokenSupported(address indexed token, bool supported);
event Deposited(address indexed token, address indexed from, uint256 amount);
event Withdrawn(address indexed token, address indexed to, uint256 amount);
event RiskParamsUpdated(
uint256 maxUtilizationLtvBps,
uint256 maxFlashShareOfVaultBps,
uint256 flashFeeBps,
uint256 mpapParityBandBps
);
constructor(
address admin,
uint256 maxUtilizationLtvBps_,
uint256 maxFlashShareOfVaultBps_,
uint256 flashFeeBps_,
uint256 mpapParityBandBps_,
uint256 maxIndexAgeSeconds_,
uint256 xauUsdAnchor8_
) {
require(admin != address(0), "LiIndexFlashVault: zero admin");
require(maxUtilizationLtvBps_ <= 10_000, "LiIndexFlashVault: ltv");
require(maxFlashShareOfVaultBps_ <= 10_000, "LiIndexFlashVault: flash share");
require(flashFeeBps_ <= 1000, "LiIndexFlashVault: fee");
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(VAULT_MANAGER_ROLE, admin);
maxUtilizationLtvBps = maxUtilizationLtvBps_;
maxFlashShareOfVaultBps = maxFlashShareOfVaultBps_;
flashFeeBps = flashFeeBps_;
mpapParityBandBps = mpapParityBandBps_;
maxIndexAgeSeconds = maxIndexAgeSeconds_;
xauUsdAnchor8 = xauUsdAnchor8_;
}
function setSupportedToken(address token, bool supported) external onlyRole(VAULT_MANAGER_ROLE) {
supportedToken[token] = supported;
emit TokenSupported(token, supported);
}
function setRiskParams(
uint256 maxUtilizationLtvBps_,
uint256 maxFlashShareOfVaultBps_,
uint256 flashFeeBps_,
uint256 mpapParityBandBps_
) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(maxUtilizationLtvBps_ <= 10_000, "LiIndexFlashVault: ltv");
require(maxFlashShareOfVaultBps_ <= 10_000, "LiIndexFlashVault: flash share");
require(flashFeeBps_ <= 1000, "LiIndexFlashVault: fee");
maxUtilizationLtvBps = maxUtilizationLtvBps_;
maxFlashShareOfVaultBps = maxFlashShareOfVaultBps_;
flashFeeBps = flashFeeBps_;
mpapParityBandBps = mpapParityBandBps_;
emit RiskParamsUpdated(maxUtilizationLtvBps_, maxFlashShareOfVaultBps_, flashFeeBps_, mpapParityBandBps_);
}
function deposit(address token, uint256 amount) external nonReentrant {
require(supportedToken[token], "LiIndexFlashVault: unsupported");
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
vaultBalance[token] += amount;
emit Deposited(token, msg.sender, amount);
}
function withdraw(address token, uint256 amount, address to) external nonReentrant onlyRole(VAULT_MANAGER_ROLE) {
require(to != address(0), "LiIndexFlashVault: zero to");
vaultBalance[token] -= amount;
IERC20(token).safeTransfer(to, amount);
emit Withdrawn(token, to, amount);
}
function currentUtilizationBps(address token) public view returns (uint256) {
uint256 bal = vaultBalance[token];
if (bal == 0) return 0;
return (outstandingFlash[token] * 10_000) / bal;
}
function flashBorrowEligible(address token) public view returns (bool) {
if (!supportedToken[token]) return false;
if (currentUtilizationBps(token) >= maxUtilizationLtvBps) return false;
IRWAToken idx = IRWAToken(token);
if (!idx.isRwaIndex()) return false;
uint256 updatedAt = idx.indexUpdatedAt();
if (updatedAt == 0 || block.timestamp - updatedAt > maxIndexAgeSeconds) return false;
uint256 indexLevel = idx.indexValue();
if (indexLevel == 0) return false;
if (xauUsdAnchor8 > 0 && mpapParityBandBps > 0) {
uint256 anchorLevel6 = (xauUsdAnchor8 / 1e2);
if (anchorLevel6 > 0) {
uint256 driftBps = indexLevel > anchorLevel6
? ((indexLevel - anchorLevel6) * 10_000) / indexLevel
: ((anchorLevel6 - indexLevel) * 10_000) / anchorLevel6;
if (driftBps >= mpapParityBandBps) return false;
}
}
return true;
}
function maxFlashLoan(address token) external view returns (uint256) {
if (!flashBorrowEligible(token)) return 0;
uint256 bal = vaultBalance[token];
uint256 outstanding = outstandingFlash[token];
uint256 lendable = (bal * maxUtilizationLtvBps) / 10_000;
if (outstanding >= lendable) return 0;
uint256 headroom = lendable - outstanding;
uint256 cap = (bal * maxFlashShareOfVaultBps) / 10_000;
return headroom < cap ? headroom : cap;
}
function flashFee(address token, uint256 amount) external view returns (uint256) {
token;
amount;
return (amount * flashFeeBps) / 10_000;
}
function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external nonReentrant returns (bool) {
require(flashBorrowEligible(token), "LiIndexFlashVault: not eligible");
require(amount <= this.maxFlashLoan(token), "LiIndexFlashVault: exceeds max");
uint256 fee = (amount * flashFeeBps) / 10_000;
outstandingFlash[token] += amount;
IERC20(token).safeTransfer(address(receiver), amount);
require(
receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _FLASH_RETURN,
"LiIndexFlashVault: callback failed"
);
IERC20(token).safeTransferFrom(address(receiver), address(this), amount + fee);
outstandingFlash[token] -= amount;
if (fee > 0) {
vaultBalance[token] += fee;
}
return true;
}
}

View File

@@ -11,6 +11,7 @@ import "./IRWAToken.sol";
import "./RWATokenInterfaces.sol";
import "./RWAEIP712.sol";
import "./libraries/RWATaxonomy.sol";
import "./IGruMonetaryPolicyGate.sol";
/**
* @title RWAToken
@@ -31,6 +32,9 @@ contract RWAToken is ERC20, Pausable, Ownable, LegallyCompliantBase, IRWAToken {
Classification private _classification;
uint256 private _indexValue;
uint256 private _indexUpdatedAt;
address private _policyGate;
event PolicyGateUpdated(address indexed previousGate, address indexed newGate);
constructor(
string memory name_,
@@ -146,6 +150,15 @@ contract RWAToken is ERC20, Pausable, Ownable, LegallyCompliantBase, IRWAToken {
|| super.supportsInterface(interfaceId);
}
function policyGate() external view returns (address) {
return _policyGate;
}
function setPolicyGate(address gate) external onlyOwner {
emit PolicyGateUpdated(_policyGate, gate);
_policyGate = gate;
}
function updateIndexValue(uint256 newValue) external override onlyRole(INDEX_PUBLISHER_ROLE) {
uint256 old = _indexValue;
_setIndexValue(newValue);
@@ -153,6 +166,9 @@ contract RWAToken is ERC20, Pausable, Ownable, LegallyCompliantBase, IRWAToken {
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
if (_policyGate != address(0)) {
require(IGruMonetaryPolicyGate(_policyGate).canMint(address(this), amount), "RWAToken: policy gate");
}
_mint(to, amount);
}