75 lines
2.5 KiB
Solidity
75 lines
2.5 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";
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
}
|
|
}
|
|
}
|