// 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"; /** * @title CWVotingEscrow * @notice Minimal vote-escrow (veCW) for emission-direction governance. */ contract CWVotingEscrow is AccessControl { using SafeERC20 for IERC20; IERC20 public immutable escrowToken; uint256 public constant MAX_LOCK_DURATION = 4 * 365 days; struct Lock { uint256 amount; uint256 unlockAt; uint256 votingPower; } mapping(address => Lock[]) public locks; event Locked(address indexed account, uint256 amount, uint256 unlockAt, uint256 votingPower); event Withdrawn(address indexed account, uint256 amount); error ZeroAddress(); error InvalidLock(); error NothingToWithdraw(); constructor(address admin, address escrowToken_) { if (admin == address(0) || escrowToken_ == address(0)) { revert ZeroAddress(); } _grantRole(DEFAULT_ADMIN_ROLE, admin); escrowToken = IERC20(escrowToken_); } function createLock(uint256 amount, uint256 lockDuration) external { if (amount == 0 || lockDuration == 0 || lockDuration > MAX_LOCK_DURATION) { revert InvalidLock(); } escrowToken.safeTransferFrom(msg.sender, address(this), amount); uint256 unlockAt = block.timestamp + lockDuration; uint256 votingPower = (amount * lockDuration) / MAX_LOCK_DURATION; locks[msg.sender].push(Lock({amount: amount, unlockAt: unlockAt, votingPower: votingPower})); emit Locked(msg.sender, amount, unlockAt, votingPower); } function withdraw(uint256 lockIndex) external { Lock storage entry = locks[msg.sender][lockIndex]; if (entry.amount == 0 || block.timestamp < entry.unlockAt) { revert NothingToWithdraw(); } uint256 amount = entry.amount; entry.amount = 0; entry.votingPower = 0; escrowToken.safeTransfer(msg.sender, amount); emit Withdrawn(msg.sender, amount); } function votingPowerOf(address account) external view returns (uint256 total) { Lock[] storage userLocks = locks[account]; for (uint256 i = 0; i < userLocks.length; i++) { if (userLocks[i].amount > 0 && block.timestamp < userLocks[i].unlockAt) { total += userLocks[i].votingPower; } } } }