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:
defiQUG
2025-12-03 21:22:31 -08:00
commit 507d9a35b1
261 changed files with 47004 additions and 0 deletions

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

View 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];
}
}

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

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

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

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

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

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

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

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