Files
smom-dbis-138/contracts/treasury/TreasuryVault.sol
2026-03-02 12:14:09 -08:00

123 lines
4.4 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title TreasuryVault
* @notice Asset custody for the export path; only approved modules can move funds; only approved tokens can leave.
* @dev Enforces per-tx cap, daily cap, and hourly rate limit. See docs/treasury/EXECUTOR_ALLOWLIST_MATRIX.md.
*/
contract TreasuryVault is AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant MODULE_ROLE = keccak256("MODULE_ROLE");
bytes32 public constant CAP_MANAGER_ROLE = keccak256("CAP_MANAGER_ROLE");
uint256 public maxPerTx;
uint256 public dailyCap;
uint256 public rateLimitPerHour;
uint256 public dailyUsed;
uint256 public dayStartTimestamp;
uint256 public hourlyCount;
uint256 public hourStartTimestamp;
mapping(address => bool) public allowedTokens;
event TransferRequested(address indexed token, address indexed toModule, uint256 amount);
event ModuleSet(address indexed module, bool approved);
event TokenSet(address indexed token, bool approved);
event CapsUpdated(uint256 maxPerTx, uint256 dailyCap, uint256 rateLimitPerHour);
error NotModule();
error TokenNotApproved();
error ExceedsPerTxCap();
error ExceedsDailyCap();
error ExceedsRateLimit();
error ZeroAddress();
error ZeroAmount();
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(CAP_MANAGER_ROLE, admin);
dayStartTimestamp = block.timestamp;
hourStartTimestamp = block.timestamp;
}
/**
* @notice Request transfer of tokens to an approved module.
* @param token Approved token address (canonical 138 list only).
* @param amount Amount to transfer.
* @param toModule Recipient module; must be an approved module.
*/
function requestTransfer(address token, uint256 amount, address toModule)
external
nonReentrant
onlyRole(MODULE_ROLE)
{
if (token == address(0) || toModule == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
if (!hasRole(MODULE_ROLE, toModule)) revert NotModule();
if (!allowedTokens[token]) revert TokenNotApproved();
if (maxPerTx != 0 && amount > maxPerTx) revert ExceedsPerTxCap();
_updateDailyHourly(amount);
if (dailyCap != 0 && dailyUsed + amount > dailyCap) revert ExceedsDailyCap();
if (rateLimitPerHour != 0 && hourlyCount > rateLimitPerHour) revert ExceedsRateLimit();
dailyUsed += amount;
IERC20(token).safeTransfer(toModule, amount);
emit TransferRequested(token, toModule, amount);
}
function _updateDailyHourly(uint256) internal {
uint256 now_ = block.timestamp;
if (now_ - dayStartTimestamp >= 1 days) {
dayStartTimestamp = now_;
dailyUsed = 0;
}
if (now_ - hourStartTimestamp >= 1 hours) {
hourStartTimestamp = now_;
hourlyCount = 0;
}
hourlyCount++;
}
function setModule(address module, bool approved) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (module == address(0)) revert ZeroAddress();
if (approved) {
_grantRole(MODULE_ROLE, module);
} else {
_revokeRole(MODULE_ROLE, module);
}
emit ModuleSet(module, approved);
}
function setToken(address token, bool approved) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (token == address(0)) revert ZeroAddress();
allowedTokens[token] = approved;
emit TokenSet(token, approved);
}
function setCaps(uint256 _maxPerTx, uint256 _dailyCap, uint256 _rateLimitPerHour)
external
onlyRole(CAP_MANAGER_ROLE)
{
maxPerTx = _maxPerTx;
dailyCap = _dailyCap;
rateLimitPerHour = _rateLimitPerHour;
emit CapsUpdated(_maxPerTx, _dailyCap, _rateLimitPerHour);
}
function deposit(address token, uint256 amount) external nonReentrant {
if (token == address(0)) revert ZeroAddress();
if (amount == 0) revert ZeroAmount();
if (!allowedTokens[token]) revert TokenNotApproved();
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
}
}