- Remove executable permissions from configuration files (.editorconfig, .env.example, .gitignore) - Remove executable permissions from documentation files (README.md, LICENSE, SECURITY.md) - Remove executable permissions from web assets (HTML, CSS, JS files) - Remove executable permissions from data files (JSON, SQL, YAML, requirements.txt) - Remove executable permissions from source code files across all apps - Add executable permissions to Python
502 lines
16 KiB
Solidity
502 lines
16 KiB
Solidity
// 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);
|
|
}
|
|
}
|