Add initial project structure and documentation files
- Created .gitignore to exclude sensitive files and directories. - Added API documentation in API_DOCUMENTATION.md. - Included deployment instructions in DEPLOYMENT.md. - Established project structure documentation in PROJECT_STRUCTURE.md. - Updated README.md with project status and team information. - Added recommendations and status tracking documents. - Introduced testing guidelines in TESTING.md. - Set up CI workflow in .github/workflows/ci.yml. - Created Dockerfile for backend and frontend setups. - Added various service and utility files for backend functionality. - Implemented frontend components and pages for user interface. - Included mobile app structure and services. - Established scripts for deployment across multiple chains.
This commit is contained in:
272
contracts/src/core/facets/CCIPFacet.sol
Normal file
272
contracts/src/core/facets/CCIPFacet.sol
Normal file
@@ -0,0 +1,272 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {ICCIPFacet} from "../../interfaces/ICCIPFacet.sol";
|
||||
import {ICCIPRouter} from "../../interfaces/ICCIPRouter.sol";
|
||||
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
|
||||
import {ILiquidityFacet} from "../../interfaces/ILiquidityFacet.sol";
|
||||
import {IVaultFacet} from "../../interfaces/IVaultFacet.sol";
|
||||
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
|
||||
|
||||
/**
|
||||
* @title CCIPFacet
|
||||
* @notice Cross-chain messaging via Chainlink CCIP with state synchronization
|
||||
*/
|
||||
contract CCIPFacet is ICCIPFacet {
|
||||
struct CCIPStorage {
|
||||
ICCIPRouter ccipRouter;
|
||||
mapping(uint256 => uint64) chainSelectors; // chainId => chainSelector
|
||||
mapping(uint64 => uint256) selectorToChain; // chainSelector => chainId
|
||||
mapping(uint256 => bool) supportedChains;
|
||||
mapping(bytes32 => bool) deliveredMessages;
|
||||
mapping(bytes32 => uint256) messageTimestamps;
|
||||
mapping(bytes32 => MessageStatus) messageStatuses;
|
||||
address authorizedSender; // Authorized sender for cross-chain messages
|
||||
}
|
||||
|
||||
enum MessageStatus {
|
||||
Pending,
|
||||
Delivered,
|
||||
Failed
|
||||
}
|
||||
|
||||
bytes32 private constant CCIP_STORAGE_POSITION = keccak256("asle.ccip.storage");
|
||||
|
||||
event MessageExecuted(bytes32 indexed messageId, MessageType messageType, bool success);
|
||||
event ChainSelectorUpdated(uint256 chainId, uint64 selector);
|
||||
|
||||
function ccipStorage() internal pure returns (CCIPStorage storage cs) {
|
||||
bytes32 position = CCIP_STORAGE_POSITION;
|
||||
assembly {
|
||||
cs.slot := position
|
||||
}
|
||||
}
|
||||
|
||||
modifier onlySupportedChain(uint256 chainId) {
|
||||
require(ccipStorage().supportedChains[chainId], "CCIPFacet: Chain not supported");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAuthorized() {
|
||||
CCIPStorage storage cs = ccipStorage();
|
||||
require(
|
||||
msg.sender == cs.authorizedSender ||
|
||||
cs.authorizedSender == address(0) ||
|
||||
LibAccessControl.hasRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender),
|
||||
"CCIPFacet: Unauthorized"
|
||||
);
|
||||
_;
|
||||
}
|
||||
|
||||
// ============ Liquidity Sync ============
|
||||
|
||||
function sendLiquiditySync(
|
||||
uint256 targetChainId,
|
||||
uint256 poolId
|
||||
) external override onlySupportedChain(targetChainId) returns (bytes32 messageId) {
|
||||
// Fetch pool data from LiquidityFacet
|
||||
ILiquidityFacet liquidityFacet = ILiquidityFacet(address(this));
|
||||
ILiquidityFacet.Pool memory pool = liquidityFacet.getPool(poolId);
|
||||
|
||||
LiquiditySyncPayload memory payload = LiquiditySyncPayload({
|
||||
poolId: poolId,
|
||||
baseReserve: pool.baseReserve,
|
||||
quoteReserve: pool.quoteReserve,
|
||||
virtualBaseReserve: pool.virtualBaseReserve,
|
||||
virtualQuoteReserve: pool.virtualQuoteReserve
|
||||
});
|
||||
|
||||
bytes memory encodedPayload = abi.encode(MessageType.LiquiditySync, payload);
|
||||
|
||||
messageId = _sendCCIPMessage(
|
||||
targetChainId,
|
||||
MessageType.LiquiditySync,
|
||||
encodedPayload
|
||||
);
|
||||
|
||||
emit CCIPMessageSent(messageId, block.chainid, targetChainId, MessageType.LiquiditySync);
|
||||
}
|
||||
|
||||
function sendVaultRebalance(
|
||||
uint256 targetChainId,
|
||||
uint256 vaultId,
|
||||
uint256 amount,
|
||||
address asset
|
||||
) external override onlySupportedChain(targetChainId) returns (bytes32 messageId) {
|
||||
VaultRebalancePayload memory payload = VaultRebalancePayload({
|
||||
vaultId: vaultId,
|
||||
targetChainId: targetChainId,
|
||||
amount: amount,
|
||||
asset: asset
|
||||
});
|
||||
|
||||
bytes memory encodedPayload = abi.encode(MessageType.VaultRebalance, payload);
|
||||
|
||||
messageId = _sendCCIPMessage(
|
||||
targetChainId,
|
||||
MessageType.VaultRebalance,
|
||||
encodedPayload
|
||||
);
|
||||
|
||||
emit VaultRebalanced(vaultId, block.chainid, targetChainId, amount);
|
||||
emit CCIPMessageSent(messageId, block.chainid, targetChainId, MessageType.VaultRebalance);
|
||||
}
|
||||
|
||||
function sendPriceDeviationWarning(
|
||||
uint256 targetChainId,
|
||||
uint256 poolId,
|
||||
uint256 deviation
|
||||
) external override onlySupportedChain(targetChainId) returns (bytes32 messageId) {
|
||||
ILiquidityFacet liquidityFacet = ILiquidityFacet(address(this));
|
||||
uint256 currentPrice = liquidityFacet.getPrice(poolId);
|
||||
|
||||
PriceDeviationPayload memory payload = PriceDeviationPayload({
|
||||
poolId: poolId,
|
||||
price: currentPrice,
|
||||
deviation: deviation,
|
||||
timestamp: block.timestamp
|
||||
});
|
||||
|
||||
bytes memory encodedPayload = abi.encode(MessageType.PriceDeviation, payload);
|
||||
|
||||
messageId = _sendCCIPMessage(
|
||||
targetChainId,
|
||||
MessageType.PriceDeviation,
|
||||
encodedPayload
|
||||
);
|
||||
|
||||
emit CCIPMessageSent(messageId, block.chainid, targetChainId, MessageType.PriceDeviation);
|
||||
}
|
||||
|
||||
// ============ Message Handling ============
|
||||
|
||||
function handleCCIPMessage(
|
||||
bytes32 messageId,
|
||||
uint256 sourceChainId,
|
||||
bytes calldata payload
|
||||
) external override onlyAuthorized {
|
||||
CCIPStorage storage cs = ccipStorage();
|
||||
require(!cs.deliveredMessages[messageId], "CCIPFacet: Message already processed");
|
||||
|
||||
cs.deliveredMessages[messageId] = true;
|
||||
cs.messageTimestamps[messageId] = block.timestamp;
|
||||
cs.messageStatuses[messageId] = MessageStatus.Pending;
|
||||
|
||||
(MessageType messageType, bytes memory data) = abi.decode(payload, (MessageType, bytes));
|
||||
|
||||
bool success = false;
|
||||
if (messageType == MessageType.LiquiditySync) {
|
||||
LiquiditySyncPayload memory syncPayload = abi.decode(data, (LiquiditySyncPayload));
|
||||
success = _handleLiquiditySync(syncPayload, sourceChainId);
|
||||
} else if (messageType == MessageType.VaultRebalance) {
|
||||
VaultRebalancePayload memory rebalancePayload = abi.decode(data, (VaultRebalancePayload));
|
||||
success = _handleVaultRebalance(rebalancePayload, sourceChainId);
|
||||
} else if (messageType == MessageType.PriceDeviation) {
|
||||
PriceDeviationPayload memory pricePayload = abi.decode(data, (PriceDeviationPayload));
|
||||
success = _handlePriceDeviation(pricePayload, sourceChainId);
|
||||
}
|
||||
|
||||
cs.messageStatuses[messageId] = success ? MessageStatus.Delivered : MessageStatus.Failed;
|
||||
emit CCIPMessageReceived(messageId, sourceChainId, messageType);
|
||||
emit MessageExecuted(messageId, messageType, success);
|
||||
}
|
||||
|
||||
// ============ Configuration ============
|
||||
|
||||
function setCCIPRouter(address router) external override {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
ccipStorage().ccipRouter = ICCIPRouter(router);
|
||||
}
|
||||
|
||||
function setSupportedChain(uint256 chainId, bool supported) external override {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
ccipStorage().supportedChains[chainId] = supported;
|
||||
}
|
||||
|
||||
function setChainSelector(uint256 chainId, uint64 selector) external {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
CCIPStorage storage cs = ccipStorage();
|
||||
cs.chainSelectors[chainId] = selector;
|
||||
cs.selectorToChain[selector] = chainId;
|
||||
emit ChainSelectorUpdated(chainId, selector);
|
||||
}
|
||||
|
||||
function setAuthorizedSender(address sender) external {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
ccipStorage().authorizedSender = sender;
|
||||
}
|
||||
|
||||
// ============ View Functions ============
|
||||
|
||||
function isChainSupported(uint256 chainId) external view override returns (bool) {
|
||||
return ccipStorage().supportedChains[chainId];
|
||||
}
|
||||
|
||||
function getMessageStatus(bytes32 messageId) external view override returns (bool delivered, uint256 timestamp) {
|
||||
CCIPStorage storage cs = ccipStorage();
|
||||
delivered = cs.deliveredMessages[messageId];
|
||||
timestamp = cs.messageTimestamps[messageId];
|
||||
}
|
||||
|
||||
function getChainSelector(uint256 chainId) external view returns (uint64) {
|
||||
return ccipStorage().chainSelectors[chainId];
|
||||
}
|
||||
|
||||
// ============ Internal Functions ============
|
||||
|
||||
function _sendCCIPMessage(
|
||||
uint256 targetChainId,
|
||||
MessageType messageType,
|
||||
bytes memory payload
|
||||
) internal returns (bytes32) {
|
||||
CCIPStorage storage cs = ccipStorage();
|
||||
require(address(cs.ccipRouter) != address(0), "CCIPFacet: Router not set");
|
||||
|
||||
uint64 chainSelector = cs.chainSelectors[targetChainId];
|
||||
require(chainSelector != 0, "CCIPFacet: Chain selector not set");
|
||||
|
||||
ICCIPRouter.EVM2AnyMessage memory message = ICCIPRouter.EVM2AnyMessage({
|
||||
receiver: abi.encode(address(this)),
|
||||
data: payload,
|
||||
tokenAmounts: new ICCIPRouter.EVMTokenAmount[](0),
|
||||
extraArgs: "",
|
||||
feeToken: address(0)
|
||||
});
|
||||
|
||||
uint256 fee = cs.ccipRouter.getFee(chainSelector, message);
|
||||
require(msg.value >= fee, "CCIPFacet: Insufficient fee");
|
||||
|
||||
return cs.ccipRouter.ccipSend{value: fee}(chainSelector, message);
|
||||
}
|
||||
|
||||
function _handleLiquiditySync(LiquiditySyncPayload memory payload, uint256 sourceChainId) internal returns (bool) {
|
||||
try this._syncPoolState(payload) {
|
||||
emit LiquiditySynced(payload.poolId, sourceChainId, payload.baseReserve, payload.quoteReserve);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function _syncPoolState(LiquiditySyncPayload memory payload) external {
|
||||
require(msg.sender == address(this), "CCIPFacet: Internal only");
|
||||
// In production, this would update pool virtual reserves based on cross-chain state
|
||||
// For now, we emit events and let the backend handle synchronization
|
||||
}
|
||||
|
||||
function _handleVaultRebalance(VaultRebalancePayload memory payload, uint256 sourceChainId) internal returns (bool) {
|
||||
// In production, this would trigger vault rebalancing logic
|
||||
// For now, emit event for backend processing
|
||||
emit VaultRebalanced(payload.vaultId, sourceChainId, payload.targetChainId, payload.amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
function _handlePriceDeviation(PriceDeviationPayload memory payload, uint256 sourceChainId) internal returns (bool) {
|
||||
// Trigger security alerts if deviation is significant
|
||||
if (payload.deviation > 500) { // 5% deviation threshold
|
||||
ISecurityFacet securityFacet = ISecurityFacet(address(this));
|
||||
// Could trigger circuit breaker or alert
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
92
contracts/src/core/facets/ChainConfigFacet.sol
Normal file
92
contracts/src/core/facets/ChainConfigFacet.sol
Normal file
@@ -0,0 +1,92 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {IChainConfigFacet} from "../../interfaces/IChainConfigFacet.sol";
|
||||
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
|
||||
|
||||
/**
|
||||
* @title ChainConfigFacet
|
||||
* @notice Manages chain-specific configurations for multi-chain operations
|
||||
*/
|
||||
contract ChainConfigFacet is IChainConfigFacet {
|
||||
struct ChainConfigStorage {
|
||||
mapping(uint256 => ChainConfig) chainConfigs;
|
||||
mapping(uint256 => bool) activeChains;
|
||||
}
|
||||
|
||||
bytes32 private constant CHAIN_CONFIG_STORAGE_POSITION = keccak256("asle.chainconfig.storage");
|
||||
|
||||
function chainConfigStorage() internal pure returns (ChainConfigStorage storage ccs) {
|
||||
bytes32 position = CHAIN_CONFIG_STORAGE_POSITION;
|
||||
assembly {
|
||||
ccs.slot := position
|
||||
}
|
||||
}
|
||||
|
||||
modifier onlyAdmin() {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
_;
|
||||
}
|
||||
|
||||
function setChainConfig(
|
||||
uint256 chainId,
|
||||
string calldata name,
|
||||
address nativeToken,
|
||||
string calldata explorerUrl,
|
||||
uint256 gasLimit,
|
||||
uint256 messageTimeout
|
||||
) external override onlyAdmin {
|
||||
ChainConfigStorage storage ccs = chainConfigStorage();
|
||||
|
||||
ccs.chainConfigs[chainId] = ChainConfig({
|
||||
chainId: chainId,
|
||||
name: name,
|
||||
nativeToken: nativeToken,
|
||||
explorerUrl: explorerUrl,
|
||||
gasLimit: gasLimit,
|
||||
messageTimeout: messageTimeout,
|
||||
active: ccs.activeChains[chainId] // Preserve existing active status
|
||||
});
|
||||
|
||||
emit ChainConfigUpdated(chainId, name, ccs.activeChains[chainId]);
|
||||
}
|
||||
|
||||
function getChainConfig(uint256 chainId) external view override returns (ChainConfig memory) {
|
||||
ChainConfigStorage storage ccs = chainConfigStorage();
|
||||
ChainConfig memory config = ccs.chainConfigs[chainId];
|
||||
require(config.chainId != 0 || chainId == 0, "ChainConfigFacet: Chain not configured");
|
||||
return config;
|
||||
}
|
||||
|
||||
function setChainActive(uint256 chainId, bool active) external override onlyAdmin {
|
||||
ChainConfigStorage storage ccs = chainConfigStorage();
|
||||
require(ccs.chainConfigs[chainId].chainId != 0 || chainId == 0, "ChainConfigFacet: Chain not configured");
|
||||
|
||||
ccs.activeChains[chainId] = active;
|
||||
ccs.chainConfigs[chainId].active = active;
|
||||
|
||||
emit ChainConfigUpdated(chainId, ccs.chainConfigs[chainId].name, active);
|
||||
}
|
||||
|
||||
function setChainGasLimit(uint256 chainId, uint256 gasLimit) external override onlyAdmin {
|
||||
ChainConfigStorage storage ccs = chainConfigStorage();
|
||||
require(ccs.chainConfigs[chainId].chainId != 0 || chainId == 0, "ChainConfigFacet: Chain not configured");
|
||||
|
||||
ccs.chainConfigs[chainId].gasLimit = gasLimit;
|
||||
emit ChainGasLimitUpdated(chainId, gasLimit);
|
||||
}
|
||||
|
||||
function setChainTimeout(uint256 chainId, uint256 timeout) external override onlyAdmin {
|
||||
ChainConfigStorage storage ccs = chainConfigStorage();
|
||||
require(ccs.chainConfigs[chainId].chainId != 0 || chainId == 0, "ChainConfigFacet: Chain not configured");
|
||||
|
||||
ccs.chainConfigs[chainId].messageTimeout = timeout;
|
||||
emit ChainTimeoutUpdated(chainId, timeout);
|
||||
}
|
||||
|
||||
function isChainActive(uint256 chainId) external view override returns (bool) {
|
||||
ChainConfigStorage storage ccs = chainConfigStorage();
|
||||
return ccs.activeChains[chainId];
|
||||
}
|
||||
}
|
||||
|
||||
268
contracts/src/core/facets/ComplianceFacet.sol
Normal file
268
contracts/src/core/facets/ComplianceFacet.sol
Normal file
@@ -0,0 +1,268 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {IComplianceFacet} from "../../interfaces/IComplianceFacet.sol";
|
||||
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
|
||||
|
||||
contract ComplianceFacet is IComplianceFacet {
|
||||
struct ComplianceStorage {
|
||||
mapping(address => UserCompliance) userCompliance;
|
||||
mapping(uint256 => ComplianceMode) vaultComplianceMode;
|
||||
mapping(address => bool) ofacSanctioned; // OFAC sanctions list
|
||||
mapping(bytes32 => bool) travelRuleTransactions; // FATF Travel Rule transaction tracking
|
||||
mapping(address => uint256) lastAuditTime;
|
||||
mapping(address => uint256) transactionCount; // Track transaction count per address
|
||||
mapping(address => uint256) dailyVolume; // Daily transaction volume
|
||||
mapping(address => uint256) lastDayReset; // Last day reset timestamp
|
||||
bool iso20022Enabled;
|
||||
bool travelRuleEnabled;
|
||||
bool automaticOFACCheck;
|
||||
uint256 travelRuleThreshold; // Minimum amount for Travel Rule (in wei)
|
||||
}
|
||||
|
||||
bytes32 private constant COMPLIANCE_STORAGE_POSITION = keccak256("asle.compliance.storage");
|
||||
|
||||
function complianceStorage() internal pure returns (ComplianceStorage storage cs) {
|
||||
bytes32 position = COMPLIANCE_STORAGE_POSITION;
|
||||
assembly {
|
||||
cs.slot := position
|
||||
}
|
||||
}
|
||||
|
||||
modifier onlyComplianceAdmin() {
|
||||
LibAccessControl.requireRole(LibAccessControl.COMPLIANCE_ADMIN_ROLE, msg.sender);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier requireCompliance(address user, ComplianceMode requiredMode) {
|
||||
require(canAccess(user, requiredMode), "ComplianceFacet: Compliance check failed");
|
||||
_;
|
||||
}
|
||||
|
||||
function setUserComplianceMode(
|
||||
address user,
|
||||
ComplianceMode mode
|
||||
) external override onlyComplianceAdmin {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
cs.userCompliance[user].mode = mode;
|
||||
cs.userCompliance[user].active = true;
|
||||
emit ComplianceModeSet(user, mode);
|
||||
}
|
||||
|
||||
function verifyKYC(address user, bool verified) external override onlyComplianceAdmin {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
cs.userCompliance[user].kycVerified = verified;
|
||||
emit KYCVerified(user, verified);
|
||||
}
|
||||
|
||||
function verifyAML(address user, bool verified) external override onlyComplianceAdmin {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
cs.userCompliance[user].amlVerified = verified;
|
||||
}
|
||||
|
||||
function getUserCompliance(
|
||||
address user
|
||||
) external view override returns (UserCompliance memory) {
|
||||
return complianceStorage().userCompliance[user];
|
||||
}
|
||||
|
||||
function canAccess(
|
||||
address user,
|
||||
ComplianceMode requiredMode
|
||||
) external view override returns (bool) {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
UserCompliance memory userComp = cs.userCompliance[user];
|
||||
|
||||
if (!userComp.active) {
|
||||
return requiredMode == ComplianceMode.Decentralized;
|
||||
}
|
||||
|
||||
if (requiredMode == ComplianceMode.Decentralized) {
|
||||
return true; // Anyone can access decentralized mode
|
||||
}
|
||||
|
||||
if (requiredMode == ComplianceMode.Fintech) {
|
||||
return userComp.mode == ComplianceMode.Fintech || userComp.mode == ComplianceMode.Regulated;
|
||||
}
|
||||
|
||||
if (requiredMode == ComplianceMode.Regulated) {
|
||||
return userComp.mode == ComplianceMode.Regulated &&
|
||||
userComp.kycVerified &&
|
||||
userComp.amlVerified;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setVaultComplianceMode(
|
||||
uint256 vaultId,
|
||||
ComplianceMode mode
|
||||
) external override onlyComplianceAdmin {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
cs.vaultComplianceMode[vaultId] = mode;
|
||||
}
|
||||
|
||||
function getVaultComplianceMode(
|
||||
uint256 vaultId
|
||||
) external view override returns (ComplianceMode) {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
return cs.vaultComplianceMode[vaultId];
|
||||
}
|
||||
|
||||
// Phase 3: Enhanced Compliance Functions
|
||||
|
||||
function checkOFACSanctions(address user) external view returns (bool) {
|
||||
return complianceStorage().ofacSanctioned[user];
|
||||
}
|
||||
|
||||
function setOFACSanctioned(address user, bool sanctioned) external onlyComplianceAdmin {
|
||||
complianceStorage().ofacSanctioned[user] = sanctioned;
|
||||
emit IComplianceFacet.OFACCheck(user, sanctioned);
|
||||
}
|
||||
|
||||
function recordTravelRule(
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount,
|
||||
bytes32 transactionHash
|
||||
) external {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
require(cs.travelRuleEnabled, "ComplianceFacet: Travel Rule not enabled");
|
||||
require(amount >= cs.travelRuleThreshold, "ComplianceFacet: Amount below Travel Rule threshold");
|
||||
|
||||
cs.travelRuleTransactions[transactionHash] = true;
|
||||
emit IComplianceFacet.TravelRuleCompliance(from, to, amount, transactionHash);
|
||||
}
|
||||
|
||||
function getTravelRuleStatus(bytes32 transactionHash) external view returns (bool) {
|
||||
return complianceStorage().travelRuleTransactions[transactionHash];
|
||||
}
|
||||
|
||||
function setTravelRuleThreshold(uint256 threshold) external onlyComplianceAdmin {
|
||||
complianceStorage().travelRuleThreshold = threshold;
|
||||
}
|
||||
|
||||
function recordISO20022Message(
|
||||
address user,
|
||||
string calldata messageType,
|
||||
bytes32 messageId
|
||||
) external onlyComplianceAdmin {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
require(cs.iso20022Enabled, "ComplianceFacet: ISO 20022 not enabled");
|
||||
|
||||
// Use events instead of storage for ISO messages (storage optimization)
|
||||
emit IComplianceFacet.ISO20022Message(user, messageType, messageId);
|
||||
}
|
||||
|
||||
function enableISO20022(bool enabled) external onlyComplianceAdmin {
|
||||
complianceStorage().iso20022Enabled = enabled;
|
||||
}
|
||||
|
||||
function enableTravelRule(bool enabled) external onlyComplianceAdmin {
|
||||
complianceStorage().travelRuleEnabled = enabled;
|
||||
}
|
||||
|
||||
function recordAudit(address user) external onlyComplianceAdmin {
|
||||
complianceStorage().lastAuditTime[user] = block.timestamp;
|
||||
}
|
||||
|
||||
function getLastAuditTime(address user) external view returns (uint256) {
|
||||
return complianceStorage().lastAuditTime[user];
|
||||
}
|
||||
|
||||
function validateTransaction(
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount
|
||||
) external view returns (bool) {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
|
||||
// Automatic OFAC sanctions check
|
||||
if (cs.automaticOFACCheck || cs.ofacSanctioned[from] || cs.ofacSanctioned[to]) {
|
||||
if (cs.ofacSanctioned[from] || cs.ofacSanctioned[to]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check compliance modes
|
||||
UserCompliance memory fromComp = cs.userCompliance[from];
|
||||
UserCompliance memory toComp = cs.userCompliance[to];
|
||||
|
||||
// Both parties must meet minimum compliance requirements
|
||||
if (fromComp.mode == ComplianceMode.Regulated || toComp.mode == ComplianceMode.Regulated) {
|
||||
return fromComp.kycVerified && fromComp.amlVerified &&
|
||||
toComp.kycVerified && toComp.amlVerified;
|
||||
}
|
||||
|
||||
// Check Travel Rule requirements
|
||||
if (cs.travelRuleEnabled && amount >= cs.travelRuleThreshold) {
|
||||
// Travel Rule compliance should be checked separately via recordTravelRule
|
||||
// This is a basic validation
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Automatic OFAC check on transaction (called by other facets)
|
||||
*/
|
||||
function performAutomaticOFACCheck(address user) external returns (bool) {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
if (cs.automaticOFACCheck) {
|
||||
// In production, this would call an external service or oracle
|
||||
// For now, just check the stored list
|
||||
return !cs.ofacSanctioned[user];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Batch set OFAC sanctions
|
||||
*/
|
||||
function batchSetOFACSanctions(address[] calldata users, bool[] calldata sanctioned) external onlyComplianceAdmin {
|
||||
require(users.length == sanctioned.length, "ComplianceFacet: Arrays length mismatch");
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
for (uint i = 0; i < users.length; i++) {
|
||||
cs.ofacSanctioned[users[i]] = sanctioned[i];
|
||||
emit IComplianceFacet.OFACCheck(users[i], sanctioned[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Enable/disable automatic OFAC checking
|
||||
*/
|
||||
function setAutomaticOFACCheck(bool enabled) external onlyComplianceAdmin {
|
||||
complianceStorage().automaticOFACCheck = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get transaction statistics for address
|
||||
*/
|
||||
function getTransactionStats(address user) external view returns (uint256 count, uint256 dailyVol) {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
// Reset daily volume if new day
|
||||
if (block.timestamp >= cs.lastDayReset[user] + 1 days) {
|
||||
dailyVol = 0;
|
||||
} else {
|
||||
dailyVol = cs.dailyVolume[user];
|
||||
}
|
||||
return (cs.transactionCount[user], dailyVol);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record transaction for compliance tracking
|
||||
*/
|
||||
function recordTransaction(address from, address to, uint256 amount) external {
|
||||
ComplianceStorage storage cs = complianceStorage();
|
||||
|
||||
// Reset daily volume if new day
|
||||
if (block.timestamp >= cs.lastDayReset[from] + 1 days) {
|
||||
cs.dailyVolume[from] = 0;
|
||||
cs.lastDayReset[from] = block.timestamp;
|
||||
}
|
||||
|
||||
cs.transactionCount[from]++;
|
||||
cs.dailyVolume[from] += amount;
|
||||
}
|
||||
}
|
||||
|
||||
24
contracts/src/core/facets/DiamondCutFacet.sol
Normal file
24
contracts/src/core/facets/DiamondCutFacet.sol
Normal file
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {IDiamondCut} from "../../interfaces/IDiamondCut.sol";
|
||||
import {LibDiamond, LibDiamondCut} from "../../libraries/LibDiamond.sol";
|
||||
|
||||
contract DiamondCutFacet is IDiamondCut {
|
||||
/// @notice Add/replace/remove any number of functions and optionally execute
|
||||
/// a function with delegatecall
|
||||
/// @param _diamondCut Contains the facet addresses and function selectors
|
||||
/// @param _init The address of the contract or facet to execute _calldata
|
||||
/// @param _calldata A function call, including function selector and arguments
|
||||
/// _calldata is executed with delegatecall on _init
|
||||
function diamondCut(
|
||||
FacetCut[] calldata _diamondCut,
|
||||
address _init,
|
||||
bytes calldata _calldata
|
||||
) external override {
|
||||
LibDiamond.enforceIsContractOwner();
|
||||
LibDiamond.diamondCut(_diamondCut, _init, _calldata);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
394
contracts/src/core/facets/GovernanceFacet.sol
Normal file
394
contracts/src/core/facets/GovernanceFacet.sol
Normal file
@@ -0,0 +1,394 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {IGovernanceFacet} from "../../interfaces/IGovernanceFacet.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {LibDiamond} from "../../libraries/LibDiamond.sol";
|
||||
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
|
||||
import {IDiamondCut} from "../../interfaces/IDiamondCut.sol";
|
||||
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
|
||||
|
||||
contract GovernanceFacet is IGovernanceFacet {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
struct GovernanceStorage {
|
||||
mapping(uint256 => Proposal) proposals;
|
||||
uint256 proposalCount;
|
||||
address governanceToken; // ERC-20 token for voting
|
||||
uint256 quorumThreshold; // Minimum votes required
|
||||
uint256 votingPeriod; // Default voting period in seconds
|
||||
uint256 timelockDelay; // Delay before execution
|
||||
mapping(uint256 => uint256) proposalTimelocks; // proposalId => execution time
|
||||
mapping(address => uint256) treasuryBalances; // token => balance
|
||||
uint256 minProposalThreshold; // Minimum tokens required to create proposal
|
||||
mapping(address => address) delegations; // delegator => delegatee
|
||||
mapping(address => uint256) checkpoints; // account => voting power checkpoint
|
||||
}
|
||||
|
||||
bytes32 private constant GOVERNANCE_STORAGE_POSITION = keccak256("asle.governance.storage");
|
||||
|
||||
function governanceStorage() internal pure returns (GovernanceStorage storage gs) {
|
||||
bytes32 position = GOVERNANCE_STORAGE_POSITION;
|
||||
assembly {
|
||||
gs.slot := position
|
||||
}
|
||||
}
|
||||
|
||||
modifier onlyProposer() {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
if (gs.governanceToken != address(0)) {
|
||||
uint256 balance = IERC20(gs.governanceToken).balanceOf(msg.sender);
|
||||
require(balance >= gs.minProposalThreshold, "GovernanceFacet: Insufficient tokens to propose");
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
function createProposal(
|
||||
ProposalType proposalType,
|
||||
string calldata description,
|
||||
bytes calldata data,
|
||||
uint256 votingPeriod
|
||||
) external override onlyProposer returns (uint256 proposalId) {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
proposalId = gs.proposalCount;
|
||||
gs.proposalCount++;
|
||||
|
||||
Proposal storage proposal = gs.proposals[proposalId];
|
||||
proposal.id = proposalId;
|
||||
proposal.proposalType = proposalType;
|
||||
proposal.status = ProposalStatus.Pending;
|
||||
proposal.proposer = msg.sender;
|
||||
proposal.description = description;
|
||||
proposal.data = data;
|
||||
proposal.startTime = block.timestamp;
|
||||
proposal.endTime = block.timestamp + (votingPeriod > 0 ? votingPeriod : gs.votingPeriod);
|
||||
proposal.forVotes = 0;
|
||||
proposal.againstVotes = 0;
|
||||
|
||||
// Auto-activate if voting period is immediate
|
||||
if (votingPeriod == 0) {
|
||||
proposal.status = ProposalStatus.Active;
|
||||
}
|
||||
|
||||
emit ProposalCreated(proposalId, proposalType, msg.sender);
|
||||
}
|
||||
|
||||
function vote(uint256 proposalId, bool support) external override {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
Proposal storage proposal = gs.proposals[proposalId];
|
||||
|
||||
require(proposal.status == ProposalStatus.Active, "GovernanceFacet: Proposal not active");
|
||||
require(block.timestamp <= proposal.endTime, "GovernanceFacet: Voting period ended");
|
||||
require(!proposal.hasVoted[msg.sender], "GovernanceFacet: Already voted");
|
||||
|
||||
address voter = msg.sender;
|
||||
address delegatee = gs.delegations[voter];
|
||||
|
||||
// If voting power is delegated, the delegatee should vote
|
||||
if (delegatee != address(0) && delegatee != voter) {
|
||||
require(msg.sender == delegatee, "GovernanceFacet: Only delegatee can vote");
|
||||
voter = delegatee;
|
||||
}
|
||||
|
||||
uint256 votingPower = _getVotingPower(voter);
|
||||
require(votingPower > 0, "GovernanceFacet: No voting power");
|
||||
|
||||
proposal.hasVoted[msg.sender] = true;
|
||||
|
||||
if (support) {
|
||||
proposal.forVotes += votingPower;
|
||||
} else {
|
||||
proposal.againstVotes += votingPower;
|
||||
}
|
||||
|
||||
emit VoteCast(proposalId, msg.sender, support, votingPower);
|
||||
|
||||
// Check if proposal can be passed
|
||||
_checkProposalStatus(proposalId);
|
||||
}
|
||||
|
||||
function executeProposal(uint256 proposalId) external override {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
Proposal storage proposal = gs.proposals[proposalId];
|
||||
|
||||
require(proposal.status == ProposalStatus.Passed, "GovernanceFacet: Proposal not passed");
|
||||
require(block.timestamp > proposal.endTime, "GovernanceFacet: Voting still active");
|
||||
|
||||
// Check timelock
|
||||
uint256 executionTime = gs.proposalTimelocks[proposalId];
|
||||
if (executionTime > 0) {
|
||||
require(block.timestamp >= executionTime, "GovernanceFacet: Timelock not expired");
|
||||
}
|
||||
|
||||
proposal.status = ProposalStatus.Executed;
|
||||
|
||||
// Execute actions if multi-action proposal
|
||||
if (proposal.actions.length > 0) {
|
||||
for (uint256 i = 0; i < proposal.actions.length; i++) {
|
||||
Action storage action = proposal.actions[i];
|
||||
require(!action.executed, "GovernanceFacet: Action already executed");
|
||||
|
||||
(bool success, ) = action.target.call{value: action.value}(action.data);
|
||||
require(success, "GovernanceFacet: Action execution failed");
|
||||
|
||||
action.executed = true;
|
||||
}
|
||||
} else {
|
||||
// Execute proposal based on type (legacy single-action)
|
||||
if (proposal.proposalType == ProposalType.TreasuryWithdrawal) {
|
||||
_executeTreasuryWithdrawal(proposal.data);
|
||||
} else if (proposal.proposalType == ProposalType.FacetUpgrade) {
|
||||
_executeFacetUpgrade(proposal.data);
|
||||
} else if (proposal.proposalType == ProposalType.EmergencyPause) {
|
||||
_executeEmergencyPause(proposal.data);
|
||||
} else if (proposal.proposalType == ProposalType.ComplianceChange) {
|
||||
_executeComplianceChange(proposal.data);
|
||||
} else if (proposal.proposalType == ProposalType.ParameterChange) {
|
||||
_executeParameterChange(proposal.data);
|
||||
}
|
||||
}
|
||||
|
||||
emit ProposalExecuted(proposalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multi-action proposal
|
||||
*/
|
||||
function createMultiActionProposal(
|
||||
string calldata description,
|
||||
Action[] calldata actions,
|
||||
uint256 votingPeriod
|
||||
) external onlyProposer returns (uint256 proposalId) {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
proposalId = gs.proposalCount;
|
||||
gs.proposalCount++;
|
||||
|
||||
Proposal storage proposal = gs.proposals[proposalId];
|
||||
proposal.id = proposalId;
|
||||
proposal.proposalType = ProposalType.ParameterChange; // Default type for multi-action
|
||||
proposal.status = ProposalStatus.Pending;
|
||||
proposal.proposer = msg.sender;
|
||||
proposal.description = description;
|
||||
proposal.startTime = block.timestamp;
|
||||
proposal.endTime = block.timestamp + (votingPeriod > 0 ? votingPeriod : gs.votingPeriod);
|
||||
proposal.forVotes = 0;
|
||||
proposal.againstVotes = 0;
|
||||
|
||||
// Store actions
|
||||
for (uint256 i = 0; i < actions.length; i++) {
|
||||
proposal.actions.push(actions[i]);
|
||||
}
|
||||
|
||||
if (votingPeriod == 0) {
|
||||
proposal.status = ProposalStatus.Active;
|
||||
}
|
||||
|
||||
emit ProposalCreated(proposalId, proposal.proposalType, msg.sender);
|
||||
}
|
||||
|
||||
function cancelProposal(uint256 proposalId) external {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
Proposal storage proposal = gs.proposals[proposalId];
|
||||
|
||||
require(proposal.proposer == msg.sender || LibAccessControl.hasRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender),
|
||||
"GovernanceFacet: Not authorized");
|
||||
require(proposal.status == ProposalStatus.Active || proposal.status == ProposalStatus.Pending,
|
||||
"GovernanceFacet: Cannot cancel");
|
||||
|
||||
proposal.status = ProposalStatus.Rejected;
|
||||
}
|
||||
|
||||
function proposeTreasuryWithdrawal(
|
||||
address recipient,
|
||||
uint256 amount,
|
||||
address token,
|
||||
string calldata reason
|
||||
) external override returns (uint256 proposalId) {
|
||||
bytes memory data = abi.encode(recipient, amount, token, reason);
|
||||
return this.createProposal(ProposalType.TreasuryWithdrawal, reason, data, 0);
|
||||
}
|
||||
|
||||
function getProposal(uint256 proposalId) external view override returns (
|
||||
uint256 id,
|
||||
ProposalType proposalType,
|
||||
ProposalStatus status,
|
||||
address proposer,
|
||||
uint256 forVotes,
|
||||
uint256 againstVotes,
|
||||
uint256 startTime,
|
||||
uint256 endTime
|
||||
) {
|
||||
Proposal storage proposal = governanceStorage().proposals[proposalId];
|
||||
return (
|
||||
proposal.id,
|
||||
proposal.proposalType,
|
||||
proposal.status,
|
||||
proposal.proposer,
|
||||
proposal.forVotes,
|
||||
proposal.againstVotes,
|
||||
proposal.startTime,
|
||||
proposal.endTime
|
||||
);
|
||||
}
|
||||
|
||||
function getTreasuryBalance(address token) external view override returns (uint256) {
|
||||
return governanceStorage().treasuryBalances[token];
|
||||
}
|
||||
|
||||
function _checkProposalStatus(uint256 proposalId) internal {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
Proposal storage proposal = gs.proposals[proposalId];
|
||||
|
||||
uint256 totalVotes = proposal.forVotes + proposal.againstVotes;
|
||||
|
||||
if (totalVotes >= gs.quorumThreshold) {
|
||||
if (proposal.forVotes > proposal.againstVotes) {
|
||||
proposal.status = ProposalStatus.Passed;
|
||||
// Set timelock
|
||||
if (gs.timelockDelay > 0) {
|
||||
gs.proposalTimelocks[proposalId] = block.timestamp + gs.timelockDelay;
|
||||
}
|
||||
} else {
|
||||
proposal.status = ProposalStatus.Rejected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _getVotingPower(address voter) internal view returns (uint256) {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
address delegatee = gs.delegations[voter];
|
||||
address account = delegatee != address(0) ? delegatee : voter;
|
||||
|
||||
if (gs.governanceToken != address(0)) {
|
||||
return IERC20(gs.governanceToken).balanceOf(account);
|
||||
}
|
||||
return 1; // Default: 1 vote per address
|
||||
}
|
||||
|
||||
// ============ Delegation Functions ============
|
||||
|
||||
function delegate(address delegatee) external override {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
address currentDelegate = gs.delegations[msg.sender];
|
||||
uint256 previousBalance = _getVotingPower(msg.sender);
|
||||
|
||||
gs.delegations[msg.sender] = delegatee;
|
||||
|
||||
uint256 newBalance = _getVotingPower(msg.sender);
|
||||
emit DelegationChanged(msg.sender, delegatee, previousBalance, newBalance);
|
||||
}
|
||||
|
||||
function delegateBySig(
|
||||
address delegator,
|
||||
address delegatee,
|
||||
uint256 nonce,
|
||||
uint256 expiry,
|
||||
uint8 v,
|
||||
bytes32 r,
|
||||
bytes32 s
|
||||
) external override {
|
||||
// EIP-712 signature verification would go here
|
||||
// For now, simplified implementation
|
||||
require(block.timestamp <= expiry, "GovernanceFacet: Signature expired");
|
||||
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
address currentDelegate = gs.delegations[delegator];
|
||||
uint256 previousBalance = _getVotingPower(delegator);
|
||||
|
||||
gs.delegations[delegator] = delegatee;
|
||||
|
||||
uint256 newBalance = _getVotingPower(delegator);
|
||||
emit DelegationChanged(delegator, delegatee, previousBalance, newBalance);
|
||||
}
|
||||
|
||||
function delegates(address delegator) external view override returns (address) {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
address delegatee = gs.delegations[delegator];
|
||||
return delegatee != address(0) ? delegatee : delegator;
|
||||
}
|
||||
|
||||
function getCurrentVotes(address account) external view override returns (uint256) {
|
||||
return _getVotingPower(account);
|
||||
}
|
||||
|
||||
function getPriorVotes(address account, uint256 blockNumber) external view override returns (uint256) {
|
||||
// Simplified: return current votes (full implementation would use checkpoints)
|
||||
return _getVotingPower(account);
|
||||
}
|
||||
|
||||
function _executeFacetUpgrade(bytes memory data) internal {
|
||||
(IDiamondCut.FacetCut[] memory cuts, address init, bytes memory initData) =
|
||||
abi.decode(data, (IDiamondCut.FacetCut[], address, bytes));
|
||||
|
||||
// Call DiamondCutFacet through Diamond
|
||||
IDiamondCut(address(this)).diamondCut(cuts, init, initData);
|
||||
}
|
||||
|
||||
function _executeEmergencyPause(bytes memory data) internal {
|
||||
ISecurityFacet.PauseReason reason = abi.decode(data, (ISecurityFacet.PauseReason));
|
||||
ISecurityFacet(address(this)).pauseSystem(reason);
|
||||
}
|
||||
|
||||
function _executeComplianceChange(bytes memory data) internal {
|
||||
// Compliance changes would be executed here
|
||||
// This is a placeholder for compliance-related actions
|
||||
}
|
||||
|
||||
function _executeParameterChange(bytes memory data) internal {
|
||||
// Parameter changes would be executed here
|
||||
// This is a placeholder for parameter updates
|
||||
}
|
||||
|
||||
function _executeTreasuryWithdrawal(bytes memory data) internal {
|
||||
(address recipient, uint256 amount, address token, ) = abi.decode(data, (address, uint256, address, string));
|
||||
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
require(gs.treasuryBalances[token] >= amount, "GovernanceFacet: Insufficient treasury balance");
|
||||
|
||||
gs.treasuryBalances[token] -= amount;
|
||||
|
||||
if (token == address(0)) {
|
||||
(bool success, ) = payable(recipient).call{value: amount}("");
|
||||
require(success, "GovernanceFacet: ETH transfer failed");
|
||||
} else {
|
||||
IERC20(token).safeTransfer(recipient, amount);
|
||||
}
|
||||
|
||||
// Sync treasury balance
|
||||
_syncTreasuryBalance(token);
|
||||
|
||||
emit TreasuryWithdrawal(recipient, amount, token);
|
||||
}
|
||||
|
||||
function _syncTreasuryBalance(address token) internal {
|
||||
GovernanceStorage storage gs = governanceStorage();
|
||||
if (token == address(0)) {
|
||||
gs.treasuryBalances[token] = address(this).balance;
|
||||
} else {
|
||||
gs.treasuryBalances[token] = IERC20(token).balanceOf(address(this));
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Admin Functions ============
|
||||
|
||||
function setGovernanceToken(address token) external {
|
||||
LibAccessControl.requireRole(LibAccessControl.GOVERNANCE_ADMIN_ROLE, msg.sender);
|
||||
governanceStorage().governanceToken = token;
|
||||
}
|
||||
|
||||
function setQuorumThreshold(uint256 threshold) external {
|
||||
LibAccessControl.requireRole(LibAccessControl.GOVERNANCE_ADMIN_ROLE, msg.sender);
|
||||
governanceStorage().quorumThreshold = threshold;
|
||||
}
|
||||
|
||||
function setTimelockDelay(uint256 delay) external {
|
||||
LibAccessControl.requireRole(LibAccessControl.GOVERNANCE_ADMIN_ROLE, msg.sender);
|
||||
governanceStorage().timelockDelay = delay;
|
||||
}
|
||||
|
||||
function syncTreasuryBalance(address token) external {
|
||||
_syncTreasuryBalance(token);
|
||||
}
|
||||
}
|
||||
|
||||
444
contracts/src/core/facets/LiquidityFacet.sol
Normal file
444
contracts/src/core/facets/LiquidityFacet.sol
Normal file
@@ -0,0 +1,444 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {ILiquidityFacet} from "../../interfaces/ILiquidityFacet.sol";
|
||||
import {PMMMath} from "../../libraries/PMMMath.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
|
||||
import {LibReentrancyGuard} from "../../libraries/LibReentrancyGuard.sol";
|
||||
import {IComplianceFacet} from "../../interfaces/IComplianceFacet.sol";
|
||||
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
|
||||
import {IOracle} from "../../interfaces/IOracle.sol";
|
||||
|
||||
/**
|
||||
* @title LiquidityFacet
|
||||
* @notice Enhanced liquidity facet with PMM, fees, access control, and compliance
|
||||
* @dev This facet manages DODO PMM pools with comprehensive security features
|
||||
*/
|
||||
contract LiquidityFacet is ILiquidityFacet {
|
||||
using PMMMath for uint256;
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
struct LiquidityStorage {
|
||||
mapping(uint256 => Pool) pools;
|
||||
mapping(uint256 => PoolConfig) poolConfigs; // poolId => config
|
||||
mapping(uint256 => mapping(address => uint256)) lpBalances; // poolId => user => lpShares
|
||||
mapping(uint256 => uint256) totalLPSupply; // poolId => total LP supply
|
||||
mapping(uint256 => address) priceFeeds; // poolId => Chainlink price feed
|
||||
mapping(address => uint256) protocolFees; // token => accumulated fees
|
||||
mapping(uint256 => mapping(address => uint256)) poolFees; // poolId => token => accumulated fees
|
||||
uint256 poolCount;
|
||||
uint256 defaultTradingFee; // Default trading fee in basis points (e.g., 30 = 0.3%)
|
||||
uint256 defaultProtocolFee; // Default protocol fee in basis points
|
||||
address feeCollector; // Address to receive protocol fees
|
||||
}
|
||||
|
||||
struct PoolConfig {
|
||||
uint256 tradingFee; // Trading fee in basis points (0-10000)
|
||||
uint256 protocolFee; // Protocol fee in basis points (0-10000)
|
||||
bool paused; // Pool-specific pause
|
||||
address oracle; // Chainlink price feed address
|
||||
uint256 lastOracleUpdate; // Timestamp of last oracle update
|
||||
uint256 oracleUpdateInterval; // Minimum interval between oracle updates
|
||||
}
|
||||
|
||||
bytes32 private constant LIQUIDITY_STORAGE_POSITION = keccak256("asle.liquidity.storage");
|
||||
|
||||
// Events
|
||||
event PoolPaused(uint256 indexed poolId, bool paused);
|
||||
event OraclePriceUpdated(uint256 indexed poolId, uint256 newPrice);
|
||||
event TradingFeeCollected(uint256 indexed poolId, address token, uint256 amount);
|
||||
event ProtocolFeeCollected(address token, uint256 amount);
|
||||
event FeeCollectorUpdated(address newFeeCollector);
|
||||
event PoolFeeUpdated(uint256 indexed poolId, uint256 tradingFee, uint256 protocolFee);
|
||||
|
||||
function liquidityStorage() internal pure returns (LiquidityStorage storage ls) {
|
||||
bytes32 position = LIQUIDITY_STORAGE_POSITION;
|
||||
assembly {
|
||||
ls.slot := position
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Access Control Modifiers ============
|
||||
|
||||
modifier onlyPoolCreator() {
|
||||
LibAccessControl.requireRole(LibAccessControl.POOL_CREATOR_ROLE, msg.sender);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAdmin() {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier whenPoolNotPaused(uint256 poolId) {
|
||||
LiquidityStorage storage ls = liquidityStorage();
|
||||
require(!ls.poolConfigs[poolId].paused, "LiquidityFacet: Pool is paused");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier nonReentrant() {
|
||||
LibReentrancyGuard.enter();
|
||||
_;
|
||||
LibReentrancyGuard.exit();
|
||||
}
|
||||
|
||||
// ============ Pool Creation ============
|
||||
|
||||
/**
|
||||
* @notice Create a new PMM liquidity pool (backward compatible with interface)
|
||||
*/
|
||||
function createPool(
|
||||
address baseToken,
|
||||
address quoteToken,
|
||||
uint256 initialBaseReserve,
|
||||
uint256 initialQuoteReserve,
|
||||
uint256 virtualBaseReserve,
|
||||
uint256 virtualQuoteReserve,
|
||||
uint256 k,
|
||||
uint256 oraclePrice
|
||||
) external override returns (uint256 poolId) {
|
||||
return _createPool(
|
||||
baseToken,
|
||||
quoteToken,
|
||||
initialBaseReserve,
|
||||
initialQuoteReserve,
|
||||
virtualBaseReserve,
|
||||
virtualQuoteReserve,
|
||||
k,
|
||||
oraclePrice,
|
||||
address(0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Create a new PMM liquidity pool with oracle
|
||||
*/
|
||||
function createPoolWithOracle(
|
||||
address baseToken,
|
||||
address quoteToken,
|
||||
uint256 initialBaseReserve,
|
||||
uint256 initialQuoteReserve,
|
||||
uint256 virtualBaseReserve,
|
||||
uint256 virtualQuoteReserve,
|
||||
uint256 k,
|
||||
uint256 oraclePrice,
|
||||
address oracle
|
||||
) external onlyPoolCreator returns (uint256 poolId) {
|
||||
return _createPool(
|
||||
baseToken,
|
||||
quoteToken,
|
||||
initialBaseReserve,
|
||||
initialQuoteReserve,
|
||||
virtualBaseReserve,
|
||||
virtualQuoteReserve,
|
||||
k,
|
||||
oraclePrice,
|
||||
oracle
|
||||
);
|
||||
}
|
||||
|
||||
function _createPool(
|
||||
address baseToken,
|
||||
address quoteToken,
|
||||
uint256 initialBaseReserve,
|
||||
uint256 initialQuoteReserve,
|
||||
uint256 virtualBaseReserve,
|
||||
uint256 virtualQuoteReserve,
|
||||
uint256 k,
|
||||
uint256 oraclePrice,
|
||||
address oracle
|
||||
) internal returns (uint256 poolId) {
|
||||
// Check if system is paused
|
||||
ISecurityFacet securityFacet = ISecurityFacet(address(this));
|
||||
require(!securityFacet.isPaused(), "LiquidityFacet: System is paused");
|
||||
|
||||
require(baseToken != address(0) && quoteToken != address(0), "LiquidityFacet: Invalid tokens");
|
||||
require(baseToken != quoteToken, "LiquidityFacet: Tokens must be different");
|
||||
require(k <= 1e18, "LiquidityFacet: k must be <= 1");
|
||||
require(virtualBaseReserve > 0 && virtualQuoteReserve > 0, "LiquidityFacet: Virtual reserves must be > 0");
|
||||
require(oraclePrice > 0, "LiquidityFacet: Oracle price must be > 0");
|
||||
|
||||
LiquidityStorage storage ls = liquidityStorage();
|
||||
poolId = ls.poolCount;
|
||||
ls.poolCount++;
|
||||
|
||||
Pool storage pool = ls.pools[poolId];
|
||||
pool.baseToken = baseToken;
|
||||
pool.quoteToken = quoteToken;
|
||||
pool.baseReserve = initialBaseReserve;
|
||||
pool.quoteReserve = initialQuoteReserve;
|
||||
pool.virtualBaseReserve = virtualBaseReserve;
|
||||
pool.virtualQuoteReserve = virtualQuoteReserve;
|
||||
pool.k = k;
|
||||
pool.oraclePrice = oraclePrice;
|
||||
pool.active = true;
|
||||
|
||||
// Set pool configuration
|
||||
PoolConfig storage config = ls.poolConfigs[poolId];
|
||||
config.tradingFee = ls.defaultTradingFee > 0 ? ls.defaultTradingFee : 30; // 0.3% default
|
||||
config.protocolFee = ls.defaultProtocolFee > 0 ? ls.defaultProtocolFee : 10; // 0.1% default
|
||||
config.paused = false;
|
||||
config.oracle = oracle;
|
||||
config.lastOracleUpdate = block.timestamp;
|
||||
config.oracleUpdateInterval = 3600; // 1 hour default
|
||||
|
||||
// Transfer initial tokens
|
||||
if (initialBaseReserve > 0) {
|
||||
IERC20(baseToken).safeTransferFrom(msg.sender, address(this), initialBaseReserve);
|
||||
}
|
||||
if (initialQuoteReserve > 0) {
|
||||
IERC20(quoteToken).safeTransferFrom(msg.sender, address(this), initialQuoteReserve);
|
||||
}
|
||||
|
||||
emit PoolCreated(poolId, baseToken, quoteToken);
|
||||
}
|
||||
|
||||
// ============ Liquidity Management ============
|
||||
|
||||
/**
|
||||
* @notice Add liquidity to a pool
|
||||
*/
|
||||
function addLiquidity(
|
||||
uint256 poolId,
|
||||
uint256 baseAmount,
|
||||
uint256 quoteAmount
|
||||
) external override whenPoolNotPaused(poolId) nonReentrant returns (uint256 lpShares) {
|
||||
// Check compliance
|
||||
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
|
||||
IComplianceFacet.ComplianceMode mode = complianceFacet.getVaultComplianceMode(poolId);
|
||||
require(complianceFacet.canAccess(msg.sender, mode), "LiquidityFacet: Compliance check failed");
|
||||
|
||||
LiquidityStorage storage ls = liquidityStorage();
|
||||
Pool storage pool = ls.pools[poolId];
|
||||
require(pool.active, "LiquidityFacet: Pool not active");
|
||||
|
||||
// Transfer tokens
|
||||
if (baseAmount > 0) {
|
||||
IERC20(pool.baseToken).safeTransferFrom(msg.sender, address(this), baseAmount);
|
||||
}
|
||||
if (quoteAmount > 0) {
|
||||
IERC20(pool.quoteToken).safeTransferFrom(msg.sender, address(this), quoteAmount);
|
||||
}
|
||||
|
||||
// Calculate LP shares
|
||||
lpShares = PMMMath.calculateLPShares(
|
||||
baseAmount,
|
||||
quoteAmount,
|
||||
pool.baseReserve,
|
||||
pool.quoteReserve,
|
||||
ls.totalLPSupply[poolId]
|
||||
);
|
||||
|
||||
// Update reserves
|
||||
pool.baseReserve += baseAmount;
|
||||
pool.quoteReserve += quoteAmount;
|
||||
ls.lpBalances[poolId][msg.sender] += lpShares;
|
||||
ls.totalLPSupply[poolId] += lpShares;
|
||||
|
||||
emit LiquidityAdded(poolId, msg.sender, baseAmount, quoteAmount);
|
||||
}
|
||||
|
||||
// ============ Swapping ============
|
||||
|
||||
/**
|
||||
* @notice Execute a swap in the pool
|
||||
*/
|
||||
function swap(
|
||||
uint256 poolId,
|
||||
address tokenIn,
|
||||
uint256 amountIn,
|
||||
uint256 minAmountOut
|
||||
) external override whenPoolNotPaused(poolId) nonReentrant returns (uint256 amountOut) {
|
||||
// Check security (circuit breaker)
|
||||
ISecurityFacet securityFacet = ISecurityFacet(address(this));
|
||||
require(securityFacet.checkCircuitBreaker(poolId, amountIn), "LiquidityFacet: Circuit breaker triggered");
|
||||
|
||||
// Check compliance
|
||||
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
|
||||
require(
|
||||
complianceFacet.validateTransaction(msg.sender, address(this), amountIn),
|
||||
"LiquidityFacet: Compliance validation failed"
|
||||
);
|
||||
|
||||
LiquidityStorage storage ls = liquidityStorage();
|
||||
Pool storage pool = ls.pools[poolId];
|
||||
require(pool.active, "LiquidityFacet: Pool not active");
|
||||
require(tokenIn == pool.baseToken || tokenIn == pool.quoteToken, "LiquidityFacet: Invalid token");
|
||||
|
||||
// Update oracle price if available and needed
|
||||
_updateOraclePrice(poolId);
|
||||
|
||||
// Transfer input token
|
||||
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
|
||||
|
||||
bool isBaseIn = (tokenIn == pool.baseToken);
|
||||
address tokenOut = isBaseIn ? pool.quoteToken : pool.baseToken;
|
||||
|
||||
// Calculate output using PMM formula
|
||||
amountOut = PMMMath.calculateSwapOutput(
|
||||
amountIn,
|
||||
isBaseIn ? pool.baseReserve : pool.quoteReserve,
|
||||
isBaseIn ? pool.quoteReserve : pool.baseReserve,
|
||||
isBaseIn ? pool.virtualBaseReserve : pool.virtualQuoteReserve,
|
||||
isBaseIn ? pool.virtualQuoteReserve : pool.virtualBaseReserve,
|
||||
pool.k,
|
||||
pool.oraclePrice
|
||||
);
|
||||
|
||||
require(amountOut >= minAmountOut, "LiquidityFacet: Slippage too high");
|
||||
|
||||
// Calculate and collect fees
|
||||
PoolConfig storage config = ls.poolConfigs[poolId];
|
||||
uint256 tradingFeeAmount = (amountOut * config.tradingFee) / 10000;
|
||||
uint256 protocolFeeAmount = (tradingFeeAmount * config.protocolFee) / 10000;
|
||||
uint256 poolFeeAmount = tradingFeeAmount - protocolFeeAmount;
|
||||
|
||||
amountOut -= tradingFeeAmount;
|
||||
|
||||
// Update reserves (after fees)
|
||||
if (isBaseIn) {
|
||||
pool.baseReserve += amountIn;
|
||||
pool.quoteReserve -= (amountOut + tradingFeeAmount);
|
||||
} else {
|
||||
pool.quoteReserve += amountIn;
|
||||
pool.baseReserve -= (amountOut + tradingFeeAmount);
|
||||
}
|
||||
|
||||
// Collect fees
|
||||
if (poolFeeAmount > 0) {
|
||||
ls.poolFees[poolId][tokenOut] += poolFeeAmount;
|
||||
}
|
||||
if (protocolFeeAmount > 0) {
|
||||
ls.protocolFees[tokenOut] += protocolFeeAmount;
|
||||
}
|
||||
|
||||
// Transfer output token
|
||||
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
|
||||
|
||||
emit Swap(poolId, msg.sender, tokenIn, tokenOut, amountIn, amountOut);
|
||||
emit TradingFeeCollected(poolId, tokenOut, tradingFeeAmount);
|
||||
}
|
||||
|
||||
// ============ View Functions ============
|
||||
|
||||
function getPool(uint256 poolId) external view override returns (Pool memory) {
|
||||
return liquidityStorage().pools[poolId];
|
||||
}
|
||||
|
||||
function getPrice(uint256 poolId) external view override returns (uint256) {
|
||||
Pool memory pool = liquidityStorage().pools[poolId];
|
||||
return PMMMath.calculatePrice(
|
||||
pool.oraclePrice,
|
||||
pool.k,
|
||||
pool.quoteReserve,
|
||||
pool.virtualQuoteReserve
|
||||
);
|
||||
}
|
||||
|
||||
function getQuote(
|
||||
uint256 poolId,
|
||||
address tokenIn,
|
||||
uint256 amountIn
|
||||
) external view override returns (uint256 amountOut) {
|
||||
Pool memory pool = liquidityStorage().pools[poolId];
|
||||
require(tokenIn == pool.baseToken || tokenIn == pool.quoteToken, "LiquidityFacet: Invalid token");
|
||||
|
||||
bool isBaseIn = (tokenIn == pool.baseToken);
|
||||
amountOut = PMMMath.calculateSwapOutput(
|
||||
amountIn,
|
||||
isBaseIn ? pool.baseReserve : pool.quoteReserve,
|
||||
isBaseIn ? pool.quoteReserve : pool.baseReserve,
|
||||
isBaseIn ? pool.virtualBaseReserve : pool.virtualQuoteReserve,
|
||||
isBaseIn ? pool.virtualQuoteReserve : pool.virtualBaseReserve,
|
||||
pool.k,
|
||||
pool.oraclePrice
|
||||
);
|
||||
}
|
||||
|
||||
// ============ Admin Functions ============
|
||||
|
||||
/**
|
||||
* @notice Update oracle price for a pool
|
||||
*/
|
||||
function updateOraclePrice(uint256 poolId) external {
|
||||
_updateOraclePrice(poolId);
|
||||
}
|
||||
|
||||
function _updateOraclePrice(uint256 poolId) internal {
|
||||
LiquidityStorage storage ls = liquidityStorage();
|
||||
PoolConfig storage config = ls.poolConfigs[poolId];
|
||||
|
||||
if (config.oracle == address(0)) return;
|
||||
if (block.timestamp < config.lastOracleUpdate + config.oracleUpdateInterval) return;
|
||||
|
||||
try IOracle(config.oracle).latestRoundData() returns (
|
||||
uint80,
|
||||
int256 price,
|
||||
uint256,
|
||||
uint256 updatedAt,
|
||||
uint80
|
||||
) {
|
||||
require(price > 0, "LiquidityFacet: Invalid oracle price");
|
||||
require(updatedAt > 0, "LiquidityFacet: Stale oracle data");
|
||||
|
||||
Pool storage pool = ls.pools[poolId];
|
||||
pool.oraclePrice = uint256(price);
|
||||
config.lastOracleUpdate = block.timestamp;
|
||||
|
||||
emit OraclePriceUpdated(poolId, uint256(price));
|
||||
} catch {
|
||||
// Oracle call failed, skip update
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Pause or unpause a pool
|
||||
*/
|
||||
function setPoolPaused(uint256 poolId, bool paused) external onlyAdmin {
|
||||
LiquidityStorage storage ls = liquidityStorage();
|
||||
ls.poolConfigs[poolId].paused = paused;
|
||||
emit PoolPaused(poolId, paused);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set pool fees
|
||||
*/
|
||||
function setPoolFees(uint256 poolId, uint256 tradingFee, uint256 protocolFee) external onlyAdmin {
|
||||
require(tradingFee <= 1000, "LiquidityFacet: Trading fee too high"); // Max 10%
|
||||
require(protocolFee <= 5000, "LiquidityFacet: Protocol fee too high"); // Max 50% of trading fee
|
||||
|
||||
LiquidityStorage storage ls = liquidityStorage();
|
||||
ls.poolConfigs[poolId].tradingFee = tradingFee;
|
||||
ls.poolConfigs[poolId].protocolFee = protocolFee;
|
||||
emit PoolFeeUpdated(poolId, tradingFee, protocolFee);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set Chainlink oracle for a pool
|
||||
*/
|
||||
function setPoolOracle(uint256 poolId, address oracle) external onlyAdmin {
|
||||
LiquidityStorage storage ls = liquidityStorage();
|
||||
ls.poolConfigs[poolId].oracle = oracle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Collect protocol fees
|
||||
*/
|
||||
function collectProtocolFees(address token) external {
|
||||
LiquidityStorage storage ls = liquidityStorage();
|
||||
address collector = ls.feeCollector != address(0) ? ls.feeCollector : msg.sender;
|
||||
require(collector == msg.sender || LibAccessControl.hasRole(LibAccessControl.FEE_COLLECTOR_ROLE, msg.sender),
|
||||
"LiquidityFacet: Not authorized");
|
||||
|
||||
uint256 amount = ls.protocolFees[token];
|
||||
require(amount > 0, "LiquidityFacet: No fees to collect");
|
||||
|
||||
ls.protocolFees[token] = 0;
|
||||
IERC20(token).safeTransfer(collector, amount);
|
||||
|
||||
emit ProtocolFeeCollected(token, amount);
|
||||
}
|
||||
}
|
||||
|
||||
103
contracts/src/core/facets/ProposalTemplateFacet.sol
Normal file
103
contracts/src/core/facets/ProposalTemplateFacet.sol
Normal file
@@ -0,0 +1,103 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {IProposalTemplateFacet} from "../../interfaces/IProposalTemplateFacet.sol";
|
||||
import {IGovernanceFacet} from "../../interfaces/IGovernanceFacet.sol";
|
||||
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
|
||||
|
||||
contract ProposalTemplateFacet is IProposalTemplateFacet {
|
||||
struct TemplateStorage {
|
||||
mapping(uint256 => ProposalTemplate) templates;
|
||||
uint256 templateCount;
|
||||
}
|
||||
|
||||
bytes32 private constant TEMPLATE_STORAGE_POSITION = keccak256("asle.proposaltemplate.storage");
|
||||
|
||||
function templateStorage() internal pure returns (TemplateStorage storage ts) {
|
||||
bytes32 position = TEMPLATE_STORAGE_POSITION;
|
||||
assembly {
|
||||
ts.slot := position
|
||||
}
|
||||
}
|
||||
|
||||
modifier onlyAdmin() {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
_;
|
||||
}
|
||||
|
||||
function createTemplate(
|
||||
string calldata name,
|
||||
string calldata description,
|
||||
IGovernanceFacet.ProposalType proposalType,
|
||||
bytes calldata templateData
|
||||
) external override onlyAdmin returns (uint256 templateId) {
|
||||
TemplateStorage storage ts = templateStorage();
|
||||
templateId = ts.templateCount;
|
||||
ts.templateCount++;
|
||||
|
||||
ts.templates[templateId] = ProposalTemplate({
|
||||
id: templateId,
|
||||
name: name,
|
||||
description: description,
|
||||
proposalType: proposalType,
|
||||
templateData: templateData,
|
||||
active: true
|
||||
});
|
||||
|
||||
emit TemplateCreated(templateId, name, proposalType);
|
||||
}
|
||||
|
||||
function getTemplate(uint256 templateId) external view override returns (
|
||||
uint256 id,
|
||||
string memory name,
|
||||
string memory description,
|
||||
IGovernanceFacet.ProposalType proposalType,
|
||||
bytes memory templateData,
|
||||
bool active
|
||||
) {
|
||||
TemplateStorage storage ts = templateStorage();
|
||||
ProposalTemplate storage template = ts.templates[templateId];
|
||||
require(template.id != 0 || templateId == 0, "ProposalTemplateFacet: Template not found");
|
||||
|
||||
return (
|
||||
template.id,
|
||||
template.name,
|
||||
template.description,
|
||||
template.proposalType,
|
||||
template.templateData,
|
||||
template.active
|
||||
);
|
||||
}
|
||||
|
||||
function setTemplateActive(uint256 templateId, bool active) external override onlyAdmin {
|
||||
TemplateStorage storage ts = templateStorage();
|
||||
require(ts.templates[templateId].id != 0 || templateId == 0, "ProposalTemplateFacet: Template not found");
|
||||
|
||||
ts.templates[templateId].active = active;
|
||||
emit TemplateUpdated(templateId, active);
|
||||
}
|
||||
|
||||
function createProposalFromTemplate(
|
||||
uint256 templateId,
|
||||
bytes calldata parameters,
|
||||
uint256 votingPeriod
|
||||
) external override returns (uint256 proposalId) {
|
||||
TemplateStorage storage ts = templateStorage();
|
||||
ProposalTemplate storage template = ts.templates[templateId];
|
||||
require(template.id != 0 || templateId == 0, "ProposalTemplateFacet: Template not found");
|
||||
require(template.active, "ProposalTemplateFacet: Template not active");
|
||||
|
||||
// Merge template data with parameters
|
||||
bytes memory proposalData = abi.encodePacked(template.templateData, parameters);
|
||||
|
||||
// Call GovernanceFacet to create proposal
|
||||
IGovernanceFacet governanceFacet = IGovernanceFacet(address(this));
|
||||
proposalId = governanceFacet.createProposal(
|
||||
template.proposalType,
|
||||
template.description,
|
||||
proposalData,
|
||||
votingPeriod
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
189
contracts/src/core/facets/RWAFacet.sol
Normal file
189
contracts/src/core/facets/RWAFacet.sol
Normal file
@@ -0,0 +1,189 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {IRWAFacet} from "../../interfaces/IRWAFacet.sol";
|
||||
import {IERC1404} from "../../interfaces/IERC1404.sol";
|
||||
import {IComplianceFacet} from "../../interfaces/IComplianceFacet.sol";
|
||||
import {IOracle} from "../../interfaces/IOracle.sol";
|
||||
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
|
||||
contract RWAFacet is IRWAFacet, IERC1404 {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
struct RWAStorage {
|
||||
mapping(uint256 => RWA) rwas;
|
||||
mapping(uint256 => address) valueOracles; // tokenId => oracle address
|
||||
mapping(uint256 => bool) transferRestricted; // tokenId => restricted
|
||||
mapping(uint256 => uint256) lastValueUpdate; // tokenId => timestamp
|
||||
uint256 rwaCount;
|
||||
}
|
||||
|
||||
// ERC-1404 restriction codes
|
||||
uint8 private constant SUCCESS = 0;
|
||||
uint8 private constant COMPLIANCE_FAILURE = 1;
|
||||
uint8 private constant HOLDER_NOT_VERIFIED = 2;
|
||||
uint8 private constant TRANSFER_RESTRICTED = 3;
|
||||
|
||||
bytes32 private constant RWA_STORAGE_POSITION = keccak256("asle.rwa.storage");
|
||||
|
||||
function rwaStorage() internal pure returns (RWAStorage storage rs) {
|
||||
bytes32 position = RWA_STORAGE_POSITION;
|
||||
assembly {
|
||||
rs.slot := position
|
||||
}
|
||||
}
|
||||
|
||||
function tokenizeRWA(
|
||||
address assetContract,
|
||||
string calldata assetType,
|
||||
uint256 totalValue,
|
||||
bytes calldata complianceData
|
||||
) external override returns (uint256 tokenId) {
|
||||
// Check compliance - RWA tokenization typically requires Regulated mode
|
||||
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
|
||||
require(
|
||||
complianceFacet.canAccess(msg.sender, IComplianceFacet.ComplianceMode.Regulated),
|
||||
"RWAFacet: Regulated compliance required"
|
||||
);
|
||||
|
||||
RWAStorage storage rs = rwaStorage();
|
||||
tokenId = rs.rwaCount;
|
||||
rs.rwaCount++;
|
||||
|
||||
RWA storage rwa = rs.rwas[tokenId];
|
||||
rwa.tokenId = tokenId;
|
||||
rwa.assetContract = assetContract;
|
||||
rwa.assetType = assetType;
|
||||
rwa.totalValue = totalValue;
|
||||
rwa.fractionalizedAmount = 0;
|
||||
rwa.active = true;
|
||||
rs.lastValueUpdate[tokenId] = block.timestamp;
|
||||
|
||||
emit RWATokenized(tokenId, assetContract, assetType, totalValue);
|
||||
}
|
||||
|
||||
function fractionalizeRWA(
|
||||
uint256 tokenId,
|
||||
uint256 amount,
|
||||
address recipient
|
||||
) external override returns (uint256 shares) {
|
||||
RWAStorage storage rs = rwaStorage();
|
||||
RWA storage rwa = rs.rwas[tokenId];
|
||||
|
||||
require(rwa.active, "RWAFacet: RWA not active");
|
||||
require(amount > 0, "RWAFacet: Amount must be > 0");
|
||||
require(rwa.fractionalizedAmount + amount <= rwa.totalValue, "RWAFacet: Exceeds total value");
|
||||
|
||||
// Verify recipient compliance
|
||||
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
|
||||
require(
|
||||
complianceFacet.canAccess(recipient, IComplianceFacet.ComplianceMode.Regulated),
|
||||
"RWAFacet: Recipient must have regulated compliance"
|
||||
);
|
||||
require(
|
||||
complianceFacet.validateTransaction(msg.sender, recipient, amount),
|
||||
"RWAFacet: Compliance validation failed"
|
||||
);
|
||||
|
||||
rwa.fractionalizedAmount += amount;
|
||||
rwa.verifiedHolders[recipient] = true;
|
||||
|
||||
shares = amount; // 1:1 for simplicity, could use different ratio
|
||||
|
||||
emit RWAFractionalized(tokenId, recipient, amount);
|
||||
}
|
||||
|
||||
function getRWA(uint256 tokenId) external view override returns (
|
||||
address assetContract,
|
||||
string memory assetType,
|
||||
uint256 totalValue,
|
||||
uint256 fractionalizedAmount,
|
||||
bool active
|
||||
) {
|
||||
RWA storage rwa = rwaStorage().rwas[tokenId];
|
||||
return (
|
||||
rwa.assetContract,
|
||||
rwa.assetType,
|
||||
rwa.totalValue,
|
||||
rwa.fractionalizedAmount,
|
||||
rwa.active
|
||||
);
|
||||
}
|
||||
|
||||
function verifyHolder(uint256 tokenId, address holder) external view override returns (bool) {
|
||||
return rwaStorage().rwas[tokenId].verifiedHolders[holder];
|
||||
}
|
||||
|
||||
// ============ ERC-1404 Transfer Restrictions ============
|
||||
|
||||
function detectTransferRestriction(address from, address to, uint256 amount) external view override returns (uint8) {
|
||||
// Find which RWA token this relates to (simplified - in production would need token mapping)
|
||||
RWAStorage storage rs = rwaStorage();
|
||||
|
||||
// Check compliance
|
||||
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
|
||||
if (!complianceFacet.validateTransaction(from, to, amount)) {
|
||||
return COMPLIANCE_FAILURE;
|
||||
}
|
||||
|
||||
// Check holder verification for all RWAs
|
||||
// In production, would check specific token
|
||||
for (uint256 i = 0; i < rs.rwaCount; i++) {
|
||||
if (rs.transferRestricted[i]) {
|
||||
if (!rs.rwas[i].verifiedHolders[to]) {
|
||||
return HOLDER_NOT_VERIFIED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
function messageForTransferRestriction(uint8 restrictionCode) external pure override returns (string memory) {
|
||||
if (restrictionCode == COMPLIANCE_FAILURE) return "Transfer failed compliance check";
|
||||
if (restrictionCode == HOLDER_NOT_VERIFIED) return "Recipient not verified holder";
|
||||
if (restrictionCode == TRANSFER_RESTRICTED) return "Transfer restricted for this token";
|
||||
return "Transfer allowed";
|
||||
}
|
||||
|
||||
// ============ Asset Value Management ============
|
||||
|
||||
function updateAssetValue(uint256 tokenId, address oracle) external {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
RWAStorage storage rs = rwaStorage();
|
||||
rs.valueOracles[tokenId] = oracle;
|
||||
rs.lastValueUpdate[tokenId] = block.timestamp;
|
||||
}
|
||||
|
||||
function getAssetValue(uint256 tokenId) external view returns (uint256) {
|
||||
RWAStorage storage rs = rwaStorage();
|
||||
address oracle = rs.valueOracles[tokenId];
|
||||
|
||||
if (oracle == address(0)) {
|
||||
return rs.rwas[tokenId].totalValue;
|
||||
}
|
||||
|
||||
try IOracle(oracle).latestRoundData() returns (
|
||||
uint80,
|
||||
int256 price,
|
||||
uint256,
|
||||
uint256 updatedAt,
|
||||
uint80
|
||||
) {
|
||||
require(price > 0, "RWAFacet: Invalid oracle price");
|
||||
require(updatedAt > 0, "RWAFacet: Stale oracle data");
|
||||
return uint256(price);
|
||||
} catch {
|
||||
return rs.rwas[tokenId].totalValue; // Fallback to stored value
|
||||
}
|
||||
}
|
||||
|
||||
function setTransferRestricted(uint256 tokenId, bool restricted) external {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
rwaStorage().transferRestricted[tokenId] = restricted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
218
contracts/src/core/facets/SecurityFacet.sol
Normal file
218
contracts/src/core/facets/SecurityFacet.sol
Normal file
@@ -0,0 +1,218 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
|
||||
import {LibDiamond} from "../../libraries/LibDiamond.sol";
|
||||
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
|
||||
import {ILiquidityFacet} from "../../interfaces/ILiquidityFacet.sol";
|
||||
|
||||
contract SecurityFacet is ISecurityFacet {
|
||||
struct SecurityStorage {
|
||||
bool paused;
|
||||
PauseReason pauseReason;
|
||||
address pausedBy;
|
||||
uint256 pauseTime;
|
||||
uint256 maxPauseDuration; // Maximum pause duration in seconds (0 = unlimited)
|
||||
mapping(uint256 => CircuitBreaker) circuitBreakers;
|
||||
mapping(string => uint256) lastAuditTime;
|
||||
mapping(uint256 => uint256) poolPriceHistory; // poolId => last price
|
||||
mapping(uint256 => uint256) maxPriceDeviation; // poolId => max deviation in basis points
|
||||
}
|
||||
|
||||
bytes32 private constant SECURITY_STORAGE_POSITION = keccak256("asle.security.storage");
|
||||
|
||||
function securityStorage() internal pure returns (SecurityStorage storage ss) {
|
||||
bytes32 position = SECURITY_STORAGE_POSITION;
|
||||
assembly {
|
||||
ss.slot := position
|
||||
}
|
||||
}
|
||||
|
||||
modifier whenNotPaused() {
|
||||
require(!securityStorage().paused, "SecurityFacet: System is paused");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAuthorized() {
|
||||
require(
|
||||
LibAccessControl.hasRole(LibAccessControl.SECURITY_ADMIN_ROLE, msg.sender) ||
|
||||
LibAccessControl.hasRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender),
|
||||
"SecurityFacet: Not authorized"
|
||||
);
|
||||
_;
|
||||
}
|
||||
|
||||
function pauseSystem(PauseReason reason) external override onlyAuthorized {
|
||||
SecurityStorage storage ss = securityStorage();
|
||||
require(!ss.paused, "SecurityFacet: Already paused");
|
||||
|
||||
ss.paused = true;
|
||||
ss.pauseReason = reason;
|
||||
ss.pausedBy = msg.sender;
|
||||
ss.pauseTime = block.timestamp;
|
||||
|
||||
emit SystemPaused(reason, msg.sender);
|
||||
}
|
||||
|
||||
function pauseSystemWithDuration(PauseReason reason, uint256 duration) external onlyAuthorized {
|
||||
SecurityStorage storage ss = securityStorage();
|
||||
require(!ss.paused, "SecurityFacet: Already paused");
|
||||
|
||||
ss.paused = true;
|
||||
ss.pauseReason = reason;
|
||||
ss.pausedBy = msg.sender;
|
||||
ss.pauseTime = block.timestamp;
|
||||
ss.maxPauseDuration = duration;
|
||||
|
||||
emit SystemPaused(reason, msg.sender);
|
||||
}
|
||||
|
||||
function unpauseSystem() external override onlyAuthorized {
|
||||
SecurityStorage storage ss = securityStorage();
|
||||
require(ss.paused, "SecurityFacet: Not paused");
|
||||
|
||||
// Check if pause has expired (if max duration is set)
|
||||
if (ss.maxPauseDuration > 0) {
|
||||
require(block.timestamp >= ss.pauseTime + ss.maxPauseDuration, "SecurityFacet: Pause duration not expired");
|
||||
}
|
||||
|
||||
ss.paused = false;
|
||||
address unpauser = msg.sender;
|
||||
ss.maxPauseDuration = 0;
|
||||
|
||||
emit SystemUnpaused(unpauser);
|
||||
}
|
||||
|
||||
function isPaused() external view override returns (bool) {
|
||||
return securityStorage().paused;
|
||||
}
|
||||
|
||||
function setCircuitBreaker(
|
||||
uint256 poolId,
|
||||
uint256 threshold,
|
||||
uint256 timeWindow
|
||||
) external override onlyAuthorized {
|
||||
SecurityStorage storage ss = securityStorage();
|
||||
ss.circuitBreakers[poolId] = CircuitBreaker({
|
||||
threshold: threshold,
|
||||
timeWindow: timeWindow,
|
||||
currentValue: 0,
|
||||
windowStart: block.timestamp,
|
||||
triggered: false
|
||||
});
|
||||
}
|
||||
|
||||
function checkCircuitBreaker(uint256 poolId, uint256 value) external override returns (bool) {
|
||||
SecurityStorage storage ss = securityStorage();
|
||||
CircuitBreaker storage cb = ss.circuitBreakers[poolId];
|
||||
|
||||
if (cb.triggered) {
|
||||
return false; // Circuit breaker already triggered
|
||||
}
|
||||
|
||||
// Reset window if expired
|
||||
if (block.timestamp > cb.windowStart + cb.timeWindow) {
|
||||
cb.windowStart = block.timestamp;
|
||||
cb.currentValue = 0;
|
||||
}
|
||||
|
||||
cb.currentValue += value;
|
||||
|
||||
if (cb.currentValue > cb.threshold) {
|
||||
cb.triggered = true;
|
||||
emit CircuitBreakerTriggered(poolId, cb.currentValue);
|
||||
|
||||
// Automatically pause if circuit breaker triggers
|
||||
if (!ss.paused) {
|
||||
ss.paused = true;
|
||||
ss.pauseReason = PauseReason.CircuitBreaker;
|
||||
ss.pausedBy = address(this);
|
||||
ss.pauseTime = block.timestamp;
|
||||
emit SystemPaused(PauseReason.CircuitBreaker, address(this));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetCircuitBreaker(uint256 poolId) external onlyAuthorized {
|
||||
SecurityStorage storage ss = securityStorage();
|
||||
CircuitBreaker storage cb = ss.circuitBreakers[poolId];
|
||||
require(cb.triggered, "SecurityFacet: Circuit breaker not triggered");
|
||||
|
||||
cb.triggered = false;
|
||||
cb.currentValue = 0;
|
||||
cb.windowStart = block.timestamp;
|
||||
}
|
||||
|
||||
function triggerCircuitBreaker(uint256 poolId) external override {
|
||||
SecurityStorage storage ss = securityStorage();
|
||||
CircuitBreaker storage cb = ss.circuitBreakers[poolId];
|
||||
|
||||
require(!cb.triggered, "SecurityFacet: Already triggered");
|
||||
|
||||
cb.triggered = true;
|
||||
emit CircuitBreakerTriggered(poolId, cb.currentValue);
|
||||
|
||||
// Optionally pause the system
|
||||
// Note: This would need to be called externally or through Diamond
|
||||
// pauseSystem(PauseReason.CircuitBreaker);
|
||||
}
|
||||
|
||||
function recordSecurityAudit(string calldata auditType, bool passed) external override onlyAuthorized {
|
||||
SecurityStorage storage ss = securityStorage();
|
||||
ss.lastAuditTime[auditType] = block.timestamp;
|
||||
|
||||
emit SecurityAudit(block.timestamp, auditType, passed);
|
||||
|
||||
if (!passed && !ss.paused) {
|
||||
ss.paused = true;
|
||||
ss.pauseReason = PauseReason.ComplianceViolation;
|
||||
ss.pausedBy = msg.sender;
|
||||
ss.pauseTime = block.timestamp;
|
||||
emit SystemPaused(PauseReason.ComplianceViolation, msg.sender);
|
||||
}
|
||||
}
|
||||
|
||||
function checkPriceDeviation(uint256 poolId, uint256 currentPrice) external returns (bool) {
|
||||
SecurityStorage storage ss = securityStorage();
|
||||
uint256 lastPrice = ss.poolPriceHistory[poolId];
|
||||
uint256 maxDeviation = ss.maxPriceDeviation[poolId];
|
||||
|
||||
if (lastPrice == 0) {
|
||||
ss.poolPriceHistory[poolId] = currentPrice;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (maxDeviation == 0) {
|
||||
maxDeviation = 1000; // Default 10% deviation
|
||||
}
|
||||
|
||||
uint256 deviation;
|
||||
if (currentPrice > lastPrice) {
|
||||
deviation = ((currentPrice - lastPrice) * 10000) / lastPrice;
|
||||
} else {
|
||||
deviation = ((lastPrice - currentPrice) * 10000) / lastPrice;
|
||||
}
|
||||
|
||||
if (deviation > maxDeviation) {
|
||||
// Trigger circuit breaker or pause
|
||||
CircuitBreaker storage cb = ss.circuitBreakers[poolId];
|
||||
if (!cb.triggered) {
|
||||
cb.triggered = true;
|
||||
emit CircuitBreakerTriggered(poolId, deviation);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ss.poolPriceHistory[poolId] = currentPrice;
|
||||
return true;
|
||||
}
|
||||
|
||||
function setMaxPriceDeviation(uint256 poolId, uint256 maxDeviation) external onlyAuthorized {
|
||||
securityStorage().maxPriceDeviation[poolId] = maxDeviation;
|
||||
}
|
||||
}
|
||||
|
||||
585
contracts/src/core/facets/VaultFacet.sol
Normal file
585
contracts/src/core/facets/VaultFacet.sol
Normal file
@@ -0,0 +1,585 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {IVaultFacet} from "../../interfaces/IVaultFacet.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
|
||||
import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {LibAccessControl} from "../../libraries/LibAccessControl.sol";
|
||||
import {LibReentrancyGuard} from "../../libraries/LibReentrancyGuard.sol";
|
||||
import {IComplianceFacet} from "../../interfaces/IComplianceFacet.sol";
|
||||
import {ISecurityFacet} from "../../interfaces/ISecurityFacet.sol";
|
||||
|
||||
/**
|
||||
* @title VaultFacet
|
||||
* @notice Complete ERC-4626 and ERC-1155 vault implementation with fees, access control, and compliance
|
||||
* @dev Implements tokenized vault standard with multi-asset support
|
||||
*/
|
||||
contract VaultFacet is IVaultFacet, IERC1155Receiver {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
struct VaultStorage {
|
||||
mapping(uint256 => Vault) vaults;
|
||||
mapping(uint256 => VaultConfig) vaultConfigs; // vaultId => config
|
||||
mapping(uint256 => mapping(address => uint256)) balances; // vaultId => user => shares
|
||||
mapping(uint256 => mapping(address => mapping(address => uint256))) allowances; // vaultId => owner => spender => amount
|
||||
mapping(uint256 => mapping(address => mapping(uint256 => uint256))) multiAssetBalances; // vaultId => user => tokenId => balance
|
||||
mapping(uint256 => address[]) multiAssetTokens; // vaultId => token addresses
|
||||
mapping(address => uint256) protocolFees; // token => accumulated fees
|
||||
mapping(uint256 => mapping(address => uint256)) vaultFees; // vaultId => token => accumulated fees
|
||||
uint256 vaultCount;
|
||||
uint256 defaultDepositFee; // Default deposit fee in basis points
|
||||
uint256 defaultWithdrawalFee; // Default withdrawal fee in basis points
|
||||
uint256 defaultManagementFee; // Default management fee per year in basis points
|
||||
address feeCollector;
|
||||
}
|
||||
|
||||
struct VaultConfig {
|
||||
uint256 depositFee; // Deposit fee in basis points (0-10000)
|
||||
uint256 withdrawalFee; // Withdrawal fee in basis points (0-10000)
|
||||
uint256 managementFee; // Management fee per year in basis points
|
||||
uint256 lastFeeCollection; // Timestamp of last fee collection
|
||||
bool paused; // Vault-specific pause
|
||||
bool allowListEnabled; // Enable allowlist for deposits
|
||||
mapping(address => bool) allowedAddresses; // Allowlist addresses
|
||||
}
|
||||
|
||||
bytes32 private constant VAULT_STORAGE_POSITION = keccak256("asle.vault.storage");
|
||||
uint256 private constant MAX_BPS = 10000;
|
||||
uint256 private constant SECONDS_PER_YEAR = 365 days;
|
||||
|
||||
// Events
|
||||
event VaultPaused(uint256 indexed vaultId, bool paused);
|
||||
event FeeCollected(uint256 indexed vaultId, address token, uint256 amount);
|
||||
event ProtocolFeeCollected(address token, uint256 amount);
|
||||
event Approval(uint256 indexed vaultId, address indexed owner, address indexed spender, uint256 value);
|
||||
event MultiAssetDeposit(uint256 indexed vaultId, address indexed user, address token, uint256 tokenId, uint256 amount);
|
||||
event MultiAssetWithdraw(uint256 indexed vaultId, address indexed user, address token, uint256 tokenId, uint256 amount);
|
||||
|
||||
function vaultStorage() internal pure returns (VaultStorage storage vs) {
|
||||
bytes32 position = VAULT_STORAGE_POSITION;
|
||||
assembly {
|
||||
vs.slot := position
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Modifiers ============
|
||||
|
||||
modifier onlyVaultCreator() {
|
||||
LibAccessControl.requireRole(LibAccessControl.VAULT_CREATOR_ROLE, msg.sender);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAdmin() {
|
||||
LibAccessControl.requireRole(LibAccessControl.DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier whenVaultNotPaused(uint256 vaultId) {
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
require(!vs.vaultConfigs[vaultId].paused, "VaultFacet: Vault is paused");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier nonReentrant() {
|
||||
LibReentrancyGuard.enter();
|
||||
_;
|
||||
LibReentrancyGuard.exit();
|
||||
}
|
||||
|
||||
// ============ Vault Creation ============
|
||||
|
||||
/**
|
||||
* @notice Create a new vault (ERC-4626 or ERC-1155)
|
||||
*/
|
||||
function createVault(
|
||||
address asset,
|
||||
bool isMultiAsset
|
||||
) external override returns (uint256 vaultId) {
|
||||
if (!isMultiAsset) {
|
||||
require(asset != address(0), "VaultFacet: Asset required for ERC-4626");
|
||||
}
|
||||
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
vaultId = vs.vaultCount;
|
||||
vs.vaultCount++;
|
||||
|
||||
Vault storage vault = vs.vaults[vaultId];
|
||||
vault.asset = asset;
|
||||
vault.isMultiAsset = isMultiAsset;
|
||||
vault.totalAssets = 0;
|
||||
vault.totalSupply = 0;
|
||||
vault.active = true;
|
||||
|
||||
// Set default configuration
|
||||
VaultConfig storage config = vs.vaultConfigs[vaultId];
|
||||
config.depositFee = vs.defaultDepositFee > 0 ? vs.defaultDepositFee : 0;
|
||||
config.withdrawalFee = vs.defaultWithdrawalFee > 0 ? vs.defaultWithdrawalFee : 0;
|
||||
config.managementFee = vs.defaultManagementFee > 0 ? vs.defaultManagementFee : 0;
|
||||
config.lastFeeCollection = block.timestamp;
|
||||
config.paused = false;
|
||||
config.allowListEnabled = false;
|
||||
|
||||
emit VaultCreated(vaultId, asset, isMultiAsset);
|
||||
}
|
||||
|
||||
// ============ ERC-4626 Functions ============
|
||||
|
||||
/**
|
||||
* @notice Returns the asset token address
|
||||
*/
|
||||
function asset(uint256 vaultId) external view returns (address) {
|
||||
return vaultStorage().vaults[vaultId].asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns total assets managed by vault
|
||||
*/
|
||||
function totalAssets(uint256 vaultId) external view returns (uint256) {
|
||||
Vault storage vault = vaultStorage().vaults[vaultId];
|
||||
return vault.totalAssets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Convert assets to shares
|
||||
*/
|
||||
function convertToShares(
|
||||
uint256 vaultId,
|
||||
uint256 assets
|
||||
) public view override returns (uint256 shares) {
|
||||
Vault storage vault = vaultStorage().vaults[vaultId];
|
||||
if (vault.totalSupply == 0) {
|
||||
shares = assets; // 1:1 for first deposit
|
||||
} else {
|
||||
shares = (assets * vault.totalSupply) / vault.totalAssets;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Convert shares to assets
|
||||
*/
|
||||
function convertToAssets(
|
||||
uint256 vaultId,
|
||||
uint256 shares
|
||||
) public view override returns (uint256 assets) {
|
||||
Vault storage vault = vaultStorage().vaults[vaultId];
|
||||
if (vault.totalSupply == 0) {
|
||||
assets = 0;
|
||||
} else {
|
||||
assets = (shares * vault.totalAssets) / vault.totalSupply;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Maximum assets that can be deposited
|
||||
*/
|
||||
function maxDeposit(uint256 vaultId, address) external pure returns (uint256) {
|
||||
return type(uint256).max; // No deposit limit
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Preview shares for deposit
|
||||
*/
|
||||
function previewDeposit(uint256 vaultId, uint256 assets) external view returns (uint256) {
|
||||
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
|
||||
uint256 assetsAfterFee = assets - (assets * config.depositFee / MAX_BPS);
|
||||
return convertToShares(vaultId, assetsAfterFee);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Deposit assets and receive shares
|
||||
*/
|
||||
function deposit(
|
||||
uint256 vaultId,
|
||||
uint256 assets,
|
||||
address receiver
|
||||
) external override whenVaultNotPaused(vaultId) nonReentrant returns (uint256 shares) {
|
||||
// Check compliance
|
||||
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
|
||||
IComplianceFacet.ComplianceMode mode = complianceFacet.getVaultComplianceMode(vaultId);
|
||||
require(complianceFacet.canAccess(msg.sender, mode), "VaultFacet: Compliance check failed");
|
||||
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
Vault storage vault = vs.vaults[vaultId];
|
||||
require(vault.active, "VaultFacet: Vault not active");
|
||||
require(!vault.isMultiAsset, "VaultFacet: Use multi-asset deposit for ERC-1155 vaults");
|
||||
require(assets > 0, "VaultFacet: Assets must be > 0");
|
||||
|
||||
// Check allowlist if enabled
|
||||
VaultConfig storage config = vs.vaultConfigs[vaultId];
|
||||
if (config.allowListEnabled) {
|
||||
require(config.allowedAddresses[msg.sender], "VaultFacet: Address not allowed");
|
||||
}
|
||||
|
||||
IERC20 assetToken = IERC20(vault.asset);
|
||||
assetToken.safeTransferFrom(msg.sender, address(this), assets);
|
||||
|
||||
// Calculate and collect deposit fee
|
||||
uint256 depositFeeAmount = (assets * config.depositFee) / MAX_BPS;
|
||||
uint256 assetsAfterFee = assets - depositFeeAmount;
|
||||
|
||||
if (depositFeeAmount > 0) {
|
||||
vs.vaultFees[vaultId][vault.asset] += depositFeeAmount;
|
||||
}
|
||||
|
||||
shares = convertToShares(vaultId, assetsAfterFee);
|
||||
vault.totalAssets += assetsAfterFee;
|
||||
vault.totalSupply += shares;
|
||||
vs.balances[vaultId][receiver] += shares;
|
||||
|
||||
emit Deposit(vaultId, receiver, assets, shares);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Maximum shares that can be minted
|
||||
*/
|
||||
function maxMint(uint256 vaultId, address) external pure returns (uint256) {
|
||||
return type(uint256).max; // No mint limit
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Preview assets needed to mint shares
|
||||
*/
|
||||
function previewMint(uint256 vaultId, uint256 shares) external view returns (uint256) {
|
||||
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
|
||||
uint256 assetsNeeded = convertToAssets(vaultId, shares);
|
||||
// Add deposit fee
|
||||
return assetsNeeded + (assetsNeeded * config.depositFee / (MAX_BPS - config.depositFee));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Mint shares for assets
|
||||
*/
|
||||
function mint(uint256 vaultId, uint256 shares, address receiver) external whenVaultNotPaused(vaultId) nonReentrant returns (uint256 assets) {
|
||||
// Check compliance
|
||||
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
|
||||
IComplianceFacet.ComplianceMode mode = complianceFacet.getVaultComplianceMode(vaultId);
|
||||
require(complianceFacet.canAccess(msg.sender, mode), "VaultFacet: Compliance check failed");
|
||||
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
Vault storage vault = vs.vaults[vaultId];
|
||||
require(vault.active, "VaultFacet: Vault not active");
|
||||
require(!vault.isMultiAsset, "VaultFacet: Use multi-asset mint for ERC-1155 vaults");
|
||||
|
||||
assets = previewMint(vaultId, shares);
|
||||
IERC20 assetToken = IERC20(vault.asset);
|
||||
assetToken.safeTransferFrom(msg.sender, address(this), assets);
|
||||
|
||||
// Calculate and collect deposit fee
|
||||
VaultConfig storage config = vs.vaultConfigs[vaultId];
|
||||
uint256 depositFeeAmount = (assets * config.depositFee) / MAX_BPS;
|
||||
uint256 assetsAfterFee = assets - depositFeeAmount;
|
||||
|
||||
if (depositFeeAmount > 0) {
|
||||
vs.vaultFees[vaultId][vault.asset] += depositFeeAmount;
|
||||
}
|
||||
|
||||
vault.totalAssets += assetsAfterFee;
|
||||
vault.totalSupply += shares;
|
||||
vs.balances[vaultId][receiver] += shares;
|
||||
|
||||
emit Deposit(vaultId, receiver, assets, shares);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Maximum assets that can be withdrawn
|
||||
*/
|
||||
function maxWithdraw(uint256 vaultId, address owner) external view returns (uint256) {
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
return convertToAssets(vaultId, vs.balances[vaultId][owner]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Preview shares needed to withdraw assets
|
||||
*/
|
||||
function previewWithdraw(uint256 vaultId, uint256 assets) external view returns (uint256) {
|
||||
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
|
||||
uint256 assetsAfterFee = assets - (assets * config.withdrawalFee / MAX_BPS);
|
||||
return convertToShares(vaultId, assetsAfterFee);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Withdraw assets by burning shares
|
||||
*/
|
||||
function withdraw(
|
||||
uint256 vaultId,
|
||||
uint256 shares,
|
||||
address receiver,
|
||||
address owner
|
||||
) external override whenVaultNotPaused(vaultId) nonReentrant returns (uint256 assets) {
|
||||
// Check authorization
|
||||
if (msg.sender != owner) {
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
uint256 allowed = vs.allowances[vaultId][owner][msg.sender];
|
||||
require(allowed >= shares, "VaultFacet: Insufficient allowance");
|
||||
vs.allowances[vaultId][owner][msg.sender] -= shares;
|
||||
}
|
||||
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
Vault storage vault = vs.vaults[vaultId];
|
||||
require(vault.active, "VaultFacet: Vault not active");
|
||||
require(!vault.isMultiAsset, "VaultFacet: Use multi-asset withdraw for ERC-1155 vaults");
|
||||
require(shares > 0, "VaultFacet: Shares must be > 0");
|
||||
require(vs.balances[vaultId][owner] >= shares, "VaultFacet: Insufficient shares");
|
||||
|
||||
assets = convertToAssets(vaultId, shares);
|
||||
require(assets <= vault.totalAssets, "VaultFacet: Insufficient assets");
|
||||
|
||||
// Calculate and collect withdrawal fee
|
||||
VaultConfig storage config = vs.vaultConfigs[vaultId];
|
||||
uint256 withdrawalFeeAmount = (assets * config.withdrawalFee) / MAX_BPS;
|
||||
uint256 assetsAfterFee = assets - withdrawalFeeAmount;
|
||||
|
||||
if (withdrawalFeeAmount > 0) {
|
||||
vs.vaultFees[vaultId][vault.asset] += withdrawalFeeAmount;
|
||||
}
|
||||
|
||||
// Update state
|
||||
vault.totalAssets -= assets;
|
||||
vault.totalSupply -= shares;
|
||||
vs.balances[vaultId][owner] -= shares;
|
||||
|
||||
IERC20(vault.asset).safeTransfer(receiver, assetsAfterFee);
|
||||
|
||||
emit Withdraw(vaultId, receiver, assets, shares);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Maximum shares that can be redeemed
|
||||
*/
|
||||
function maxRedeem(uint256 vaultId, address owner) external view returns (uint256) {
|
||||
return vaultStorage().balances[vaultId][owner];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Preview assets for redeeming shares
|
||||
*/
|
||||
function previewRedeem(uint256 vaultId, uint256 shares) external view returns (uint256) {
|
||||
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
|
||||
uint256 assets = convertToAssets(vaultId, shares);
|
||||
uint256 withdrawalFeeAmount = (assets * config.withdrawalFee) / MAX_BPS;
|
||||
return assets - withdrawalFeeAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Redeem shares for assets
|
||||
*/
|
||||
function redeem(uint256 vaultId, uint256 shares, address receiver, address owner) external whenVaultNotPaused(vaultId) nonReentrant returns (uint256 assets) {
|
||||
// Check authorization
|
||||
if (msg.sender != owner) {
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
uint256 allowed = vs.allowances[vaultId][owner][msg.sender];
|
||||
require(allowed >= shares, "VaultFacet: Insufficient allowance");
|
||||
vs.allowances[vaultId][owner][msg.sender] -= shares;
|
||||
}
|
||||
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
Vault storage vault = vs.vaults[vaultId];
|
||||
require(vault.active, "VaultFacet: Vault not active");
|
||||
require(!vault.isMultiAsset, "VaultFacet: Use multi-asset redeem for ERC-1155 vaults");
|
||||
require(vs.balances[vaultId][owner] >= shares, "VaultFacet: Insufficient shares");
|
||||
|
||||
assets = convertToAssets(vaultId, shares);
|
||||
|
||||
// Calculate and collect withdrawal fee
|
||||
VaultConfig storage config = vs.vaultConfigs[vaultId];
|
||||
uint256 withdrawalFeeAmount = (assets * config.withdrawalFee) / MAX_BPS;
|
||||
uint256 assetsAfterFee = assets - withdrawalFeeAmount;
|
||||
|
||||
if (withdrawalFeeAmount > 0) {
|
||||
vs.vaultFees[vaultId][vault.asset] += withdrawalFeeAmount;
|
||||
}
|
||||
|
||||
// Update state
|
||||
vault.totalAssets -= assets;
|
||||
vault.totalSupply -= shares;
|
||||
vs.balances[vaultId][owner] -= shares;
|
||||
|
||||
IERC20(vault.asset).safeTransfer(receiver, assetsAfterFee);
|
||||
|
||||
emit Withdraw(vaultId, receiver, assets, shares);
|
||||
}
|
||||
|
||||
// ============ Approval Mechanism ============
|
||||
|
||||
/**
|
||||
* @notice Approve spender to withdraw shares
|
||||
*/
|
||||
function approve(uint256 vaultId, address spender, uint256 amount) external returns (bool) {
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
vs.allowances[vaultId][msg.sender][spender] = amount;
|
||||
emit Approval(vaultId, msg.sender, spender, amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get approval amount
|
||||
*/
|
||||
function allowance(uint256 vaultId, address owner, address spender) external view returns (uint256) {
|
||||
return vaultStorage().allowances[vaultId][owner][spender];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get balance of shares
|
||||
*/
|
||||
function balanceOf(uint256 vaultId, address account) external view returns (uint256) {
|
||||
return vaultStorage().balances[vaultId][account];
|
||||
}
|
||||
|
||||
// ============ ERC-1155 Multi-Asset Functions ============
|
||||
|
||||
/**
|
||||
* @notice Deposit multiple assets into ERC-1155 vault
|
||||
*/
|
||||
function depositMultiAsset(
|
||||
uint256 vaultId,
|
||||
address token,
|
||||
uint256 tokenId,
|
||||
uint256 amount
|
||||
) external whenVaultNotPaused(vaultId) nonReentrant {
|
||||
// Check compliance
|
||||
IComplianceFacet complianceFacet = IComplianceFacet(address(this));
|
||||
IComplianceFacet.ComplianceMode mode = complianceFacet.getVaultComplianceMode(vaultId);
|
||||
require(complianceFacet.canAccess(msg.sender, mode), "VaultFacet: Compliance check failed");
|
||||
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
Vault storage vault = vs.vaults[vaultId];
|
||||
require(vault.active, "VaultFacet: Vault not active");
|
||||
require(vault.isMultiAsset, "VaultFacet: Not a multi-asset vault");
|
||||
|
||||
IERC1155(token).safeTransferFrom(msg.sender, address(this), tokenId, amount, "");
|
||||
vs.multiAssetBalances[vaultId][msg.sender][tokenId] += amount;
|
||||
|
||||
// Track token addresses
|
||||
bool tokenExists = false;
|
||||
for (uint i = 0; i < vs.multiAssetTokens[vaultId].length; i++) {
|
||||
if (vs.multiAssetTokens[vaultId][i] == token) {
|
||||
tokenExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!tokenExists) {
|
||||
vs.multiAssetTokens[vaultId].push(token);
|
||||
}
|
||||
|
||||
emit MultiAssetDeposit(vaultId, msg.sender, token, tokenId, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Withdraw multiple assets from ERC-1155 vault
|
||||
*/
|
||||
function withdrawMultiAsset(
|
||||
uint256 vaultId,
|
||||
address token,
|
||||
uint256 tokenId,
|
||||
uint256 amount
|
||||
) external whenVaultNotPaused(vaultId) nonReentrant {
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
Vault storage vault = vs.vaults[vaultId];
|
||||
require(vault.active, "VaultFacet: Vault not active");
|
||||
require(vault.isMultiAsset, "VaultFacet: Not a multi-asset vault");
|
||||
require(vs.multiAssetBalances[vaultId][msg.sender][tokenId] >= amount, "VaultFacet: Insufficient balance");
|
||||
|
||||
vs.multiAssetBalances[vaultId][msg.sender][tokenId] -= amount;
|
||||
IERC1155(token).safeTransferFrom(address(this), msg.sender, tokenId, amount, "");
|
||||
|
||||
emit MultiAssetWithdraw(vaultId, msg.sender, token, tokenId, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get multi-asset balance
|
||||
*/
|
||||
function getMultiAssetBalance(uint256 vaultId, address user, address token, uint256 tokenId) external view returns (uint256) {
|
||||
return vaultStorage().multiAssetBalances[vaultId][user][tokenId];
|
||||
}
|
||||
|
||||
// ============ ERC-1155 Receiver ============
|
||||
|
||||
function onERC1155Received(
|
||||
address,
|
||||
address,
|
||||
uint256,
|
||||
uint256,
|
||||
bytes calldata
|
||||
) external pure returns (bytes4) {
|
||||
return IERC1155Receiver.onERC1155Received.selector;
|
||||
}
|
||||
|
||||
function onERC1155BatchReceived(
|
||||
address,
|
||||
address,
|
||||
uint256[] calldata,
|
||||
uint256[] calldata,
|
||||
bytes calldata
|
||||
) external pure returns (bytes4) {
|
||||
return IERC1155Receiver.onERC1155BatchReceived.selector;
|
||||
}
|
||||
|
||||
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
|
||||
return interfaceId == type(IERC1155Receiver).interfaceId;
|
||||
}
|
||||
|
||||
// ============ View Functions ============
|
||||
|
||||
function getVault(uint256 vaultId) external view override returns (Vault memory) {
|
||||
return vaultStorage().vaults[vaultId];
|
||||
}
|
||||
|
||||
// ============ Admin Functions ============
|
||||
|
||||
/**
|
||||
* @notice Pause or unpause a vault
|
||||
*/
|
||||
function setVaultPaused(uint256 vaultId, bool paused) external onlyAdmin {
|
||||
vaultStorage().vaultConfigs[vaultId].paused = paused;
|
||||
emit VaultPaused(vaultId, paused);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set vault fees
|
||||
*/
|
||||
function setVaultFees(uint256 vaultId, uint256 depositFee, uint256 withdrawalFee, uint256 managementFee) external onlyAdmin {
|
||||
require(depositFee <= 1000, "VaultFacet: Deposit fee too high");
|
||||
require(withdrawalFee <= 1000, "VaultFacet: Withdrawal fee too high");
|
||||
require(managementFee <= 2000, "VaultFacet: Management fee too high");
|
||||
|
||||
VaultConfig storage config = vaultStorage().vaultConfigs[vaultId];
|
||||
config.depositFee = depositFee;
|
||||
config.withdrawalFee = withdrawalFee;
|
||||
config.managementFee = managementFee;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Collect management fees
|
||||
*/
|
||||
function collectManagementFees(uint256 vaultId) external {
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
Vault storage vault = vs.vaults[vaultId];
|
||||
VaultConfig storage config = vs.vaultConfigs[vaultId];
|
||||
|
||||
uint256 timeElapsed = block.timestamp - config.lastFeeCollection;
|
||||
uint256 feeAmount = (vault.totalAssets * config.managementFee * timeElapsed) / (MAX_BPS * SECONDS_PER_YEAR);
|
||||
|
||||
if (feeAmount > 0 && feeAmount < vault.totalAssets) {
|
||||
vs.vaultFees[vaultId][vault.asset] += feeAmount;
|
||||
vault.totalAssets -= feeAmount;
|
||||
}
|
||||
|
||||
config.lastFeeCollection = block.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Collect vault fees
|
||||
*/
|
||||
function collectVaultFees(uint256 vaultId, address token) external onlyAdmin {
|
||||
VaultStorage storage vs = vaultStorage();
|
||||
uint256 amount = vs.vaultFees[vaultId][token];
|
||||
require(amount > 0, "VaultFacet: No fees to collect");
|
||||
|
||||
vs.vaultFees[vaultId][token] = 0;
|
||||
IERC20(token).safeTransfer(vs.feeCollector != address(0) ? vs.feeCollector : msg.sender, amount);
|
||||
|
||||
emit FeeCollected(vaultId, token, amount);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user