// SPDX-License-Identifier: UNLICENSED pragma solidity =0.8.18; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "../SomaGuard/utils/GuardableUpgradeable.sol"; import "./extensions/TokenRecoveryUpgradeable.sol"; import "./ILockdrop.sol"; /** * @notice Implementation of the {ILockdrop} interface. */ contract Lockdrop is ILockdrop, TokenRecoveryUpgradeable, GuardableUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; using EnumerableSet for EnumerableSet.AddressSet; /** * @inheritdoc ILockdrop */ bytes32 public constant override GLOBAL_ADMIN_ROLE = keccak256("Lockdrop.GLOBAL_ADMIN_ROLE"); /** * @inheritdoc ILockdrop */ bytes32 public override LOCAL_ADMIN_ROLE; /** * @inheritdoc ILockdrop */ uint256 public override id; /** * @inheritdoc ILockdrop */ address public override asset; /** * @inheritdoc ILockdrop */ address public override withdrawTo; DateConfig private _dateConfig; mapping(bytes32 => Pool) private _pools; mapping(address => mapping(bytes32 => uint256)) private _lockDurations; /** * @notice The modifier that restricts a function caller to accounts that have the GLOBAL_ADMIN_ROLE * or the LOCAL_ADMIN_ROLE. */ modifier onlyAdmin() { address sender = _msgSender(); require(hasRole(GLOBAL_ADMIN_ROLE, sender) || hasRole(LOCAL_ADMIN_ROLE, sender), "Lockdrop: ADMIN ONLY"); _; } /** * @inheritdoc ILockdrop */ function initialize(uint256 _id, address _asset, address _withdrawTo, DateConfig calldata _initDateConfig) external override initializer { require(_asset != address(0), "Lockdrop: INVALID ASSET"); require(_withdrawTo != address(0), "Lockdrop: INVALID WITHDRAW TO"); LOCAL_ADMIN_ROLE = keccak256(abi.encodePacked(address(this), GLOBAL_ADMIN_ROLE)); __Guardable__init(); __ReentrancyGuard_init(); __TokenRecovery__init_unchained(new address[](0)); id = _id; asset = _asset; _disableTokenRecovery(_asset); _setWithdrawTo(_withdrawTo); _updateDateConfig(_initDateConfig); } /** * @inheritdoc ILockdrop */ function setWithdrawTo(address account) external override onlyMaster { _setWithdrawTo(account); } /** * @notice Checks if Lockdrop inherits a given contract interface. * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(GuardableUpgradeable, TokenRecoveryUpgradeable) returns (bool) { return interfaceId == type(ILockdrop).interfaceId || super.supportsInterface(interfaceId); } /** * @inheritdoc ILockdrop */ function dateConfig() external view override returns (DateConfig memory) { return _dateConfig; } /** * @inheritdoc ILockdrop */ function balanceOf(bytes32 poolId, address account) external view override returns (uint256) { return _pools[poolId].balances[account]; } /** * @inheritdoc ILockdrop */ function lockDuration(bytes32 poolId, address account) external view override returns (uint256) { return _lockDurations[account][poolId]; } /** * @inheritdoc ILockdrop */ function enabled(bytes32 poolId) external view override returns (bool) { return _pools[poolId].enabled; } /** * @inheritdoc ILockdrop */ function requiredPrivileges(bytes32 poolId) external view override returns (bytes32) { require(_pools[poolId].enabled, "Lockdrop: INVALID_POOL_ID"); return _pools[poolId].requiredPrivileges; } /** * @inheritdoc ILockdrop */ function updateDateConfig(DateConfig calldata newConfig) external override onlyAdmin { _updateDateConfig(newConfig); } /** * @inheritdoc ILockdrop */ function updatePool(bytes32 _poolId, bytes32 _requiredPrivileges, bool _enabled) external override onlyAdmin { _pools[_poolId].enabled = _enabled; _pools[_poolId].requiredPrivileges = _requiredPrivileges; emit PoolUpdated(_poolId, _requiredPrivileges, _enabled, _msgSender()); } /** * @inheritdoc ILockdrop */ function updateLockDuration(bytes32 poolId, uint256 newLockDuration) external override whenNotPaused { address sender = _msgSender(); require(_pools[poolId].balances[sender] != 0, "Lockdrop: ZERO DELEGATION"); _updateLockDuration(poolId, newLockDuration, sender); } /** * @inheritdoc ILockdrop */ function withdraw(uint256 amount) external override onlyAdmin { IERC20(asset).safeTransfer(withdrawTo, amount); } /** * @inheritdoc ILockdrop */ function moveDelegation(bytes32 fromPoolId, bytes32 toPoolId, uint256 amount, uint256 toPoolLockDuration) external override whenNotPaused nonReentrant { Pool storage fromPool = _pools[fromPoolId]; Pool storage toPool = _pools[toPoolId]; DateConfig memory dates = _dateConfig; address sender = _msgSender(); ISomaGuard guard = ISomaGuard(SOMA.guard()); require(fromPoolId != toPoolId, "Lockdrop: SAME POOLS"); require(dates.startDate <= block.timestamp, "Lockdrop: NOT STARTED"); require(block.timestamp < dates.endDate, "Lockdrop: ENDED"); require(amount > 0, "Lockdrop: ZERO AMOUNT"); require(fromPool.enabled, "Lockdrop: POOL DISABLED"); require(toPool.enabled, "Lockdrop: POOL DISABLED"); require(fromPool.balances[sender] >= amount, "Lockdrop: INSUFFICIENT BALANCE"); require(guard.check(sender, toPool.requiredPrivileges), "Lockdrop: NO PRIVILEGES"); require(guard.check(sender, fromPool.requiredPrivileges), "Lockdrop: NO PRIVILEGES"); fromPool.balances[sender] -= amount; toPool.balances[sender] += amount; if (fromPool.balances[sender] == 0) { _updateLockDuration(fromPoolId, 0, sender); } _updateLockDuration(toPoolId, toPoolLockDuration, sender); emit DelegationMoved(fromPoolId, toPoolId, amount, _msgSender()); } /** * @inheritdoc ILockdrop */ function removeDelegation(bytes32 poolId, uint256 amount) external whenNotPaused nonReentrant { Pool storage pool = _pools[poolId]; DateConfig memory dates = _dateConfig; address sender = _msgSender(); require(amount > 0, "Lockdrop: ZERO AMOUNT"); require(pool.enabled, "Lockdrop: POOL DISABLED"); require(dates.startDate <= block.timestamp, "Lockdrop: NOT STARTED"); require(block.timestamp < dates.removeDelegationEnd, "Lockdrop: REMOVE DELEGATION ENDED"); require(amount <= pool.balances[sender], "Lockdrop: INSUFFICIENT WITHDRAWABLE BALANCE"); require(ISomaGuard(SOMA.guard()).check(sender, pool.requiredPrivileges), "Lockdrop: NO PRIVILEGES"); pool.balances[sender] -= amount; if (pool.balances[sender] == 0) { _updateLockDuration(poolId, 0, sender); } // slither-disable-next-line reentrancy-no-eth IERC20(asset).safeTransfer(sender, amount); emit DelegationRemoved(poolId, amount, sender); } /** * @inheritdoc ILockdrop */ function delegate(bytes32 poolId, uint256 amount, uint256 lockDuration_) external override whenNotPaused nonReentrant { Pool storage toPool = _pools[poolId]; address sender = _msgSender(); require(amount > 0, "Lockdrop: ZERO AMOUNT"); require(ISomaGuard(SOMA.guard()).check(sender, toPool.requiredPrivileges), "Lockdrop: NO PRIVILEGES"); _updateLockDuration(poolId, lockDuration_, sender); uint256 prevBalance = IERC20(asset).balanceOf(address(this)); // slither-disable-next-line reentrancy-no-eth IERC20(asset).safeTransferFrom(sender, address(this), amount); uint256 receivedAmount = IERC20(asset).balanceOf(address(this)) - prevBalance; toPool.balances[sender] += receivedAmount; emit DelegationAdded(poolId, receivedAmount, lockDuration_, _msgSender()); } function _updateLockDuration(bytes32 poolId, uint256 newLockDuration, address account) private { DateConfig memory dates = _dateConfig; require(_pools[poolId].enabled, "Lockdrop: POOL DISABLED"); require(dates.startDate <= block.timestamp, "Lockdrop: NOT STARTED"); require(block.timestamp < dates.endDate, "Lockdrop: ENDED"); emit LockDurationUpdated(poolId, _lockDurations[account][poolId], newLockDuration, account); _lockDurations[account][poolId] = newLockDuration; } function _updateDateConfig(DateConfig calldata _newDateConfig) private { require( _newDateConfig.startDate <= _newDateConfig.removeDelegationEnd && _newDateConfig.removeDelegationEnd <= _newDateConfig.endDate, "Lockdrop: INVALID DATE CONFIGURATION" ); emit DatesUpdated(_dateConfig, _newDateConfig, _msgSender()); _dateConfig = _newDateConfig; } function _setWithdrawTo(address account) private { require(account != address(0), "Lockdrop: INVALID WITHDRAW TO"); emit WithdrawToUpdated(withdrawTo, account, _msgSender()); withdrawTo = account; } }