├── README.md ├── .gitignore ├── contracts ├── interfaces │ ├── solidly │ │ ├── IVeDist.sol │ │ ├── IBribe.sol │ │ ├── IBaseV1Minter.sol │ │ ├── IGauge.sol │ │ ├── IBaseV1Voter.sol │ │ └── IVotingEscrow.sol │ ├── solidex │ │ ├── ISolidexVoter.sol │ │ ├── IFeeDistributor.sol │ │ ├── IVeDepositor.sol │ │ ├── ISolidexToken.sol │ │ ├── ISexPartners.sol │ │ ├── ILpDepositToken.sol │ │ ├── ITokenLocker.sol │ │ └── ILpDepositor.sol │ └── IERC20.sol ├── dependencies │ ├── Ownable.sol │ ├── SafeERC20.sol │ └── Address.sol ├── Token.sol ├── LpDepositToken.sol ├── StakingRewards.sol ├── FeeDistributor.sol ├── Whitelister.sol ├── SexPartners.sol ├── VeDepositor.sol ├── TokenLocker.sol ├── LpDepositor.sol └── SolidexVoter.sol ├── brownie-config.yaml ├── deployment-addresses.json └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # solidex 2 | A yield optimizer for Solidly built on Fantom. 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .env 3 | .history 4 | .hypothesis/ 5 | build/ 6 | reports/ 7 | -------------------------------------------------------------------------------- /contracts/interfaces/solidly/IVeDist.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface IVeDist { 4 | function claim(uint _tokenId) external returns (uint); 5 | } 6 | -------------------------------------------------------------------------------- /contracts/interfaces/solidly/IBribe.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface IBribe { 4 | function getReward(uint tokenId, address[] memory tokens) external; 5 | } 6 | -------------------------------------------------------------------------------- /contracts/interfaces/solidly/IBaseV1Minter.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface IBaseV1Minter { 4 | function active_period() external view returns (uint256); 5 | } 6 | -------------------------------------------------------------------------------- /contracts/interfaces/solidex/ISolidexVoter.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface ISolidexVoter { 4 | function setTokenID(uint256 tokenID) external returns (bool); 5 | } 6 | -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | networks: 2 | development: 3 | cmd_settings: 4 | accounts: 20 5 | 6 | 7 | autofetch_sources: true 8 | 9 | compiler: 10 | solc: 11 | version: 0.8.11 12 | -------------------------------------------------------------------------------- /contracts/interfaces/solidex/IFeeDistributor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface IFeeDistributor { 4 | function depositFee(address _token, uint256 _amount) external returns (bool); 5 | } 6 | -------------------------------------------------------------------------------- /contracts/interfaces/solidex/IVeDepositor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "../IERC20.sol"; 4 | 5 | interface IVeDepositor is IERC20 { 6 | function depositTokens(uint256 amount) external returns (bool); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/interfaces/solidex/ISolidexToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "../IERC20.sol"; 4 | 5 | interface ISolidexToken is IERC20 { 6 | function mint(address _to, uint256 _value) external returns (bool); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/interfaces/solidex/ISexPartners.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface ISexPartners { 4 | function earlyPartnerPct() external view returns (uint256); 5 | function isEarlyPartner(address account) external view returns (bool); 6 | } 7 | -------------------------------------------------------------------------------- /contracts/interfaces/solidly/IGauge.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface IGauge { 4 | function deposit(uint amount, uint tokenId) external; 5 | function withdraw(uint amount) external; 6 | function getReward(address account, address[] memory tokens) external; 7 | function earned(address token, address account) external view returns (uint256); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/solidex/ILpDepositToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface IDepositToken { 4 | function pool() external view returns (address); 5 | function initialize(address pool) external returns (bool); 6 | function mint(address to, uint256 value) external returns (bool); 7 | function burn(address from, uint256 value) external returns (bool); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/solidex/ITokenLocker.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface ITokenLocker { 4 | function getWeek() external view returns (uint256); 5 | function weeklyWeight(address user, uint256 week) external view returns (uint256, uint256); 6 | function userWeight(address _user) external view returns (uint256); 7 | function startTime() external view returns (uint256); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/solidex/ILpDepositor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface ILpDepositor { 4 | function setTokenID(uint256 tokenID) external returns (bool); 5 | function userBalances(address user, address pool) external view returns (uint256); 6 | function totalBalances(address pool) external view returns (uint256); 7 | function transferDeposit(address pool, address from, address to, uint256 amount) external returns (bool); 8 | function whitelist(address token) external returns (bool); 9 | } 10 | -------------------------------------------------------------------------------- /contracts/interfaces/solidly/IBaseV1Voter.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface IBaseV1Voter { 4 | function bribes(address gauge) external view returns (address bribe); 5 | function gauges(address pool) external view returns (address gauge); 6 | function poolForGauge(address gauge) external view returns (address pool); 7 | function createGauge(address pool) external returns (address); 8 | function vote(uint tokenId, address[] calldata pools, int256[] calldata weights) external; 9 | function whitelist(address token, uint tokenId) external; 10 | function listing_fee() external view returns (uint256); 11 | function _ve() external view returns (address); 12 | function isWhitelisted(address pool) external view returns (bool); 13 | } 14 | -------------------------------------------------------------------------------- /deployment-addresses.json: -------------------------------------------------------------------------------- 1 | { 2 | "SexPartners": "0x24c0e3b0eA69bd967d7ccA322801be7Cd53586a2", 3 | "FeeDistributor": "0xA5e76B97e12567bbA2e822aC68842097034C55e7", 4 | "SolidexVoter": "0xca082181C4f4a811bed68Ab61De6aDCe11158948", 5 | "TokenLocker": "0xDcC208496B8fcc8E99741df8c6b8856F1ba1C71F", 6 | "Whitelister": "0xb7714B6402ff461f202dA8347d1D7c6Bb16F675d", 7 | "LpDepositor": "0x26E1A0d851CF28E697870e1b7F053B605C8b060F", 8 | "StakingRewards": "0x7FcE87e203501C3a035CbBc5f0Ee72661976D6E1", 9 | "SolidexToken": "0xD31Fcd1f7Ba190dBc75354046F6024A9b86014d7", 10 | "VeDepositor": "0x41adAc6C1Ff52C5e27568f27998d747F7b69795B", 11 | "SOLIDsex/SOLID-LP": "0x62E2819Dd417F3b430B6fa5Fd34a49A377A02ac8", 12 | "SEX/WFTM-LP": "0xFCEC86aF8774d69e2e4412B8De3f4aBf1f671ecC" 13 | } 14 | -------------------------------------------------------------------------------- /contracts/interfaces/solidly/IVotingEscrow.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | interface IVotingEscrow { 4 | function increase_amount(uint256 tokenID, uint256 value) external; 5 | function increase_unlock_time(uint256 tokenID, uint256 duration) external; 6 | function merge(uint256 fromID, uint256 toID) external; 7 | function locked(uint256 tokenID) external view returns (uint256 amount, uint256 unlockTime); 8 | function setApprovalForAll(address operator, bool approved) external; 9 | function transferFrom(address from, address to, uint256 tokenID) external; 10 | function safeTransferFrom(address from, address to, uint tokenId) external; 11 | function ownerOf(uint tokenId) external view returns (address); 12 | function balanceOfNFT(uint tokenId) external view returns (uint); 13 | function isApprovedOrOwner(address, uint) external view returns (bool); 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Solidex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/dependencies/Ownable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (access/Ownable.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Contract module which provides a basic access control mechanism, where 8 | * there is an account (an owner) that can be granted exclusive access to 9 | * specific functions. 10 | * 11 | * By default, the owner account will be the one that deploys the contract. This 12 | * can later be changed with {transferOwnership}. 13 | * 14 | * This module is used through inheritance. It will make available the modifier 15 | * `onlyOwner`, which can be applied to your functions to restrict their use to 16 | * the owner. 17 | */ 18 | abstract contract Ownable { 19 | address public owner; 20 | 21 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 22 | 23 | /** 24 | * @dev Initializes the contract setting the deployer as the initial owner. 25 | */ 26 | constructor() { 27 | _transferOwnership(msg.sender); 28 | } 29 | 30 | /** 31 | * @dev Throws if called by any account other than the owner. 32 | */ 33 | modifier onlyOwner() { 34 | require(owner == msg.sender, "Ownable: caller is not the owner"); 35 | _; 36 | } 37 | 38 | /** 39 | * @dev Leaves the contract without owner. It will not be possible to call 40 | * `onlyOwner` functions anymore. Can only be called by the current owner. 41 | * 42 | * NOTE: Renouncing ownership will leave the contract without an owner, 43 | * thereby removing any functionality that is only available to the owner. 44 | */ 45 | function renounceOwnership() public virtual onlyOwner { 46 | _transferOwnership(address(0)); 47 | } 48 | 49 | /** 50 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 51 | * Can only be called by the current owner. 52 | */ 53 | function transferOwnership(address newOwner) public virtual onlyOwner { 54 | require(newOwner != address(0), "Ownable: new owner is the zero address"); 55 | _transferOwnership(newOwner); 56 | } 57 | 58 | /** 59 | * @dev Transfers ownership of the contract to a new account (`newOwner`). 60 | * Internal function without access restriction. 61 | */ 62 | function _transferOwnership(address newOwner) internal virtual { 63 | address oldOwner = owner; 64 | owner = newOwner; 65 | emit OwnershipTransferred(oldOwner, newOwner); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/Token.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./dependencies/Ownable.sol"; 4 | import "./interfaces/IERC20.sol"; 5 | 6 | 7 | contract SolidexToken is IERC20, Ownable { 8 | 9 | string public constant name = "Solidex"; 10 | string public constant symbol = "SEX"; 11 | uint8 public constant decimals = 18; 12 | uint256 public override totalSupply; 13 | 14 | mapping(address => uint256) public override balanceOf; 15 | mapping(address => mapping(address => uint256)) public override allowance; 16 | mapping(address => bool) public minters; 17 | 18 | constructor() { 19 | emit Transfer(address(0), msg.sender, 0); 20 | } 21 | 22 | /** 23 | @notice Approve contracts to mint and renounce ownership 24 | @dev In production the only minters should be `LpDepositor` and `SexPartners` 25 | Addresses are given via dynamic array to allow extra minters during testing 26 | */ 27 | function setMinters(address[] calldata _minters) external onlyOwner { 28 | for (uint256 i = 0; i < _minters.length; i++) { 29 | minters[_minters[i]] = true; 30 | } 31 | 32 | renounceOwnership(); 33 | } 34 | 35 | function approve(address _spender, uint256 _value) external override returns (bool) { 36 | allowance[msg.sender][_spender] = _value; 37 | emit Approval(msg.sender, _spender, _value); 38 | return true; 39 | } 40 | 41 | /** shared logic for transfer and transferFrom */ 42 | function _transfer(address _from, address _to, uint256 _value) internal { 43 | require(balanceOf[_from] >= _value, "Insufficient balance"); 44 | balanceOf[_from] -= _value; 45 | balanceOf[_to] += _value; 46 | emit Transfer(_from, _to, _value); 47 | } 48 | 49 | /** 50 | @notice Transfer tokens to a specified address 51 | @param _to The address to transfer to 52 | @param _value The amount to be transferred 53 | @return Success boolean 54 | */ 55 | function transfer(address _to, uint256 _value) public override returns (bool) { 56 | _transfer(msg.sender, _to, _value); 57 | return true; 58 | } 59 | 60 | /** 61 | @notice Transfer tokens from one address to another 62 | @param _from The address which you want to send tokens from 63 | @param _to The address which you want to transfer to 64 | @param _value The amount of tokens to be transferred 65 | @return Success boolean 66 | */ 67 | function transferFrom( 68 | address _from, 69 | address _to, 70 | uint256 _value 71 | ) 72 | public 73 | override 74 | returns (bool) 75 | { 76 | require(allowance[_from][msg.sender] >= _value, "Insufficient allowance"); 77 | if (allowance[_from][msg.sender] != type(uint).max) { 78 | allowance[_from][msg.sender] -= _value; 79 | } 80 | _transfer(_from, _to, _value); 81 | return true; 82 | } 83 | 84 | function mint(address _to, uint256 _value) external returns (bool) { 85 | require(minters[msg.sender], "Not a minter"); 86 | balanceOf[_to] += _value; 87 | totalSupply += _value; 88 | emit Transfer(address(0), _to, _value); 89 | return true; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.11; 4 | 5 | /** 6 | * Based on the OpenZeppelin IER20 interface: 7 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol 8 | * 9 | * @dev Interface of the ERC20 standard as defined in the EIP. 10 | */ 11 | interface IERC20 { 12 | /** 13 | * @dev Returns the amount of tokens in existence. 14 | */ 15 | function totalSupply() external view returns (uint256); 16 | 17 | /** 18 | * @dev Returns the amount of tokens owned by `account`. 19 | */ 20 | function balanceOf(address account) external view returns (uint256); 21 | 22 | /** 23 | * @dev Moves `amount` tokens from the caller's account to `recipient`. 24 | * 25 | * Returns a boolean value indicating whether the operation succeeded. 26 | * 27 | * Emits a {Transfer} event. 28 | */ 29 | function transfer(address recipient, uint256 amount) external returns (bool); 30 | 31 | /** 32 | * @dev Returns the remaining number of tokens that `spender` will be 33 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 34 | * zero by default. 35 | * 36 | * This value changes when {approve} or {transferFrom} are called. 37 | */ 38 | function allowance(address owner, address spender) external view returns (uint256); 39 | 40 | /** 41 | * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. 42 | * 43 | * Returns a boolean value indicating whether the operation succeeded. 44 | * 45 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 46 | * that someone may use both the old and the new allowance by unfortunate 47 | * transaction ordering. One possible solution to mitigate this race 48 | * condition is to first reduce the spender's allowance to 0 and set the 49 | * desired value afterwards: 50 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 51 | * 52 | * Emits an {Approval} event. 53 | */ 54 | function approve(address spender, uint256 amount) external returns (bool); 55 | 56 | /** 57 | * @dev Moves `amount` tokens from `sender` to `recipient` using the 58 | * allowance mechanism. `amount` is then deducted from the caller's 59 | * allowance. 60 | * 61 | * Returns a boolean value indicating whether the operation succeeded. 62 | * 63 | * Emits a {Transfer} event. 64 | */ 65 | function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); 66 | 67 | function name() external view returns (string memory); 68 | function symbol() external view returns (string memory); 69 | function decimals() external view returns (uint8); 70 | 71 | /** 72 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 73 | * another (`to`). 74 | * 75 | * Note that `value` may be zero. 76 | */ 77 | event Transfer(address indexed from, address indexed to, uint256 value); 78 | 79 | /** 80 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 81 | * a call to {approve}. `value` is the new allowance. 82 | */ 83 | event Approval(address indexed owner, address indexed spender, uint256 value); 84 | } 85 | -------------------------------------------------------------------------------- /contracts/LpDepositToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./interfaces/IERC20.sol"; 4 | import "./interfaces/solidex/ILpDepositor.sol"; 5 | 6 | 7 | contract DepositToken is IERC20 { 8 | 9 | string public name; 10 | string public symbol; 11 | uint8 public constant decimals = 18; 12 | 13 | mapping(address => mapping(address => uint256)) public override allowance; 14 | 15 | ILpDepositor public depositor; 16 | address public pool; 17 | 18 | constructor() { 19 | // set to prevent the implementation contract from being initialized 20 | pool = address(0xdead); 21 | } 22 | 23 | /** 24 | @dev Initializes the contract after deployment via a minimal proxy 25 | */ 26 | function initialize(address _pool) external returns (bool) { 27 | require(pool == address(0)); 28 | pool = _pool; 29 | depositor = ILpDepositor(msg.sender); 30 | string memory _symbol = IERC20(pool).symbol(); 31 | name = string(abi.encodePacked("Solidex ", _symbol, " Deposit")); 32 | symbol = string(abi.encodePacked("sex-", _symbol)); 33 | emit Transfer(address(0), msg.sender, 0); 34 | return true; 35 | } 36 | 37 | function balanceOf(address account) external view returns (uint256) { 38 | return depositor.userBalances(account, pool); 39 | } 40 | 41 | function totalSupply() external view returns (uint256) { 42 | return depositor.totalBalances(pool); 43 | } 44 | 45 | function approve(address _spender, uint256 _value) external override returns (bool) { 46 | allowance[msg.sender][_spender] = _value; 47 | emit Approval(msg.sender, _spender, _value); 48 | return true; 49 | } 50 | 51 | /** shared logic for transfer and transferFrom */ 52 | function _transfer(address _from, address _to, uint256 _value) internal { 53 | if (_value > 0) { 54 | depositor.transferDeposit(pool, _from, _to, _value); 55 | } 56 | emit Transfer(_from, _to, _value); 57 | } 58 | 59 | /** 60 | @notice Transfer tokens to a specified address 61 | @param _to The address to transfer to 62 | @param _value The amount to be transferred 63 | @return Success boolean 64 | */ 65 | function transfer(address _to, uint256 _value) public override returns (bool) { 66 | _transfer(msg.sender, _to, _value); 67 | return true; 68 | } 69 | 70 | /** 71 | @notice Transfer tokens from one address to another 72 | @param _from The address which you want to send tokens from 73 | @param _to The address which you want to transfer to 74 | @param _value The amount of tokens to be transferred 75 | @return Success boolean 76 | */ 77 | function transferFrom( 78 | address _from, 79 | address _to, 80 | uint256 _value 81 | ) 82 | public 83 | override 84 | returns (bool) 85 | { 86 | require(allowance[_from][msg.sender] >= _value, "Insufficient allowance"); 87 | if (allowance[_from][msg.sender] != type(uint).max) { 88 | allowance[_from][msg.sender] -= _value; 89 | } 90 | _transfer(_from, _to, _value); 91 | return true; 92 | } 93 | 94 | /** 95 | @dev Only callable ty `LpDepositor`. Used to trigger a `Transfer` event 96 | upon deposit of LP tokens, to aid accounting in block explorers. 97 | */ 98 | function mint(address _to, uint256 _value) external returns (bool) { 99 | require(msg.sender == address(depositor)); 100 | emit Transfer(address(0), _to, _value); 101 | return true; 102 | } 103 | 104 | /** 105 | @dev Only callable ty `LpDepositor`. Used to trigger a `Transfer` event 106 | upon withdrawal of LP tokens, to aid accounting in block explorers. 107 | */ 108 | function burn(address _from, uint256 _value) external returns (bool) { 109 | require(msg.sender == address(depositor)); 110 | emit Transfer(_from, address(0), _value); 111 | return true; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /contracts/dependencies/SafeERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (token/ERC20/utils/SafeERC20.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "../interfaces/IERC20.sol"; 7 | import "./Address.sol"; 8 | 9 | /** 10 | * @title SafeERC20 11 | * @dev Wrappers around ERC20 operations that throw on failure (when the token 12 | * contract returns false). Tokens that return no value (and instead revert or 13 | * throw on failure) are also supported, non-reverting calls are assumed to be 14 | * successful. 15 | * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, 16 | * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. 17 | */ 18 | library SafeERC20 { 19 | using Address for address; 20 | 21 | function safeTransfer( 22 | IERC20 token, 23 | address to, 24 | uint256 value 25 | ) internal { 26 | _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); 27 | } 28 | 29 | function safeTransferFrom( 30 | IERC20 token, 31 | address from, 32 | address to, 33 | uint256 value 34 | ) internal { 35 | _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); 36 | } 37 | 38 | /** 39 | * @dev Deprecated. This function has issues similar to the ones found in 40 | * {IERC20-approve}, and its usage is discouraged. 41 | * 42 | * Whenever possible, use {safeIncreaseAllowance} and 43 | * {safeDecreaseAllowance} instead. 44 | */ 45 | function safeApprove( 46 | IERC20 token, 47 | address spender, 48 | uint256 value 49 | ) internal { 50 | // safeApprove should only be called when setting an initial allowance, 51 | // or when resetting it to zero. To increase and decrease it, use 52 | // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' 53 | require( 54 | (value == 0) || (token.allowance(address(this), spender) == 0), 55 | "SafeERC20: approve from non-zero to non-zero allowance" 56 | ); 57 | _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); 58 | } 59 | 60 | function safeIncreaseAllowance( 61 | IERC20 token, 62 | address spender, 63 | uint256 value 64 | ) internal { 65 | uint256 newAllowance = token.allowance(address(this), spender) + value; 66 | _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); 67 | } 68 | 69 | function safeDecreaseAllowance( 70 | IERC20 token, 71 | address spender, 72 | uint256 value 73 | ) internal { 74 | unchecked { 75 | uint256 oldAllowance = token.allowance(address(this), spender); 76 | require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); 77 | uint256 newAllowance = oldAllowance - value; 78 | _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); 79 | } 80 | } 81 | 82 | /** 83 | * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement 84 | * on the return value: the return value is optional (but if data is returned, it must not be false). 85 | * @param token The token targeted by the call. 86 | * @param data The call data (encoded using abi.encode or one of its variants). 87 | */ 88 | function _callOptionalReturn(IERC20 token, bytes memory data) private { 89 | // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since 90 | // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that 91 | // the target address contains contract code and also asserts for success in the low-level call. 92 | 93 | bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); 94 | if (returndata.length > 0) { 95 | // Return data is optional 96 | require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /contracts/StakingRewards.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./dependencies/Ownable.sol"; 4 | import "./dependencies/SafeERC20.sol"; 5 | import "./interfaces/IERC20.sol"; 6 | 7 | 8 | contract StakingRewards is Ownable { 9 | using SafeERC20 for IERC20; 10 | 11 | /* ========== STATE VARIABLES ========== */ 12 | 13 | struct Reward { 14 | uint256 periodFinish; 15 | uint256 rewardRate; 16 | uint256 lastUpdateTime; 17 | uint256 rewardPerTokenStored; 18 | uint256 balance; 19 | } 20 | IERC20 public stakingToken; 21 | address[2] public rewardTokens; 22 | mapping(address => Reward) public rewardData; 23 | 24 | // user -> reward token -> amount 25 | mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid; 26 | mapping(address => mapping(address => uint256)) public rewards; 27 | 28 | uint256 public totalSupply; 29 | mapping(address => uint256) public balanceOf; 30 | 31 | uint256 public constant REWARDS_DURATION = 86400 * 7; 32 | 33 | event RewardAdded(address indexed rewardsToken, uint256 reward); 34 | event Staked(address indexed user, uint256 amount); 35 | event Withdrawn(address indexed user, uint256 amount); 36 | event RewardPaid(address indexed user, address indexed rewardsToken, uint256 reward); 37 | 38 | function setAddresses( 39 | address _stakingToken, 40 | address[2] memory _rewardTokens 41 | ) external onlyOwner { 42 | stakingToken = IERC20(_stakingToken); // SOLIDsex 43 | rewardTokens = _rewardTokens; // SOLID, SEX 44 | 45 | renounceOwnership(); 46 | } 47 | 48 | function lastTimeRewardApplicable(address _rewardsToken) public view returns (uint256) { 49 | uint256 periodFinish = rewardData[_rewardsToken].periodFinish; 50 | return block.timestamp < periodFinish ? block.timestamp : periodFinish; 51 | } 52 | 53 | function rewardPerToken(address _rewardsToken) public view returns (uint256) { 54 | if (totalSupply == 0) { 55 | return rewardData[_rewardsToken].rewardPerTokenStored; 56 | } 57 | uint256 duration = lastTimeRewardApplicable(_rewardsToken) - rewardData[_rewardsToken].lastUpdateTime; 58 | uint256 pending = duration * rewardData[_rewardsToken].rewardRate * 1e18 / totalSupply; 59 | return 60 | rewardData[_rewardsToken].rewardPerTokenStored + pending; 61 | } 62 | 63 | function earned(address account, address _rewardsToken) public view returns (uint256) { 64 | uint256 rpt = rewardPerToken(_rewardsToken) - userRewardPerTokenPaid[account][_rewardsToken]; 65 | return balanceOf[account] * rpt / 1e18 + rewards[account][_rewardsToken]; 66 | } 67 | 68 | function getRewardForDuration(address _rewardsToken) external view returns (uint256) { 69 | return rewardData[_rewardsToken].rewardRate * REWARDS_DURATION; 70 | } 71 | 72 | function stake(uint256 amount) external updateReward(msg.sender) { 73 | require(amount > 0, "Cannot stake 0"); 74 | totalSupply += amount; 75 | balanceOf[msg.sender] += amount; 76 | stakingToken.safeTransferFrom(msg.sender, address(this), amount); 77 | emit Staked(msg.sender, amount); 78 | } 79 | 80 | function withdraw(uint256 amount) public updateReward(msg.sender) { 81 | require(amount > 0, "Cannot withdraw 0"); 82 | totalSupply -= amount; 83 | balanceOf[msg.sender] -= amount; 84 | stakingToken.safeTransfer(msg.sender, amount); 85 | emit Withdrawn(msg.sender, amount); 86 | } 87 | 88 | function getReward() public updateReward(msg.sender) { 89 | 90 | for (uint i; i < rewardTokens.length; i++) { 91 | address token = rewardTokens[i]; 92 | Reward storage r = rewardData[token]; 93 | if (block.timestamp + REWARDS_DURATION > r.periodFinish + 3600) { 94 | // if last reward update was more than 1 hour ago, check for new rewards 95 | uint256 unseen = IERC20(token).balanceOf(address(this)) - r.balance; 96 | _notifyRewardAmount(r, unseen); 97 | emit RewardAdded(token, unseen); 98 | } 99 | uint256 reward = rewards[msg.sender][token]; 100 | if (reward > 0) { 101 | rewards[msg.sender][token] = 0; 102 | r.balance -= reward; 103 | IERC20(token).safeTransfer(msg.sender, reward); 104 | emit RewardPaid(msg.sender, token, reward); 105 | } 106 | } 107 | } 108 | 109 | function exit() external { 110 | withdraw(balanceOf[msg.sender]); 111 | getReward(); 112 | } 113 | 114 | function _notifyRewardAmount(Reward storage r, uint256 reward) internal { 115 | 116 | if (block.timestamp >= r.periodFinish) { 117 | r.rewardRate = reward / REWARDS_DURATION; 118 | } else { 119 | uint256 remaining = r.periodFinish - block.timestamp; 120 | uint256 leftover = remaining * r.rewardRate; 121 | r.rewardRate = (reward + leftover) / REWARDS_DURATION; 122 | } 123 | r.lastUpdateTime = block.timestamp; 124 | r.periodFinish = block.timestamp + REWARDS_DURATION; 125 | r.balance += reward; 126 | } 127 | 128 | modifier updateReward(address account) { 129 | for (uint i; i < rewardTokens.length; i++) { 130 | address token = rewardTokens[i]; 131 | rewardData[token].rewardPerTokenStored = rewardPerToken(token); 132 | rewardData[token].lastUpdateTime = lastTimeRewardApplicable(token); 133 | if (account != address(0)) { 134 | rewards[account][token] = earned(account, token); 135 | userRewardPerTokenPaid[account][token] = rewardData[token].rewardPerTokenStored; 136 | } 137 | } 138 | _; 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /contracts/FeeDistributor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./dependencies/Ownable.sol"; 4 | import "./dependencies/SafeERC20.sol"; 5 | import "./interfaces/IERC20.sol"; 6 | import "./interfaces/solidex/ITokenLocker.sol"; 7 | 8 | 9 | contract FeeDistributor is Ownable { 10 | using SafeERC20 for IERC20; 11 | 12 | struct StreamData { 13 | uint256 start; 14 | uint256 amount; 15 | uint256 claimed; 16 | } 17 | 18 | // Fees are transferred into this contract as they are collected, and in the same tokens 19 | // that they are collected in. The total amount collected each week is recorded in 20 | // `weeklyFeeAmounts`. At the end of a week, the fee amounts are streamed out over 21 | // the following week based on each user's lock weight at the end of that week. Data 22 | // about the active stream for each token is tracked in `activeUserStream` 23 | 24 | // fee token -> week -> total amount received that week 25 | mapping(address => mapping(uint256 => uint256)) public weeklyFeeAmounts; 26 | // user -> fee token -> data about the active stream 27 | mapping(address => mapping(address => StreamData)) activeUserStream; 28 | 29 | // array of all fee tokens that have been added 30 | address[] public feeTokens; 31 | // private mapping for tracking which addresses were added to `feeTokens` 32 | mapping(address => bool) seenFees; 33 | 34 | ITokenLocker public tokenLocker; 35 | uint256 public startTime; 36 | 37 | uint256 constant WEEK = 86400 * 7; 38 | 39 | event FeesReceived( 40 | address indexed caller, 41 | address indexed token, 42 | uint256 indexed week, 43 | uint256 amount 44 | ); 45 | event FeesClaimed( 46 | address indexed caller, 47 | address indexed receiver, 48 | address indexed token, 49 | uint256 amount 50 | ); 51 | 52 | function setAddresses(ITokenLocker _tokenLocker) external onlyOwner { 53 | tokenLocker = _tokenLocker; 54 | startTime = _tokenLocker.startTime(); 55 | 56 | renounceOwnership(); 57 | } 58 | 59 | function getWeek() public view returns (uint256) { 60 | if (startTime == 0) return 0; 61 | return (block.timestamp - startTime) / 604800; 62 | } 63 | 64 | function feeTokensLength() external view returns (uint) { 65 | return feeTokens.length; 66 | } 67 | 68 | /** 69 | @notice Deposit protocol fees into the contract, to be distributed to lockers 70 | @dev Caller must have given approval for this contract to transfer `_token` 71 | @param _token Token being deposited 72 | @param _amount Amount of the token to deposit 73 | */ 74 | function depositFee(address _token, uint256 _amount) 75 | external 76 | returns (bool) 77 | { 78 | if (_amount > 0) { 79 | if (!seenFees[_token]) { 80 | seenFees[_token] = true; 81 | feeTokens.push(_token); 82 | } 83 | uint256 received = IERC20(_token).balanceOf(address(this)); 84 | IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); 85 | received = IERC20(_token).balanceOf(address(this)) - received; 86 | uint256 week = getWeek(); 87 | weeklyFeeAmounts[_token][week] += received; 88 | emit FeesReceived(msg.sender, _token, week, _amount); 89 | } 90 | return true; 91 | } 92 | 93 | /** 94 | @notice Get an array of claimable amounts of different tokens accrued from protocol fees 95 | @param _user Address to query claimable amounts for 96 | @param _tokens List of tokens to query claimable amounts of 97 | */ 98 | function claimable(address _user, address[] calldata _tokens) 99 | external 100 | view 101 | returns (uint256[] memory amounts) 102 | { 103 | amounts = new uint256[](_tokens.length); 104 | for (uint256 i = 0; i < _tokens.length; i++) { 105 | (amounts[i], ) = _getClaimable(_user, _tokens[i]); 106 | } 107 | return amounts; 108 | } 109 | 110 | /** 111 | @notice Claim accrued protocol fees according to a locked balance in `TokenLocker`. 112 | @dev Fees are claimable up to the end of the previous week. Claimable fees from more 113 | than one week ago are released immediately, fees from the previous week are streamed. 114 | @param _user Address to claim for. Any account can trigger a claim for any other account. 115 | @param _tokens Array of tokens to claim for. 116 | @return claimedAmounts Array of amounts claimed. 117 | */ 118 | function claim(address _user, address[] calldata _tokens) 119 | external 120 | returns (uint256[] memory claimedAmounts) 121 | { 122 | claimedAmounts = new uint256[](_tokens.length); 123 | StreamData memory stream; 124 | for (uint256 i = 0; i < _tokens.length; i++) { 125 | address token = _tokens[i]; 126 | (claimedAmounts[i], stream) = _getClaimable(_user, token); 127 | activeUserStream[_user][token] = stream; 128 | IERC20(token).safeTransfer(_user, claimedAmounts[i]); 129 | emit FeesClaimed(msg.sender, _user, token, claimedAmounts[i]); 130 | } 131 | return claimedAmounts; 132 | } 133 | 134 | function _getClaimable(address _user, address _token) 135 | internal 136 | view 137 | returns (uint256, StreamData memory) 138 | { 139 | uint256 claimableWeek = getWeek(); 140 | 141 | if (claimableWeek == 0) { 142 | // the first full week hasn't completed yet 143 | return (0, StreamData({start: startTime, amount: 0, claimed: 0})); 144 | } 145 | 146 | // the previous week is the claimable one 147 | claimableWeek -= 1; 148 | StreamData memory stream = activeUserStream[_user][_token]; 149 | uint256 lastClaimWeek; 150 | if (stream.start == 0) { 151 | lastClaimWeek = 0; 152 | } else { 153 | lastClaimWeek = (stream.start - startTime) / WEEK; 154 | } 155 | 156 | uint256 amount; 157 | if (claimableWeek == lastClaimWeek) { 158 | // special case: claim is happening in the same week as a previous claim 159 | uint256 previouslyClaimed = stream.claimed; 160 | stream = _buildStreamData(_user, _token, claimableWeek); 161 | amount = stream.claimed - previouslyClaimed; 162 | return (amount, stream); 163 | } 164 | 165 | if (stream.start > 0) { 166 | // if there is a partially claimed week, get the unclaimed amount and increment 167 | // `lastClaimWeeek` so we begin iteration on the following week 168 | amount = stream.amount - stream.claimed; 169 | lastClaimWeek += 1; 170 | } 171 | 172 | // iterate over weeks that have passed fully without any claims 173 | for (uint256 i = lastClaimWeek; i < claimableWeek; i++) { 174 | (uint256 userWeight, uint256 totalWeight) = tokenLocker.weeklyWeight(_user, i); 175 | if (userWeight == 0) continue; 176 | amount += weeklyFeeAmounts[_token][i] * userWeight / totalWeight; 177 | } 178 | 179 | // add a partial amount for the active week 180 | stream = _buildStreamData(_user, _token, claimableWeek); 181 | 182 | return (amount + stream.claimed, stream); 183 | } 184 | 185 | function _buildStreamData( 186 | address _user, 187 | address _token, 188 | uint256 _week 189 | ) internal view returns (StreamData memory) { 190 | uint256 start = startTime + _week * WEEK; 191 | (uint256 userWeight, uint256 totalWeight) = tokenLocker.weeklyWeight(_user, _week); 192 | uint256 amount; 193 | uint256 claimed; 194 | if (userWeight > 0) { 195 | amount = weeklyFeeAmounts[_token][_week] * userWeight / totalWeight; 196 | claimed = amount * (block.timestamp - 604800 - start) / WEEK; 197 | } 198 | return StreamData({start: start, amount: amount, claimed: claimed}); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /contracts/Whitelister.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./dependencies/Ownable.sol"; 4 | import "./interfaces/IERC20.sol"; 5 | import "./interfaces/solidex/ILpDepositor.sol"; 6 | import "./interfaces/solidex/ISexPartners.sol"; 7 | import "./interfaces/solidly/IBaseV1Voter.sol"; 8 | import "./interfaces/solidex/IVeDepositor.sol"; 9 | import "./interfaces/solidex/IFeeDistributor.sol"; 10 | 11 | 12 | contract Whitelister is IERC20, Ownable { 13 | 14 | string public constant name = "Solidex Whitelisting Token"; 15 | string public constant symbol = "SEX-WL"; 16 | uint8 public constant decimals = 18; 17 | uint256 public override totalSupply; 18 | 19 | mapping(address => uint256) public override balanceOf; 20 | mapping(address => mapping(address => uint256)) public override allowance; 21 | 22 | mapping(address => uint256) public lastEarlyPartnerMint; 23 | 24 | IERC20 public immutable SOLID; 25 | IBaseV1Voter public immutable solidlyVoter; 26 | 27 | ILpDepositor public lpDepositor; 28 | ISexPartners public sexPartners; 29 | IVeDepositor public SOLIDsex; 30 | IFeeDistributor public feeDistributor; 31 | 32 | uint256 public biddingPeriodEnd; 33 | uint256 public highestBid; 34 | address public highestBidder; 35 | 36 | event HigestBid(address indexed user, uint256 amount); 37 | event NewBiddingPeriod(uint256 indexed end); 38 | event Whitelisted(address indexed token); 39 | 40 | constructor(IERC20 _solid, IBaseV1Voter _solidlyVoter) { 41 | SOLID = _solid; 42 | solidlyVoter = _solidlyVoter; 43 | emit Transfer(address(0), msg.sender, 0); 44 | } 45 | 46 | function setAddresses( 47 | ILpDepositor _lpDepositor, 48 | ISexPartners _partners, 49 | IVeDepositor _solidsex, 50 | IFeeDistributor _distributor 51 | ) external onlyOwner { 52 | lpDepositor = _lpDepositor; 53 | sexPartners = _partners; 54 | SOLIDsex = _solidsex; 55 | feeDistributor = _distributor; 56 | 57 | SOLID.approve(address(_solidsex), type(uint256).max); 58 | SOLIDsex.approve(address(_distributor), type(uint256).max); 59 | 60 | renounceOwnership(); 61 | } 62 | 63 | function approve(address _spender, uint256 _value) external override returns (bool) { 64 | allowance[msg.sender][_spender] = _value; 65 | emit Approval(msg.sender, _spender, _value); 66 | return true; 67 | } 68 | 69 | /** shared logic for transfer and transferFrom */ 70 | function _transfer(address _from, address _to, uint256 _value) internal { 71 | require(balanceOf[_from] >= _value, "Insufficient balance"); 72 | balanceOf[_from] -= _value; 73 | balanceOf[_to] += _value; 74 | emit Transfer(_from, _to, _value); 75 | } 76 | 77 | /** 78 | @notice Transfer tokens to a specified address 79 | @param _to The address to transfer to 80 | @param _value The amount to be transferred 81 | @return Success boolean 82 | */ 83 | function transfer(address _to, uint256 _value) public override returns (bool) { 84 | _transfer(msg.sender, _to, _value); 85 | return true; 86 | } 87 | 88 | /** 89 | @notice Transfer tokens from one address to another 90 | @param _from The address which you want to send tokens from 91 | @param _to The address which you want to transfer to 92 | @param _value The amount of tokens to be transferred 93 | @return Success boolean 94 | */ 95 | function transferFrom( 96 | address _from, 97 | address _to, 98 | uint256 _value 99 | ) 100 | public 101 | override 102 | returns (bool) 103 | { 104 | require(allowance[_from][msg.sender] >= _value, "Insufficient allowance"); 105 | if (allowance[_from][msg.sender] != type(uint).max) { 106 | allowance[_from][msg.sender] -= _value; 107 | } 108 | _transfer(_from, _to, _value); 109 | return true; 110 | } 111 | 112 | /** 113 | @notice Mint three free whitelist tokens as an early partner 114 | @dev Each early partner may call this once every 30 days 115 | */ 116 | function earlyPartnerMint() external { 117 | require(sexPartners.isEarlyPartner(msg.sender), "Not an early partner"); 118 | require(lastEarlyPartnerMint[msg.sender] + 86400 * 30 < block.timestamp, "One mint per month"); 119 | 120 | lastEarlyPartnerMint[msg.sender] = block.timestamp; 121 | balanceOf[msg.sender] += 3e18; 122 | totalSupply += 3e18; 123 | emit Transfer(address(0), msg.sender, 3e18); 124 | } 125 | 126 | function isActiveBiddingPeriod() public view returns (bool) { 127 | return biddingPeriodEnd >= block.timestamp; 128 | } 129 | 130 | function canClaimFinishedBid() public view returns (bool) { 131 | return biddingPeriodEnd > 0 && biddingPeriodEnd < block.timestamp; 132 | } 133 | 134 | function minimumBid() public view returns (uint256) { 135 | if (isActiveBiddingPeriod()) { 136 | return highestBid * 101 / 100; 137 | } 138 | uint256 fee = solidlyVoter.listing_fee(); 139 | // quote 0.1% higher as ve expansion between quote time and submit time can change listing_fee 140 | return fee / 10 + (fee / 1000); 141 | } 142 | 143 | function _minimumBid() internal view returns (uint256) { 144 | if (isActiveBiddingPeriod()) { 145 | return highestBid * 101 / 100; 146 | } 147 | return solidlyVoter.listing_fee() / 10; 148 | } 149 | 150 | /** 151 | @notice Bid to purchase a whitelist token with SOLID 152 | @dev Each bidding period lasts for three days. The initial bid must be 153 | at least 10% of the current solidly listing fee. Subsequent bids 154 | must increase the bid by at least 1%. The full SOLID amount is 155 | transferred from the bidder during the call, and the amount taken 156 | from the previous bidder is refunded. 157 | @param amount Amount of SOLID to bid 158 | */ 159 | function bid(uint256 amount) external { 160 | require(amount >= _minimumBid(), "Below minimum bid"); 161 | 162 | if (canClaimFinishedBid()) { 163 | // if the winning bid from the previous period was not claimed, 164 | // execute it prior to starting a new period 165 | claimFinishedBid(); 166 | } else if (highestBid != 0) { 167 | // if there is already a previous bid, return it to the bidder 168 | SOLID.transfer(highestBidder, highestBid); 169 | } 170 | 171 | if (biddingPeriodEnd == 0) { 172 | // if this is the start of a new period, set the end as +3 days 173 | biddingPeriodEnd = block.timestamp + 86400 * 3; 174 | emit NewBiddingPeriod(biddingPeriodEnd); 175 | } 176 | 177 | // transfer SOLID from the caller and record them as the highest bidder 178 | SOLID.transferFrom(msg.sender, address(this), amount); 179 | highestBid = amount; 180 | highestBidder = msg.sender; 181 | emit HigestBid(msg.sender, amount); 182 | } 183 | 184 | /** 185 | @notice Mint a new whitelist token for the highest bidder in the finished period 186 | @dev Placing a bid to start a new period will also triggers a claim 187 | */ 188 | function claimFinishedBid() public { 189 | require(biddingPeriodEnd > 0 && biddingPeriodEnd < block.timestamp, "No pending claim"); 190 | 191 | SOLIDsex.depositTokens(highestBid); 192 | feeDistributor.depositFee(address(SOLIDsex), highestBid); 193 | 194 | balanceOf[highestBidder] += 1e18; 195 | totalSupply += 1e18; 196 | 197 | highestBid = 0; 198 | highestBidder = address(0); 199 | biddingPeriodEnd = 0; 200 | 201 | emit Transfer(address(0), highestBidder, 1e18); 202 | } 203 | 204 | /** 205 | @notice Whitelist a new token in Solidly 206 | @dev This function burns 1 whitelist token from the caller's balance 207 | @param token Address of the token to whitelist 208 | */ 209 | function whitelist(address token) external { 210 | require(balanceOf[msg.sender] >= 1e18, "Insufficient balance"); 211 | 212 | balanceOf[msg.sender] -= 1e18; 213 | totalSupply -= 1e18; 214 | emit Transfer(msg.sender, address(0), 1e18); 215 | 216 | lpDepositor.whitelist(token); 217 | emit Whitelisted(token); 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /contracts/SexPartners.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./dependencies/Ownable.sol"; 4 | import "./interfaces/IERC20.sol"; 5 | import "./interfaces/solidly/IVotingEscrow.sol"; 6 | import "./interfaces/solidly/IBaseV1Minter.sol"; 7 | import "./interfaces/solidex/ISolidexToken.sol"; 8 | 9 | 10 | contract SexPartners is Ownable { 11 | 12 | IVotingEscrow public immutable votingEscrow; 13 | IBaseV1Minter public immutable solidMinter; 14 | 15 | IERC20 public SOLIDsex; 16 | ISolidexToken public SEX; 17 | uint256 public tokenID; 18 | 19 | // current number of early SEX partners 20 | uint256 public partnerCount; 21 | // timestamp after which new SEX partners receive a reduced 22 | // amount of SEX in perpetuity (1 day prior to SOLID emissions starting) 23 | uint256 public earlyPartnerDeadline; 24 | // timestamp after which new SEX partners are no longer accepted 25 | uint256 public finalPartnerDeadline; 26 | 27 | // number of tokens that have been minted via this contract 28 | uint256 public totalMinted; 29 | // total % of the total supply that this contract is entitled to mint 30 | uint256 public totalMintPct; 31 | 32 | struct UserWeight { 33 | uint256 tranche; 34 | uint256 weight; 35 | uint256 claimed; 36 | } 37 | 38 | struct Tranche { 39 | uint256 minted; 40 | uint256 weight; 41 | uint256 mintPct; 42 | } 43 | 44 | // partners, vests 45 | Tranche[2] public trancheData; 46 | 47 | mapping (address => UserWeight) public userData; 48 | mapping (address => bool) public isEarlyPartner; 49 | 50 | // maximum number of SEX partners 51 | uint256 public constant MAX_PARTNER_COUNT = 15; 52 | 53 | constructor( 54 | IVotingEscrow _votingEscrow, 55 | IBaseV1Minter _minter, 56 | address[] memory _receivers, 57 | uint256[] memory _weights 58 | ) { 59 | votingEscrow = _votingEscrow; 60 | solidMinter = _minter; 61 | 62 | uint256 totalWeight; 63 | require(_receivers.length == _weights.length); 64 | for (uint i = 0; i < _receivers.length; i++) { 65 | totalWeight += _weights[i]; 66 | // set claimed to 1 to avoid initial claim requirement for vestees calling `claim` 67 | userData[_receivers[i]] = UserWeight({tranche: 1, weight: _weights[i], claimed: 1}); 68 | } 69 | 70 | trancheData[1].weight = totalWeight; 71 | trancheData[1].mintPct = 20; 72 | totalMintPct = 20; 73 | } 74 | 75 | function setAddresses(IERC20 _solidsex, ISolidexToken _sex) external onlyOwner { 76 | SOLIDsex = _solidsex; 77 | SEX = _sex; 78 | 79 | renounceOwnership(); 80 | } 81 | 82 | function onERC721Received( 83 | address _operator, 84 | address _from, 85 | uint256 _tokenID, 86 | bytes calldata 87 | ) external returns (bytes4) { 88 | UserWeight storage u = userData[_operator]; 89 | require(u.tranche == 0, "Conflict of interest!"); 90 | require(u.weight == 0, "Already a partner"); 91 | require(partnerCount < MAX_PARTNER_COUNT, "No more SEX partners allowed!"); 92 | (uint256 amount,) = votingEscrow.locked(_tokenID); 93 | 94 | if (tokenID == 0) { 95 | // when receiving the first NFT, track the tokenID and set the partnership deadlines 96 | tokenID = _tokenID; 97 | earlyPartnerDeadline = solidMinter.active_period() + 86400 * 6; 98 | finalPartnerDeadline = earlyPartnerDeadline + 86400 * 14; 99 | isEarlyPartner[_operator] = true; 100 | 101 | } else if (block.timestamp < earlyPartnerDeadline) { 102 | // subsequent NFTs received before the early deadline are merged with the first 103 | votingEscrow.merge(_tokenID, tokenID); 104 | isEarlyPartner[_operator] = true; 105 | 106 | } else if (block.timestamp < finalPartnerDeadline) { 107 | require(_tokenID < 26, "Only early protocol NFTs are eligible"); 108 | require(address(SOLIDsex) != address(0), "Addresses not set"); 109 | 110 | // NFTs received after the early deadline are immediately converted to SOLIDsex 111 | votingEscrow.safeTransferFrom(address(this), address(SOLIDsex), _tokenID); 112 | SOLIDsex.transfer(_operator, amount); 113 | 114 | // SEX advance has a 50% immediate penalty and a linear decay to zero over 2 weeks 115 | amount = amount / 2 * (finalPartnerDeadline - block.timestamp) / (86400 * 14); 116 | uint256 advance = amount / 10; 117 | SEX.mint(_operator, advance); 118 | u.claimed = advance; 119 | trancheData[0].minted += advance; 120 | totalMinted += advance; 121 | 122 | } else revert("SEX in perpetuity no longer available"); 123 | 124 | u.weight += amount; 125 | trancheData[0].weight += amount; 126 | partnerCount += 1; 127 | 128 | return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); 129 | } 130 | 131 | function earlyPartnerPct() public view returns (uint256) { 132 | if (partnerCount < 11) return 10; 133 | return partnerCount; 134 | } 135 | 136 | function claimable(address account) external view returns (uint256) { 137 | if (block.timestamp <= finalPartnerDeadline) return 0; 138 | UserWeight storage u = userData[account]; 139 | Tranche memory t = trancheData[u.tranche]; 140 | 141 | uint256 _totalMintPct = totalMintPct; 142 | if (trancheData[0].mintPct == 0) { 143 | _totalMintPct += earlyPartnerPct(); 144 | if (u.tranche == 0) t.mintPct = earlyPartnerPct(); 145 | } 146 | 147 | uint256 supply = SEX.totalSupply() - totalMinted; 148 | uint256 mintable = (supply * 100 / (100 - _totalMintPct) - supply) * t.mintPct / _totalMintPct; 149 | if (mintable < t.minted) mintable = t.minted; 150 | 151 | uint256 totalClaimable = mintable * u.weight / t.weight; 152 | if (totalClaimable < u.claimed) return 0; 153 | return totalClaimable - u.claimed; 154 | 155 | } 156 | 157 | function claim() external returns (uint256) { 158 | UserWeight storage u = userData[msg.sender]; 159 | Tranche storage t = trancheData[u.tranche]; 160 | 161 | require(u.weight > 0, "Not a SEX partner"); 162 | require(u.claimed > 0, "Must make initial claim first"); 163 | require(block.timestamp > finalPartnerDeadline, "Cannot claim yet"); 164 | 165 | if (trancheData[0].mintPct == 0) { 166 | trancheData[0].mintPct = earlyPartnerPct(); 167 | totalMintPct += trancheData[0].mintPct; 168 | } 169 | 170 | // mint new SEX based on supply that was minted via regular emissions 171 | uint256 supply = SEX.totalSupply() - totalMinted; 172 | uint256 mintable = (supply * 100 / (100 - totalMintPct) - supply) * t.mintPct / totalMintPct; 173 | if (mintable > t.minted) { 174 | uint256 amount = mintable - t.minted; 175 | SEX.mint(address(this), amount); 176 | t.minted = mintable; 177 | totalMinted += amount; 178 | } 179 | 180 | uint256 totalClaimable = t.minted * u.weight / t.weight; 181 | if (totalClaimable > u.claimed) { 182 | uint256 amount = totalClaimable - u.claimed; 183 | SEX.transfer(msg.sender, amount); 184 | u.claimed = totalClaimable; 185 | return amount; 186 | } 187 | return 0; 188 | 189 | } 190 | 191 | function earlyPartnerClaim() external returns (uint256) { 192 | require(block.timestamp > earlyPartnerDeadline, "Cannot claim yet"); 193 | require(owner == address(0), "Addresses not set"); 194 | UserWeight storage u = userData[msg.sender]; 195 | require(u.tranche == 0 && u.weight > 0, "Not a SEX partner"); 196 | require(u.claimed == 0, "SEX advance already claimed"); 197 | Tranche storage t = trancheData[0]; 198 | 199 | if (votingEscrow.ownerOf(tokenID) == address(this)) { 200 | // transfer the NFT to mint early partner SOLIDsex 201 | votingEscrow.safeTransferFrom(address(this), address(SOLIDsex), tokenID); 202 | } 203 | 204 | // transfer owed SOLIDsex 205 | uint256 amount = u.weight; 206 | SOLIDsex.transfer(msg.sender, amount); 207 | 208 | // mint SEX advance 209 | amount /= 10; 210 | u.claimed = amount; 211 | t.minted += amount; 212 | totalMinted += amount; 213 | SEX.mint(msg.sender, amount); 214 | 215 | return amount; 216 | } 217 | 218 | } -------------------------------------------------------------------------------- /contracts/VeDepositor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./dependencies/Ownable.sol"; 4 | import "./interfaces/IERC20.sol"; 5 | import "./interfaces/solidly/IVotingEscrow.sol"; 6 | import "./interfaces/solidly/IVeDist.sol"; 7 | import "./interfaces/solidex/ILpDepositor.sol"; 8 | import "./interfaces/solidex/IFeeDistributor.sol"; 9 | import "./interfaces/solidex/ISolidexVoter.sol"; 10 | 11 | 12 | contract VeDepositor is IERC20, Ownable { 13 | 14 | string public constant name = "SOLIDsex: Tokenized veSOLID"; 15 | string public constant symbol = "SOLIDsex"; 16 | uint8 public constant decimals = 18; 17 | uint256 public override totalSupply; 18 | 19 | mapping(address => uint256) public override balanceOf; 20 | mapping(address => mapping(address => uint256)) public override allowance; 21 | 22 | // Solidly contracts 23 | IERC20 public immutable token; 24 | IVotingEscrow public immutable votingEscrow; 25 | IVeDist public immutable veDistributor; 26 | 27 | // Solidex contracts 28 | ILpDepositor public lpDepositor; 29 | ISolidexVoter public solidexVoter; 30 | IFeeDistributor public feeDistributor; 31 | 32 | uint256 public tokenID; 33 | uint256 public unlockTime; 34 | 35 | uint256 constant MAX_LOCK_TIME = 86400 * 365 * 4; 36 | uint256 constant WEEK = 86400 * 7; 37 | 38 | event ClaimedFromVeDistributor(address indexed user, uint256 amount); 39 | event Merged(address indexed user, uint256 tokenID, uint256 amount); 40 | event UnlockTimeUpdated(uint256 unlockTime); 41 | 42 | constructor( 43 | IERC20 _token, 44 | IVotingEscrow _votingEscrow, 45 | IVeDist _veDist 46 | ) { 47 | token = _token; 48 | votingEscrow = _votingEscrow; 49 | veDistributor = _veDist; 50 | 51 | // approve vesting escrow to transfer SOLID (for adding to lock) 52 | _token.approve(address(_votingEscrow), type(uint256).max); 53 | emit Transfer(address(0), msg.sender, 0); 54 | } 55 | 56 | function setAddresses( 57 | ILpDepositor _lpDepositor, 58 | ISolidexVoter _solidexVoter, 59 | IFeeDistributor _feeDistributor 60 | ) external onlyOwner { 61 | lpDepositor = _lpDepositor; 62 | solidexVoter = _solidexVoter; 63 | feeDistributor = _feeDistributor; 64 | 65 | // approve fee distributor to transfer this token (for distributing SOLIDsex) 66 | allowance[address(this)][address(_feeDistributor)] = type(uint256).max; 67 | renounceOwnership(); 68 | } 69 | 70 | 71 | function approve(address _spender, uint256 _value) 72 | external 73 | override 74 | returns (bool) 75 | { 76 | allowance[msg.sender][_spender] = _value; 77 | emit Approval(msg.sender, _spender, _value); 78 | return true; 79 | } 80 | 81 | /** shared logic for transfer and transferFrom */ 82 | function _transfer( 83 | address _from, 84 | address _to, 85 | uint256 _value 86 | ) internal { 87 | require(balanceOf[_from] >= _value, "Insufficient balance"); 88 | balanceOf[_from] -= _value; 89 | balanceOf[_to] += _value; 90 | emit Transfer(_from, _to, _value); 91 | } 92 | 93 | /** 94 | @notice Transfer tokens to a specified address 95 | @param _to The address to transfer to 96 | @param _value The amount to be transferred 97 | @return Success boolean 98 | */ 99 | function transfer(address _to, uint256 _value) 100 | public 101 | override 102 | returns (bool) 103 | { 104 | _transfer(msg.sender, _to, _value); 105 | return true; 106 | } 107 | 108 | /** 109 | @notice Transfer tokens from one address to another 110 | @param _from The address which you want to send tokens from 111 | @param _to The address which you want to transfer to 112 | @param _value The amount of tokens to be transferred 113 | @return Success boolean 114 | */ 115 | function transferFrom( 116 | address _from, 117 | address _to, 118 | uint256 _value 119 | ) public override returns (bool) { 120 | require(allowance[_from][msg.sender] >= _value, "Insufficient allowance"); 121 | if (allowance[_from][msg.sender] != type(uint256).max) { 122 | allowance[_from][msg.sender] -= _value; 123 | } 124 | _transfer(_from, _to, _value); 125 | return true; 126 | } 127 | 128 | function onERC721Received( 129 | address _operator, 130 | address _from, 131 | uint256 _tokenID, 132 | bytes calldata 133 | ) external returns (bytes4) { 134 | require(msg.sender == address(votingEscrow), "Can only receive veSOLID NFTs"); 135 | (uint256 amount, uint256 end) = votingEscrow.locked(_tokenID); 136 | 137 | if (tokenID == 0) { 138 | tokenID = _tokenID; 139 | unlockTime = end; 140 | solidexVoter.setTokenID(tokenID); 141 | votingEscrow.safeTransferFrom(address(this), address(lpDepositor), _tokenID); 142 | } else { 143 | votingEscrow.merge(_tokenID, tokenID); 144 | if (end > unlockTime) unlockTime = end; 145 | emit Merged(_operator, _tokenID, amount); 146 | } 147 | 148 | _mint(_operator, amount); 149 | extendLockTime(); 150 | 151 | return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); 152 | } 153 | 154 | /** 155 | @notice Merge a veSOLID NFT previously sent to this contract 156 | with the main Solidex NFT 157 | @dev This is primarily meant to allow claiming balances from NFTs 158 | incorrectly sent using `transferFrom`. To deposit an NFT 159 | you should always use `safeTransferFrom`. 160 | @param _tokenID ID of the NFT to merge 161 | @return bool success 162 | */ 163 | function merge(uint256 _tokenID) external returns (bool) { 164 | require(tokenID != _tokenID); 165 | (uint256 amount, uint256 end) = votingEscrow.locked(_tokenID); 166 | require(amount > 0); 167 | 168 | votingEscrow.merge(_tokenID, tokenID); 169 | if (end > unlockTime) unlockTime = end; 170 | emit Merged(msg.sender, _tokenID, amount); 171 | 172 | _mint(msg.sender, amount); 173 | extendLockTime(); 174 | 175 | return true; 176 | } 177 | 178 | /** 179 | @notice Deposit SOLID tokens and mint SOLIDsex 180 | @param _amount Amount of SOLID to deposit 181 | @return bool success 182 | */ 183 | function depositTokens(uint256 _amount) external returns (bool) { 184 | require(tokenID != 0, "First deposit must be NFT"); 185 | 186 | token.transferFrom(msg.sender, address(this), _amount); 187 | votingEscrow.increase_amount(tokenID, _amount); 188 | _mint(msg.sender, _amount); 189 | extendLockTime(); 190 | 191 | return true; 192 | } 193 | 194 | /** 195 | @notice Extend the lock time of the protocol's veSOLID NFT 196 | @dev Lock times are also extended each time new SOLIDsex is minted. 197 | If the lock time is already at the maximum duration, calling 198 | this function does nothing. 199 | */ 200 | function extendLockTime() public { 201 | uint256 maxUnlock = ((block.timestamp + MAX_LOCK_TIME) / WEEK) * WEEK; 202 | if (maxUnlock > unlockTime) { 203 | votingEscrow.increase_unlock_time(tokenID, MAX_LOCK_TIME); 204 | unlockTime = maxUnlock; 205 | emit UnlockTimeUpdated(unlockTime); 206 | } 207 | } 208 | 209 | /** 210 | @notice Claim veSOLID received via ve(3,3) 211 | @dev This function is unguarded, anyone can call to claim at any time. 212 | The new veSOLID is represented by newly minted SOLIDsex, which is 213 | then sent to `FeeDistributor` and streamed to SEX lockers starting 214 | at the beginning of the following epoch week. 215 | */ 216 | function claimFromVeDistributor() external returns (bool) { 217 | veDistributor.claim(tokenID); 218 | 219 | // calculate the amount by comparing the change in the locked balance 220 | // to the known total supply, this is necessary because anyone can call 221 | // `veDistributor.claim` for any NFT 222 | (uint256 amount,) = votingEscrow.locked(tokenID); 223 | amount -= totalSupply; 224 | 225 | if (amount > 0) { 226 | _mint(address(this), amount); 227 | feeDistributor.depositFee(address(this), balanceOf[address(this)]); 228 | emit ClaimedFromVeDistributor(address(this), amount); 229 | } 230 | 231 | return true; 232 | } 233 | 234 | function _mint(address _user, uint256 _amount) internal { 235 | balanceOf[_user] += _amount; 236 | totalSupply += _amount; 237 | emit Transfer(address(0), _user, _amount); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /contracts/dependencies/Address.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts v4.4.1 (utils/Address.sol) 3 | 4 | pragma solidity ^0.8.1; 5 | 6 | /** 7 | * @dev Collection of functions related to the address type 8 | */ 9 | library Address { 10 | /** 11 | * @dev Returns true if `account` is a contract. 12 | * 13 | * [IMPORTANT] 14 | * ==== 15 | * It is unsafe to assume that an address for which this function returns 16 | * false is an externally-owned account (EOA) and not a contract. 17 | * 18 | * Among others, `isContract` will return false for the following 19 | * types of addresses: 20 | * 21 | * - an externally-owned account 22 | * - a contract in construction 23 | * - an address where a contract will be created 24 | * - an address where a contract lived, but was destroyed 25 | * ==== 26 | * 27 | * [IMPORTANT] 28 | * ==== 29 | * You shouldn't rely on `isContract` to protect against flash loan attacks! 30 | * 31 | * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets 32 | * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract 33 | * constructor. 34 | * ==== 35 | */ 36 | function isContract(address account) internal view returns (bool) { 37 | // This method relies on extcodesize/address.code.length, which returns 0 38 | // for contracts in construction, since the code is only stored at the end 39 | // of the constructor execution. 40 | 41 | return account.code.length > 0; 42 | } 43 | 44 | /** 45 | * @dev Replacement for Solidity's `transfer`: sends `amount` wei to 46 | * `recipient`, forwarding all available gas and reverting on errors. 47 | * 48 | * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost 49 | * of certain opcodes, possibly making contracts go over the 2300 gas limit 50 | * imposed by `transfer`, making them unable to receive funds via 51 | * `transfer`. {sendValue} removes this limitation. 52 | * 53 | * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. 54 | * 55 | * IMPORTANT: because control is transferred to `recipient`, care must be 56 | * taken to not create reentrancy vulnerabilities. Consider using 57 | * {ReentrancyGuard} or the 58 | * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. 59 | */ 60 | function sendValue(address payable recipient, uint256 amount) internal { 61 | require(address(this).balance >= amount, "Address: insufficient balance"); 62 | 63 | (bool success, ) = recipient.call{value: amount}(""); 64 | require(success, "Address: unable to send value, recipient may have reverted"); 65 | } 66 | 67 | /** 68 | * @dev Performs a Solidity function call using a low level `call`. A 69 | * plain `call` is an unsafe replacement for a function call: use this 70 | * function instead. 71 | * 72 | * If `target` reverts with a revert reason, it is bubbled up by this 73 | * function (like regular Solidity function calls). 74 | * 75 | * Returns the raw returned data. To convert to the expected return value, 76 | * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. 77 | * 78 | * Requirements: 79 | * 80 | * - `target` must be a contract. 81 | * - calling `target` with `data` must not revert. 82 | * 83 | * _Available since v3.1._ 84 | */ 85 | function functionCall(address target, bytes memory data) internal returns (bytes memory) { 86 | return functionCall(target, data, "Address: low-level call failed"); 87 | } 88 | 89 | /** 90 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with 91 | * `errorMessage` as a fallback revert reason when `target` reverts. 92 | * 93 | * _Available since v3.1._ 94 | */ 95 | function functionCall( 96 | address target, 97 | bytes memory data, 98 | string memory errorMessage 99 | ) internal returns (bytes memory) { 100 | return functionCallWithValue(target, data, 0, errorMessage); 101 | } 102 | 103 | /** 104 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], 105 | * but also transferring `value` wei to `target`. 106 | * 107 | * Requirements: 108 | * 109 | * - the calling contract must have an ETH balance of at least `value`. 110 | * - the called Solidity function must be `payable`. 111 | * 112 | * _Available since v3.1._ 113 | */ 114 | function functionCallWithValue( 115 | address target, 116 | bytes memory data, 117 | uint256 value 118 | ) internal returns (bytes memory) { 119 | return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); 120 | } 121 | 122 | /** 123 | * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but 124 | * with `errorMessage` as a fallback revert reason when `target` reverts. 125 | * 126 | * _Available since v3.1._ 127 | */ 128 | function functionCallWithValue( 129 | address target, 130 | bytes memory data, 131 | uint256 value, 132 | string memory errorMessage 133 | ) internal returns (bytes memory) { 134 | require(address(this).balance >= value, "Address: insufficient balance for call"); 135 | require(isContract(target), "Address: call to non-contract"); 136 | 137 | (bool success, bytes memory returndata) = target.call{value: value}(data); 138 | return verifyCallResult(success, returndata, errorMessage); 139 | } 140 | 141 | /** 142 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], 143 | * but performing a static call. 144 | * 145 | * _Available since v3.3._ 146 | */ 147 | function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { 148 | return functionStaticCall(target, data, "Address: low-level static call failed"); 149 | } 150 | 151 | /** 152 | * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], 153 | * but performing a static call. 154 | * 155 | * _Available since v3.3._ 156 | */ 157 | function functionStaticCall( 158 | address target, 159 | bytes memory data, 160 | string memory errorMessage 161 | ) internal view returns (bytes memory) { 162 | require(isContract(target), "Address: static call to non-contract"); 163 | 164 | (bool success, bytes memory returndata) = target.staticcall(data); 165 | return verifyCallResult(success, returndata, errorMessage); 166 | } 167 | 168 | /** 169 | * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], 170 | * but performing a delegate call. 171 | * 172 | * _Available since v3.4._ 173 | */ 174 | function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { 175 | return functionDelegateCall(target, data, "Address: low-level delegate call failed"); 176 | } 177 | 178 | /** 179 | * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], 180 | * but performing a delegate call. 181 | * 182 | * _Available since v3.4._ 183 | */ 184 | function functionDelegateCall( 185 | address target, 186 | bytes memory data, 187 | string memory errorMessage 188 | ) internal returns (bytes memory) { 189 | require(isContract(target), "Address: delegate call to non-contract"); 190 | 191 | (bool success, bytes memory returndata) = target.delegatecall(data); 192 | return verifyCallResult(success, returndata, errorMessage); 193 | } 194 | 195 | /** 196 | * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the 197 | * revert reason using the provided one. 198 | * 199 | * _Available since v4.3._ 200 | */ 201 | function verifyCallResult( 202 | bool success, 203 | bytes memory returndata, 204 | string memory errorMessage 205 | ) internal pure returns (bytes memory) { 206 | if (success) { 207 | return returndata; 208 | } else { 209 | // Look for revert reason and bubble it up if present 210 | if (returndata.length > 0) { 211 | // The easiest way to bubble the revert reason is using memory via assembly 212 | 213 | assembly { 214 | let returndata_size := mload(returndata) 215 | revert(add(32, returndata), returndata_size) 216 | } 217 | } else { 218 | revert(errorMessage); 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /contracts/TokenLocker.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./dependencies/Ownable.sol"; 4 | import "./dependencies/SafeERC20.sol"; 5 | import "./interfaces/IERC20.sol"; 6 | 7 | 8 | contract TokenLocker is Ownable { 9 | using SafeERC20 for IERC20; 10 | 11 | struct StreamData { 12 | uint256 start; 13 | uint256 amount; 14 | uint256 claimed; 15 | } 16 | 17 | // `weeklyTotalWeight` and `weeklyWeightOf` track the total lock weight for each week, 18 | // calculated as the sum of [number of tokens] * [weeks to unlock] for all active locks. 19 | // The array index corresponds to the number of the epoch week. 20 | uint256[9362] public weeklyTotalWeight; 21 | mapping(address => uint256[9362]) public weeklyWeightOf; 22 | 23 | // `weeklyUnlocksOf` tracks the actual deposited token balances. Any non-zero value 24 | // stored at an index < `getWeek` is considered unlocked and may be withdrawn 25 | mapping(address => uint256[9362]) public weeklyUnlocksOf; 26 | 27 | // `withdrawnUntil` tracks the most recent week for which each user has withdrawn their 28 | // expired token locks. Values in `weeklyUnlocksOf` with an index less than the related 29 | // value within `withdrawnUntil` have already been withdrawn. 30 | mapping(address => uint256) withdrawnUntil; 31 | 32 | // After a lock expires, a user calls to `initiateExitStream` and the withdrawable tokens 33 | // are streamed out linearly over the following week. This array is used to track data 34 | // related to the exit stream. 35 | mapping(address => StreamData) public exitStream; 36 | 37 | IERC20 public SEX; 38 | uint256 public immutable startTime; 39 | 40 | uint256 constant WEEK = 86400 * 7; 41 | 42 | uint256 public immutable MAX_LOCK_WEEKS; 43 | 44 | event NewLock(address indexed user, uint256 amount, uint256 lockWeeks); 45 | event ExtendLock( 46 | address indexed user, 47 | uint256 amount, 48 | uint256 oldWeeks, 49 | uint256 newWeeks 50 | ); 51 | event NewExitStream( 52 | address indexed user, 53 | uint256 startTime, 54 | uint256 amount 55 | ); 56 | event ExitStreamWithdrawal( 57 | address indexed user, 58 | uint256 claimed, 59 | uint256 remaining 60 | ); 61 | 62 | constructor( 63 | uint256 _maxLockWeeks 64 | ) { 65 | MAX_LOCK_WEEKS = _maxLockWeeks; 66 | startTime = block.timestamp / WEEK * WEEK; 67 | } 68 | 69 | function setAddresses(IERC20 _sex) external onlyOwner { 70 | SEX = _sex; 71 | 72 | renounceOwnership(); 73 | } 74 | 75 | function getWeek() public view returns (uint256) { 76 | return (block.timestamp - startTime) / WEEK; 77 | } 78 | 79 | /** 80 | @notice Get the current lock weight for a user 81 | */ 82 | function userWeight(address _user) external view returns (uint256) { 83 | return weeklyWeightOf[_user][getWeek()]; 84 | } 85 | 86 | /** 87 | @notice Get the total balance held in this contract for a user, 88 | including both active and expired locks 89 | */ 90 | function userBalance(address _user) 91 | external 92 | view 93 | returns (uint256 balance) 94 | { 95 | uint256 i = withdrawnUntil[_user] + 1; 96 | uint256 finish = getWeek() + MAX_LOCK_WEEKS + 1; 97 | while (i < finish) { 98 | balance += weeklyUnlocksOf[_user][i]; 99 | i++; 100 | } 101 | return balance; 102 | } 103 | 104 | /** 105 | @notice Get the current total lock weight 106 | */ 107 | function totalWeight() external view returns (uint256) { 108 | return weeklyTotalWeight[getWeek()]; 109 | } 110 | 111 | /** 112 | @notice Get the user lock weight and total lock weight for the given week 113 | */ 114 | function weeklyWeight(address _user, uint256 _week) external view returns (uint256, uint256) { 115 | return (weeklyWeightOf[_user][_week], weeklyTotalWeight[_week]); 116 | } 117 | 118 | /** 119 | @notice Get data on a user's active token locks 120 | @param _user Address to query data for 121 | @return lockData dynamic array of [weeks until expiration, balance of lock] 122 | */ 123 | function getActiveUserLocks(address _user) 124 | external 125 | view 126 | returns (uint256[2][] memory lockData) 127 | { 128 | uint256 length = 0; 129 | uint256 week = getWeek(); 130 | for (uint256 i = week + 1; i < week + MAX_LOCK_WEEKS + 1; i++) { 131 | if (weeklyUnlocksOf[_user][i] > 0) length++; 132 | } 133 | lockData = new uint256[2][](length); 134 | uint256 x = 0; 135 | for (uint256 i = week + 1; i < week + MAX_LOCK_WEEKS + 1; i++) { 136 | if (weeklyUnlocksOf[_user][i] > 0) { 137 | lockData[x] = [i - week, weeklyUnlocksOf[_user][i]]; 138 | x++; 139 | } 140 | } 141 | return lockData; 142 | } 143 | 144 | /** 145 | @notice Deposit tokens into the contract to create a new lock. 146 | @dev A lock is created for a given number of weeks. Minimum 1, maximum `MAX_LOCK_WEEKS`. 147 | A user can have more than one lock active at a time. A user's total "lock weight" 148 | is calculated as the sum of [number of tokens] * [weeks until unlock] for all 149 | active locks. Fees are distributed porportionally according to a user's lock 150 | weight as a percentage of the total lock weight. At the start of each new week, 151 | each lock's weeks until unlock is reduced by 1. Locks that reach 0 week no longer 152 | receive any weight, and tokens may be withdrawn by calling `initiateExitStream`. 153 | @param _user Address to create a new lock for (does not have to be the caller) 154 | @param _amount Amount of SEX to lock. This balance transfered from the caller. 155 | @param _weeks The number of weeks for the lock. 156 | */ 157 | function lock( 158 | address _user, 159 | uint256 _amount, 160 | uint256 _weeks 161 | ) external returns (bool) { 162 | require(_weeks > 0, "Min 1 week"); 163 | require(_weeks <= MAX_LOCK_WEEKS, "Exceeds MAX_LOCK_WEEKS"); 164 | require(_amount > 0, "Amount must be nonzero"); 165 | 166 | SEX.safeTransferFrom(msg.sender, address(this), _amount); 167 | 168 | uint256 start = getWeek(); 169 | _increaseAmount(weeklyTotalWeight, start, _amount, _weeks, 0); 170 | _increaseAmount(weeklyWeightOf[_user], start, _amount, _weeks, 0); 171 | 172 | uint256 end = start + _weeks; 173 | weeklyUnlocksOf[_user][end] = weeklyUnlocksOf[_user][end] + _amount; 174 | 175 | emit NewLock(_user, _amount, _weeks); 176 | return true; 177 | } 178 | 179 | /** 180 | @notice Extend the length of an existing lock. 181 | @param _amount Amount of SEX to extend the lock for. When the value given equals 182 | the total size of the existing lock, the entire lock is moved. 183 | If the amount is less, then the lock is effectively split into 184 | two locks, with a portion of the balance extended to the new length 185 | and the remaining balance at the old length. 186 | @param _weeks The number of weeks for the lock that is being extended. 187 | @param _newWeeks The number of weeks to extend the lock until. 188 | */ 189 | function extendLock( 190 | uint256 _amount, 191 | uint256 _weeks, 192 | uint256 _newWeeks 193 | ) external returns (bool) { 194 | require(_weeks > 0, "Min 1 week"); 195 | require(_newWeeks <= MAX_LOCK_WEEKS, "Exceeds MAX_LOCK_WEEKS"); 196 | require(_weeks < _newWeeks, "newWeeks must be greater than weeks"); 197 | require(_amount > 0, "Amount must be nonzero"); 198 | 199 | uint256[9362] storage unlocks = weeklyUnlocksOf[msg.sender]; 200 | uint256 start = getWeek(); 201 | uint256 end = start + _weeks; 202 | unlocks[end] = unlocks[end] - _amount; 203 | end = start + _newWeeks; 204 | unlocks[end] = unlocks[end] + _amount; 205 | 206 | _increaseAmount(weeklyTotalWeight, start, _amount, _newWeeks, _weeks); 207 | _increaseAmount( 208 | weeklyWeightOf[msg.sender], 209 | start, 210 | _amount, 211 | _newWeeks, 212 | _weeks 213 | ); 214 | 215 | emit ExtendLock(msg.sender, _amount, _weeks, _newWeeks); 216 | return true; 217 | } 218 | 219 | /** 220 | @notice Create an exit stream, to withdraw tokens in expired locks over 1 week 221 | */ 222 | function initiateExitStream() external returns (bool) { 223 | StreamData storage stream = exitStream[msg.sender]; 224 | uint256 streamable = streamableBalance(msg.sender); 225 | require(streamable > 0, "No withdrawable balance"); 226 | 227 | uint256 amount = stream.amount - stream.claimed + streamable; 228 | exitStream[msg.sender] = StreamData({ 229 | start: block.timestamp, 230 | amount: amount, 231 | claimed: 0 232 | }); 233 | withdrawnUntil[msg.sender] = getWeek(); 234 | 235 | emit NewExitStream(msg.sender, block.timestamp, amount); 236 | return true; 237 | } 238 | 239 | /** 240 | @notice Withdraw tokens from an active or completed exit stream 241 | */ 242 | function withdrawExitStream() external returns (bool) { 243 | StreamData storage stream = exitStream[msg.sender]; 244 | uint256 amount; 245 | if (stream.start > 0) { 246 | amount = claimableExitStreamBalance(msg.sender); 247 | if (stream.start + WEEK < block.timestamp) { 248 | delete exitStream[msg.sender]; 249 | } else { 250 | stream.claimed = stream.claimed + amount; 251 | } 252 | SEX.safeTransfer(msg.sender, amount); 253 | } 254 | emit ExitStreamWithdrawal( 255 | msg.sender, 256 | amount, 257 | stream.amount - stream.claimed 258 | ); 259 | return true; 260 | } 261 | 262 | /** 263 | @notice Get the amount of SEX in expired locks that is 264 | eligible to be released via an exit stream. 265 | */ 266 | function streamableBalance(address _user) public view returns (uint256) { 267 | uint256 finishedWeek = getWeek(); 268 | 269 | uint256[9362] storage unlocks = weeklyUnlocksOf[_user]; 270 | uint256 amount; 271 | 272 | for ( 273 | uint256 last = withdrawnUntil[_user] + 1; 274 | last <= finishedWeek; 275 | last++ 276 | ) { 277 | amount = amount + unlocks[last]; 278 | } 279 | return amount; 280 | } 281 | 282 | /** 283 | @notice Get the amount of SEX available to withdraw 284 | from the active exit stream. 285 | */ 286 | function claimableExitStreamBalance(address _user) 287 | public 288 | view 289 | returns (uint256) 290 | { 291 | StreamData storage stream = exitStream[_user]; 292 | if (stream.start == 0) return 0; 293 | if (stream.start + WEEK < block.timestamp) { 294 | return stream.amount - stream.claimed; 295 | } else { 296 | uint256 claimable = stream.amount * (block.timestamp - stream.start) / WEEK; 297 | return claimable - stream.claimed; 298 | } 299 | } 300 | 301 | /** 302 | @dev Increase the amount within a lock weight array over a given time period 303 | */ 304 | function _increaseAmount( 305 | uint256[9362] storage _record, 306 | uint256 _start, 307 | uint256 _amount, 308 | uint256 _rounds, 309 | uint256 _oldRounds 310 | ) internal { 311 | uint256 oldEnd = _start + _oldRounds; 312 | uint256 end = _start + _rounds; 313 | for (uint256 i = _start; i < end; i++) { 314 | uint256 amount = _amount * (end - i); 315 | if (i < oldEnd) { 316 | amount -= _amount * (oldEnd - i); 317 | } 318 | _record[i] += amount; 319 | } 320 | } 321 | 322 | } 323 | -------------------------------------------------------------------------------- /contracts/LpDepositor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./dependencies/Ownable.sol"; 4 | import "./dependencies/SafeERC20.sol"; 5 | import "./interfaces/IERC20.sol"; 6 | import "./interfaces/solidly/IBaseV1Voter.sol"; 7 | import "./interfaces/solidly/IGauge.sol"; 8 | import "./interfaces/solidly/IBribe.sol"; 9 | import "./interfaces/solidly/IVotingEscrow.sol"; 10 | import "./interfaces/solidex/IFeeDistributor.sol"; 11 | import "./interfaces/solidex/ISolidexToken.sol"; 12 | import "./interfaces/solidex/ILpDepositToken.sol"; 13 | import "./interfaces/solidex/IVeDepositor.sol"; 14 | 15 | 16 | contract LpDepositor is Ownable { 17 | 18 | using SafeERC20 for IERC20; 19 | 20 | // solidly contracts 21 | IERC20 public immutable SOLID; 22 | IVotingEscrow public immutable votingEscrow; 23 | IBaseV1Voter public immutable solidlyVoter; 24 | 25 | // solidex contracts 26 | ISolidexToken public SEX; 27 | IVeDepositor public SOLIDsex; 28 | IFeeDistributor public feeDistributor; 29 | address public stakingRewards; 30 | address public tokenWhitelister; 31 | address public depositTokenImplementation; 32 | 33 | uint256 public tokenID; 34 | 35 | struct Amounts { 36 | uint256 solid; 37 | uint256 sex; 38 | } 39 | 40 | // pool -> gauge 41 | mapping(address => address) public gaugeForPool; 42 | // pool -> bribe 43 | mapping(address => address) public bribeForPool; 44 | // pool -> solidex deposit token 45 | mapping(address => address) public tokenForPool; 46 | // user -> pool -> deposit amount 47 | mapping(address => mapping(address => uint256)) public userBalances; 48 | // pool -> total deposit amount 49 | mapping(address => uint256) public totalBalances; 50 | // pool -> integrals 51 | mapping(address => Amounts) public rewardIntegral; 52 | // user -> pool -> integrals 53 | mapping(address => mapping(address => Amounts)) public rewardIntegralFor; 54 | // user -> pool -> claimable 55 | mapping(address => mapping(address => Amounts)) claimable; 56 | 57 | // internal accounting to track SOLID fees for SOLIDsex stakers and SEX lockers 58 | uint256 unclaimedSolidBonus; 59 | 60 | event RewardAdded(address indexed rewardsToken, uint256 reward); 61 | event Deposited(address indexed user, address indexed pool, uint256 amount); 62 | event Withdrawn(address indexed user, address indexed pool, uint256 amount); 63 | event RewardPaid(address indexed user, address indexed rewardsToken, uint256 reward); 64 | event TransferDeposit(address indexed pool, address indexed from, address indexed to, uint256 amount); 65 | 66 | constructor( 67 | IERC20 _solid, 68 | IVotingEscrow _votingEscrow, 69 | IBaseV1Voter _solidlyVoter 70 | 71 | ) { 72 | SOLID = _solid; 73 | votingEscrow = _votingEscrow; 74 | solidlyVoter = _solidlyVoter; 75 | } 76 | 77 | function setAddresses( 78 | ISolidexToken _sex, 79 | IVeDepositor _solidsex, 80 | address _solidexVoter, 81 | IFeeDistributor _feeDistributor, 82 | address _stakingRewards, 83 | address _tokenWhitelister, 84 | address _depositToken 85 | ) external onlyOwner { 86 | SEX = _sex; 87 | SOLIDsex = _solidsex; 88 | feeDistributor = _feeDistributor; 89 | stakingRewards = _stakingRewards; 90 | tokenWhitelister = _tokenWhitelister; 91 | depositTokenImplementation = _depositToken; 92 | 93 | SOLID.approve(address(_solidsex), type(uint256).max); 94 | _solidsex.approve(address(_feeDistributor), type(uint256).max); 95 | votingEscrow.setApprovalForAll(_solidexVoter, true); 96 | votingEscrow.setApprovalForAll(address(_solidsex), true); 97 | 98 | renounceOwnership(); 99 | } 100 | 101 | /** 102 | @dev Ensure SOLID, SEX and SOLIDsex are whitelisted 103 | */ 104 | function whitelistProtocolTokens() external { 105 | require(tokenID != 0, "No initial NFT deposit"); 106 | if (!solidlyVoter.isWhitelisted(address(SOLID))) { 107 | solidlyVoter.whitelist(address(SOLID), tokenID); 108 | } 109 | if (!solidlyVoter.isWhitelisted(address(SOLIDsex))) { 110 | solidlyVoter.whitelist(address(SOLIDsex), tokenID); 111 | } 112 | if (!solidlyVoter.isWhitelisted(address(SEX))) { 113 | solidlyVoter.whitelist(address(SEX), tokenID); 114 | } 115 | } 116 | 117 | /** 118 | @notice Get pending SOLID and SEX rewards earned by `account` 119 | @param account Account to query pending rewards for 120 | @param pools List of pool addresses to query rewards for 121 | @return pending Array of tuples of (SOLID rewards, SEX rewards) for each item in `pool` 122 | */ 123 | function pendingRewards( 124 | address account, 125 | address[] calldata pools 126 | ) 127 | external 128 | view 129 | returns (Amounts[] memory pending) 130 | { 131 | pending = new Amounts[](pools.length); 132 | for (uint256 i = 0; i < pools.length; i++) { 133 | address pool = pools[i]; 134 | pending[i] = claimable[account][pool]; 135 | uint256 balance = userBalances[account][pool]; 136 | if (balance == 0) continue; 137 | 138 | Amounts memory integral = rewardIntegral[pool]; 139 | uint256 total = totalBalances[pool]; 140 | if (total > 0) { 141 | uint256 delta = IGauge(gaugeForPool[pool]).earned(address(SOLID), address(this)); 142 | delta -= delta * 15 / 100; 143 | integral.solid += 1e18 * delta / total; 144 | integral.sex += 1e18 * (delta * 10000 / 42069) / total; 145 | } 146 | 147 | Amounts storage integralFor = rewardIntegralFor[account][pool]; 148 | if (integralFor.solid < integral.solid) { 149 | pending[i].solid += balance * (integral.solid - integralFor.solid) / 1e18; 150 | pending[i].sex += balance * (integral.sex - integralFor.sex) / 1e18; 151 | } 152 | } 153 | return pending; 154 | } 155 | 156 | /** 157 | @notice Deposit Solidly LP tokens into a gauge via this contract 158 | @dev Each deposit is also represented via a new ERC20, the address 159 | is available by querying `tokenForPool(pool)` 160 | @param pool Address of the pool token to deposit 161 | @param amount Quantity of tokens to deposit 162 | */ 163 | function deposit(address pool, uint256 amount) external { 164 | require(tokenID != 0, "Must lock SOLID first"); 165 | require(amount > 0, "Cannot deposit zero"); 166 | 167 | address gauge = gaugeForPool[pool]; 168 | uint256 total = totalBalances[pool]; 169 | uint256 balance = userBalances[msg.sender][pool]; 170 | 171 | if (gauge == address(0)) { 172 | gauge = solidlyVoter.gauges(pool); 173 | if (gauge == address(0)) { 174 | gauge = solidlyVoter.createGauge(pool); 175 | } 176 | gaugeForPool[pool] = gauge; 177 | bribeForPool[pool] = solidlyVoter.bribes(gauge); 178 | tokenForPool[pool] = _deployDepositToken(pool); 179 | IERC20(pool).approve(gauge, type(uint256).max); 180 | } else { 181 | _updateIntegrals(msg.sender, pool, gauge, balance, total); 182 | } 183 | 184 | IERC20(pool).transferFrom(msg.sender, address(this), amount); 185 | IGauge(gauge).deposit(amount, tokenID); 186 | 187 | userBalances[msg.sender][pool] = balance + amount; 188 | totalBalances[pool] = total + amount; 189 | IDepositToken(tokenForPool[pool]).mint(msg.sender, amount); 190 | emit Deposited(msg.sender, pool, amount); 191 | } 192 | 193 | /** 194 | @notice Withdraw Solidly LP tokens 195 | @param pool Address of the pool token to withdraw 196 | @param amount Quantity of tokens to withdraw 197 | */ 198 | function withdraw(address pool, uint256 amount) external { 199 | address gauge = gaugeForPool[pool]; 200 | uint256 total = totalBalances[pool]; 201 | uint256 balance = userBalances[msg.sender][pool]; 202 | 203 | require(gauge != address(0), "Unknown pool"); 204 | require(amount > 0, "Cannot withdraw zero"); 205 | require(balance >= amount, "Insufficient deposit"); 206 | 207 | _updateIntegrals(msg.sender, pool, gauge, balance, total); 208 | 209 | userBalances[msg.sender][pool] = balance - amount; 210 | totalBalances[pool] = total - amount; 211 | 212 | IDepositToken(tokenForPool[pool]).burn(msg.sender, amount); 213 | IGauge(gauge).withdraw(amount); 214 | IERC20(pool).transfer(msg.sender, amount); 215 | emit Withdrawn(msg.sender, pool, amount); 216 | } 217 | 218 | /** 219 | @notice Claim SOLID and SEX rewards earned from depositing LP tokens 220 | @dev An additional 5% of SEX is also minted for `StakingRewards` 221 | @param pools List of pools to claim for 222 | */ 223 | function getReward(address[] calldata pools) external { 224 | Amounts memory claims; 225 | for (uint256 i = 0; i < pools.length; i++) { 226 | address pool = pools[i]; 227 | address gauge = gaugeForPool[pool]; 228 | uint256 total = totalBalances[pool]; 229 | uint256 balance = userBalances[msg.sender][pool]; 230 | _updateIntegrals(msg.sender, pool, gauge, balance, total); 231 | claims.solid += claimable[msg.sender][pool].solid; 232 | claims.sex += claimable[msg.sender][pool].sex; 233 | delete claimable[msg.sender][pool]; 234 | } 235 | if (claims.solid > 0) { 236 | SOLID.transfer(msg.sender, claims.solid); 237 | emit RewardPaid(msg.sender, address(SOLID), claims.solid); 238 | } 239 | if (claims.sex > 0) { 240 | SEX.mint(msg.sender, claims.sex); 241 | emit RewardPaid(msg.sender, address(SEX), claims.sex); 242 | // mint an extra 5% for SOLIDsex stakers 243 | SEX.mint(address(stakingRewards), claims.sex * 100 / 95 - claims.sex); 244 | emit RewardPaid(address(stakingRewards), address(SEX), claims.sex * 100 / 95 - claims.sex); 245 | } 246 | } 247 | 248 | /** 249 | @notice Claim incentive tokens from gauge and/or bribe contracts 250 | and transfer them to `FeeDistributor` 251 | @dev This method is unguarded, anyone can claim any reward at any time. 252 | Claimed tokens are streamed to SEX lockers starting at the beginning 253 | of the following epoch week. 254 | @param pool Address of the pool token to claim for 255 | @param gaugeRewards List of incentive tokens to claim for in the pool's gauge 256 | @param bribeRewards List of incentive tokens to claim for in the pool's bribe contract 257 | */ 258 | function claimLockerRewards( 259 | address pool, 260 | address[] calldata gaugeRewards, 261 | address[] calldata bribeRewards 262 | ) external { 263 | // claim pending gauge rewards for this pool to update `unclaimedSolidBonus` 264 | address gauge = gaugeForPool[pool]; 265 | require(gauge != address(0), "Unknown pool"); 266 | _updateIntegrals(address(0), pool, gauge, 0, totalBalances[pool]); 267 | 268 | address distributor = address(feeDistributor); 269 | uint256 amount; 270 | 271 | // fetch gauge rewards and push to the fee distributor 272 | if (gaugeRewards.length > 0) { 273 | IGauge(gauge).getReward(address(this), gaugeRewards); 274 | for (uint i = 0; i < gaugeRewards.length; i++) { 275 | IERC20 reward = IERC20(gaugeRewards[i]); 276 | require(reward != SOLID, "!SOLID as gauge reward"); 277 | amount = IERC20(reward).balanceOf(address(this)); 278 | if (amount == 0) continue; 279 | if (reward.allowance(address(this), distributor) == 0) { 280 | reward.safeApprove(distributor, type(uint256).max); 281 | } 282 | IFeeDistributor(distributor).depositFee(address(reward), amount); 283 | } 284 | } 285 | 286 | // fetch bribe rewards and push to the fee distributor 287 | if (bribeRewards.length > 0) { 288 | uint256 solidBalance = SOLID.balanceOf(address(this)); 289 | IBribe(bribeForPool[pool]).getReward(tokenID, bribeRewards); 290 | for (uint i = 0; i < bribeRewards.length; i++) { 291 | IERC20 reward = IERC20(bribeRewards[i]); 292 | if (reward == SOLID) { 293 | // when SOLID is received as a bribe, add it to the balance 294 | // that will be converted to SOLIDsex prior to distribution 295 | uint256 newBalance = SOLID.balanceOf(address(this)); 296 | unclaimedSolidBonus += newBalance - solidBalance; 297 | solidBalance = newBalance; 298 | continue; 299 | } 300 | amount = reward.balanceOf(address(this)); 301 | if (amount == 0) continue; 302 | if (reward.allowance(address(this), distributor) == 0) { 303 | reward.safeApprove(distributor, type(uint256).max); 304 | } 305 | IFeeDistributor(distributor).depositFee(address(reward), amount); 306 | } 307 | } 308 | 309 | amount = unclaimedSolidBonus; 310 | if (amount > 0) { 311 | // lock 5% of earned SOLID and distribute SOLIDsex to SEX lockers 312 | uint256 lockAmount = amount / 3; 313 | SOLIDsex.depositTokens(lockAmount); 314 | IFeeDistributor(distributor).depositFee(address(SOLIDsex), lockAmount); 315 | 316 | // distribute 10% of earned SOLID to SOLIDsex stakers 317 | amount -= lockAmount; 318 | SOLID.transfer(address(stakingRewards), amount); 319 | unclaimedSolidBonus = 0; 320 | } 321 | } 322 | 323 | // External guarded functions - only callable by other protocol contracts ** // 324 | 325 | function transferDeposit(address pool, address from, address to, uint256 amount) external returns (bool) { 326 | require(msg.sender == tokenForPool[pool], "Unauthorized caller"); 327 | require(amount > 0, "Cannot transfer zero"); 328 | 329 | address gauge = gaugeForPool[pool]; 330 | uint256 total = totalBalances[pool]; 331 | 332 | uint256 balance = userBalances[from][pool]; 333 | require(balance >= amount, "Insufficient balance"); 334 | _updateIntegrals(from, pool, gauge, balance, total); 335 | userBalances[from][pool] = balance - amount; 336 | 337 | balance = userBalances[to][pool]; 338 | _updateIntegrals(to, pool, gauge, balance, total - amount); 339 | userBalances[to][pool] = balance + amount; 340 | emit TransferDeposit(pool, from, to, amount); 341 | return true; 342 | } 343 | 344 | function whitelist(address token) external returns (bool) { 345 | require(msg.sender == tokenWhitelister, "Only whitelister"); 346 | require(votingEscrow.balanceOfNFT(tokenID) > solidlyVoter.listing_fee(), "Not enough veSOLID"); 347 | solidlyVoter.whitelist(token, tokenID); 348 | return true; 349 | } 350 | 351 | function onERC721Received( 352 | address _operator, 353 | address _from, 354 | uint256 _tokenID, 355 | bytes calldata 356 | )external returns (bytes4) { 357 | // VeDepositor transfers the NFT to this contract so this callback is required 358 | require(_operator == address(SOLIDsex)); 359 | 360 | if (tokenID == 0) { 361 | tokenID = _tokenID; 362 | } 363 | 364 | return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); 365 | } 366 | 367 | // ** Internal functions ** // 368 | 369 | function _deployDepositToken(address pool) internal returns (address token) { 370 | // taken from https://solidity-by-example.org/app/minimal-proxy/ 371 | bytes20 targetBytes = bytes20(depositTokenImplementation); 372 | assembly { 373 | let clone := mload(0x40) 374 | mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) 375 | mstore(add(clone, 0x14), targetBytes) 376 | mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) 377 | token := create(0, clone, 0x37) 378 | } 379 | IDepositToken(token).initialize(pool); 380 | return token; 381 | } 382 | 383 | function _updateIntegrals( 384 | address user, 385 | address pool, 386 | address gauge, 387 | uint256 balance, 388 | uint256 total 389 | ) internal { 390 | Amounts memory integral = rewardIntegral[pool]; 391 | if (total > 0) { 392 | uint256 delta = SOLID.balanceOf(address(this)); 393 | address[] memory rewards = new address[](1); 394 | rewards[0] = address(SOLID); 395 | IGauge(gauge).getReward(address(this), rewards); 396 | delta = SOLID.balanceOf(address(this)) - delta; 397 | if (delta > 0) { 398 | uint256 fee = delta * 15 / 100; 399 | delta -= fee; 400 | unclaimedSolidBonus += fee; 401 | 402 | integral.solid += 1e18 * delta / total; 403 | integral.sex += 1e18 * (delta * 10000 / 42069) / total; 404 | rewardIntegral[pool] = integral; 405 | } 406 | } 407 | if (user != address(0)) { 408 | Amounts memory integralFor = rewardIntegralFor[user][pool]; 409 | if (integralFor.solid < integral.solid) { 410 | Amounts storage claims = claimable[user][pool]; 411 | claims.solid += balance * (integral.solid - integralFor.solid) / 1e18; 412 | claims.sex += balance * (integral.sex - integralFor.sex) / 1e18; 413 | rewardIntegralFor[user][pool] = integral; 414 | } 415 | } 416 | } 417 | 418 | } 419 | -------------------------------------------------------------------------------- /contracts/SolidexVoter.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.11; 2 | 3 | import "./dependencies/Ownable.sol"; 4 | import "./interfaces/solidex/ITokenLocker.sol"; 5 | import "./interfaces/solidex/ISexPartners.sol"; 6 | import "./interfaces/solidly/IBaseV1Voter.sol"; 7 | 8 | 9 | contract SolidexVoter is Ownable { 10 | 11 | uint256 public tokenID; 12 | 13 | IBaseV1Voter public immutable solidlyVoter; 14 | 15 | ITokenLocker public tokenLocker; 16 | ISexPartners public sexPartners; 17 | address public veDepositor; 18 | 19 | uint256 constant WEEK = 86400 * 7; 20 | uint256 public startTime; 21 | 22 | // the maximum number of pools to submit a vote for 23 | // must be low enough that `submitVotes` can submit the vote 24 | // data without the call reaching the block gas limit 25 | uint256 public constant MAX_SUBMITTED_VOTES = 50; 26 | 27 | // beyond the top `MAX_SUBMITTED_VOTES` pools, we also record several 28 | // more highest-voted pools. this mitigates against inaccuracies 29 | // in the lower end of the vote weights that can be caused by negative voting. 30 | uint256 constant MAX_VOTES_WITH_BUFFER = MAX_SUBMITTED_VOTES + 10; 31 | 32 | // token -> week -> weight allocated 33 | mapping(address => mapping(uint256 => int256)) public poolVotes; 34 | // user -> week -> weight used 35 | mapping(address => mapping(uint256 => uint256)) public userVotes; 36 | 37 | // pool -> number of early partners protecting from negative vote weight 38 | mapping(address => uint256) public poolProtectionCount; 39 | // early partner -> negative vote protection data (visible via `getPoolProtectionData`) 40 | mapping(address => ProtectionData) poolProtectionData; 41 | // SOLIDsex/SOLID and SEX/WFTM pool addresses 42 | address[2] public fixedVotePools; 43 | 44 | // [uint24 id][int40 poolVotes] 45 | // handled as an array of uint64 to allow memory-to-storage copy 46 | mapping(uint256 => uint64[MAX_VOTES_WITH_BUFFER]) topVotes; 47 | 48 | address[] poolAddresses; 49 | mapping(address => PoolData) poolData; 50 | 51 | uint256 lastWeek; // week of the last received vote (+1) 52 | uint256 topVotesLength; // actual number of items stored in `topVotes` 53 | uint256 minTopVote; // smallest vote-weight for pools included in `topVotes` 54 | uint256 minTopVoteIndex; // `topVotes` index where the smallest vote is stored (+1) 55 | 56 | struct ProtectionData { 57 | address[2] pools; 58 | uint40 lastUpdate; 59 | } 60 | struct Vote { 61 | address pool; 62 | int256 weight; 63 | } 64 | struct PoolData { 65 | uint24 addressIndex; 66 | uint16 currentWeek; 67 | uint8 topVotesIndex; 68 | } 69 | 70 | event VotedForPoolIncentives( 71 | address indexed voter, 72 | address[] pools, 73 | int256[] voteWeights, 74 | uint256 usedWeight, 75 | uint256 totalWeight 76 | ); 77 | event PoolProtectionSet( 78 | address address1, 79 | address address2, 80 | uint40 lastUpdate 81 | ); 82 | event SubmittedVote( 83 | address caller, 84 | address[] pools, 85 | int256[] weights 86 | ); 87 | 88 | constructor(IBaseV1Voter _voter) { 89 | solidlyVoter = _voter; 90 | 91 | // position 0 is empty so that an ID of 0 can be interpreted as unset 92 | poolAddresses.push(address(0)); 93 | } 94 | 95 | function setAddresses( 96 | ITokenLocker _tokenLocker, 97 | ISexPartners _sexPartners, 98 | address _veDepositor, 99 | address[2] calldata _fixedVotePools 100 | ) external onlyOwner { 101 | tokenLocker = _tokenLocker; 102 | sexPartners = _sexPartners; 103 | veDepositor = _veDepositor; 104 | startTime = _tokenLocker.startTime(); 105 | 106 | // hardcoded pools always receive 5% of the vote 107 | // and cannot receive negative vote weights 108 | fixedVotePools = _fixedVotePools; 109 | poolProtectionCount[_fixedVotePools[0]] = 1; 110 | poolProtectionCount[_fixedVotePools[1]] = 1; 111 | 112 | renounceOwnership(); 113 | } 114 | 115 | function setTokenID(uint256 _tokenID) external returns (bool) { 116 | require(msg.sender == veDepositor); 117 | tokenID = _tokenID; 118 | return true; 119 | } 120 | 121 | function getWeek() public view returns (uint256) { 122 | if (startTime == 0) return 0; 123 | return (block.timestamp - startTime) / 604800; 124 | } 125 | 126 | /** 127 | @notice The current pools and weights that would be submitted 128 | when calling `submitVotes` 129 | */ 130 | function getCurrentVotes() external view returns (Vote[] memory votes) { 131 | (address[] memory pools, int256[] memory weights) = _currentVotes(); 132 | votes = new Vote[](pools.length); 133 | for (uint i = 0; i < votes.length; i++) { 134 | votes[i] = Vote({pool: pools[i], weight: weights[i]}); 135 | } 136 | return votes; 137 | } 138 | 139 | function getPoolProtectionData(address user) external view returns (uint256 lastUpdate, address[2] memory pools) { 140 | return (poolProtectionData[user].lastUpdate, poolProtectionData[user].pools); 141 | } 142 | 143 | /** 144 | @notice Get an account's unused vote weight for for the current week 145 | @param _user Address to query 146 | @return uint Amount of unused weight 147 | */ 148 | function availableVotes(address _user) 149 | external 150 | view 151 | returns (uint256) 152 | { 153 | uint256 week = getWeek(); 154 | uint256 usedWeight = userVotes[_user][week]; 155 | uint256 totalWeight = tokenLocker.userWeight(_user) / 1e18; 156 | return totalWeight - usedWeight; 157 | } 158 | 159 | /** 160 | @notice Vote for one or more pools 161 | @dev Vote-weights received via this function are aggregated but not sent to Solidly. 162 | To submit the vote to solidly you must call `submitVotes`. 163 | Voting does not carry over between weeks, votes must be resubmitted. 164 | @param _pools Array of pool addresses to vote for 165 | @param _weights Array of vote weights. Votes can be negative, the total weight calculated 166 | from absolute values. 167 | */ 168 | function voteForPools(address[] calldata _pools, int256[] calldata _weights) external { 169 | require(_pools.length == _weights.length, "_pools.length != _weights.length"); 170 | require(_pools.length > 0, "Must vote for at least one pool"); 171 | 172 | uint256 week = getWeek(); 173 | uint256 totalUserWeight; 174 | 175 | // copy these values into memory to avoid repeated SLOAD / SSTORE ops 176 | uint256 _topVotesLengthMem = topVotesLength; 177 | uint256 _minTopVoteMem = minTopVote; 178 | uint256 _minTopVoteIndexMem = minTopVoteIndex; 179 | uint64[MAX_VOTES_WITH_BUFFER] memory t = topVotes[week]; 180 | 181 | if (week + 1 > lastWeek) { 182 | _topVotesLengthMem = 0; 183 | _minTopVoteMem = 0; 184 | lastWeek = week + 1; 185 | } 186 | for (uint x = 0; x < _pools.length; x++) { 187 | address _pool = _pools[x]; 188 | int256 _weight = _weights[x]; 189 | totalUserWeight += abs(_weight); 190 | 191 | require(_weight != 0, "Cannot vote zero"); 192 | if (_weight < 0) { 193 | require(poolProtectionCount[_pool] == 0, "Pool is protected from negative votes"); 194 | } 195 | 196 | // update accounting for this week's votes 197 | int256 poolWeight = poolVotes[_pool][week]; 198 | uint256 id = poolData[_pool].addressIndex; 199 | if (poolWeight == 0 || poolData[_pool].currentWeek <= week) { 200 | require(solidlyVoter.gauges(_pool) != address(0), "Pool has no gauge"); 201 | if (id == 0) { 202 | id = poolAddresses.length; 203 | poolAddresses.push(_pool); 204 | } 205 | poolData[_pool] = PoolData({ 206 | addressIndex: uint24(id), 207 | currentWeek: uint16(week + 1), 208 | topVotesIndex: 0 209 | }); 210 | } 211 | 212 | int256 newPoolWeight = poolWeight + _weight; 213 | uint256 absNewPoolWeight = abs(newPoolWeight); 214 | assert(absNewPoolWeight < 2 ** 39); // this should never be possible 215 | 216 | poolVotes[_pool][week] = newPoolWeight; 217 | 218 | if (poolData[_pool].topVotesIndex > 0) { 219 | // pool already exists within the list 220 | uint256 voteIndex = poolData[_pool].topVotesIndex - 1; 221 | 222 | if (newPoolWeight == 0) { 223 | // pool has a new vote-weight of 0 and so is being removed 224 | poolData[_pool] = PoolData({ 225 | addressIndex: uint24(id), 226 | currentWeek: 0, 227 | topVotesIndex: 0 228 | }); 229 | _topVotesLengthMem -= 1; 230 | if (voteIndex == _topVotesLengthMem) { 231 | delete t[voteIndex]; 232 | } else { 233 | t[voteIndex] = t[_topVotesLengthMem]; 234 | uint256 addressIndex = t[voteIndex] >> 40; 235 | poolData[poolAddresses[addressIndex]].topVotesIndex = uint8(voteIndex + 1); 236 | delete t[_topVotesLengthMem]; 237 | if (_minTopVoteIndexMem > _topVotesLengthMem) { 238 | // the value we just shifted was the minimum weight 239 | _minTopVoteIndexMem = voteIndex + 1; 240 | // continue here to avoid iterating to locate the new min index 241 | continue; 242 | } 243 | } 244 | } else { 245 | // modify existing record for this pool within `topVotes` 246 | t[voteIndex] = pack(id, newPoolWeight); 247 | if (absNewPoolWeight < _minTopVoteMem) { 248 | // if new weight is also the new minimum weight 249 | _minTopVoteMem = absNewPoolWeight; 250 | _minTopVoteIndexMem = voteIndex + 1; 251 | // continue here to avoid iterating to locate the new min voteIndex 252 | continue; 253 | } 254 | } 255 | if (voteIndex == _minTopVoteIndexMem - 1) { 256 | // iterate to find the new minimum weight 257 | (_minTopVoteMem, _minTopVoteIndexMem) = _findMinTopVote(t, _topVotesLengthMem); 258 | } 259 | } else if (_topVotesLengthMem < MAX_VOTES_WITH_BUFFER) { 260 | // pool is not in `topVotes`, and `topVotes` contains less than 261 | // MAX_VOTES_WITH_BUFFER items, append 262 | t[_topVotesLengthMem] = pack(id, newPoolWeight); 263 | _topVotesLengthMem += 1; 264 | poolData[_pool].topVotesIndex = uint8(_topVotesLengthMem); 265 | if (absNewPoolWeight < _minTopVoteMem || _minTopVoteMem == 0) { 266 | // new weight is the new minimum weight 267 | _minTopVoteMem = absNewPoolWeight; 268 | _minTopVoteIndexMem = poolData[_pool].topVotesIndex; 269 | } 270 | } else if (absNewPoolWeight > _minTopVoteMem) { 271 | // `topVotes` contains MAX_VOTES_WITH_BUFFER items, 272 | // pool is not in the array, and weight exceeds current minimum weight 273 | 274 | // replace the pool at the current minimum weight index 275 | uint256 addressIndex = t[_minTopVoteIndexMem - 1] >> 40; 276 | poolData[poolAddresses[addressIndex]] = PoolData({ 277 | addressIndex: uint24(addressIndex), 278 | currentWeek: 0, 279 | topVotesIndex: 0 280 | }); 281 | t[_minTopVoteIndexMem - 1] = pack(id, newPoolWeight); 282 | poolData[_pool].topVotesIndex = uint8(_minTopVoteIndexMem); 283 | 284 | // iterate to find the new minimum weight 285 | (_minTopVoteMem, _minTopVoteIndexMem) = _findMinTopVote(t, MAX_VOTES_WITH_BUFFER); 286 | } 287 | } 288 | 289 | // make sure user has not exceeded available weight 290 | totalUserWeight += userVotes[msg.sender][week]; 291 | uint256 totalWeight = tokenLocker.userWeight(msg.sender) / 1e18; 292 | require(totalUserWeight <= totalWeight, "Available votes exceeded"); 293 | 294 | // write memory vars back to storage 295 | topVotes[week] = t; 296 | topVotesLength = _topVotesLengthMem; 297 | minTopVote = _minTopVoteMem; 298 | minTopVoteIndex = _minTopVoteIndexMem; 299 | userVotes[msg.sender][week] = totalUserWeight; 300 | 301 | emit VotedForPoolIncentives( 302 | msg.sender, 303 | _pools, 304 | _weights, 305 | totalUserWeight, 306 | totalWeight 307 | ); 308 | } 309 | 310 | /** 311 | @notice Submit the current votes to Solidly 312 | @dev This function is unguarded and so votes may be submitted at any time. 313 | Solidly has no restriction on the frequency that an account may vote, 314 | however emissions are only calculated from the active votes at the 315 | beginning of each epoch week. 316 | */ 317 | function submitVotes() external returns (bool) { 318 | (address[] memory pools, int256[] memory weights) = _currentVotes(); 319 | solidlyVoter.vote(tokenID, pools, weights); 320 | emit SubmittedVote(msg.sender, pools, weights); 321 | return true; 322 | } 323 | 324 | /** 325 | @notice Submit pool addresses for protection from negative votes 326 | @dev Only available to early partners 327 | @param _pools Addresses of protected pools 328 | */ 329 | function setPoolProtection(address[2] calldata _pools) external { 330 | require(sexPartners.isEarlyPartner(msg.sender), "Only early partners"); 331 | ProtectionData storage data = poolProtectionData[msg.sender]; 332 | require(block.timestamp - 86400 * 15 > data.lastUpdate, "Can only modify every 15 days"); 333 | 334 | for (uint i = 0; i < 2; i++) { 335 | (address removed, address added) = (data.pools[i], _pools[i]); 336 | if (removed != address(0)) poolProtectionCount[removed] -= 1; 337 | if (added != address(0)) poolProtectionCount[added] += 1; 338 | } 339 | 340 | if (_pools[0] == address(0) && _pools[1] == address(0)) data.lastUpdate = 0; 341 | else data.lastUpdate = uint40(block.timestamp); 342 | 343 | emit PoolProtectionSet(_pools[0], _pools[1], data.lastUpdate); 344 | data.pools = _pools; 345 | } 346 | 347 | function _currentVotes() internal view returns ( 348 | address[] memory pools, 349 | int256[] memory weights 350 | ) { 351 | uint256 week = getWeek(); 352 | uint256 length = 2; // length is always +2 to ensure room for the hardcoded gauges 353 | if (week + 1 == lastWeek) { 354 | // `lastWeek` only updates on a call to `voteForPool` 355 | // if the current week is > `lastWeek`, there have not been any votes this week 356 | length += topVotesLength; 357 | } 358 | 359 | uint256[MAX_VOTES_WITH_BUFFER] memory absWeights; 360 | pools = new address[](length); 361 | weights = new int256[](length); 362 | 363 | // unpack `topVotes` 364 | for (uint256 i = 0; i < length - 2; i++) { 365 | (uint256 id, int256 weight) = unpack(topVotes[week][i]); 366 | pools[i] = poolAddresses[id]; 367 | weights[i] = weight; 368 | absWeights[i] = abs(weight); 369 | } 370 | 371 | // if more than `MAX_SUBMITTED_VOTES` pools have votes, discard the lowest weights 372 | if (length > MAX_SUBMITTED_VOTES + 2) { 373 | while (length > MAX_SUBMITTED_VOTES + 2) { 374 | uint256 minValue = type(uint256).max; 375 | uint256 minIndex = 0; 376 | for (uint256 i = 0; i < length - 2; i++) { 377 | uint256 weight = absWeights[i]; 378 | if (weight < minValue) { 379 | minValue = weight; 380 | minIndex = i; 381 | } 382 | } 383 | uint idx = length - 3; 384 | weights[minIndex] = weights[idx]; 385 | pools[minIndex] = pools[idx]; 386 | absWeights[minIndex] = absWeights[idx]; 387 | delete weights[idx]; 388 | delete pools[idx]; 389 | length -= 1; 390 | } 391 | assembly { 392 | mstore(pools, length) 393 | mstore(weights, length) 394 | } 395 | } 396 | 397 | // calculate absolute total weight and find the indexes for the hardcoded pools 398 | uint256 totalWeight; 399 | uint256[2] memory fixedVoteIds; 400 | address[2] memory _fixedVotePools = fixedVotePools; 401 | for (uint256 i = 0; i < length - 2; i++) { 402 | totalWeight += absWeights[i]; 403 | if (pools[i] == _fixedVotePools[0]) fixedVoteIds[0] = i + 1; 404 | else if (pools[i] == _fixedVotePools[1]) fixedVoteIds[1] = i + 1; 405 | } 406 | 407 | // add 5% hardcoded vote for SOLIDsex/SOLID and SEX/WFTM 408 | int256 fixedWeight = int256(totalWeight * 11 / 200); 409 | if (fixedWeight == 0 ) fixedWeight = 1; 410 | length -= 2; 411 | for (uint i = 0; i < 2; i++) { 412 | if (fixedVoteIds[i] == 0) { 413 | pools[length + i] = _fixedVotePools[i]; 414 | weights[length + i] = fixedWeight; 415 | } else { 416 | weights[fixedVoteIds[i] - 1] += fixedWeight; 417 | } 418 | } 419 | 420 | return (pools, weights); 421 | } 422 | 423 | function _findMinTopVote(uint64[MAX_VOTES_WITH_BUFFER] memory t, uint256 length) internal pure returns (uint256, uint256) { 424 | uint256 _minTopVoteMem = type(uint256).max; 425 | uint256 _minTopVoteIndexMem; 426 | for (uint i = 0; i < length; i ++) { 427 | uint256 value = t[i] % 2 ** 39; 428 | if (value < _minTopVoteMem) { 429 | _minTopVoteMem = value; 430 | _minTopVoteIndexMem = i + 1; 431 | } 432 | } 433 | return (_minTopVoteMem, _minTopVoteIndexMem); 434 | } 435 | 436 | function abs(int256 value) internal pure returns (uint256) { 437 | return uint256(value > 0 ? value : -value); 438 | } 439 | 440 | function pack(uint256 id, int256 weight) internal pure returns (uint64) { 441 | // tightly pack as [uint24 id][int40 weight] for storage in `topVotes` 442 | uint64 value = uint64((id << 40) + abs(weight)); 443 | if (weight < 0) value += 2**39; 444 | return value; 445 | } 446 | 447 | function unpack(uint256 value) internal pure returns (uint256 id, int256 weight) { 448 | // unpack a value in `topVotes` 449 | id = (value >> 40); 450 | weight = int256(value % 2**40); 451 | if (weight > 2**39) weight = -(weight % 2**39); 452 | return (id, weight); 453 | } 454 | 455 | } 456 | --------------------------------------------------------------------------------