WIP: Zedxion bridge, GRU monetary policy gate, LiIndex flash vault, cw-settlement
This commit is contained in:
@@ -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}))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
217
contracts/bridge/ZedxionCustomBridge.sol
Normal file
217
contracts/bridge/ZedxionCustomBridge.sol
Normal 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 {}
|
||||
}
|
||||
158
contracts/bridge/adapters/evm/ZedxionAdapter.sol
Normal file
158
contracts/bridge/adapters/evm/ZedxionAdapter.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
35
contracts/bridge/interfaces/IZedxionTransport.sol
Normal file
35
contracts/bridge/interfaces/IZedxionTransport.sol
Normal 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);
|
||||
}
|
||||
72
contracts/compliance/libraries/MonetaryFormulas.sol
Normal file
72
contracts/compliance/libraries/MonetaryFormulas.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
93
contracts/cw-settlement/CWBuybackExecutor.sol
Normal file
93
contracts/cw-settlement/CWBuybackExecutor.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
162
contracts/cw-settlement/CWNavOracle.sol
Normal file
162
contracts/cw-settlement/CWNavOracle.sol
Normal 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];
|
||||
}
|
||||
}
|
||||
57
contracts/cw-settlement/CWProtocolTreasury.sol
Normal file
57
contracts/cw-settlement/CWProtocolTreasury.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
189
contracts/cw-settlement/CWRedemptionQueue.sol
Normal file
189
contracts/cw-settlement/CWRedemptionQueue.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
66
contracts/cw-settlement/CWStabilityFund.sol
Normal file
66
contracts/cw-settlement/CWStabilityFund.sol
Normal 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));
|
||||
}
|
||||
}
|
||||
74
contracts/cw-settlement/CWVotingEscrow.sol
Normal file
74
contracts/cw-settlement/CWVotingEscrow.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
193
contracts/flash/AaveUniV2CwStableRebalanceFlashReceiver.sol
Normal file
193
contracts/flash/AaveUniV2CwStableRebalanceFlashReceiver.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
105
contracts/rwa/GruMonetaryPolicyGate.sol
Normal file
105
contracts/rwa/GruMonetaryPolicyGate.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
43
contracts/rwa/IGruMonetaryPolicyGate.sol
Normal file
43
contracts/rwa/IGruMonetaryPolicyGate.sol
Normal 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);
|
||||
}
|
||||
184
contracts/rwa/LiIndexFlashVault.sol
Normal file
184
contracts/rwa/LiIndexFlashVault.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user