// 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/proxy/Clones.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "../SomaGuard/utils/GuardableUpgradeable.sol"; import "../Lockdrop/extensions/TokenRecoveryUpgradeable.sol"; import "./SomaEarnToken.sol"; import "./ISomaEarn.sol"; /** * @notice Implementation of the {ISomaEarn} interface. */ contract SomaEarn is ISomaEarn, TokenRecoveryUpgradeable, GuardableUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; using Address for address; using EnumerableSet for EnumerableSet.AddressSet; /** * @inheritdoc ISomaEarn */ address public constant override SOMA_EARN_TOKEN = 0x9c80fD7d6D0e064Ad72F42eC60A8386b944C49e9; /** * @inheritdoc ISomaEarn */ bytes32 public constant override GLOBAL_ADMIN_ROLE = keccak256("SomaEarn.GLOBAL_ADMIN_ROLE"); /** * @inheritdoc ISomaEarn */ bytes32 public override LOCAL_ADMIN_ROLE; /** * @inheritdoc ISomaEarn */ uint256 public override id; /** * @inheritdoc ISomaEarn */ address public override asset; /** * @inheritdoc ISomaEarn */ address public override withdrawTo; ///////////DateConfig private _dateConfig; uint48 private _startDate; uint48 private _endDate; mapping(bytes32 => Pool) private _pools; mapping(address => DelegationConfig) private _delegationConfigs; /** * @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), "SomaEarn: ADMIN ONLY"); _; } /** * @inheritdoc ISomaEarn */ function initialize(uint256 _id, address _asset, address _withdrawTo, uint48 _initStartDate, uint48 _initEndDate) external override initializer { LOCAL_ADMIN_ROLE = keccak256(abi.encodePacked(address(this), GLOBAL_ADMIN_ROLE)); __Guardable__init(); __ReentrancyGuard_init_unchained(); __TokenRecovery__init_unchained(new address[](0)); id = _id; asset = _asset; _disableTokenRecovery(_asset); _setWithdrawTo(_withdrawTo); _updateDateConfig(_initStartDate, _initEndDate); } /** * @inheritdoc ISomaEarn */ function setWithdrawTo(address account) external override onlyMasterOrSubMaster { _setWithdrawTo(account); } /** * @notice Checks if SomaEarn inherits a given contract interface. * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(GuardableUpgradeable, TokenRecoveryUpgradeable) returns (bool) { return interfaceId == type(ISomaEarn).interfaceId || super.supportsInterface(interfaceId); } /** * @inheritdoc ISomaEarn */ function startDate() external view override returns (uint48) { return _startDate; } /** * @inheritdoc ISomaEarn */ function endDate() external view override returns (uint48) { return _endDate; } /** * @inheritdoc ISomaEarn */ function balanceOf(bytes32 poolId, address account) external view override returns (uint256) { return IERC20(token(poolId)).balanceOf(account); } /** * @inheritdoc ISomaEarn */ function userDelegation(bytes32 poolId, address account) external view override returns (uint256) { return _pools[poolId].userDelegation[account]; } /** * @inheritdoc ISomaEarn */ function token(bytes32 poolId) public view returns (address) { require(_pools[poolId].enabled, "SomaEarn: INVALID_POOL_ID"); return Clones.predictDeterministicAddress(SOMA_EARN_TOKEN, poolId); } /** * @inheritdoc ISomaEarn */ function delegationConfig(address account) external view override returns (DelegationConfig memory) { return _delegationConfigs[account]; } /** * @inheritdoc ISomaEarn */ function enabled(bytes32 poolId) external view override returns (bool) { return _pools[poolId].enabled; } /** * @inheritdoc ISomaEarn */ function requiredPrivileges(bytes32 poolId) external view override returns (bytes32) { require(_pools[poolId].enabled, "SomaEarn: INVALID_POOL_ID"); return SomaEarnToken(token(poolId)).requiredPrivileges(); } /** * @inheritdoc ISomaEarn */ function maxUserDelegation(bytes32 poolId) external view override returns (uint256) { require(_pools[poolId].enabled, "SomaEarn: INVALID_POOL_ID"); return _pools[poolId].maxUserDelegation; } /** * @inheritdoc ISomaEarn */ function maxTotalDelegation(bytes32 poolId) external view override returns (uint256) { require(_pools[poolId].enabled, "SomaEarn: INVALID_POOL_ID"); return _pools[poolId].maxTotalDelegation; } /** * @inheritdoc ISomaEarn */ function updateDateConfig(uint48 _newStartDate, uint48 _newEndDate) external override onlyAdmin { _updateDateConfig(_newStartDate, _newEndDate); } /** * @inheritdoc ISomaEarn */ function updatePool( bytes32 poolId, uint256 _maxUserDelegation, uint256 _maxTotalDelegation, bytes32 _requiredPrivileges, bool _enabled ) external override onlyAdmin nonReentrant { require(_maxUserDelegation <= _maxTotalDelegation, "SomaEarn: INVALID_DELEGATION_LIMITS"); Pool storage _pool = _pools[poolId]; // TODO should these values be saved via the token? Should the token be pausable the same way in which a pool can be disabled? // TODO If the pool is disabled, should people be allowed to transfer the tokens. _pool.maxTotalDelegation = _maxTotalDelegation; _pool.maxUserDelegation = _maxUserDelegation; _pool.enabled = _enabled; if (_pool.enabled) { SomaEarnToken(_createToken(poolId)).updateRequiredPrivileges(_requiredPrivileges); } emit PoolUpdated(poolId, _maxUserDelegation, _maxTotalDelegation, _requiredPrivileges, _enabled, _msgSender()); } /** * @inheritdoc ISomaEarn */ function withdraw(uint256 amount) external override onlyAdmin { IERC20(asset).safeTransfer(withdrawTo, amount); } /** * @inheritdoc ISomaEarn */ function moveDelegation(bytes32 fromPoolId, bytes32 toPoolId, uint256 amount) external override whenNotPaused nonReentrant { Pool storage fromPool = _pools[fromPoolId]; Pool storage toPool = _pools[toPoolId]; address sender = _msgSender(); require(fromPoolId != toPoolId, "SomaEarn: SAME POOLS"); require(_startDate <= block.timestamp, "SomaEarn: NOT STARTED"); require(block.timestamp < _endDate, "SomaEarn: DELEGATIONS COMPLETED"); require(amount > 0, "SomaEarn: ZERO AMOUNT"); require(fromPool.enabled, "SomaEarn: POOL DISABLED"); require(toPool.enabled, "SomaEarn: POOL DISABLED"); { // burn the tokens so they can get new tokens SomaEarnToken fromToken = SomaEarnToken(token(fromPoolId)); fromToken.burn(sender, amount); } { // mint new tokens from new pool SomaEarnToken toToken = SomaEarnToken(token(toPoolId)); toToken.mint(sender, amount); } fromPool.userDelegation[sender] -= amount; toPool.userDelegation[sender] += amount; IERC20 _token = IERC20(token(toPoolId)); require(_token.totalSupply() <= toPool.maxTotalDelegation, "SomaEarn: MAX_DELEGATION_EXCEED"); require(toPool.userDelegation[sender] <= toPool.maxUserDelegation, "SomaEarn: MAX_USER_DELEGATION"); emit DelegationMoved(fromPoolId, toPoolId, amount, _msgSender()); } /** * @inheritdoc ISomaEarn */ function delegate(bytes32 poolId, uint256 amount) external override whenNotPaused nonReentrant { Pool storage toPool = _pools[poolId]; address sender = _msgSender(); require(amount > 0, "SomaEarn: ZERO AMOUNT"); require(toPool.enabled, "SomaEarn: POOL DISABLED"); require(_startDate <= block.timestamp, "SomaEarn: NOT STARTED"); require(block.timestamp < _endDate, "SomaEarn: DELEGATIONS COMPLETED"); 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; SomaEarnToken _token = SomaEarnToken(token(poolId)); _token.mint(sender, receivedAmount); toPool.userDelegation[sender] += amount; require(_token.totalSupply() <= toPool.maxTotalDelegation, "SomaEarn: MAX_DELEGATION_EXCEED"); require(toPool.userDelegation[sender] <= toPool.maxUserDelegation, "SomaEarn: MAX_USER_DELEGATION"); emit DelegationAdded(poolId, receivedAmount, _msgSender()); } /** * @inheritdoc ISomaEarn */ function updateDelegationConfig(DelegationConfig calldata _newConfig) external override whenNotPaused { address sender = _msgSender(); DelegationConfig memory _prevConfig = _delegationConfigs[sender]; require(_newConfig.percentLocked <= 100, "SomaEarn: INVALID PERCENT"); require(_startDate <= block.timestamp, "SomaEarn: NOT STARTED"); require(block.timestamp < _endDate, "SomaEarn: COMPLETED"); _delegationConfigs[sender] = _newConfig; emit DelegationConfigUpdated(_prevConfig, _newConfig, _msgSender()); } function _updateDateConfig(uint48 _newStartDate, uint48 _newEndDate) private { emit DatesUpdated(_startDate, _endDate, _newStartDate, _newEndDate, _msgSender()); if (_startDate != _newStartDate) { _startDate = _newStartDate; } if (_endDate != _newEndDate) { _endDate = _newEndDate; } } function _setWithdrawTo(address account) private { emit WithdrawToUpdated(withdrawTo, account, _msgSender()); withdrawTo = account; } function _createToken(bytes32 poolId) private returns (address tokenAddress) { tokenAddress = token(poolId); if (!tokenAddress.isContract()) { Clones.cloneDeterministic(SOMA_EARN_TOKEN, poolId); // slither-disable-next-line reentrancy-no-eth SomaEarnToken(tokenAddress).initialize(poolId); emit TokenCreated(poolId, tokenAddress); } } }