123 lines
4.4 KiB
Solidity
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);
|
|
}
|
|
}
|