docs(plan): update milestone planning with phase 4 focus and success metrics
- Update priority areas from "100% COMPLETE" to "Next Priority Areas" with phase 4 focus - Mark Smart Contract Development as 🔄 NEXT and Advanced AI Features as 🔄 FUTURE - Restructure development timeline with Q2 2026 marked as COMPLETED, Q3 2026 as CURRENT PHASE - Add Q4 2026 future planning section with weeks 25-36 roadmap - Reorganize next development steps into completed and future sections - Add comprehensive success metrics and
This commit is contained in:
270
contracts/contracts/ContractRegistry.sol
Normal file
270
contracts/contracts/ContractRegistry.sol
Normal file
@@ -0,0 +1,270 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "../interfaces/IModularContracts.sol";
|
||||
|
||||
/**
|
||||
* @title ContractRegistry
|
||||
* @dev Central registry for all modular puzzle pieces
|
||||
* @notice Enables seamless inter-contract communication and supports upgrades
|
||||
*/
|
||||
contract ContractRegistry is IContractRegistry, Ownable, ReentrancyGuard, Pausable {
|
||||
|
||||
// State variables
|
||||
uint256 public version = 1;
|
||||
mapping(bytes32 => address) public contractAddresses;
|
||||
mapping(bytes32 => uint256) public contractVersions;
|
||||
mapping(address => bytes32) public addressToId;
|
||||
bytes32[] public contractIds;
|
||||
|
||||
// Events
|
||||
event ContractRegistered(bytes32 indexed contractId, address indexed contractAddress, uint256 version);
|
||||
event ContractUpdated(bytes32 indexed contractId, address indexed oldAddress, address indexed newAddress);
|
||||
event ContractDeregistered(bytes32 indexed contractId, address indexed contractAddress);
|
||||
event RegistryPaused(address indexed pausedBy);
|
||||
event RegistryUnpaused(address indexed unpausedBy);
|
||||
|
||||
// Errors
|
||||
error ContractAlreadyRegistered(bytes32 contractId);
|
||||
error ContractNotFound(bytes32 contractId);
|
||||
error InvalidAddress(address contractAddress);
|
||||
error RegistryPausedError();
|
||||
error NotAuthorized();
|
||||
|
||||
modifier whenNotPausedRegistry() {
|
||||
if (paused()) revert RegistryPausedError();
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validAddress(address contractAddress) {
|
||||
if (contractAddress == address(0)) revert InvalidAddress(contractAddress);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAuthorized() {
|
||||
if (msg.sender != owner() && !isContract(msg.sender)) revert NotAuthorized();
|
||||
_;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Register the registry itself
|
||||
bytes32 registryId = keccak256(abi.encodePacked("ContractRegistry"));
|
||||
contractAddresses[registryId] = address(this);
|
||||
contractVersions[registryId] = version;
|
||||
addressToId[address(this)] = registryId;
|
||||
contractIds.push(registryId);
|
||||
|
||||
emit ContractRegistered(registryId, address(this), version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Initialize the registry (implements IModularContract)
|
||||
*/
|
||||
function initialize(address /*registry*/) external pure override {
|
||||
// Registry doesn't need external initialization
|
||||
revert("Self-initialization not allowed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upgrade the registry version
|
||||
*/
|
||||
function upgrade(address /*newImplementation*/) external override onlyOwner {
|
||||
version++;
|
||||
emit ContractUpdated(keccak256(abi.encodePacked("ContractRegistry")), address(this), address(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pause the registry
|
||||
*/
|
||||
function pause() external override onlyOwner {
|
||||
_pause();
|
||||
emit RegistryPaused(msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unpause the registry
|
||||
*/
|
||||
function unpause() external override onlyOwner {
|
||||
_unpause();
|
||||
emit RegistryUnpaused(msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get the current version
|
||||
*/
|
||||
function getVersion() external view override returns (uint256) {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Register a new contract
|
||||
*/
|
||||
function registerContract(bytes32 contractId, address contractAddress)
|
||||
external
|
||||
override
|
||||
onlyAuthorized
|
||||
whenNotPausedRegistry
|
||||
validAddress(contractAddress)
|
||||
nonReentrant
|
||||
{
|
||||
if (contractAddresses[contractId] != address(0)) {
|
||||
revert ContractAlreadyRegistered(contractId);
|
||||
}
|
||||
|
||||
contractAddresses[contractId] = contractAddress;
|
||||
contractVersions[contractId] = 1;
|
||||
addressToId[contractAddress] = contractId;
|
||||
contractIds.push(contractId);
|
||||
|
||||
emit ContractRegistered(contractId, contractAddress, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get a contract address by ID
|
||||
*/
|
||||
function getContract(bytes32 contractId) external view override returns (address) {
|
||||
address contractAddress = contractAddresses[contractId];
|
||||
if (contractAddress == address(0)) {
|
||||
revert ContractNotFound(contractId);
|
||||
}
|
||||
return contractAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update an existing contract address
|
||||
*/
|
||||
function updateContract(bytes32 contractId, address newAddress)
|
||||
external
|
||||
override
|
||||
onlyAuthorized
|
||||
whenNotPausedRegistry
|
||||
validAddress(newAddress)
|
||||
nonReentrant
|
||||
{
|
||||
address oldAddress = contractAddresses[contractId];
|
||||
if (oldAddress == address(0)) {
|
||||
revert ContractNotFound(contractId);
|
||||
}
|
||||
|
||||
contractAddresses[contractId] = newAddress;
|
||||
contractVersions[contractId]++;
|
||||
delete addressToId[oldAddress];
|
||||
addressToId[newAddress] = contractId;
|
||||
|
||||
emit ContractUpdated(contractId, oldAddress, newAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deregister a contract
|
||||
*/
|
||||
function deregisterContract(bytes32 contractId) external onlyAuthorized whenNotPausedRegistry nonReentrant {
|
||||
address contractAddress = contractAddresses[contractId];
|
||||
if (contractAddress == address(0)) {
|
||||
revert ContractNotFound(contractId);
|
||||
}
|
||||
|
||||
delete contractAddresses[contractId];
|
||||
delete contractVersions[contractId];
|
||||
delete addressToId[contractAddress];
|
||||
|
||||
// Remove from contractIds array
|
||||
for (uint256 i = 0; i < contractIds.length; i++) {
|
||||
if (contractIds[i] == contractId) {
|
||||
contractIds[i] = contractIds[contractIds.length - 1];
|
||||
contractIds.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit ContractDeregistered(contractId, contractAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev List all registered contracts
|
||||
*/
|
||||
function listContracts() external view override returns (bytes32[] memory, address[] memory) {
|
||||
bytes32[] memory ids = new bytes32[](contractIds.length);
|
||||
address[] memory addresses = new address[](contractIds.length);
|
||||
|
||||
for (uint256 i = 0; i < contractIds.length; i++) {
|
||||
ids[i] = contractIds[i];
|
||||
addresses[i] = contractAddresses[contractIds[i]];
|
||||
}
|
||||
|
||||
return (ids, addresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get contract version
|
||||
*/
|
||||
function getContractVersion(bytes32 contractId) external view returns (uint256) {
|
||||
return contractVersions[contractId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if an address is a registered contract
|
||||
*/
|
||||
function isRegisteredContract(address contractAddress) external view returns (bool) {
|
||||
bytes32 contractId = addressToId[contractAddress];
|
||||
return contractAddresses[contractId] != address(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get contract ID by address
|
||||
*/
|
||||
function getContractId(address contractAddress) external view returns (bytes32) {
|
||||
return addressToId[contractAddress];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Batch register contracts
|
||||
*/
|
||||
function batchRegisterContracts(bytes32[] memory _contractIds, address[] memory _contractAddresses)
|
||||
external
|
||||
onlyAuthorized
|
||||
whenNotPausedRegistry
|
||||
{
|
||||
require(_contractIds.length == _contractAddresses.length, "Array length mismatch");
|
||||
|
||||
for (uint256 i = 0; i < _contractIds.length; i++) {
|
||||
if (_contractAddresses[i] != address(0) && contractAddresses[_contractIds[i]] == address(0)) {
|
||||
contractAddresses[_contractIds[i]] = _contractAddresses[i];
|
||||
contractVersions[_contractIds[i]] = 1;
|
||||
addressToId[_contractAddresses[i]] = _contractIds[i];
|
||||
|
||||
emit ContractRegistered(_contractIds[i], _contractAddresses[i], 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if address is a contract
|
||||
*/
|
||||
function isContract(address addr) internal view returns (bool) {
|
||||
uint256 size;
|
||||
assembly {
|
||||
size := extcodesize(addr)
|
||||
}
|
||||
return size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get registry statistics
|
||||
*/
|
||||
function getRegistryStats() external view returns (
|
||||
uint256 totalContracts,
|
||||
uint256 totalVersion,
|
||||
bool isPaused,
|
||||
address owner
|
||||
) {
|
||||
return (
|
||||
contractIds.length,
|
||||
version,
|
||||
paused(),
|
||||
this.owner()
|
||||
);
|
||||
}
|
||||
}
|
||||
514
contracts/contracts/DAOGovernanceEnhanced.sol
Normal file
514
contracts/contracts/DAOGovernanceEnhanced.sol
Normal file
@@ -0,0 +1,514 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/IModularContracts.sol";
|
||||
import "./ContractRegistry.sol";
|
||||
|
||||
/**
|
||||
* @title DAOGovernanceEnhanced
|
||||
* @dev Enhanced multi-jurisdictional DAO framework with modular integrations
|
||||
* @notice Integrates with TreasuryManager, CrossChainGovernance, and PerformanceAggregator
|
||||
*/
|
||||
contract DAOGovernanceEnhanced is IModularContract, Ownable, ReentrancyGuard {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
// State variables
|
||||
uint256 public version = 2; // Enhanced version
|
||||
IERC20 public governanceToken;
|
||||
ContractRegistry public registry;
|
||||
ITreasuryManager public treasuryManager;
|
||||
ICrossChainGovernance public crossChainGovernance;
|
||||
IPerformanceAggregator public performanceAggregator;
|
||||
|
||||
// Staking Parameters
|
||||
uint256 public minStakeAmount;
|
||||
uint256 public unbondingPeriod = 7 days;
|
||||
|
||||
// Enhanced Staker struct
|
||||
struct Staker {
|
||||
uint256 amount;
|
||||
uint256 unbondingAmount;
|
||||
uint256 unbondingCompleteTime;
|
||||
uint256 lastStakeTime;
|
||||
uint256 reputationScore;
|
||||
uint256 votingPower;
|
||||
bool isActive;
|
||||
}
|
||||
|
||||
// Enhanced Proposal struct
|
||||
enum ProposalState { Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed }
|
||||
enum ProposalType { TREASURY_ALLOCATION, PARAMETER_CHANGE, CROSS_CHAIN, REWARD_DISTRIBUTION }
|
||||
|
||||
struct Proposal {
|
||||
uint256 id;
|
||||
address proposer;
|
||||
string region; // "" for global
|
||||
string descriptionHash;
|
||||
uint256 forVotes;
|
||||
uint256 againstVotes;
|
||||
uint256 abstainVotes;
|
||||
uint256 startTime;
|
||||
uint256 endTime;
|
||||
bool executed;
|
||||
bool canceled;
|
||||
ProposalState state;
|
||||
ProposalType proposalType;
|
||||
address targetContract;
|
||||
bytes callData;
|
||||
uint256 value;
|
||||
mapping(address => bool) hasVoted;
|
||||
mapping(address => uint8) voteType; // 0=against, 1=for, 2=abstain
|
||||
}
|
||||
|
||||
// Cross-chain proposal
|
||||
struct CrossChainProposal {
|
||||
uint256 sourceChainId;
|
||||
bytes32 proposalHash;
|
||||
uint256 localProposalId;
|
||||
bool isValidated;
|
||||
uint256 validationTime;
|
||||
address validator;
|
||||
bytes32 validationProof;
|
||||
}
|
||||
|
||||
// Mappings
|
||||
mapping(address => Staker) public stakers;
|
||||
mapping(uint256 => Proposal) public proposals;
|
||||
mapping(string => mapping(address => bool)) public isRegionalCouncilMember;
|
||||
mapping(string => address[]) public regionalCouncilMembers;
|
||||
mapping(uint256 => CrossChainProposal) public crossChainProposals;
|
||||
mapping(uint256 => uint256) public proposalToCrossChain;
|
||||
|
||||
// Counters
|
||||
uint256 public proposalCount;
|
||||
uint256 public totalStaked;
|
||||
uint256[] public activeProposalIds;
|
||||
|
||||
// Events
|
||||
event Staked(address indexed user, uint256 amount);
|
||||
event Unstaked(address indexed user, uint256 amount);
|
||||
event ProposalCreated(uint256 indexed id, address proposer, string region, ProposalType proposalType);
|
||||
event VoteCast(address indexed voter, uint256 indexed proposalId, uint8 voteType, uint256 weight);
|
||||
event ProposalExecuted(uint256 indexed id);
|
||||
event CrossChainProposalSubmitted(uint256 indexed localId, uint256 sourceChainId, bytes32 proposalHash);
|
||||
event ReputationUpdated(address indexed staker, uint256 newReputation);
|
||||
event VotingPowerUpdated(address indexed staker, uint256 newVotingPower);
|
||||
|
||||
// Errors
|
||||
error InvalidAmount(uint256 amount);
|
||||
error InsufficientStake(uint256 required, uint256 available);
|
||||
error ProposalNotFound(uint256 proposalId);
|
||||
error ProposalNotActive(uint256 proposalId);
|
||||
error AlreadyVoted(uint256 proposalId, address voter);
|
||||
error InvalidVoteType(uint8 voteType);
|
||||
error NotCouncilMember(string region, address member);
|
||||
error RegistryNotSet();
|
||||
error CrossChainValidationFailed(uint256 proposalId);
|
||||
|
||||
modifier validAmount(uint256 amount) {
|
||||
if (amount == 0) revert InvalidAmount(amount);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validProposal(uint256 proposalId) {
|
||||
if (proposals[proposalId].id == 0) revert ProposalNotFound(proposalId);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyActiveStaker() {
|
||||
if (stakers[msg.sender].amount < minStakeAmount) revert InsufficientStake(minStakeAmount, stakers[msg.sender].amount);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyCouncilMember(string memory region) {
|
||||
if (bytes(region).length > 0 && !isRegionalCouncilMember[region][msg.sender]) {
|
||||
revert NotCouncilMember(region, msg.sender);
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
modifier registrySet() {
|
||||
if (address(registry) == address(0)) revert RegistryNotSet();
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _governanceToken, uint256 _minStakeAmount) {
|
||||
governanceToken = IERC20(_governanceToken);
|
||||
minStakeAmount = _minStakeAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Initialize the enhanced DAO governance (implements IModularContract)
|
||||
*/
|
||||
function initialize(address _registry) external override {
|
||||
require(address(registry) == address(0), "Already initialized");
|
||||
registry = ContractRegistry(_registry);
|
||||
|
||||
// Register this contract
|
||||
bytes32 contractId = keccak256(abi.encodePacked("DAOGovernanceEnhanced"));
|
||||
registry.registerContract(contractId, address(this));
|
||||
|
||||
// Get integration addresses from registry
|
||||
treasuryManager = ITreasuryManager(registry.getContract(keccak256(abi.encodePacked("TreasuryManager"))));
|
||||
crossChainGovernance = ICrossChainGovernance(registry.getContract(keccak256(abi.encodePacked("CrossChainGovernance"))));
|
||||
performanceAggregator = IPerformanceAggregator(registry.getContract(keccak256(abi.encodePacked("PerformanceAggregator"))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upgrade the contract
|
||||
*/
|
||||
function upgrade(address newImplementation) external override onlyOwner {
|
||||
version++;
|
||||
// Implementation upgrade logic would go here
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pause the contract
|
||||
*/
|
||||
function pause() external override onlyOwner {
|
||||
// Implementation would use Pausable mixin
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unpause the contract
|
||||
*/
|
||||
function unpause() external override onlyOwner {
|
||||
// Implementation would use Pausable mixin
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get current version
|
||||
*/
|
||||
function getVersion() external view override returns (uint256) {
|
||||
return version;
|
||||
}
|
||||
|
||||
// --- Enhanced Staking ---
|
||||
|
||||
function stake(uint256 _amount) external nonReentrant validAmount(_amount) {
|
||||
governanceToken.safeTransferFrom(msg.sender, address(this), _amount);
|
||||
|
||||
Staker storage staker = stakers[msg.sender];
|
||||
staker.amount += _amount;
|
||||
staker.lastStakeTime = block.timestamp;
|
||||
staker.isActive = true;
|
||||
totalStaked += _amount;
|
||||
|
||||
// Update voting power based on reputation
|
||||
_updateVotingPower(msg.sender);
|
||||
|
||||
require(staker.amount >= minStakeAmount, "Below min stake");
|
||||
|
||||
emit Staked(msg.sender, _amount);
|
||||
}
|
||||
|
||||
function initiateUnstake(uint256 _amount) external nonReentrant {
|
||||
Staker storage staker = stakers[msg.sender];
|
||||
require(_amount > 0 && staker.amount >= _amount, "Invalid amount");
|
||||
require(staker.unbondingAmount == 0, "Unbonding already in progress");
|
||||
|
||||
staker.amount -= _amount;
|
||||
staker.unbondingAmount = _amount;
|
||||
staker.unbondingCompleteTime = block.timestamp + unbondingPeriod;
|
||||
totalStaked -= _amount;
|
||||
|
||||
// Update voting power
|
||||
_updateVotingPower(msg.sender);
|
||||
}
|
||||
|
||||
function completeUnstake() external nonReentrant {
|
||||
Staker storage staker = stakers[msg.sender];
|
||||
require(staker.unbondingAmount > 0, "Nothing to unstake");
|
||||
require(block.timestamp >= staker.unbondingCompleteTime, "Unbonding not complete");
|
||||
|
||||
uint256 amount = staker.unbondingAmount;
|
||||
staker.unbondingAmount = 0;
|
||||
|
||||
governanceToken.safeTransfer(msg.sender, amount);
|
||||
|
||||
emit Unstaked(msg.sender, amount);
|
||||
}
|
||||
|
||||
// --- Enhanced Proposals & Voting ---
|
||||
|
||||
function createProposal(
|
||||
string calldata _region,
|
||||
string calldata _descriptionHash,
|
||||
uint256 _votingPeriod,
|
||||
ProposalType _proposalType,
|
||||
address _targetContract,
|
||||
bytes calldata _callData,
|
||||
uint256 _value
|
||||
) external onlyActiveStaker onlyCouncilMember(_region) nonReentrant returns (uint256) {
|
||||
proposalCount++;
|
||||
Proposal storage p = proposals[proposalCount];
|
||||
|
||||
p.id = proposalCount;
|
||||
p.proposer = msg.sender;
|
||||
p.region = _region;
|
||||
p.descriptionHash = _descriptionHash;
|
||||
p.startTime = block.timestamp;
|
||||
p.endTime = block.timestamp + _votingPeriod;
|
||||
p.state = ProposalState.Active;
|
||||
p.proposalType = _proposalType;
|
||||
p.targetContract = _targetContract;
|
||||
p.callData = _callData;
|
||||
p.value = _value;
|
||||
|
||||
activeProposalIds.push(proposalCount);
|
||||
|
||||
emit ProposalCreated(p.id, msg.sender, _region, _proposalType);
|
||||
return p.id;
|
||||
}
|
||||
|
||||
function castVote(uint256 _proposalId, uint8 _voteType) external validProposal(_proposalId) nonReentrant {
|
||||
Proposal storage p = proposals[_proposalId];
|
||||
require(block.timestamp >= p.startTime && block.timestamp <= p.endTime, "Voting closed");
|
||||
require(!p.hasVoted[msg.sender], "Already voted");
|
||||
require(_voteType <= 2, "Invalid vote type");
|
||||
|
||||
uint256 weight = _calculateVotingWeight(msg.sender, p.region);
|
||||
require(weight > 0, "No voting weight");
|
||||
|
||||
p.hasVoted[msg.sender] = true;
|
||||
p.voteType[msg.sender] = _voteType;
|
||||
|
||||
if (_voteType == 0) { // Against
|
||||
p.againstVotes += weight;
|
||||
} else if (_voteType == 1) { // For
|
||||
p.forVotes += weight;
|
||||
} else { // Abstain
|
||||
p.abstainVotes += weight;
|
||||
}
|
||||
|
||||
emit VoteCast(msg.sender, _proposalId, _voteType, weight);
|
||||
}
|
||||
|
||||
function executeProposal(uint256 _proposalId) external validProposal(_proposalId) nonReentrant {
|
||||
Proposal storage p = proposals[_proposalId];
|
||||
require(block.timestamp > p.endTime, "Voting not ended");
|
||||
require(p.state == ProposalState.Active, "Invalid proposal state");
|
||||
|
||||
// Check if proposal passed
|
||||
uint256 totalVotes = p.forVotes + p.againstVotes + p.abstainVotes;
|
||||
bool passed = p.forVotes > p.againstVotes && totalVotes > 0;
|
||||
|
||||
if (passed) {
|
||||
p.state = ProposalState.Succeeded;
|
||||
_executeProposalAction(_proposalId);
|
||||
p.state = ProposalState.Executed;
|
||||
} else {
|
||||
p.state = ProposalState.Defeated;
|
||||
}
|
||||
|
||||
// Remove from active proposals
|
||||
_removeFromActiveProposals(_proposalId);
|
||||
|
||||
emit ProposalExecuted(_proposalId);
|
||||
}
|
||||
|
||||
// --- Cross-Chain Integration ---
|
||||
|
||||
function submitCrossChainProposal(
|
||||
uint256 _sourceChainId,
|
||||
bytes32 _proposalHash,
|
||||
string calldata _descriptionHash
|
||||
) external onlyActiveStaker nonReentrant returns (uint256) {
|
||||
// Create local proposal for cross-chain validation
|
||||
bytes memory callData = abi.encodeWithSignature("validateCrossChainProposal(uint256,bytes32)", _sourceChainId, _proposalHash);
|
||||
|
||||
uint256 localProposalId = _createCrossChainProposal(
|
||||
_descriptionHash,
|
||||
7 days,
|
||||
ProposalType.CROSS_CHAIN,
|
||||
address(crossChainGovernance),
|
||||
callData,
|
||||
0
|
||||
);
|
||||
|
||||
// Store cross-chain reference
|
||||
crossChainProposals[localProposalId] = CrossChainProposal({
|
||||
sourceChainId: _sourceChainId,
|
||||
proposalHash: _proposalHash,
|
||||
localProposalId: localProposalId,
|
||||
isValidated: false,
|
||||
validationTime: 0,
|
||||
validator: address(0),
|
||||
validationProof: bytes32(0)
|
||||
});
|
||||
|
||||
proposalToCrossChain[localProposalId] = _sourceChainId;
|
||||
|
||||
emit CrossChainProposalSubmitted(localProposalId, _sourceChainId, _proposalHash);
|
||||
|
||||
return localProposalId;
|
||||
}
|
||||
|
||||
function _createCrossChainProposal(
|
||||
string calldata _descriptionHash,
|
||||
uint256 _votingPeriod,
|
||||
ProposalType _proposalType,
|
||||
address _targetContract,
|
||||
bytes memory _callData,
|
||||
uint256 _value
|
||||
) internal onlyActiveStaker nonReentrant returns (uint256) {
|
||||
proposalCount++;
|
||||
Proposal storage p = proposals[proposalCount];
|
||||
|
||||
p.id = proposalCount;
|
||||
p.proposer = msg.sender;
|
||||
p.region = "";
|
||||
p.descriptionHash = _descriptionHash;
|
||||
p.startTime = block.timestamp;
|
||||
p.endTime = block.timestamp + _votingPeriod;
|
||||
p.state = ProposalState.Active;
|
||||
p.proposalType = _proposalType;
|
||||
p.targetContract = _targetContract;
|
||||
p.callData = _callData;
|
||||
p.value = _value;
|
||||
|
||||
activeProposalIds.push(proposalCount);
|
||||
|
||||
emit ProposalCreated(p.id, msg.sender, "", _proposalType);
|
||||
return p.id;
|
||||
}
|
||||
|
||||
function validateCrossChainVote(uint256 _proposalId, bytes32 _voteProof) external {
|
||||
if (address(crossChainGovernance) != address(0)) {
|
||||
crossChainGovernance.validateCrossChainVote(_proposalId, _voteProof);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Internal Functions ---
|
||||
|
||||
function _calculateVotingWeight(address _voter, string memory _region) internal view returns (uint256) {
|
||||
Staker memory staker = stakers[_voter];
|
||||
|
||||
// Regional council members have equal voting power
|
||||
if (bytes(_region).length > 0 && isRegionalCouncilMember[_region][_voter]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Global voting based on stake and reputation
|
||||
return staker.votingPower;
|
||||
}
|
||||
|
||||
function _executeProposalAction(uint256 _proposalId) internal {
|
||||
Proposal storage p = proposals[_proposalId];
|
||||
|
||||
if (p.targetContract != address(0) && p.callData.length > 0) {
|
||||
// Execute the proposal action
|
||||
(bool success, ) = p.targetContract.call{value: p.value}(p.callData);
|
||||
require(success, "Execution failed");
|
||||
}
|
||||
}
|
||||
|
||||
function _updateVotingPower(address _staker) internal {
|
||||
Staker storage staker = stakers[_staker];
|
||||
|
||||
// Base voting power is stake amount
|
||||
uint256 basePower = staker.amount;
|
||||
|
||||
// Apply reputation multiplier
|
||||
uint256 reputationMultiplier = 10000; // 1x default
|
||||
if (address(performanceAggregator) != address(0)) {
|
||||
uint256 reputation = performanceAggregator.getReputationScore(_staker);
|
||||
reputationMultiplier = performanceAggregator.calculateAPYMultiplier(reputation);
|
||||
}
|
||||
|
||||
staker.votingPower = (basePower * reputationMultiplier) / 10000;
|
||||
staker.reputationScore = performanceAggregator.getReputationScore(_staker);
|
||||
|
||||
emit VotingPowerUpdated(_staker, staker.votingPower);
|
||||
emit ReputationUpdated(_staker, staker.reputationScore);
|
||||
}
|
||||
|
||||
function _removeFromActiveProposals(uint256 _proposalId) internal {
|
||||
for (uint256 i = 0; i < activeProposalIds.length; i++) {
|
||||
if (activeProposalIds[i] == _proposalId) {
|
||||
activeProposalIds[i] = activeProposalIds[activeProposalIds.length - 1];
|
||||
activeProposalIds.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- View Functions ---
|
||||
|
||||
function getStakerInfo(address _staker) external view returns (
|
||||
uint256 amount,
|
||||
uint256 votingPower,
|
||||
uint256 reputationScore,
|
||||
bool isActive
|
||||
) {
|
||||
Staker memory staker = stakers[_staker];
|
||||
return (
|
||||
staker.amount,
|
||||
staker.votingPower,
|
||||
staker.reputationScore,
|
||||
staker.isActive
|
||||
);
|
||||
}
|
||||
|
||||
function getProposalInfo(uint256 _proposalId) external view returns (
|
||||
address proposer,
|
||||
string memory region,
|
||||
ProposalState state,
|
||||
ProposalType proposalType,
|
||||
uint256 forVotes,
|
||||
uint256 againstVotes,
|
||||
uint256 abstainVotes,
|
||||
uint256 startTime,
|
||||
uint256 endTime
|
||||
) {
|
||||
Proposal storage p = proposals[_proposalId];
|
||||
return (
|
||||
p.proposer,
|
||||
p.region,
|
||||
p.state,
|
||||
p.proposalType,
|
||||
p.forVotes,
|
||||
p.againstVotes,
|
||||
p.abstainVotes,
|
||||
p.startTime,
|
||||
p.endTime
|
||||
);
|
||||
}
|
||||
|
||||
function getActiveProposals() external view returns (uint256[] memory) {
|
||||
return activeProposalIds;
|
||||
}
|
||||
|
||||
function getRegionalCouncilMembers(string memory _region) external view returns (address[] memory) {
|
||||
return regionalCouncilMembers[_region];
|
||||
}
|
||||
|
||||
// --- Admin Functions ---
|
||||
|
||||
function setRegionalCouncilMember(string calldata _region, address _member, bool _status) external onlyOwner {
|
||||
isRegionalCouncilMember[_region][_member] = _status;
|
||||
if (_status) {
|
||||
regionalCouncilMembers[_region].push(_member);
|
||||
}
|
||||
}
|
||||
|
||||
function setMinStakeAmount(uint256 _minStakeAmount) external onlyOwner {
|
||||
minStakeAmount = _minStakeAmount;
|
||||
}
|
||||
|
||||
function setUnbondingPeriod(uint256 _unbondingPeriod) external onlyOwner {
|
||||
unbondingPeriod = _unbondingPeriod;
|
||||
}
|
||||
|
||||
function emergencyPause() external onlyOwner {
|
||||
// Emergency pause functionality
|
||||
}
|
||||
|
||||
function emergencyUnpause() external onlyOwner {
|
||||
// Emergency unpause functionality
|
||||
}
|
||||
}
|
||||
606
contracts/contracts/PerformanceAggregator.sol
Normal file
606
contracts/contracts/PerformanceAggregator.sol
Normal file
@@ -0,0 +1,606 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "../interfaces/IModularContracts.sol";
|
||||
import "./ContractRegistry.sol";
|
||||
|
||||
/**
|
||||
* @title PerformanceAggregator
|
||||
* @dev Cross-contract performance data aggregation with reputation scoring
|
||||
* @notice Integrates with AgentStaking, AgentBounty, and PerformanceVerifier
|
||||
*/
|
||||
contract PerformanceAggregator is IPerformanceAggregator, Ownable, ReentrancyGuard, Pausable {
|
||||
|
||||
// State variables
|
||||
uint256 public version = 1;
|
||||
ContractRegistry public registry;
|
||||
address public performanceVerifier;
|
||||
address public agentBounty;
|
||||
address public agentStaking;
|
||||
|
||||
// Performance metrics
|
||||
struct PerformanceMetrics {
|
||||
uint256 totalTasks;
|
||||
uint256 completedTasks;
|
||||
uint256 failedTasks;
|
||||
uint256 averageAccuracy;
|
||||
uint256 totalEarnings;
|
||||
uint256 lastUpdated;
|
||||
uint256 reputationScore;
|
||||
uint256 performanceTier; // 0-5 performance tiers
|
||||
}
|
||||
|
||||
// Performance history
|
||||
struct PerformanceRecord {
|
||||
uint256 recordId;
|
||||
address agent;
|
||||
uint256 score;
|
||||
uint256 accuracy;
|
||||
uint256 earnings;
|
||||
uint256 timestamp;
|
||||
string taskType;
|
||||
bool isPositive;
|
||||
}
|
||||
|
||||
// Performance tier thresholds
|
||||
struct PerformanceTier {
|
||||
uint256 minScore;
|
||||
uint256 maxScore;
|
||||
uint256 apyMultiplier; // In basis points (10000 = 1x)
|
||||
string name;
|
||||
}
|
||||
|
||||
// Mappings
|
||||
mapping(address => PerformanceMetrics) public agentMetrics;
|
||||
mapping(uint256 => PerformanceRecord) public performanceRecords;
|
||||
mapping(address => uint256[]) public agentRecords;
|
||||
mapping(uint256 => address) public recordToAgent;
|
||||
mapping(uint256 => PerformanceTier) public performanceTiers;
|
||||
|
||||
// Counters
|
||||
uint256 public recordCounter;
|
||||
uint256 public tierCounter;
|
||||
uint256[] public recordIds;
|
||||
|
||||
// Constants
|
||||
uint256 public constant INITIAL_REPUTATION = 5000; // 50% initial reputation
|
||||
uint256 public constant MAX_REPUTATION = 10000; // 100% max reputation
|
||||
uint256 public constant MIN_REPUTATION = 0; // 0% min reputation
|
||||
uint256 public constant REPUTATION_DECAY_RATE = 100; // 1% decay per day
|
||||
uint256 public constant DECAY_INTERVAL = 1 days;
|
||||
uint256 public constant MAX_HISTORY_RECORDS = 1000; // Max records per agent
|
||||
|
||||
// Events
|
||||
event PerformanceUpdated(address indexed agent, uint256 score, uint256 reputation);
|
||||
event ReputationCalculated(address indexed agent, uint256 reputation, uint256 tier);
|
||||
event PerformanceRecordAdded(uint256 indexed recordId, address indexed agent, uint256 score);
|
||||
event PerformanceTierUpdated(uint256 indexed tier, uint256 minScore, uint256 maxScore, uint256 multiplier);
|
||||
event AgentTierChanged(address indexed agent, uint256 oldTier, uint256 newTier);
|
||||
event BatchPerformanceUpdated(address[] agents, uint256[] scores);
|
||||
|
||||
// Errors
|
||||
error InvalidScore(uint256 score);
|
||||
error InvalidAgent(address agent);
|
||||
error InvalidAccuracy(uint256 accuracy);
|
||||
error RecordNotFound(uint256 recordId);
|
||||
error MaxHistoryReached(address agent);
|
||||
error RegistryNotSet();
|
||||
error NotAuthorized();
|
||||
|
||||
modifier validScore(uint256 score) {
|
||||
if (score > MAX_REPUTATION) revert InvalidScore(score);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validAgent(address agent) {
|
||||
if (agent == address(0)) revert InvalidAgent(agent);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validAccuracy(uint256 accuracy) {
|
||||
if (accuracy > 10000) revert InvalidAccuracy(accuracy); // Accuracy in basis points
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAuthorized() {
|
||||
if (msg.sender != owner() && msg.sender != performanceVerifier && msg.sender != agentBounty && msg.sender != agentStaking) {
|
||||
revert NotAuthorized();
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
modifier registrySet() {
|
||||
if (address(registry) == address(0)) revert RegistryNotSet();
|
||||
_;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Initialize default performance tiers
|
||||
_initializeDefaultTiers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Initialize the performance aggregator (implements IModularContract)
|
||||
*/
|
||||
function initialize(address _registry) external override {
|
||||
require(address(registry) == address(0), "Already initialized");
|
||||
registry = ContractRegistry(_registry);
|
||||
|
||||
// Register this contract
|
||||
bytes32 contractId = keccak256(abi.encodePacked("PerformanceAggregator"));
|
||||
registry.registerContract(contractId, address(this));
|
||||
|
||||
// Get integration addresses from registry
|
||||
performanceVerifier = registry.getContract(keccak256(abi.encodePacked("PerformanceVerifier")));
|
||||
agentBounty = registry.getContract(keccak256(abi.encodePacked("AgentBounty")));
|
||||
agentStaking = registry.getContract(keccak256(abi.encodePacked("AgentStaking")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upgrade the contract
|
||||
*/
|
||||
function upgrade(address newImplementation) external override onlyOwner {
|
||||
version++;
|
||||
// Implementation upgrade logic would go here
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pause the contract
|
||||
*/
|
||||
function pause() external override onlyOwner {
|
||||
_pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unpause the contract
|
||||
*/
|
||||
function unpause() external override onlyOwner {
|
||||
_unpause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get current version
|
||||
*/
|
||||
function getVersion() external view override returns (uint256) {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update agent performance
|
||||
*/
|
||||
function updateAgentPerformance(address agent, uint256 score)
|
||||
external
|
||||
override
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAgent(agent)
|
||||
validScore(score)
|
||||
nonReentrant
|
||||
{
|
||||
_updatePerformance(agent, score, 0, 0, "general", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update agent performance with detailed metrics
|
||||
*/
|
||||
function updateAgentPerformanceDetailed(
|
||||
address agent,
|
||||
uint256 score,
|
||||
uint256 accuracy,
|
||||
uint256 earnings,
|
||||
string memory taskType,
|
||||
bool isPositive
|
||||
)
|
||||
external
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAgent(agent)
|
||||
validScore(score)
|
||||
validAccuracy(accuracy)
|
||||
nonReentrant
|
||||
{
|
||||
_updatePerformance(agent, score, accuracy, earnings, taskType, isPositive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Batch update agent performance
|
||||
*/
|
||||
function batchUpdatePerformance(address[] memory agents, uint256[] memory scores)
|
||||
external
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
nonReentrant
|
||||
{
|
||||
require(agents.length == scores.length, "Array length mismatch");
|
||||
|
||||
for (uint256 i = 0; i < agents.length; i++) {
|
||||
if (agents[i] != address(0) && scores[i] <= MAX_REPUTATION) {
|
||||
_updatePerformance(agents[i], scores[i], 0, 0, "batch", true);
|
||||
}
|
||||
}
|
||||
|
||||
emit BatchPerformanceUpdated(agents, scores);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal performance update logic
|
||||
*/
|
||||
function _updatePerformance(
|
||||
address agent,
|
||||
uint256 score,
|
||||
uint256 accuracy,
|
||||
uint256 earnings,
|
||||
string memory taskType,
|
||||
bool isPositive
|
||||
) internal {
|
||||
PerformanceMetrics storage metrics = agentMetrics[agent];
|
||||
|
||||
// Initialize if new agent
|
||||
if (metrics.lastUpdated == 0) {
|
||||
metrics.totalTasks = 0;
|
||||
metrics.completedTasks = 0;
|
||||
metrics.failedTasks = 0;
|
||||
metrics.averageAccuracy = 0;
|
||||
metrics.totalEarnings = 0;
|
||||
metrics.reputationScore = INITIAL_REPUTATION;
|
||||
metrics.performanceTier = 0;
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
metrics.totalTasks++;
|
||||
if (isPositive) {
|
||||
metrics.completedTasks++;
|
||||
} else {
|
||||
metrics.failedTasks++;
|
||||
}
|
||||
|
||||
// Update average accuracy
|
||||
if (accuracy > 0) {
|
||||
metrics.averageAccuracy = (metrics.averageAccuracy * (metrics.totalTasks - 1) + accuracy) / metrics.totalTasks;
|
||||
}
|
||||
|
||||
// Update earnings
|
||||
if (earnings > 0) {
|
||||
metrics.totalEarnings += earnings;
|
||||
}
|
||||
|
||||
// Calculate new reputation score
|
||||
uint256 newReputation = _calculateReputation(metrics, score);
|
||||
uint256 oldTier = metrics.performanceTier;
|
||||
uint256 newTier = _getPerformanceTier(newReputation);
|
||||
|
||||
// Update reputation and tier
|
||||
metrics.reputationScore = newReputation;
|
||||
metrics.performanceTier = newTier;
|
||||
metrics.lastUpdated = block.timestamp;
|
||||
|
||||
// Add performance record
|
||||
uint256 recordId = ++recordCounter;
|
||||
performanceRecords[recordId] = PerformanceRecord({
|
||||
recordId: recordId,
|
||||
agent: agent,
|
||||
score: score,
|
||||
accuracy: accuracy,
|
||||
earnings: earnings,
|
||||
timestamp: block.timestamp,
|
||||
taskType: taskType,
|
||||
isPositive: isPositive
|
||||
});
|
||||
|
||||
agentRecords[agent].push(recordId);
|
||||
recordToAgent[recordId] = agent;
|
||||
recordIds.push(recordId);
|
||||
|
||||
// Limit history size
|
||||
if (agentRecords[agent].length > MAX_HISTORY_RECORDS) {
|
||||
uint256 oldRecordId = agentRecords[agent][0];
|
||||
delete performanceRecords[oldRecordId];
|
||||
delete recordToAgent[oldRecordId];
|
||||
|
||||
// Shift array
|
||||
for (uint256 i = 0; i < agentRecords[agent].length - 1; i++) {
|
||||
agentRecords[agent][i] = agentRecords[agent][i + 1];
|
||||
}
|
||||
agentRecords[agent].pop();
|
||||
}
|
||||
|
||||
emit PerformanceUpdated(agent, score, newReputation);
|
||||
emit ReputationCalculated(agent, newReputation, newTier);
|
||||
emit PerformanceRecordAdded(recordId, agent, score);
|
||||
|
||||
if (oldTier != newTier) {
|
||||
emit AgentTierChanged(agent, oldTier, newTier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Calculate reputation score based on metrics
|
||||
*/
|
||||
function _calculateReputation(PerformanceMetrics memory metrics, uint256 currentScore) internal view returns (uint256) {
|
||||
// Base score from current performance
|
||||
uint256 baseScore = currentScore;
|
||||
|
||||
// Adjust based on task completion rate
|
||||
uint256 completionRate = metrics.totalTasks > 0 ?
|
||||
(metrics.completedTasks * 10000) / metrics.totalTasks : 0;
|
||||
|
||||
// Adjust based on average accuracy
|
||||
uint256 accuracyBonus = metrics.averageAccuracy > 0 ?
|
||||
(metrics.averageAccuracy * 2000) / 10000 : 0; // Max 20% bonus
|
||||
|
||||
// Adjust based on earnings (logarithmic scaling)
|
||||
uint256 earningsBonus = 0;
|
||||
if (metrics.totalEarnings > 0) {
|
||||
// Logarithmic scaling: every 10x increase in earnings adds 10% bonus
|
||||
earningsBonus = (1000 * log10(metrics.totalEarnings)) / 10000;
|
||||
}
|
||||
|
||||
// Apply time decay
|
||||
uint256 timeSinceLastUpdate = block.timestamp - metrics.lastUpdated;
|
||||
uint256 decayAmount = (timeSinceLastUpdate * REPUTATION_DECAY_RATE) / DECAY_INTERVAL;
|
||||
|
||||
// Calculate final reputation
|
||||
uint256 finalScore = baseScore + completionRate + accuracyBonus + earningsBonus - decayAmount;
|
||||
|
||||
// Clamp to valid range
|
||||
if (finalScore > MAX_REPUTATION) {
|
||||
finalScore = MAX_REPUTATION;
|
||||
} else if (finalScore < MIN_REPUTATION) {
|
||||
finalScore = MIN_REPUTATION;
|
||||
}
|
||||
|
||||
return finalScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Simple logarithm approximation for earnings bonus
|
||||
*/
|
||||
function log10(uint256 value) internal pure returns (uint256) {
|
||||
if (value == 0) return 0;
|
||||
if (value < 10) return 0;
|
||||
if (value < 100) return 1;
|
||||
if (value < 1000) return 2;
|
||||
if (value < 10000) return 3;
|
||||
if (value < 100000) return 4;
|
||||
if (value < 1000000) return 5;
|
||||
if (value < 10000000) return 6;
|
||||
if (value < 100000000) return 7;
|
||||
if (value < 1000000000) return 8;
|
||||
if (value < 10000000000) return 9;
|
||||
return 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get performance tier for reputation score
|
||||
*/
|
||||
function _getPerformanceTier(uint256 reputation) internal view returns (uint256) {
|
||||
for (uint256 i = tierCounter; i > 0; i--) {
|
||||
PerformanceTier memory tier = performanceTiers[i];
|
||||
if (reputation >= tier.minScore && reputation <= tier.maxScore) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0; // Default tier
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get reputation score for agent
|
||||
*/
|
||||
function getReputationScore(address agent) external view override returns (uint256) {
|
||||
return agentMetrics[agent].reputationScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Calculate APY multiplier for reputation
|
||||
*/
|
||||
function calculateAPYMultiplier(uint256 reputation) external view override returns (uint256) {
|
||||
uint256 tier = _getPerformanceTier(reputation);
|
||||
if (tier > 0) {
|
||||
return performanceTiers[tier].apyMultiplier;
|
||||
}
|
||||
return 10000; // 1x default multiplier
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get performance history for agent
|
||||
*/
|
||||
function getPerformanceHistory(address agent) external view override returns (uint256[] memory) {
|
||||
return agentRecords[agent];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get detailed performance metrics for agent
|
||||
*/
|
||||
function getAgentMetrics(address agent) external view returns (
|
||||
uint256 totalTasks,
|
||||
uint256 completedTasks,
|
||||
uint256 failedTasks,
|
||||
uint256 averageAccuracy,
|
||||
uint256 totalEarnings,
|
||||
uint256 reputationScore,
|
||||
uint256 performanceTier,
|
||||
uint256 lastUpdated
|
||||
) {
|
||||
PerformanceMetrics memory metrics = agentMetrics[agent];
|
||||
return (
|
||||
metrics.totalTasks,
|
||||
metrics.completedTasks,
|
||||
metrics.failedTasks,
|
||||
metrics.averageAccuracy,
|
||||
metrics.totalEarnings,
|
||||
metrics.reputationScore,
|
||||
metrics.performanceTier,
|
||||
metrics.lastUpdated
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get performance record details
|
||||
*/
|
||||
function getPerformanceRecord(uint256 recordId) external view returns (
|
||||
address agent,
|
||||
uint256 score,
|
||||
uint256 accuracy,
|
||||
uint256 earnings,
|
||||
uint256 timestamp,
|
||||
string memory taskType,
|
||||
bool isPositive
|
||||
) {
|
||||
PerformanceRecord memory record = performanceRecords[recordId];
|
||||
return (
|
||||
record.agent,
|
||||
record.score,
|
||||
record.accuracy,
|
||||
record.earnings,
|
||||
record.timestamp,
|
||||
record.taskType,
|
||||
record.isPositive
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get performance tier details
|
||||
*/
|
||||
function getPerformanceTier(uint256 tierId) external view returns (
|
||||
uint256 minScore,
|
||||
uint256 maxScore,
|
||||
uint256 apyMultiplier,
|
||||
string memory name
|
||||
) {
|
||||
PerformanceTier memory tier = performanceTiers[tierId];
|
||||
return (
|
||||
tier.minScore,
|
||||
tier.maxScore,
|
||||
tier.apyMultiplier,
|
||||
tier.name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get all performance tiers
|
||||
*/
|
||||
function getAllPerformanceTiers() external view returns (uint256[] memory) {
|
||||
uint256[] memory tierIds = new uint256[](tierCounter);
|
||||
for (uint256 i = 1; i <= tierCounter; i++) {
|
||||
tierIds[i - 1] = i;
|
||||
}
|
||||
return tierIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update performance tier
|
||||
*/
|
||||
function updatePerformanceTier(
|
||||
uint256 tierId,
|
||||
uint256 minScore,
|
||||
uint256 maxScore,
|
||||
uint256 apyMultiplier,
|
||||
string memory name
|
||||
) external onlyOwner {
|
||||
require(tierId > 0 && tierId <= tierCounter, "Invalid tier ID");
|
||||
|
||||
performanceTiers[tierId] = PerformanceTier({
|
||||
minScore: minScore,
|
||||
maxScore: maxScore,
|
||||
apyMultiplier: apyMultiplier,
|
||||
name: name
|
||||
});
|
||||
|
||||
emit PerformanceTierUpdated(tierId, minScore, maxScore, apyMultiplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Add new performance tier
|
||||
*/
|
||||
function addPerformanceTier(
|
||||
uint256 minScore,
|
||||
uint256 maxScore,
|
||||
uint256 apyMultiplier,
|
||||
string memory name
|
||||
) external onlyOwner {
|
||||
require(minScore <= maxScore, "Invalid score range");
|
||||
require(apyMultiplier <= 50000, "Multiplier too high"); // Max 5x
|
||||
|
||||
tierCounter++;
|
||||
performanceTiers[tierCounter] = PerformanceTier({
|
||||
minScore: minScore,
|
||||
maxScore: maxScore,
|
||||
apyMultiplier: apyMultiplier,
|
||||
name: name
|
||||
});
|
||||
|
||||
emit PerformanceTierUpdated(tierCounter, minScore, maxScore, apyMultiplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Initialize default performance tiers
|
||||
*/
|
||||
function _initializeDefaultTiers() internal {
|
||||
tierCounter++;
|
||||
performanceTiers[tierCounter] = PerformanceTier({
|
||||
minScore: 0,
|
||||
maxScore: 2000,
|
||||
apyMultiplier: 8000, // 0.8x
|
||||
name: "Bronze"
|
||||
});
|
||||
|
||||
tierCounter++;
|
||||
performanceTiers[tierCounter] = PerformanceTier({
|
||||
minScore: 2001,
|
||||
maxScore: 4000,
|
||||
apyMultiplier: 10000, // 1x
|
||||
name: "Silver"
|
||||
});
|
||||
|
||||
tierCounter++;
|
||||
performanceTiers[tierCounter] = PerformanceTier({
|
||||
minScore: 4001,
|
||||
maxScore: 6000,
|
||||
apyMultiplier: 12000, // 1.2x
|
||||
name: "Gold"
|
||||
});
|
||||
|
||||
tierCounter++;
|
||||
performanceTiers[tierCounter] = PerformanceTier({
|
||||
minScore: 6001,
|
||||
maxScore: 8000,
|
||||
apyMultiplier: 15000, // 1.5x
|
||||
name: "Platinum"
|
||||
});
|
||||
|
||||
tierCounter++;
|
||||
performanceTiers[tierCounter] = PerformanceTier({
|
||||
minScore: 8001,
|
||||
maxScore: 10000,
|
||||
apyMultiplier: 20000, // 2x
|
||||
name: "Diamond"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get aggregator statistics
|
||||
*/
|
||||
function getAggregatorStats() external view returns (
|
||||
uint256 totalAgents,
|
||||
uint256 totalRecords,
|
||||
uint256 averageReputation,
|
||||
uint256 activeAgents
|
||||
) {
|
||||
uint256 _totalAgents = 0;
|
||||
uint256 _totalReputation = 0;
|
||||
uint256 _activeAgents = 0;
|
||||
|
||||
// This would require iterating through all agents, which is gas-intensive
|
||||
// For production, consider using a different approach or off-chain calculation
|
||||
|
||||
return (
|
||||
_totalAgents,
|
||||
recordCounter,
|
||||
_totalReputation,
|
||||
_activeAgents
|
||||
);
|
||||
}
|
||||
}
|
||||
584
contracts/contracts/RewardDistributor.sol
Normal file
584
contracts/contracts/RewardDistributor.sol
Normal file
@@ -0,0 +1,584 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/IModularContracts.sol";
|
||||
import "./ContractRegistry.sol";
|
||||
|
||||
/**
|
||||
* @title RewardDistributor
|
||||
* @dev Multi-token reward distribution engine with automated claiming
|
||||
* @notice Integrates with AgentStaking, TreasuryManager, and PerformanceAggregator
|
||||
*/
|
||||
contract RewardDistributor is IRewardDistributor, Ownable, ReentrancyGuard, Pausable {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
// State variables
|
||||
uint256 public version = 1;
|
||||
ContractRegistry public registry;
|
||||
address public performanceAggregator;
|
||||
address public stakingPoolFactory;
|
||||
|
||||
// Reward pool
|
||||
struct RewardPool {
|
||||
uint256 poolId;
|
||||
address token;
|
||||
uint256 totalRewards;
|
||||
uint256 distributedRewards;
|
||||
uint256 claimedRewards;
|
||||
bool isActive;
|
||||
uint256 createdAt;
|
||||
address creator;
|
||||
string description;
|
||||
}
|
||||
|
||||
// Reward claim
|
||||
struct RewardClaim {
|
||||
uint256 claimId;
|
||||
uint256 poolId;
|
||||
address recipient;
|
||||
uint256 amount;
|
||||
uint256 claimedAt;
|
||||
bool isClaimed;
|
||||
bytes32 merkleProof; // For batch claims
|
||||
}
|
||||
|
||||
// Performance-based reward
|
||||
struct PerformanceReward {
|
||||
uint256 rewardId;
|
||||
address agent;
|
||||
uint256 baseAmount;
|
||||
uint256 performanceMultiplier; // In basis points (10000 = 1x)
|
||||
uint256 finalAmount;
|
||||
bool isDistributed;
|
||||
uint256 distributedAt;
|
||||
}
|
||||
|
||||
// Mappings
|
||||
mapping(uint256 => RewardPool) public rewardPools;
|
||||
mapping(uint256 => RewardClaim) public rewardClaims;
|
||||
mapping(uint256 => PerformanceReward) public performanceRewards;
|
||||
mapping(address => uint256[]) public userClaims;
|
||||
mapping(address => uint256) public userTotalRewards;
|
||||
mapping(address => mapping(address => uint256)) public userTokenRewards; // user => token => amount
|
||||
mapping(uint256 => uint256[]) public poolClaims;
|
||||
mapping(address => uint256[]) public agentPerformanceRewards;
|
||||
|
||||
// Counters
|
||||
uint256 public poolCounter;
|
||||
uint256 public claimCounter;
|
||||
uint256 public performanceRewardCounter;
|
||||
uint256[] public activePoolIds;
|
||||
|
||||
// Constants
|
||||
uint256 public constant MIN_REWARD_AMOUNT = 1 * 10**18; // 1 token minimum
|
||||
uint256 public constant MAX_PERFORMANCE_MULTIPLIER = 50000; // 5x max multiplier
|
||||
uint256 public constant DEFAULT_PERFORMANCE_MULTIPLIER = 10000; // 1x default
|
||||
uint256 public constant CLAIM_FEE_PERCENTAGE = 10; // 0.1% claim fee
|
||||
uint256 public constant BASIS_POINTS = 10000;
|
||||
|
||||
// Events
|
||||
event RewardPoolCreated(uint256 indexed poolId, address indexed token, uint256 totalRewards, string description);
|
||||
event RewardsDistributed(uint256 indexed poolId, uint256 recipientCount, uint256 totalAmount);
|
||||
event RewardClaimed(uint256 indexed claimId, address indexed recipient, uint256 amount);
|
||||
event PerformanceRewardCreated(uint256 indexed rewardId, address indexed agent, uint256 baseAmount);
|
||||
event PerformanceRewardDistributed(uint256 indexed rewardId, address indexed agent, uint256 finalAmount);
|
||||
event PoolDeactivated(uint256 indexed poolId);
|
||||
event TokensDeposited(address indexed token, address indexed depositor, uint256 amount);
|
||||
|
||||
// Errors
|
||||
error InvalidAmount(uint256 amount);
|
||||
error PoolNotFound(uint256 poolId);
|
||||
error PoolNotActive(uint256 poolId);
|
||||
error InsufficientPoolBalance(uint256 poolId, uint256 requested, uint256 available);
|
||||
error ClaimNotFound(uint256 claimId);
|
||||
error ClaimAlreadyUsed(uint256 claimId);
|
||||
error InvalidRecipient(address recipient);
|
||||
error InvalidToken(address token);
|
||||
error PerformanceMultiplierTooHigh(uint256 multiplier);
|
||||
error RegistryNotSet();
|
||||
error NotAuthorized();
|
||||
|
||||
modifier validAmount(uint256 amount) {
|
||||
if (amount < MIN_REWARD_AMOUNT) revert InvalidAmount(amount);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validPool(uint256 poolId) {
|
||||
if (rewardPools[poolId].poolId == 0) revert PoolNotFound(poolId);
|
||||
if (!rewardPools[poolId].isActive) revert PoolNotActive(poolId);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validRecipient(address recipient) {
|
||||
if (recipient == address(0)) revert InvalidRecipient(recipient);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAuthorized() {
|
||||
if (msg.sender != owner() && msg.sender != stakingPoolFactory && msg.sender != performanceAggregator) {
|
||||
revert NotAuthorized();
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
modifier registrySet() {
|
||||
if (address(registry) == address(0)) revert RegistryNotSet();
|
||||
_;
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* @dev Initialize the reward distributor (implements IModularContract)
|
||||
*/
|
||||
function initialize(address _registry) external override {
|
||||
require(address(registry) == address(0), "Already initialized");
|
||||
registry = ContractRegistry(_registry);
|
||||
|
||||
// Register this contract if not already registered
|
||||
bytes32 contractId = keccak256(abi.encodePacked("RewardDistributor"));
|
||||
try registry.getContract(contractId) returns (address) {
|
||||
// Already registered, skip
|
||||
} catch {
|
||||
// Not registered, register now
|
||||
registry.registerContract(contractId, address(this));
|
||||
}
|
||||
|
||||
// Get integration addresses from registry
|
||||
try registry.getContract(keccak256(abi.encodePacked("PerformanceAggregator"))) returns (address perfAddress) {
|
||||
performanceAggregator = perfAddress;
|
||||
} catch {
|
||||
// PerformanceAggregator not found, keep as zero address
|
||||
}
|
||||
|
||||
try registry.getContract(keccak256(abi.encodePacked("StakingPoolFactory"))) returns (address stakingAddress) {
|
||||
stakingPoolFactory = stakingAddress;
|
||||
} catch {
|
||||
// StakingPoolFactory not found, keep as zero address
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upgrade the contract
|
||||
*/
|
||||
function upgrade(address newImplementation) external override onlyOwner {
|
||||
version++;
|
||||
// Implementation upgrade logic would go here
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pause the contract
|
||||
*/
|
||||
function pause() external override onlyOwner {
|
||||
_pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unpause the contract
|
||||
*/
|
||||
function unpause() external override onlyOwner {
|
||||
_unpause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get current version
|
||||
*/
|
||||
function getVersion() external view override returns (uint256) {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Create a reward pool
|
||||
*/
|
||||
function createRewardPool(address token, uint256 totalRewards)
|
||||
external
|
||||
override
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAmount(totalRewards)
|
||||
nonReentrant
|
||||
returns (uint256)
|
||||
{
|
||||
if (token == address(0)) revert InvalidToken(token);
|
||||
|
||||
uint256 poolId = ++poolCounter;
|
||||
rewardPools[poolId] = RewardPool({
|
||||
poolId: poolId,
|
||||
token: token,
|
||||
totalRewards: totalRewards,
|
||||
distributedRewards: 0,
|
||||
claimedRewards: 0,
|
||||
isActive: true,
|
||||
createdAt: block.timestamp,
|
||||
creator: msg.sender,
|
||||
description: ""
|
||||
});
|
||||
|
||||
activePoolIds.push(poolId);
|
||||
|
||||
emit RewardPoolCreated(poolId, token, totalRewards, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Create a reward pool with description
|
||||
*/
|
||||
function createRewardPoolWithDescription(
|
||||
address token,
|
||||
uint256 totalRewards,
|
||||
string memory description
|
||||
)
|
||||
external
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAmount(totalRewards)
|
||||
nonReentrant
|
||||
{
|
||||
if (token == address(0)) revert InvalidToken(token);
|
||||
|
||||
uint256 poolId = ++poolCounter;
|
||||
rewardPools[poolId] = RewardPool({
|
||||
poolId: poolId,
|
||||
token: token,
|
||||
totalRewards: totalRewards,
|
||||
distributedRewards: 0,
|
||||
claimedRewards: 0,
|
||||
isActive: true,
|
||||
createdAt: block.timestamp,
|
||||
creator: msg.sender,
|
||||
description: description
|
||||
});
|
||||
|
||||
activePoolIds.push(poolId);
|
||||
|
||||
emit RewardPoolCreated(poolId, token, totalRewards, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Distribute rewards to multiple recipients
|
||||
*/
|
||||
function distributeRewards(uint256 poolId, address[] memory recipients, uint256[] memory amounts)
|
||||
external
|
||||
override
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validPool(poolId)
|
||||
nonReentrant
|
||||
{
|
||||
require(recipients.length == amounts.length, "Array length mismatch");
|
||||
|
||||
RewardPool storage pool = rewardPools[poolId];
|
||||
uint256 totalAmount = 0;
|
||||
|
||||
// Calculate total amount and check pool balance
|
||||
for (uint256 i = 0; i < amounts.length; i++) {
|
||||
require(recipients[i] != address(0), "Invalid recipient");
|
||||
totalAmount += amounts[i];
|
||||
}
|
||||
|
||||
uint256 availableBalance = pool.totalRewards - pool.distributedRewards;
|
||||
if (totalAmount > availableBalance) {
|
||||
revert InsufficientPoolBalance(poolId, totalAmount, availableBalance);
|
||||
}
|
||||
|
||||
// Create claims and update user balances
|
||||
for (uint256 i = 0; i < recipients.length; i++) {
|
||||
if (amounts[i] > 0) {
|
||||
uint256 claimId = ++claimCounter;
|
||||
|
||||
rewardClaims[claimId] = RewardClaim({
|
||||
claimId: claimId,
|
||||
poolId: poolId,
|
||||
recipient: recipients[i],
|
||||
amount: amounts[i],
|
||||
claimedAt: 0,
|
||||
isClaimed: false,
|
||||
merkleProof: bytes32(0)
|
||||
});
|
||||
|
||||
userClaims[recipients[i]].push(claimId);
|
||||
userTotalRewards[recipients[i]] += amounts[i];
|
||||
userTokenRewards[recipients[i]][pool.token] += amounts[i];
|
||||
poolClaims[poolId].push(claimId);
|
||||
}
|
||||
}
|
||||
|
||||
// Update pool
|
||||
pool.distributedRewards += totalAmount;
|
||||
|
||||
emit RewardsDistributed(poolId, recipients.length, totalAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Claim a reward
|
||||
*/
|
||||
function claimReward(uint256 claimId)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
nonReentrant
|
||||
{
|
||||
RewardClaim storage claim = rewardClaims[claimId];
|
||||
|
||||
if (claim.claimId == 0) revert ClaimNotFound(claimId);
|
||||
if (claim.isClaimed) revert ClaimAlreadyUsed(claimId);
|
||||
if (msg.sender != claim.recipient) revert InvalidRecipient(msg.sender);
|
||||
|
||||
// Mark as claimed
|
||||
claim.isClaimed = true;
|
||||
claim.claimedAt = block.timestamp;
|
||||
|
||||
// Update pool
|
||||
RewardPool storage pool = rewardPools[claim.poolId];
|
||||
pool.claimedRewards += claim.amount;
|
||||
|
||||
// Calculate claim fee
|
||||
uint256 fee = (claim.amount * CLAIM_FEE_PERCENTAGE) / BASIS_POINTS;
|
||||
uint256 netAmount = claim.amount - fee;
|
||||
|
||||
// Transfer tokens
|
||||
IERC20(pool.token).safeTransfer(claim.recipient, netAmount);
|
||||
|
||||
// Transfer fee to owner
|
||||
if (fee > 0) {
|
||||
IERC20(pool.token).safeTransfer(owner(), fee);
|
||||
}
|
||||
|
||||
emit RewardClaimed(claimId, claim.recipient, netAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Create performance-based reward
|
||||
*/
|
||||
function createPerformanceReward(address agent, uint256 baseAmount)
|
||||
external
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAmount(baseAmount)
|
||||
nonReentrant
|
||||
{
|
||||
if (agent == address(0)) revert InvalidRecipient(agent);
|
||||
|
||||
uint256 rewardId = ++performanceRewardCounter;
|
||||
|
||||
// Get performance multiplier from PerformanceAggregator
|
||||
uint256 performanceMultiplier = DEFAULT_PERFORMANCE_MULTIPLIER;
|
||||
if (performanceAggregator != address(0)) {
|
||||
try IPerformanceAggregator(performanceAggregator).getReputationScore(agent) returns (uint256 reputation) {
|
||||
performanceMultiplier = IPerformanceAggregator(performanceAggregator).calculateAPYMultiplier(reputation);
|
||||
if (performanceMultiplier > MAX_PERFORMANCE_MULTIPLIER) {
|
||||
performanceMultiplier = MAX_PERFORMANCE_MULTIPLIER;
|
||||
}
|
||||
} catch {
|
||||
// Use default multiplier if call fails
|
||||
performanceMultiplier = DEFAULT_PERFORMANCE_MULTIPLIER;
|
||||
}
|
||||
}
|
||||
|
||||
uint256 finalAmount = (baseAmount * performanceMultiplier) / BASIS_POINTS;
|
||||
|
||||
performanceRewards[rewardId] = PerformanceReward({
|
||||
rewardId: rewardId,
|
||||
agent: agent,
|
||||
baseAmount: baseAmount,
|
||||
performanceMultiplier: performanceMultiplier,
|
||||
finalAmount: finalAmount,
|
||||
isDistributed: false,
|
||||
distributedAt: 0
|
||||
});
|
||||
|
||||
agentPerformanceRewards[agent].push(rewardId);
|
||||
|
||||
emit PerformanceRewardCreated(rewardId, agent, baseAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Distribute performance reward
|
||||
*/
|
||||
function distributePerformanceReward(uint256 rewardId)
|
||||
external
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
nonReentrant
|
||||
{
|
||||
PerformanceReward storage reward = performanceRewards[rewardId];
|
||||
|
||||
if (reward.rewardId == 0) revert ClaimNotFound(rewardId);
|
||||
if (reward.isDistributed) revert ClaimAlreadyUsed(rewardId);
|
||||
|
||||
// Mark as distributed
|
||||
reward.isDistributed = true;
|
||||
reward.distributedAt = block.timestamp;
|
||||
|
||||
// Update user rewards
|
||||
userTotalRewards[reward.agent] += reward.finalAmount;
|
||||
|
||||
emit PerformanceRewardDistributed(rewardId, reward.agent, reward.finalAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get pool balance
|
||||
*/
|
||||
function getPoolBalance(uint256 poolId) external view override returns (uint256) {
|
||||
RewardPool memory pool = rewardPools[poolId];
|
||||
return pool.totalRewards - pool.distributedRewards;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get user rewards
|
||||
*/
|
||||
function getUserRewards(address user) external view override returns (uint256) {
|
||||
return userTotalRewards[user];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get user rewards for specific token
|
||||
*/
|
||||
function getUserTokenRewards(address user, address token) external view returns (uint256) {
|
||||
return userTokenRewards[user][token];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get user claim IDs
|
||||
*/
|
||||
function getUserClaims(address user) external view returns (uint256[] memory) {
|
||||
return userClaims[user];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get pool claim IDs
|
||||
*/
|
||||
function getPoolClaims(uint256 poolId) external view returns (uint256[] memory) {
|
||||
return poolClaims[poolId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get agent performance reward IDs
|
||||
*/
|
||||
function getAgentPerformanceRewards(address agent) external view returns (uint256[] memory) {
|
||||
return agentPerformanceRewards[agent];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get active pool IDs
|
||||
*/
|
||||
function getActivePoolIds() external view returns (uint256[] memory) {
|
||||
return activePoolIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get pool details
|
||||
*/
|
||||
function getPoolDetails(uint256 poolId) external view returns (
|
||||
address token,
|
||||
uint256 totalRewards,
|
||||
uint256 distributedRewards,
|
||||
uint256 claimedRewards,
|
||||
bool isActive,
|
||||
string memory description
|
||||
) {
|
||||
RewardPool memory pool = rewardPools[poolId];
|
||||
return (
|
||||
pool.token,
|
||||
pool.totalRewards,
|
||||
pool.distributedRewards,
|
||||
pool.claimedRewards,
|
||||
pool.isActive,
|
||||
pool.description
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get claim details
|
||||
*/
|
||||
function getClaimDetails(uint256 claimId) external view returns (
|
||||
uint256 poolId,
|
||||
address recipient,
|
||||
uint256 amount,
|
||||
bool isClaimed,
|
||||
uint256 claimedAt
|
||||
) {
|
||||
RewardClaim memory claim = rewardClaims[claimId];
|
||||
return (
|
||||
claim.poolId,
|
||||
claim.recipient,
|
||||
claim.amount,
|
||||
claim.isClaimed,
|
||||
claim.claimedAt
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deactivate a reward pool
|
||||
*/
|
||||
function deactivatePool(uint256 poolId) external onlyAuthorized validPool(poolId) {
|
||||
rewardPools[poolId].isActive = false;
|
||||
|
||||
// Remove from active pools
|
||||
for (uint256 i = 0; i < activePoolIds.length; i++) {
|
||||
if (activePoolIds[i] == poolId) {
|
||||
activePoolIds[i] = activePoolIds[activePoolIds.length - 1];
|
||||
activePoolIds.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit PoolDeactivated(poolId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deposit tokens for rewards
|
||||
*/
|
||||
function depositTokens(address token, uint256 amount) external whenNotPaused validAmount(amount) {
|
||||
if (token == address(0)) revert InvalidToken(token);
|
||||
|
||||
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
|
||||
|
||||
emit TokensDeposited(token, msg.sender, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Emergency withdraw tokens
|
||||
*/
|
||||
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
|
||||
if (token == address(0)) revert InvalidToken(token);
|
||||
|
||||
IERC20(token).safeTransfer(msg.sender, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get reward distributor statistics
|
||||
*/
|
||||
function getRewardStats() external view returns (
|
||||
uint256 totalPools,
|
||||
uint256 activePools,
|
||||
uint256 totalClaims,
|
||||
uint256 totalDistributed,
|
||||
uint256 totalClaimed
|
||||
) {
|
||||
uint256 _activePools = 0;
|
||||
uint256 _totalDistributed = 0;
|
||||
uint256 _totalClaimed = 0;
|
||||
|
||||
for (uint256 i = 0; i < activePoolIds.length; i++) {
|
||||
RewardPool memory pool = rewardPools[activePoolIds[i]];
|
||||
if (pool.isActive) {
|
||||
_activePools++;
|
||||
_totalDistributed += pool.distributedRewards;
|
||||
_totalClaimed += pool.claimedRewards;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
poolCounter,
|
||||
_activePools,
|
||||
claimCounter,
|
||||
_totalDistributed,
|
||||
_totalClaimed
|
||||
);
|
||||
}
|
||||
}
|
||||
682
contracts/contracts/StakingPoolFactory.sol
Normal file
682
contracts/contracts/StakingPoolFactory.sol
Normal file
@@ -0,0 +1,682 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/IModularContracts.sol";
|
||||
import "./ContractRegistry.sol";
|
||||
|
||||
/**
|
||||
* @title StakingPoolFactory
|
||||
* @dev Dynamic staking pool creation with performance-based APY
|
||||
* @notice Integrates with AgentStaking, PerformanceAggregator, and RewardDistributor
|
||||
*/
|
||||
contract StakingPoolFactory is IStakingPoolFactory, Ownable, ReentrancyGuard, Pausable {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
// State variables
|
||||
uint256 public version = 1;
|
||||
IERC20 public stakingToken;
|
||||
ContractRegistry public registry;
|
||||
IPerformanceAggregator public performanceAggregator;
|
||||
IRewardDistributor public rewardDistributor;
|
||||
|
||||
// Staking pool
|
||||
struct StakingPool {
|
||||
uint256 poolId;
|
||||
string poolName;
|
||||
uint256 baseAPY; // In basis points (10000 = 100%)
|
||||
uint256 currentAPY;
|
||||
uint256 totalStaked;
|
||||
uint256 lockPeriod;
|
||||
uint256 minStakeAmount;
|
||||
uint256 maxStakeAmount;
|
||||
uint256 totalStakers;
|
||||
uint256 createdRewards;
|
||||
uint256 distributedRewards;
|
||||
bool isActive;
|
||||
uint256 createdAt;
|
||||
address creator;
|
||||
string description;
|
||||
}
|
||||
|
||||
// Staking position
|
||||
struct StakingPosition {
|
||||
uint256 positionId;
|
||||
uint256 poolId;
|
||||
address staker;
|
||||
uint256 amount;
|
||||
uint256 lockStart;
|
||||
uint256 lockEnd;
|
||||
uint256 apyAtStake;
|
||||
uint256 accumulatedRewards;
|
||||
uint256 lastRewardTime;
|
||||
bool isActive;
|
||||
bool isWithdrawn;
|
||||
}
|
||||
|
||||
// Pool performance metrics
|
||||
struct PoolPerformance {
|
||||
uint256 totalRewardsGenerated;
|
||||
uint256 averageStakingDuration;
|
||||
uint256 retentionRate; // In basis points
|
||||
uint256 utilizationRate; // In basis points
|
||||
uint256 distributedRewards;
|
||||
uint256 lastUpdated;
|
||||
}
|
||||
|
||||
// Mappings
|
||||
mapping(uint256 => StakingPool) public stakingPools;
|
||||
mapping(uint256 => StakingPosition) public stakingPositions;
|
||||
mapping(uint256 => uint256[]) public poolStakers;
|
||||
mapping(address => uint256[]) public stakerPositions;
|
||||
mapping(uint256 => PoolPerformance) public poolPerformance;
|
||||
mapping(string => uint256) public poolNameToId;
|
||||
|
||||
// Counters
|
||||
uint256 public poolCounter;
|
||||
uint256 public positionCounter;
|
||||
uint256[] public activePoolIds;
|
||||
|
||||
// Constants
|
||||
uint256 public constant MIN_BASE_APY = 100; // 1% minimum
|
||||
uint256 public constant MAX_BASE_APY = 5000; // 50% maximum
|
||||
uint256 public constant DEFAULT_LOCK_PERIOD = 30 days;
|
||||
uint256 public constant MAX_LOCK_PERIOD = 365 days;
|
||||
uint256 public constant DEFAULT_MIN_STAKE = 100 * 10**18; // 100 tokens
|
||||
uint256 public constant DEFAULT_MAX_STAKE = 1000000 * 10**18; // 1M tokens
|
||||
uint256 public constant PERFORMANCE_UPDATE_INTERVAL = 1 days;
|
||||
uint256 public constant BASIS_POINTS = 10000;
|
||||
|
||||
// Events
|
||||
event PoolCreated(uint256 indexed poolId, string poolName, uint256 baseAPY, uint256 lockPeriod);
|
||||
event PoolUpdated(uint256 indexed poolId, uint256 newAPY);
|
||||
event Staked(uint256 indexed positionId, uint256 indexed poolId, address indexed staker, uint256 amount);
|
||||
event Unstaked(uint256 indexed positionId, address indexed staker, uint256 amount, uint256 rewards);
|
||||
event RewardsDistributed(uint256 indexed poolId, uint256 totalAmount);
|
||||
event PoolDeactivated(uint256 indexed poolId);
|
||||
event PerformanceUpdated(uint256 indexed poolId, uint256 newAPY);
|
||||
|
||||
// Errors
|
||||
error InvalidAmount(uint256 amount);
|
||||
error PoolNotFound(uint256 poolId);
|
||||
error PoolNotActive(uint256 poolId);
|
||||
error PositionNotFound(uint256 positionId);
|
||||
error PositionNotActive(uint256 positionId);
|
||||
error InvalidLockPeriod(uint256 period);
|
||||
error InvalidAPY(uint256 apy);
|
||||
error InsufficientBalance(uint256 requested, uint256 available);
|
||||
error LockPeriodNotEnded(uint256 positionId);
|
||||
error MaxStakeReached(uint256 poolId);
|
||||
error RegistryNotSet();
|
||||
error NotAuthorized();
|
||||
|
||||
modifier validAmount(uint256 amount) {
|
||||
if (amount == 0) revert InvalidAmount(amount);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validPool(uint256 poolId) {
|
||||
if (stakingPools[poolId].poolId == 0) revert PoolNotFound(poolId);
|
||||
if (!stakingPools[poolId].isActive) revert PoolNotActive(poolId);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validPosition(uint256 positionId) {
|
||||
if (stakingPositions[positionId].positionId == 0) revert PositionNotFound(positionId);
|
||||
if (!stakingPositions[positionId].isActive) revert PositionNotActive(positionId);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validLockPeriod(uint256 period) {
|
||||
if (period < DEFAULT_LOCK_PERIOD || period > MAX_LOCK_PERIOD) {
|
||||
revert InvalidLockPeriod(period);
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validAPY(uint256 apy) {
|
||||
if (apy < MIN_BASE_APY || apy > MAX_BASE_APY) revert InvalidAPY(apy);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAuthorized() {
|
||||
if (msg.sender != owner() && msg.sender != address(rewardDistributor)) {
|
||||
revert NotAuthorized();
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
modifier registrySet() {
|
||||
if (address(registry) == address(0)) revert RegistryNotSet();
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _stakingToken) {
|
||||
stakingToken = IERC20(_stakingToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Initialize the staking pool factory (implements IModularContract)
|
||||
*/
|
||||
function initialize(address _registry) external override {
|
||||
require(address(registry) == address(0), "Already initialized");
|
||||
registry = ContractRegistry(_registry);
|
||||
|
||||
// Register this contract
|
||||
bytes32 contractId = keccak256(abi.encodePacked("StakingPoolFactory"));
|
||||
registry.registerContract(contractId, address(this));
|
||||
|
||||
// Get integration addresses from registry
|
||||
performanceAggregator = IPerformanceAggregator(registry.getContract(keccak256(abi.encodePacked("PerformanceAggregator"))));
|
||||
rewardDistributor = IRewardDistributor(registry.getContract(keccak256(abi.encodePacked("RewardDistributor"))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upgrade the contract
|
||||
*/
|
||||
function upgrade(address newImplementation) external override onlyOwner {
|
||||
version++;
|
||||
// Implementation upgrade logic would go here
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pause the contract
|
||||
*/
|
||||
function pause() external override onlyOwner {
|
||||
_pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unpause the contract
|
||||
*/
|
||||
function unpause() external override onlyOwner {
|
||||
_unpause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get current version
|
||||
*/
|
||||
function getVersion() external view override returns (uint256) {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Create a new staking pool
|
||||
*/
|
||||
function createPool(string memory poolName, uint256 baseAPY, uint256 lockPeriod)
|
||||
external
|
||||
override
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAPY(baseAPY)
|
||||
validLockPeriod(lockPeriod)
|
||||
nonReentrant
|
||||
returns (uint256)
|
||||
{
|
||||
require(bytes(poolName).length > 0, "Empty pool name");
|
||||
require(poolNameToId[poolName] == 0, "Pool name already exists");
|
||||
|
||||
uint256 poolId = ++poolCounter;
|
||||
stakingPools[poolId] = StakingPool({
|
||||
poolId: poolId,
|
||||
poolName: poolName,
|
||||
baseAPY: baseAPY,
|
||||
currentAPY: baseAPY,
|
||||
totalStaked: 0,
|
||||
lockPeriod: lockPeriod,
|
||||
minStakeAmount: DEFAULT_MIN_STAKE,
|
||||
maxStakeAmount: DEFAULT_MAX_STAKE,
|
||||
totalStakers: 0,
|
||||
createdRewards: 0,
|
||||
distributedRewards: 0,
|
||||
isActive: true,
|
||||
createdAt: block.timestamp,
|
||||
creator: msg.sender,
|
||||
description: ""
|
||||
});
|
||||
|
||||
poolNameToId[poolName] = poolId;
|
||||
activePoolIds.push(poolId);
|
||||
|
||||
// Initialize performance tracking
|
||||
poolPerformance[poolId] = PoolPerformance({
|
||||
totalRewardsGenerated: 0,
|
||||
averageStakingDuration: 0,
|
||||
retentionRate: 10000, // 100% initial
|
||||
utilizationRate: 0,
|
||||
distributedRewards: 0,
|
||||
lastUpdated: block.timestamp
|
||||
});
|
||||
|
||||
emit PoolCreated(poolId, poolName, baseAPY, lockPeriod);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Create a staking pool with custom parameters
|
||||
*/
|
||||
function createPoolWithParameters(
|
||||
string memory poolName,
|
||||
uint256 baseAPY,
|
||||
uint256 lockPeriod,
|
||||
uint256 minStakeAmount,
|
||||
uint256 maxStakeAmount,
|
||||
string memory description
|
||||
)
|
||||
external
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAPY(baseAPY)
|
||||
validLockPeriod(lockPeriod)
|
||||
nonReentrant
|
||||
{
|
||||
require(bytes(poolName).length > 0, "Empty pool name");
|
||||
require(poolNameToId[poolName] == 0, "Pool name already exists");
|
||||
require(minStakeAmount > 0 && minStakeAmount <= maxStakeAmount, "Invalid stake amounts");
|
||||
|
||||
uint256 poolId = ++poolCounter;
|
||||
stakingPools[poolId] = StakingPool({
|
||||
poolId: poolId,
|
||||
poolName: poolName,
|
||||
baseAPY: baseAPY,
|
||||
currentAPY: baseAPY,
|
||||
totalStaked: 0,
|
||||
lockPeriod: lockPeriod,
|
||||
minStakeAmount: minStakeAmount,
|
||||
maxStakeAmount: maxStakeAmount,
|
||||
totalStakers: 0,
|
||||
createdRewards: 0,
|
||||
distributedRewards: 0,
|
||||
isActive: true,
|
||||
createdAt: block.timestamp,
|
||||
creator: msg.sender,
|
||||
description: description
|
||||
});
|
||||
|
||||
poolNameToId[poolName] = poolId;
|
||||
activePoolIds.push(poolId);
|
||||
|
||||
// Initialize performance tracking
|
||||
poolPerformance[poolId] = PoolPerformance({
|
||||
totalRewardsGenerated: 0,
|
||||
averageStakingDuration: 0,
|
||||
retentionRate: 10000,
|
||||
utilizationRate: 0,
|
||||
distributedRewards: 0,
|
||||
lastUpdated: block.timestamp
|
||||
});
|
||||
|
||||
emit PoolCreated(poolId, poolName, baseAPY, lockPeriod);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update pool APY
|
||||
*/
|
||||
function updatePoolAPY(uint256 poolId, uint256 newAPY)
|
||||
external
|
||||
override
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validPool(poolId)
|
||||
validAPY(newAPY)
|
||||
nonReentrant
|
||||
{
|
||||
StakingPool storage pool = stakingPools[poolId];
|
||||
pool.currentAPY = newAPY;
|
||||
|
||||
emit PoolUpdated(poolId, newAPY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Stake in a pool
|
||||
*/
|
||||
function stakeInPool(uint256 poolId, uint256 amount)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
validPool(poolId)
|
||||
validAmount(amount)
|
||||
nonReentrant
|
||||
{
|
||||
StakingPool storage pool = stakingPools[poolId];
|
||||
|
||||
// Check stake limits
|
||||
if (amount < pool.minStakeAmount) {
|
||||
revert InvalidAmount(amount);
|
||||
}
|
||||
|
||||
if (pool.totalStaked + amount > pool.maxStakeAmount) {
|
||||
revert MaxStakeReached(poolId);
|
||||
}
|
||||
|
||||
// Check user balance
|
||||
uint256 userBalance = stakingToken.balanceOf(msg.sender);
|
||||
if (amount > userBalance) {
|
||||
revert InsufficientBalance(amount, userBalance);
|
||||
}
|
||||
|
||||
// Create staking position
|
||||
uint256 positionId = ++positionCounter;
|
||||
stakingPositions[positionId] = StakingPosition({
|
||||
positionId: positionId,
|
||||
poolId: poolId,
|
||||
staker: msg.sender,
|
||||
amount: amount,
|
||||
lockStart: block.timestamp,
|
||||
lockEnd: block.timestamp + pool.lockPeriod,
|
||||
apyAtStake: pool.currentAPY,
|
||||
accumulatedRewards: 0,
|
||||
lastRewardTime: block.timestamp,
|
||||
isActive: true,
|
||||
isWithdrawn: false
|
||||
});
|
||||
|
||||
// Update pool
|
||||
pool.totalStaked += amount;
|
||||
pool.totalStakers++;
|
||||
poolStakers[poolId].push(positionId);
|
||||
stakerPositions[msg.sender].push(positionId);
|
||||
|
||||
// Update performance metrics
|
||||
_updatePoolPerformance(poolId);
|
||||
|
||||
// Transfer tokens
|
||||
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
|
||||
|
||||
emit Staked(positionId, poolId, msg.sender, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unstake from a pool
|
||||
*/
|
||||
function unstakeFromPool(uint256 poolId, uint256 amount)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
validPool(poolId)
|
||||
validAmount(amount)
|
||||
nonReentrant
|
||||
{
|
||||
// Find user's position in the pool
|
||||
uint256[] memory userPositions = stakerPositions[msg.sender];
|
||||
uint256 positionId = 0;
|
||||
bool found = false;
|
||||
|
||||
for (uint256 i = 0; i < userPositions.length; i++) {
|
||||
if (stakingPositions[userPositions[i]].poolId == poolId &&
|
||||
stakingPositions[userPositions[i]].isActive &&
|
||||
!stakingPositions[userPositions[i]].isWithdrawn) {
|
||||
positionId = userPositions[i];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
revert PositionNotFound(positionId);
|
||||
}
|
||||
|
||||
StakingPosition storage position = stakingPositions[positionId];
|
||||
|
||||
// Check lock period
|
||||
if (block.timestamp < position.lockEnd) {
|
||||
revert LockPeriodNotEnded(positionId);
|
||||
}
|
||||
|
||||
// Check amount
|
||||
if (amount > position.amount) {
|
||||
revert InvalidAmount(amount);
|
||||
}
|
||||
|
||||
// Calculate rewards
|
||||
uint256 rewards = _calculateRewards(position);
|
||||
position.accumulatedRewards += rewards;
|
||||
|
||||
// Update position
|
||||
position.amount -= amount;
|
||||
if (position.amount == 0) {
|
||||
position.isActive = false;
|
||||
position.isWithdrawn = true;
|
||||
}
|
||||
|
||||
// Update pool
|
||||
StakingPool storage pool = stakingPools[poolId];
|
||||
pool.totalStaked -= amount;
|
||||
if (position.amount == 0) {
|
||||
pool.totalStakers--;
|
||||
}
|
||||
|
||||
// Update performance metrics
|
||||
_updatePoolPerformance(poolId);
|
||||
|
||||
// Transfer tokens and rewards
|
||||
stakingToken.safeTransfer(msg.sender, amount);
|
||||
|
||||
if (rewards > 0 && address(rewardDistributor) != address(0)) {
|
||||
// Create reward claim
|
||||
uint256 rewardPoolId = 1; // Assuming first pool, or get it dynamically
|
||||
rewardDistributor.createRewardPool(address(stakingToken), rewards);
|
||||
rewardDistributor.distributeRewards(
|
||||
rewardPoolId,
|
||||
new address[](1),
|
||||
new uint256[](1)
|
||||
);
|
||||
}
|
||||
|
||||
emit Unstaked(positionId, msg.sender, amount, rewards);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get pool performance
|
||||
*/
|
||||
function getPoolPerformance(uint256 poolId) external view override returns (uint256) {
|
||||
StakingPool memory pool = stakingPools[poolId];
|
||||
PoolPerformance memory performance = poolPerformance[poolId];
|
||||
|
||||
// Calculate performance score based on multiple factors
|
||||
uint256 utilizationScore = pool.totalStaked > 0 ?
|
||||
(pool.totalStaked * 10000) / pool.maxStakeAmount : 0;
|
||||
|
||||
uint256 retentionScore = performance.retentionRate;
|
||||
|
||||
uint256 rewardEfficiency = performance.totalRewardsGenerated > 0 ?
|
||||
(performance.distributedRewards * 10000) / performance.totalRewardsGenerated : 10000;
|
||||
|
||||
// Weighted average performance score
|
||||
uint256 performanceScore = (utilizationScore * 40 + retentionScore * 30 + rewardEfficiency * 30) / 100;
|
||||
|
||||
return performanceScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Calculate rewards for a position
|
||||
*/
|
||||
function _calculateRewards(StakingPosition memory position) internal view returns (uint256) {
|
||||
uint256 timeElapsed = block.timestamp - position.lastRewardTime;
|
||||
uint256 stakingDuration = block.timestamp - position.lockStart;
|
||||
|
||||
// Base rewards calculation
|
||||
uint256 baseRewards = (position.amount * position.apyAtStake * timeElapsed) / (BASIS_POINTS * 365 days);
|
||||
|
||||
// Performance bonus
|
||||
uint256 performanceBonus = 0;
|
||||
if (address(performanceAggregator) != address(0)) {
|
||||
uint256 reputationScore = performanceAggregator.getReputationScore(position.staker);
|
||||
uint256 multiplier = performanceAggregator.calculateAPYMultiplier(reputationScore);
|
||||
performanceBonus = (baseRewards * (multiplier - BASIS_POINTS)) / BASIS_POINTS;
|
||||
}
|
||||
|
||||
return baseRewards + performanceBonus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update pool performance metrics
|
||||
*/
|
||||
function _updatePoolPerformance(uint256 poolId) internal {
|
||||
PoolPerformance storage performance = poolPerformance[poolId];
|
||||
StakingPool storage pool = stakingPools[poolId];
|
||||
|
||||
// Update utilization rate
|
||||
performance.utilizationRate = pool.totalStaked > 0 ?
|
||||
(pool.totalStaked * 10000) / pool.maxStakeAmount : 0;
|
||||
|
||||
// Update retention rate (simplified calculation)
|
||||
if (pool.totalStakers > 0) {
|
||||
uint256 activePositions = 0;
|
||||
uint256[] memory positions = poolStakers[poolId];
|
||||
for (uint256 i = 0; i < positions.length; i++) {
|
||||
if (stakingPositions[positions[i]].isActive) {
|
||||
activePositions++;
|
||||
}
|
||||
}
|
||||
performance.retentionRate = (activePositions * 10000) / pool.totalStakers;
|
||||
}
|
||||
|
||||
// Update distributed rewards (simplified)
|
||||
performance.distributedRewards = performance.totalRewardsGenerated; // Simplified for now
|
||||
|
||||
performance.lastUpdated = block.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get pool details
|
||||
*/
|
||||
function getPoolDetails(uint256 poolId) external view returns (
|
||||
string memory poolName,
|
||||
uint256 baseAPY,
|
||||
uint256 currentAPY,
|
||||
uint256 totalStaked,
|
||||
uint256 lockPeriod,
|
||||
uint256 totalStakers,
|
||||
bool isActive,
|
||||
string memory description
|
||||
) {
|
||||
StakingPool memory pool = stakingPools[poolId];
|
||||
return (
|
||||
pool.poolName,
|
||||
pool.baseAPY,
|
||||
pool.currentAPY,
|
||||
pool.totalStaked,
|
||||
pool.lockPeriod,
|
||||
pool.totalStakers,
|
||||
pool.isActive,
|
||||
pool.description
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get position details
|
||||
*/
|
||||
function getPositionDetails(uint256 positionId) external view returns (
|
||||
uint256 poolId,
|
||||
address staker,
|
||||
uint256 amount,
|
||||
uint256 lockStart,
|
||||
uint256 lockEnd,
|
||||
uint256 apyAtStake,
|
||||
uint256 accumulatedRewards,
|
||||
bool isActive,
|
||||
bool isWithdrawn
|
||||
) {
|
||||
StakingPosition memory position = stakingPositions[positionId];
|
||||
return (
|
||||
position.poolId,
|
||||
position.staker,
|
||||
position.amount,
|
||||
position.lockStart,
|
||||
position.lockEnd,
|
||||
position.apyAtStake,
|
||||
position.accumulatedRewards,
|
||||
position.isActive,
|
||||
position.isWithdrawn
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get user positions
|
||||
*/
|
||||
function getUserPositions(address user) external view returns (uint256[] memory) {
|
||||
return stakerPositions[user];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get pool stakers
|
||||
*/
|
||||
function getPoolStakers(uint256 poolId) external view returns (uint256[] memory) {
|
||||
return poolStakers[poolId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get active pool IDs
|
||||
*/
|
||||
function getActivePoolIds() external view returns (uint256[] memory) {
|
||||
return activePoolIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get pool by name
|
||||
*/
|
||||
function getPoolByName(string memory poolName) external view returns (uint256) {
|
||||
return poolNameToId[poolName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deactivate a pool
|
||||
*/
|
||||
function deactivatePool(uint256 poolId) external onlyAuthorized validPool(poolId) {
|
||||
stakingPools[poolId].isActive = false;
|
||||
|
||||
// Remove from active pools
|
||||
for (uint256 i = 0; i < activePoolIds.length; i++) {
|
||||
if (activePoolIds[i] == poolId) {
|
||||
activePoolIds[i] = activePoolIds[activePoolIds.length - 1];
|
||||
activePoolIds.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit PoolDeactivated(poolId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Emergency withdraw
|
||||
*/
|
||||
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
|
||||
if (token == address(stakingToken)) {
|
||||
stakingToken.safeTransfer(msg.sender, amount);
|
||||
} else {
|
||||
IERC20(token).safeTransfer(msg.sender, amount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get factory statistics
|
||||
*/
|
||||
function getFactoryStats() external view returns (
|
||||
uint256 totalPools,
|
||||
uint256 activePools,
|
||||
uint256 totalStaked,
|
||||
uint256 totalStakers,
|
||||
uint256 totalPositions
|
||||
) {
|
||||
uint256 _totalStaked = 0;
|
||||
uint256 _totalStakers = 0;
|
||||
|
||||
for (uint256 i = 0; i < activePoolIds.length; i++) {
|
||||
StakingPool memory pool = stakingPools[activePoolIds[i]];
|
||||
_totalStaked += pool.totalStaked;
|
||||
_totalStakers += pool.totalStakers;
|
||||
}
|
||||
|
||||
return (
|
||||
poolCounter,
|
||||
activePoolIds.length,
|
||||
_totalStaked,
|
||||
_totalStakers,
|
||||
positionCounter
|
||||
);
|
||||
}
|
||||
}
|
||||
501
contracts/contracts/TreasuryManager.sol
Normal file
501
contracts/contracts/TreasuryManager.sol
Normal file
@@ -0,0 +1,501 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/IModularContracts.sol";
|
||||
import "./ContractRegistry.sol";
|
||||
|
||||
/**
|
||||
* @title TreasuryManager
|
||||
* @dev Modular treasury management with budget categories and automated allocation
|
||||
* @notice Integrates with DAOGovernance for automated execution and RewardDistributor for rewards
|
||||
*/
|
||||
contract TreasuryManager is ITreasuryManager, Ownable, ReentrancyGuard, Pausable {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
// State variables
|
||||
uint256 public version = 1;
|
||||
IERC20 public treasuryToken;
|
||||
ContractRegistry public registry;
|
||||
address public daoGovernance;
|
||||
|
||||
// Budget categories
|
||||
struct BudgetCategory {
|
||||
string name;
|
||||
uint256 totalBudget;
|
||||
uint256 allocatedAmount;
|
||||
uint256 spentAmount;
|
||||
bool isActive;
|
||||
uint256 createdAt;
|
||||
address creator;
|
||||
}
|
||||
|
||||
// Fund allocation
|
||||
struct FundAllocation {
|
||||
uint256 allocationId;
|
||||
string category;
|
||||
address recipient;
|
||||
uint256 totalAmount;
|
||||
uint256 releasedAmount;
|
||||
uint256 vestingPeriod;
|
||||
uint256 vestingStart;
|
||||
uint256 lastRelease;
|
||||
bool isCompleted;
|
||||
bool isActive;
|
||||
address allocatedBy;
|
||||
uint256 createdAt;
|
||||
}
|
||||
|
||||
// Mappings
|
||||
mapping(string => BudgetCategory) public budgetCategories;
|
||||
mapping(uint256 => FundAllocation) public allocations;
|
||||
mapping(string => uint256[]) public categoryAllocations;
|
||||
mapping(address => uint256[]) public recipientAllocations;
|
||||
|
||||
// Counters
|
||||
uint256 public categoryCounter;
|
||||
uint256 public allocationCounter;
|
||||
string[] public categoryNames;
|
||||
|
||||
// Constants
|
||||
uint256 public constant MIN_ALLOCATION = 100 * 10**18; // 100 tokens minimum
|
||||
uint256 public constant MAX_VESTING_PERIOD = 365 days; // 1 year maximum
|
||||
uint256 public constant DEFAULT_VESTING_PERIOD = 30 days; // 30 days default
|
||||
|
||||
// Events
|
||||
event BudgetCategoryCreated(string indexed category, uint256 budget, address indexed creator);
|
||||
event BudgetCategoryUpdated(string indexed category, uint256 newBudget);
|
||||
event FundsAllocated(uint256 indexed allocationId, string indexed category, address indexed recipient, uint256 amount);
|
||||
event FundsReleased(uint256 indexed allocationId, address indexed recipient, uint256 amount);
|
||||
event AllocationCompletedEvent(uint256 indexed allocationId);
|
||||
event TreasuryDeposited(address indexed depositor, uint256 amount);
|
||||
event TreasuryWithdrawn(address indexed recipient, uint256 amount);
|
||||
event CategoryDeactivated(string indexed category);
|
||||
|
||||
// Errors
|
||||
error InvalidAmount(uint256 amount);
|
||||
error InvalidCategory(string category);
|
||||
error InsufficientBudget(string category, uint256 requested, uint256 available);
|
||||
error AllocationNotFound(uint256 allocationId);
|
||||
error AllocationCompletedError(uint256 allocationId);
|
||||
error InvalidVestingPeriod(uint256 period);
|
||||
error InsufficientBalance(uint256 requested, uint256 available);
|
||||
error NotAuthorized();
|
||||
error RegistryNotSet();
|
||||
|
||||
modifier validAmount(uint256 amount) {
|
||||
if (amount == 0) revert InvalidAmount(amount);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validCategory(string memory category) {
|
||||
if (bytes(category).length == 0 || !budgetCategories[category].isActive) {
|
||||
revert InvalidCategory(category);
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAuthorized() {
|
||||
if (msg.sender != owner() && msg.sender != daoGovernance) revert NotAuthorized();
|
||||
_;
|
||||
}
|
||||
|
||||
modifier registrySet() {
|
||||
if (address(registry) == address(0)) revert RegistryNotSet();
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _treasuryToken) {
|
||||
treasuryToken = IERC20(_treasuryToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Initialize the treasury manager (implements IModularContract)
|
||||
*/
|
||||
function initialize(address _registry) external override {
|
||||
require(address(registry) == address(0), "Already initialized");
|
||||
registry = ContractRegistry(_registry);
|
||||
|
||||
// Register this contract if not already registered
|
||||
bytes32 contractId = keccak256(abi.encodePacked("TreasuryManager"));
|
||||
try registry.getContract(contractId) returns (address) {
|
||||
// Already registered, skip
|
||||
} catch {
|
||||
// Not registered, register now
|
||||
registry.registerContract(contractId, address(this));
|
||||
}
|
||||
|
||||
// Get DAO governance address from registry
|
||||
try registry.getContract(keccak256(abi.encodePacked("DAOGovernance"))) returns (address govAddress) {
|
||||
daoGovernance = govAddress;
|
||||
} catch {
|
||||
// DAO governance not found, keep as zero address
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upgrade the contract
|
||||
*/
|
||||
function upgrade(address newImplementation) external override onlyOwner {
|
||||
version++;
|
||||
// Implementation upgrade logic would go here
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pause the contract
|
||||
*/
|
||||
function pause() external override onlyOwner {
|
||||
_pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unpause the contract
|
||||
*/
|
||||
function unpause() external override onlyOwner {
|
||||
_unpause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get current version
|
||||
*/
|
||||
function getVersion() external view override returns (uint256) {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Create a budget category
|
||||
*/
|
||||
function createBudgetCategory(string memory category, uint256 budget)
|
||||
external
|
||||
override
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAmount(budget)
|
||||
nonReentrant
|
||||
{
|
||||
require(budgetCategories[category].createdAt == 0, "Category already exists");
|
||||
|
||||
budgetCategories[category] = BudgetCategory({
|
||||
name: category,
|
||||
totalBudget: budget,
|
||||
allocatedAmount: 0,
|
||||
spentAmount: 0,
|
||||
isActive: true,
|
||||
createdAt: block.timestamp,
|
||||
creator: msg.sender
|
||||
});
|
||||
|
||||
categoryNames.push(category);
|
||||
categoryCounter++;
|
||||
|
||||
emit BudgetCategoryCreated(category, budget, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update budget category
|
||||
*/
|
||||
function updateBudgetCategory(string memory category, uint256 newBudget)
|
||||
external
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAmount(newBudget)
|
||||
validCategory(category)
|
||||
nonReentrant
|
||||
{
|
||||
BudgetCategory storage budgetCategory = budgetCategories[category];
|
||||
|
||||
// Ensure new budget is not less than allocated amount
|
||||
require(newBudget >= budgetCategory.allocatedAmount, "Budget below allocated amount");
|
||||
|
||||
budgetCategory.totalBudget = newBudget;
|
||||
|
||||
emit BudgetCategoryUpdated(category, newBudget);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Allocate funds to a recipient
|
||||
*/
|
||||
function allocateFunds(string memory category, address recipient, uint256 amount)
|
||||
external
|
||||
override
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAmount(amount)
|
||||
validCategory(category)
|
||||
nonReentrant
|
||||
{
|
||||
BudgetCategory storage budgetCategory = budgetCategories[category];
|
||||
|
||||
// Check budget availability
|
||||
uint256 availableBudget = budgetCategory.totalBudget - budgetCategory.allocatedAmount;
|
||||
if (amount > availableBudget) {
|
||||
revert InsufficientBudget(category, amount, availableBudget);
|
||||
}
|
||||
|
||||
// Check treasury balance
|
||||
uint256 treasuryBalance = treasuryToken.balanceOf(address(this));
|
||||
if (amount > treasuryBalance) {
|
||||
revert InsufficientBalance(amount, treasuryBalance);
|
||||
}
|
||||
|
||||
// Create allocation
|
||||
uint256 allocationId = ++allocationCounter;
|
||||
allocations[allocationId] = FundAllocation({
|
||||
allocationId: allocationId,
|
||||
category: category,
|
||||
recipient: recipient,
|
||||
totalAmount: amount,
|
||||
releasedAmount: 0,
|
||||
vestingPeriod: DEFAULT_VESTING_PERIOD,
|
||||
vestingStart: block.timestamp,
|
||||
lastRelease: block.timestamp,
|
||||
isCompleted: false,
|
||||
isActive: true,
|
||||
allocatedBy: msg.sender,
|
||||
createdAt: block.timestamp
|
||||
});
|
||||
|
||||
// Update budget category
|
||||
budgetCategory.allocatedAmount += amount;
|
||||
categoryAllocations[category].push(allocationId);
|
||||
recipientAllocations[recipient].push(allocationId);
|
||||
|
||||
emit FundsAllocated(allocationId, category, recipient, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Allocate funds with custom vesting period
|
||||
*/
|
||||
function allocateFundsWithVesting(
|
||||
string memory category,
|
||||
address recipient,
|
||||
uint256 amount,
|
||||
uint256 vestingPeriod
|
||||
)
|
||||
external
|
||||
onlyAuthorized
|
||||
whenNotPaused
|
||||
validAmount(amount)
|
||||
validCategory(category)
|
||||
nonReentrant
|
||||
{
|
||||
if (vestingPeriod > MAX_VESTING_PERIOD) {
|
||||
revert InvalidVestingPeriod(vestingPeriod);
|
||||
}
|
||||
|
||||
BudgetCategory storage budgetCategory = budgetCategories[category];
|
||||
|
||||
// Check budget availability
|
||||
uint256 availableBudget = budgetCategory.totalBudget - budgetCategory.allocatedAmount;
|
||||
if (amount > availableBudget) {
|
||||
revert InsufficientBudget(category, amount, availableBudget);
|
||||
}
|
||||
|
||||
// Check treasury balance
|
||||
uint256 treasuryBalance = treasuryToken.balanceOf(address(this));
|
||||
if (amount > treasuryBalance) {
|
||||
revert InsufficientBalance(amount, treasuryBalance);
|
||||
}
|
||||
|
||||
// Create allocation with custom vesting
|
||||
uint256 allocationId = ++allocationCounter;
|
||||
allocations[allocationId] = FundAllocation({
|
||||
allocationId: allocationId,
|
||||
category: category,
|
||||
recipient: recipient,
|
||||
totalAmount: amount,
|
||||
releasedAmount: 0,
|
||||
vestingPeriod: vestingPeriod,
|
||||
vestingStart: block.timestamp,
|
||||
lastRelease: block.timestamp,
|
||||
isCompleted: false,
|
||||
isActive: true,
|
||||
allocatedBy: msg.sender,
|
||||
createdAt: block.timestamp
|
||||
});
|
||||
|
||||
// Update budget category
|
||||
budgetCategory.allocatedAmount += amount;
|
||||
categoryAllocations[category].push(allocationId);
|
||||
recipientAllocations[recipient].push(allocationId);
|
||||
|
||||
emit FundsAllocated(allocationId, category, recipient, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Release vested funds
|
||||
*/
|
||||
function releaseVestedFunds(uint256 allocationId)
|
||||
external
|
||||
override
|
||||
whenNotPaused
|
||||
nonReentrant
|
||||
{
|
||||
FundAllocation storage allocation = allocations[allocationId];
|
||||
|
||||
if (allocation.allocationId == 0) {
|
||||
revert AllocationNotFound(allocationId);
|
||||
}
|
||||
|
||||
if (allocation.isCompleted) {
|
||||
revert AllocationCompletedError(allocationId);
|
||||
}
|
||||
|
||||
if (msg.sender != allocation.recipient && msg.sender != owner() && msg.sender != daoGovernance) {
|
||||
revert NotAuthorized();
|
||||
}
|
||||
|
||||
// Calculate vested amount
|
||||
uint256 vestedAmount = calculateVestedAmount(allocation);
|
||||
uint256 releasableAmount = vestedAmount - allocation.releasedAmount;
|
||||
|
||||
if (releasableAmount == 0) {
|
||||
return; // Nothing to release
|
||||
}
|
||||
|
||||
// Update allocation
|
||||
allocation.releasedAmount += releasableAmount;
|
||||
allocation.lastRelease = block.timestamp;
|
||||
|
||||
// Update budget category spent amount
|
||||
budgetCategories[allocation.category].spentAmount += releasableAmount;
|
||||
|
||||
// Check if allocation is completed
|
||||
if (allocation.releasedAmount >= allocation.totalAmount) {
|
||||
allocation.isCompleted = true;
|
||||
emit AllocationCompletedEvent(allocationId);
|
||||
}
|
||||
|
||||
// Transfer tokens
|
||||
treasuryToken.safeTransfer(allocation.recipient, releasableAmount);
|
||||
|
||||
emit FundsReleased(allocationId, allocation.recipient, releasableAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Calculate vested amount for an allocation
|
||||
*/
|
||||
function calculateVestedAmount(FundAllocation memory allocation) public view returns (uint256) {
|
||||
if (block.timestamp < allocation.vestingStart) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint256 timePassed = block.timestamp - allocation.vestingStart;
|
||||
if (timePassed >= allocation.vestingPeriod) {
|
||||
return allocation.totalAmount;
|
||||
}
|
||||
|
||||
return (allocation.totalAmount * timePassed) / allocation.vestingPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get budget balance for a category
|
||||
*/
|
||||
function getBudgetBalance(string memory category) external view override returns (uint256) {
|
||||
BudgetCategory memory budgetCategory = budgetCategories[category];
|
||||
return budgetCategory.totalBudget - budgetCategory.allocatedAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get allocation details
|
||||
*/
|
||||
function getAllocation(uint256 allocationId) external view override returns (address, uint256, uint256) {
|
||||
FundAllocation memory allocation = allocations[allocationId];
|
||||
return (allocation.recipient, allocation.totalAmount, allocation.releasedAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get vested amount for an allocation
|
||||
*/
|
||||
function getVestedAmount(uint256 allocationId) external view returns (uint256) {
|
||||
FundAllocation memory allocation = allocations[allocationId];
|
||||
return calculateVestedAmount(allocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get all allocations for a recipient
|
||||
*/
|
||||
function getRecipientAllocations(address recipient) external view returns (uint256[] memory) {
|
||||
return recipientAllocations[recipient];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get all allocations for a category
|
||||
*/
|
||||
function getCategoryAllocations(string memory category) external view returns (uint256[] memory) {
|
||||
return categoryAllocations[category];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get all budget categories
|
||||
*/
|
||||
function getBudgetCategories() external view returns (string[] memory) {
|
||||
return categoryNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get treasury statistics
|
||||
*/
|
||||
function getTreasuryStats() external view returns (
|
||||
uint256 totalBudget,
|
||||
uint256 allocatedAmount,
|
||||
uint256 spentAmount,
|
||||
uint256 availableBalance,
|
||||
uint256 activeCategories
|
||||
) {
|
||||
uint256 _totalBudget = 0;
|
||||
uint256 _allocatedAmount = 0;
|
||||
uint256 _spentAmount = 0;
|
||||
uint256 _activeCategories = 0;
|
||||
|
||||
for (uint256 i = 0; i < categoryNames.length; i++) {
|
||||
BudgetCategory memory category = budgetCategories[categoryNames[i]];
|
||||
if (category.isActive) {
|
||||
_totalBudget += category.totalBudget;
|
||||
_allocatedAmount += category.allocatedAmount;
|
||||
_spentAmount += category.spentAmount;
|
||||
_activeCategories++;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
_totalBudget,
|
||||
_allocatedAmount,
|
||||
_spentAmount,
|
||||
treasuryToken.balanceOf(address(this)),
|
||||
_activeCategories
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deposit funds into treasury
|
||||
*/
|
||||
function depositFunds(uint256 amount) external whenNotPaused validAmount(amount) nonReentrant {
|
||||
treasuryToken.safeTransferFrom(msg.sender, address(this), amount);
|
||||
emit TreasuryDeposited(msg.sender, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Emergency withdraw from treasury
|
||||
*/
|
||||
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
|
||||
if (token == address(treasuryToken)) {
|
||||
treasuryToken.safeTransfer(msg.sender, amount);
|
||||
} else {
|
||||
IERC20(token).safeTransfer(msg.sender, amount);
|
||||
}
|
||||
emit TreasuryWithdrawn(msg.sender, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deactivate a budget category
|
||||
*/
|
||||
function deactivateCategory(string memory category) external onlyAuthorized validCategory(category) {
|
||||
budgetCategories[category].isActive = false;
|
||||
emit CategoryDeactivated(category);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user