diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..7e0690e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,31 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: { } + workflow_dispatch: { } + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Formatter + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run forge fmt --check + run: forge fmt --check diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml new file mode 100644 index 0000000..fd180c6 --- /dev/null +++ b/.github/workflows/slither.yml @@ -0,0 +1,20 @@ +name: Slither +on: [push] +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run Slither + uses: crytic/slither-action@v0.3.0 + id: slither + with: + sarif: results.sarif + fail-on: none + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: ${{ steps.slither.outputs.sarif }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2b7492..6df8663 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,3 +37,9 @@ jobs: run: | forge test -vvv id: test + + - name: Run Forge coverage + run: | + forge coverage + forge coverage --report lcov + id: coverage diff --git a/README.md b/README.md index 8b89ff5..513cd26 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,5 @@ forge install forge build forge test -vv forge coverage +forge fmt ``` diff --git a/chains/PolygonLib.sol b/chains/PolygonLib.sol index a86c3b8..8bace08 100644 --- a/chains/PolygonLib.sol +++ b/chains/PolygonLib.sol @@ -5,6 +5,7 @@ import {console} from "forge-std/Test.sol"; import "../script/lib/DeployLib.sol"; library PolygonLib { + address public constant TOKEN_PEARL = 0x7238390d5f6F64e67c3211C343A410E2A3DEc142; function runDeploy(bool showLog) external { address governance = 0x520Ab98a23100369E5280d214799b1E1c0123045; @@ -23,7 +24,8 @@ library PolygonLib { vestingClaimant: vestingClaimant, vestingAmount: vestingAmount, vestingPeriod: 365 days, - vestingCliff: 180 days + vestingCliff: 180 days, + rewardToken: TOKEN_PEARL }))); if (showLog) { diff --git a/foundry.toml b/foundry.toml index 25b918f..165d858 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,4 +3,6 @@ src = "src" out = "out" libs = ["lib"] -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +[fmt] +int_types = "short" +multiline_func_header = "params_first" diff --git a/script/lib/DeployLib.sol b/script/lib/DeployLib.sol index 299529c..5687b80 100644 --- a/script/lib/DeployLib.sol +++ b/script/lib/DeployLib.sol @@ -8,9 +8,9 @@ import "../../src/Vesting.sol"; import "../../src/STGN.sol"; import "../../src/ControllableProxy.sol"; import "../../src/VeSTGN.sol"; +import "../../src/MultiGauge.sol"; library DeployLib { - struct DeployParams { address governance; uint ifoRate; @@ -18,6 +18,7 @@ library DeployLib { uint[] vestingAmount; uint vestingPeriod; uint vestingCliff; + address rewardToken; // PEARL } function deployPlatform(DeployParams memory params) external returns (address controller) { @@ -31,10 +32,12 @@ library DeployLib { } STGN stgn = new STGN(params.governance, address(ifo), 1e26, 5e25, vesting, params.vestingAmount); - ifo.setup(address(stgn)); + ifo.setup(address(_c), address(stgn), params.rewardToken); for (uint i; i < len; ++i) { - Vesting(vesting[i]).setup(address(stgn), params.vestingPeriod, params.vestingCliff, params.vestingClaimant[i]); + Vesting(vesting[i]).setup( + address(stgn), params.vestingPeriod, params.vestingCliff, params.vestingClaimant[i] + ); } ControllableProxy proxy = new ControllableProxy(); @@ -42,9 +45,15 @@ library DeployLib { proxy.initProxy(impl); VeSTGN ve = VeSTGN(address(proxy)); ve.init(address(stgn), 1e18, address(_c)); -// assertEq(IProxyControlled(proxy).implementation(), impl); + // assertEq(IProxyControlled(proxy).implementation(), impl); + + proxy = new ControllableProxy(); + impl = address(new MultiGauge()); + proxy.initProxy(impl); + MultiGauge multigauge = MultiGauge(address(proxy)); + multigauge.init(address(_c), address(ve), address(stgn)); - _c.setup(address(ifo), address(ve), address(stgn)); + _c.setup(address(ifo), address(ve), address(stgn), address(multigauge)); return address(_c); } diff --git a/src/Compounder.sol b/src/Compounder.sol index 5901f7c..079aa5d 100644 --- a/src/Compounder.sol +++ b/src/Compounder.sol @@ -10,174 +10,176 @@ import "./lib/StringLib.sol"; /// @title Gelato resolver for hard work /// @author a17 contract Compounder is Controllable { - // --- CONSTANTS --- + // --- CONSTANTS --- - string public constant VERSION = "1.0.0"; - uint public constant DELAY_RATE_DENOMINATOR = 100_000; - uint public constant HARDWORK_DELAY = 12 hours; + string public constant VERSION = "1.0.0"; + uint public constant DELAY_RATE_DENOMINATOR = 100_000; + uint public constant HARDWORK_DELAY = 12 hours; - // --- VARIABLES --- + // --- VARIABLES --- - address public owner; - address public pendingOwner; - uint public delay; - uint public maxGas; - uint public maxHwPerCall; + address public owner; + address public pendingOwner; + uint public delay; + uint public maxGas; + uint public maxHwPerCall; - mapping(address => uint) public delayRate; - mapping(address => bool) public operators; - mapping(address => bool) public excludedVaults; - uint public lastHWCall; + mapping(address => uint) public delayRate; + mapping(address => bool) public operators; + mapping(address => bool) public excludedVaults; + uint public lastHWCall; - // --- INIT --- + // --- INIT --- - function init(address controller_) external initializer { - __Controllable_init(controller_); + function init(address controller_) external initializer { + __Controllable_init(controller_); - owner = msg.sender; - delay = 1 days; - maxGas = 35 gwei; - maxHwPerCall = 5; - } - - modifier onlyOwner() { - require(msg.sender == owner, "!owner"); - _; - } + owner = msg.sender; + delay = 1 days; + maxGas = 35 gwei; + maxHwPerCall = 5; + } - // --- OWNER FUNCTIONS --- + modifier onlyOwner() { + require(msg.sender == owner, "!owner"); + _; + } - function offerOwnership(address value) external onlyOwner { - pendingOwner = value; - } + // --- OWNER FUNCTIONS --- - function acceptOwnership() external { - require(msg.sender == pendingOwner, "!pendingOwner"); - owner = pendingOwner; - pendingOwner = address(0); - } + function offerOwnership(address value) external onlyOwner { + pendingOwner = value; + } - function setDelay(uint value) external onlyOwner { - delay = value; - } + function acceptOwnership() external { + require(msg.sender == pendingOwner, "!pendingOwner"); + owner = pendingOwner; + pendingOwner = address(0); + } - function setMaxGas(uint value) external onlyOwner { - maxGas = value; - } + function setDelay(uint value) external onlyOwner { + delay = value; + } - function setMaxHwPerCall(uint value) external onlyOwner { - maxHwPerCall = value; - } + function setMaxGas(uint value) external onlyOwner { + maxGas = value; + } - function setDelayRate(address[] memory _vaults, uint value) external onlyOwner { - for (uint i; i < _vaults.length; ++i) { - delayRate[_vaults[i]] = value; + function setMaxHwPerCall(uint value) external onlyOwner { + maxHwPerCall = value; } - } - function changeOperatorStatus(address operator, bool status) external onlyOwner { - operators[operator] = status; - } + function setDelayRate(address[] memory _vaults, uint value) external onlyOwner { + for (uint i; i < _vaults.length; ++i) { + delayRate[_vaults[i]] = value; + } + } - function changeVaultExcludeStatus(address[] memory _vaults, bool status) external onlyOwner { - for (uint i; i < _vaults.length; ++i) { - excludedVaults[_vaults[i]] = status; + function changeOperatorStatus(address operator, bool status) external onlyOwner { + operators[operator] = status; } - } - - // --- MAIN LOGIC --- - - function lastHW(address vault) public view returns (uint lastHardWorkTimestamp) { - // hide warning - lastHardWorkTimestamp = 0; - return IStrategyStrict(IVault(vault).strategy()).lastHardWork(); - } - - function call(address[] memory _vaults) external returns (uint amountOfCalls) { - require(operators[msg.sender], "!operator"); - - uint _maxHwPerCall = maxHwPerCall; - uint vaultsLength = _vaults.length; - uint counter; - for (uint i; i < vaultsLength; ++i) { - address vault = _vaults[i]; - - IStrategyStrict strategy = IVault(vault).strategy(); - - try strategy.doHardWork() {} catch Error(string memory _err) { - revert(string(abi.encodePacked("Vault error: 0x", StringLib.toAsciiString(vault), " ", _err))); - } catch (bytes memory _err) { - revert(string(abi.encodePacked("Vault low-level error: 0x", StringLib.toAsciiString(vault), " ", string(_err)))); - } - counter++; - if (counter >= _maxHwPerCall) { - break; - } + + function changeVaultExcludeStatus(address[] memory _vaults, bool status) external onlyOwner { + for (uint i; i < _vaults.length; ++i) { + excludedVaults[_vaults[i]] = status; + } } - lastHWCall = block.timestamp; - return counter; - } + // --- MAIN LOGIC --- - function maxGasAdjusted() public view returns (uint) { - uint _maxGas = maxGas; + function lastHW(address vault) public view returns (uint lastHardWorkTimestamp) { + // hide warning + lastHardWorkTimestamp = 0; + return IStrategyStrict(IVault(vault).strategy()).lastHardWork(); + } - uint diff = block.timestamp - lastHWCall; - uint multiplier = diff * 100 / 1 days; - return _maxGas + _maxGas * multiplier / 100; - } + function call(address[] memory _vaults) external returns (uint amountOfCalls) { + require(operators[msg.sender], "!operator"); + + uint _maxHwPerCall = maxHwPerCall; + uint vaultsLength = _vaults.length; + uint counter; + for (uint i; i < vaultsLength; ++i) { + address vault = _vaults[i]; + + IStrategyStrict strategy = IVault(vault).strategy(); + + try strategy.doHardWork() {} + catch Error(string memory _err) { + revert(string(abi.encodePacked("Vault error: 0x", StringLib.toAsciiString(vault), " ", _err))); + } catch (bytes memory _err) { + revert( + string( + abi.encodePacked("Vault low-level error: 0x", StringLib.toAsciiString(vault), " ", string(_err)) + ) + ); + } + counter++; + if (counter >= _maxHwPerCall) { + break; + } + } - function checker() external view returns (bool canExec, bytes memory execPayload) { - if (tx.gasprice > maxGasAdjusted()) { - return (false, abi.encodePacked("Too high gas: ", Strings.toString(tx.gasprice / 1e9))); + lastHWCall = block.timestamp; + return counter; } - IController _controller = IController(controller()); - uint _delay = delay; - uint vaultsLength = _controller.vaultsListLength(); - address[] memory _vaults = new address[](vaultsLength); - uint counter; - for (uint i; i < vaultsLength; ++i) { - address vault = _controller.vaults(i); - if (!excludedVaults[vault]) { - - bool strategyNeedHardwork; - IStrategyStrict strategy = IVault(vault).strategy(); - if ( - strategy.isReadyToHardWork() - && strategy.lastHardWork() + HARDWORK_DELAY < block.timestamp -// && !splitter.pausedStrategies(address(strategy)) - && strategy.totalAssets() > 0 - ) { - strategyNeedHardwork = true; - break; - } + function maxGasAdjusted() public view returns (uint) { + uint _maxGas = maxGas; - uint delayAdjusted = _delay; - uint _delayRate = delayRate[vault]; - if (_delayRate != 0) { - delayAdjusted = _delay * _delayRate / DELAY_RATE_DENOMINATOR; + uint diff = block.timestamp - lastHWCall; + uint multiplier = diff * 100 / 1 days; + return _maxGas + _maxGas * multiplier / 100; + } + + function checker() external view returns (bool canExec, bytes memory execPayload) { + if (tx.gasprice > maxGasAdjusted()) { + return (false, abi.encodePacked("Too high gas: ", Strings.toString(tx.gasprice / 1e9))); } - if (strategyNeedHardwork && lastHW(vault) + delayAdjusted < block.timestamp) { - _vaults[i] = vault; - counter++; + IController _controller = IController(controller()); + uint _delay = delay; + uint vaultsLength = _controller.vaultsListLength(); + address[] memory _vaults = new address[](vaultsLength); + uint counter; + for (uint i; i < vaultsLength; ++i) { + address vault = _controller.vaults(i); + if (!excludedVaults[vault]) { + bool strategyNeedHardwork; + IStrategyStrict strategy = IVault(vault).strategy(); + if ( + strategy.isReadyToHardWork() && strategy.lastHardWork() + HARDWORK_DELAY < block.timestamp + // && !splitter.pausedStrategies(address(strategy)) + && strategy.totalAssets() > 0 + ) { + strategyNeedHardwork = true; + break; + } + + uint delayAdjusted = _delay; + uint _delayRate = delayRate[vault]; + if (_delayRate != 0) { + delayAdjusted = _delay * _delayRate / DELAY_RATE_DENOMINATOR; + } + + if (strategyNeedHardwork && lastHW(vault) + delayAdjusted < block.timestamp) { + _vaults[i] = vault; + counter++; + } + } } - } - } - if (counter == 0) { - return (false, bytes("No ready vaults")); - } else { - address[] memory vaultsResult = new address[](counter); - uint j; - for (uint i; i < vaultsLength; ++i) { - if (_vaults[i] != address(0)) { - vaultsResult[j] = _vaults[i]; - ++j; + if (counter == 0) { + return (false, bytes("No ready vaults")); + } else { + address[] memory vaultsResult = new address[](counter); + uint j; + for (uint i; i < vaultsLength; ++i) { + if (_vaults[i] != address(0)) { + vaultsResult[j] = _vaults[i]; + ++j; + } + } + return (true, abi.encodeWithSelector(Compounder.call.selector, vaultsResult)); } - } - return (true, abi.encodeWithSelector(Compounder.call.selector, vaultsResult)); } - } - } diff --git a/src/ControllableProxy.sol b/src/ControllableProxy.sol index 59e50a9..9ae73a7 100644 --- a/src/ControllableProxy.sol +++ b/src/ControllableProxy.sol @@ -9,29 +9,28 @@ import "./base/UpgradeableProxy.sol"; /// @dev Only Controller has access and should implement time-lock for upgrade action. /// @author belbix contract ControllableProxy is UpgradeableProxy, IProxyControlled { + /// @notice Version of the contract + /// @dev Should be incremented when contract changed + string public constant PROXY_CONTROLLED_VERSION = "1.0.0"; - /// @notice Version of the contract - /// @dev Should be incremented when contract changed - string public constant PROXY_CONTROLLED_VERSION = "1.0.0"; + /// @dev Initialize proxy implementation. Need to call after deploy new proxy. + function initProxy(address _logic) external override { + //make sure that given logic is controllable and not inited + _init(_logic); + } - /// @dev Initialize proxy implementation. Need to call after deploy new proxy. - function initProxy(address _logic) external override { - //make sure that given logic is controllable and not inited - _init(_logic); - } + /// @notice Upgrade contract logic + /// @dev Upgrade allowed only for Controller and should be done only after time-lock period + /// @param _newImplementation Implementation address + function upgrade(address _newImplementation) external override { + require(IControllable(address(this)).isController(msg.sender), "Proxy: Forbidden"); + _upgradeTo(_newImplementation); + // the new contract must have the same ABI and you must have the power to change it again + require(IControllable(address(this)).isController(msg.sender), "Proxy: Wrong implementation"); + } - /// @notice Upgrade contract logic - /// @dev Upgrade allowed only for Controller and should be done only after time-lock period - /// @param _newImplementation Implementation address - function upgrade(address _newImplementation) external override { - require(IControllable(address(this)).isController(msg.sender), "Proxy: Forbidden"); - _upgradeTo(_newImplementation); - // the new contract must have the same ABI and you must have the power to change it again - require(IControllable(address(this)).isController(msg.sender), "Proxy: Wrong implementation"); - } - - /// @notice Return current logic implementation - function implementation() external override view returns (address) { - return _implementation(); - } + /// @notice Return current logic implementation + function implementation() external view override returns (address) { + return _implementation(); + } } diff --git a/src/Controller.sol b/src/Controller.sol index 431653f..ff9e2b2 100644 --- a/src/Controller.sol +++ b/src/Controller.sol @@ -6,7 +6,6 @@ import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; import "./interfaces/IProxyControlled.sol"; import "./interfaces/IController.sol"; - contract Controller is IController { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableMap for EnumerableMap.AddressToUintMap; @@ -33,6 +32,8 @@ contract Controller is IController { address public veDistributor; + address public multigauge; + /// @dev Operators can execute not-critical functions of the platform. EnumerableSet.AddressSet internal _operators; @@ -41,7 +42,6 @@ contract Controller is IController { /// @dev Set of valid vaults EnumerableSet.AddressSet internal _vaults; - event ProxyUpgradeAnnounced(address proxy, address implementation); event ProxyUpgraded(address proxy, address implementation); event ProxyAnnounceRemoved(address proxy); @@ -56,17 +56,15 @@ contract Controller is IController { _operators.add(governance_); } - function setup(address ifo_, address ve_, address stgn_) external { + function setup(address ifo_, address ve_, address stgn_, address multigauge_) external { require( - ifo_ != address(0) - && stgn_ != address(0) - && ve_ != address(0), - "WRONG_INPUT" + ifo_ != address(0) && stgn_ != address(0) && multigauge_ != address(0) && ve_ != address(0), "WRONG_INPUT" ); - require (ifo == address(0), "ALREADY"); + require(ifo == address(0), "ALREADY"); ifo = ifo_; ve = ve_; stgn = stgn_; + multigauge = multigauge_; } function _onlyGovernance() internal view { @@ -122,10 +120,7 @@ contract Controller is IController { // UPGRADE PROXIES WITH TIME-LOCK PROTECTION // ************************************************************* - function announceProxyUpgrade( - address[] memory proxies, - address[] memory implementations - ) external { + function announceProxyUpgrade(address[] memory proxies, address[] memory implementations) external { _onlyGovernance(); require(proxies.length == implementations.length, "WRONG_INPUT"); diff --git a/src/IFO.sol b/src/IFO.sol index 43ae4e1..12e767d 100644 --- a/src/IFO.sol +++ b/src/IFO.sol @@ -1,7 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.21; -contract IFO { +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./interfaces/IController.sol"; +import "./interfaces/IIFO.sol"; + +contract IFO is IIFO { + using SafeERC20 for IERC20; // todo implement // This contract should contain all preminted STGN and allowing to change them to LP rewards until tokens exists on the balance. @@ -9,15 +14,33 @@ contract IFO { // Rewards will be sent directly to governance. address public stgn; + address public rewardToken; + address public controller; uint public immutable rate; - constructor (uint rate_) { + constructor(uint rate_) { rate = rate_; } - function setup(address stgn_) external { - require (stgn_ != address(0), "WRONG_INPUT"); - require (stgn == address(0), "ALREADY"); + function setup(address controller_, address stgn_, address rewardToken_) external { + require(stgn_ != address(0) && rewardToken_ != address(0) && controller_ != address(0), "WRONG_INPUT"); + require(stgn == address(0), "ALREADY"); stgn = stgn_; + rewardToken = rewardToken_; + controller = controller_; + // add event + } + + function exchange(uint amount) external returns (bool, uint) { + require(IController(controller).isValidVault(msg.sender), "Not valid vault"); + uint stgnBal = IERC20(stgn).balanceOf(address(this)); + uint stgnOut = amount * rate / 1e18; + if (stgnOut <= stgnBal) { + IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), amount); + IERC20(stgn).safeTransfer(msg.sender, stgnOut); + return (true, stgnOut); + } + + return (false, 0); } -} \ No newline at end of file +} diff --git a/src/MultiGauge.sol b/src/MultiGauge.sol index 6a3a2d4..cf88f15 100644 --- a/src/MultiGauge.sol +++ b/src/MultiGauge.sol @@ -9,224 +9,190 @@ import "./base/StakelessMultiPoolBase.sol"; /// @title Stakeless pool for vaults /// @author belbix contract MultiGauge is StakelessMultiPoolBase, IGauge { + // ************************************************************* + // CONSTANTS + // ************************************************************* + + /// @dev Version of this contract. Adjust manually on each code modification. + string public constant MULTI_GAUGE_VERSION = "1.0.0"; + + // ************************************************************* + // VARIABLES + // Keep names and ordering! + // Add only in the bottom. + // ************************************************************* + + /// @dev The ve token used for gauges + address public ve; + /// @dev staking token => ve owner => veId + mapping(address => mapping(address => uint)) public override veIds; + /// @dev Staking token => whitelist status + mapping(address => bool) public stakingTokens; + + // ************************************************************* + // EVENTS + // ************************************************************* + + event AddStakingToken(address token); + event Deposit(address indexed stakingToken, address indexed account, uint amount); + event Withdraw(address indexed stakingToken, address indexed account, uint amount, bool full, uint veId); + event VeTokenLocked(address indexed stakingToken, address indexed account, uint tokenId); + event VeTokenUnlocked(address indexed stakingToken, address indexed account, uint tokenId); + + // ************************************************************* + // INIT + // ************************************************************* + + function init(address controller_, address _ve, address _defaultRewardToken) external initializer { + __MultiPool_init(controller_, _defaultRewardToken, 7 days); + ve = _ve; + } + + // ************************************************************* + // OPERATOR ACTIONS + // ************************************************************* + + /// @dev Allowed contracts can whitelist token. Removing is forbidden. + function addStakingToken(address token) external override onlyAllowedContracts { + stakingTokens[token] = true; + emit AddStakingToken(token); + } + + // ************************************************************* + // CLAIMS + // ************************************************************* + + function getReward(address stakingToken, address account, address[] memory tokens) external override { + _getReward(stakingToken, account, tokens); + } + + function getAllRewards(address stakingToken, address account) external override { + _getAllRewards(stakingToken, account); + } + + function _getAllRewards(address stakingToken, address account) internal { + address[] storage rts = rewardTokens[stakingToken]; + uint length = rts.length; + address[] memory tokens = new address[](length + 1); + for (uint i; i < length; ++i) { + tokens[i] = rts[i]; + } + tokens[length] = defaultRewardToken; + _getReward(stakingToken, account, tokens); + } + + function getAllRewardsForTokens(address[] memory _stakingTokens, address account) external override { + for (uint i; i < _stakingTokens.length; i++) { + _getAllRewards(_stakingTokens[i], account); + } + } + + function _getReward(address stakingToken, address account, address[] memory tokens) internal { + // voter().distribute(stakingToken); + _getReward(stakingToken, account, tokens, account); + } + + // ************************************************************* + // VIRTUAL DEPOSIT/WITHDRAW + // ************************************************************* + + function attachVe(address stakingToken, address account, uint veId) external override { + require(IERC721(ve).ownerOf(veId) == account && account == msg.sender, "Not ve token owner"); + require(isStakeToken(stakingToken), "Wrong staking token"); + + if (veIds[stakingToken][account] == 0) { + veIds[stakingToken][account] = veId; + // voter().attachTokenToGauge(stakingToken, veId, account); + } + require(veIds[stakingToken][account] == veId, "Wrong ve"); - // ************************************************************* - // CONSTANTS - // ************************************************************* - - /// @dev Version of this contract. Adjust manually on each code modification. - string public constant MULTI_GAUGE_VERSION = "1.0.0"; - - // ************************************************************* - // VARIABLES - // Keep names and ordering! - // Add only in the bottom. - // ************************************************************* - - /// @dev The ve token used for gauges - address public ve; - /// @dev staking token => ve owner => veId - mapping(address => mapping(address => uint)) public override veIds; - /// @dev Staking token => whitelist status - mapping(address => bool) public stakingTokens; - - // ************************************************************* - // EVENTS - // ************************************************************* - - event AddStakingToken(address token); - event Deposit(address indexed stakingToken, address indexed account, uint amount); - event Withdraw(address indexed stakingToken, address indexed account, uint amount, bool full, uint veId); - event VeTokenLocked(address indexed stakingToken, address indexed account, uint tokenId); - event VeTokenUnlocked(address indexed stakingToken, address indexed account, uint tokenId); - - // ************************************************************* - // INIT - // ************************************************************* - - function init( - address controller_, - address _ve, - address _defaultRewardToken - ) external initializer { - __MultiPool_init(controller_, _defaultRewardToken, 7 days); - ve = _ve; - } - - // ************************************************************* - // OPERATOR ACTIONS - // ************************************************************* - - /// @dev Allowed contracts can whitelist token. Removing is forbidden. - function addStakingToken(address token) external override onlyAllowedContracts { - stakingTokens[token] = true; - emit AddStakingToken(token); - } - - // ************************************************************* - // CLAIMS - // ************************************************************* - - function getReward( - address stakingToken, - address account, - address[] memory tokens - ) external override { - _getReward(stakingToken, account, tokens); - } - - function getAllRewards( - address stakingToken, - address account - ) external override { - _getAllRewards(stakingToken, account); - } - - function _getAllRewards( - address stakingToken, - address account - ) internal { - address[] storage rts = rewardTokens[stakingToken]; - uint length = rts.length; - address[] memory tokens = new address[](length + 1); - for (uint i; i < length; ++i) { - tokens[i] = rts[i]; - } - tokens[length] = defaultRewardToken; - _getReward(stakingToken, account, tokens); - } - - function getAllRewardsForTokens( - address[] memory _stakingTokens, - address account - ) external override { - for (uint i; i < _stakingTokens.length; i++) { - _getAllRewards(_stakingTokens[i], account); - } - } - - function _getReward(address stakingToken, address account, address[] memory tokens) internal { -// voter().distribute(stakingToken); - _getReward(stakingToken, account, tokens, account); - } - - // ************************************************************* - // VIRTUAL DEPOSIT/WITHDRAW - // ************************************************************* - - function attachVe(address stakingToken, address account, uint veId) external override { - require(IERC721(ve).ownerOf(veId) == account && account == msg.sender, "Not ve token owner"); - require(isStakeToken(stakingToken), "Wrong staking token"); - - if (veIds[stakingToken][account] == 0) { - veIds[stakingToken][account] = veId; -// voter().attachTokenToGauge(stakingToken, veId, account); - } - require(veIds[stakingToken][account] == veId, "Wrong ve"); - - _updateDerivedBalance(stakingToken, account); - _updateRewardForAllTokens(stakingToken, account); - emit VeTokenLocked(stakingToken, account, veId); - } - - function detachVe(address stakingToken, address account, uint veId) external override { - require((IERC721(ve).ownerOf(veId) == account && msg.sender == account) - || msg.sender == address(IController(controller()).ifo()), "Not ve token owner or voter"); - require(isStakeToken(stakingToken), "Wrong staking token"); - - _unlockVeToken(stakingToken, account, veId); - _updateDerivedBalance(stakingToken, account); - _updateRewardForAllTokens(stakingToken, account); - } - - /// @dev Must be called from stakingToken when user balance changed. - function handleBalanceChange(address account) external override { - address stakingToken = msg.sender; - require(isStakeToken(stakingToken), "Wrong staking token"); - - uint stakedBalance = balanceOf[stakingToken][account]; - uint actualBalance = IERC20(stakingToken).balanceOf(account); - if (stakedBalance < actualBalance) { - _deposit(stakingToken, account, actualBalance - stakedBalance); - } else if (stakedBalance > actualBalance) { - _withdraw(stakingToken, account, stakedBalance - actualBalance, actualBalance == 0); - } - } - - function _deposit( - address stakingToken, - address account, - uint amount - ) internal { - _registerBalanceIncreasing(stakingToken, account, amount); - emit Deposit(stakingToken, account, amount); - } - - function _withdraw( - address stakingToken, - address account, - uint amount, - bool fullWithdraw - ) internal { - uint veId = 0; - if (fullWithdraw) { - veId = veIds[stakingToken][account]; - } - if (veId > 0) { - _unlockVeToken(stakingToken, account, veId); - } - _registerBalanceDecreasing(stakingToken, account, amount); - emit Withdraw( - stakingToken, - account, - amount, - fullWithdraw, - veId - ); - } - - /// @dev Balance should be recalculated after the unlock - function _unlockVeToken(address stakingToken, address account, uint veId) internal { - require(veId == veIds[stakingToken][account], "Wrong ve"); - veIds[stakingToken][account] = 0; -// voter().detachTokenFromGauge(stakingToken, veId, account); - emit VeTokenUnlocked(stakingToken, account, veId); - } - - // ************************************************************* - // LOGIC OVERRIDES - // ************************************************************* - - /// @dev Similar to Curve https://resources.curve.fi/reward-gauges/boosting-your-crv-rewards#formula - function derivedBalance( - address stakingToken, - address account - ) public override view returns (uint) { - uint _tokenId = veIds[stakingToken][account]; - uint _balance = balanceOf[stakingToken][account]; - uint _derived = _balance * 40 / 100; - uint _adjusted = 0; - uint _supply = IERC20(ve).totalSupply(); - if (account == IERC721(ve).ownerOf(_tokenId) && _supply > 0) { - _adjusted = (totalSupply[stakingToken] * IVe(ve).balanceOfNFT(_tokenId) / _supply) * 60 / 100; - } - return Math.min((_derived + _adjusted), _balance); - } - - function isStakeToken(address token) public view override returns (bool) { - return stakingTokens[token]; - } - - // ************************************************************* - // REWARDS DISTRIBUTION - // ************************************************************* - - function notifyRewardAmount(address stakingToken, address token, uint amount) external nonReentrant override { - _notifyRewardAmount(stakingToken, token, amount, true); - } - - // ************************************************************* - // VIEWS - // ************************************************************* + _updateDerivedBalance(stakingToken, account); + _updateRewardForAllTokens(stakingToken, account); + emit VeTokenLocked(stakingToken, account, veId); + } + + function detachVe(address stakingToken, address account, uint veId) external override { + require( + (IERC721(ve).ownerOf(veId) == account && msg.sender == account) + || msg.sender == address(IController(controller()).ifo()), + "Not ve token owner or voter" + ); + require(isStakeToken(stakingToken), "Wrong staking token"); + + _unlockVeToken(stakingToken, account, veId); + _updateDerivedBalance(stakingToken, account); + _updateRewardForAllTokens(stakingToken, account); + } + + /// @dev Must be called from stakingToken when user balance changed. + function handleBalanceChange(address account) external override { + address stakingToken = msg.sender; + require(isStakeToken(stakingToken), "Wrong staking token"); + + uint stakedBalance = balanceOf[stakingToken][account]; + uint actualBalance = IERC20(stakingToken).balanceOf(account); + if (stakedBalance < actualBalance) { + _deposit(stakingToken, account, actualBalance - stakedBalance); + } else if (stakedBalance > actualBalance) { + _withdraw(stakingToken, account, stakedBalance - actualBalance, actualBalance == 0); + } + } + + function _deposit(address stakingToken, address account, uint amount) internal { + _registerBalanceIncreasing(stakingToken, account, amount); + emit Deposit(stakingToken, account, amount); + } + + function _withdraw(address stakingToken, address account, uint amount, bool fullWithdraw) internal { + uint veId = 0; + if (fullWithdraw) { + veId = veIds[stakingToken][account]; + } + if (veId > 0) { + _unlockVeToken(stakingToken, account, veId); + } + _registerBalanceDecreasing(stakingToken, account, amount); + emit Withdraw(stakingToken, account, amount, fullWithdraw, veId); + } + + /// @dev Balance should be recalculated after the unlock + function _unlockVeToken(address stakingToken, address account, uint veId) internal { + require(veId == veIds[stakingToken][account], "Wrong ve"); + veIds[stakingToken][account] = 0; + // voter().detachTokenFromGauge(stakingToken, veId, account); + emit VeTokenUnlocked(stakingToken, account, veId); + } + + // ************************************************************* + // LOGIC OVERRIDES + // ************************************************************* + + /// @dev Similar to Curve https://resources.curve.fi/reward-gauges/boosting-your-crv-rewards#formula + function derivedBalance(address stakingToken, address account) public view override returns (uint) { + uint _tokenId = veIds[stakingToken][account]; + uint _balance = balanceOf[stakingToken][account]; + uint _derived = _balance * 40 / 100; + uint _adjusted = 0; + uint _supply = IERC20(ve).totalSupply(); + if (account == IERC721(ve).ownerOf(_tokenId) && _supply > 0) { + _adjusted = (totalSupply[stakingToken] * IVe(ve).balanceOfNFT(_tokenId) / _supply) * 60 / 100; + } + return Math.min((_derived + _adjusted), _balance); + } + + function isStakeToken(address token) public view override returns (bool) { + return stakingTokens[token]; + } + + // ************************************************************* + // REWARDS DISTRIBUTION + // ************************************************************* + + function notifyRewardAmount(address stakingToken, address token, uint amount) external override nonReentrant { + _notifyRewardAmount(stakingToken, token, amount, true); + } + // ************************************************************* + // VIEWS + // ************************************************************* } diff --git a/src/PearlStrategy.sol b/src/PearlStrategy.sol index d6b3572..834d53c 100644 --- a/src/PearlStrategy.sol +++ b/src/PearlStrategy.sol @@ -3,6 +3,10 @@ pragma solidity ^0.8.21; import "./base/StrategyStrictBase.sol"; import "./interfaces/IVault.sol"; +import "./interfaces/IPearlGaugeV2.sol"; +import "./interfaces/IController.sol"; +import "./interfaces/IIFO.sol"; +import "./interfaces/IGauge.sol"; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.°:°•.°+.*•´.*:*.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*/ /* Pearl strategy */ @@ -17,43 +21,75 @@ import "./interfaces/IVault.sol"; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.°:°•.°+.*•´.*:*.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*/ contract PearlStrategy is StrategyStrictBase { + using SafeERC20 for IERC20; uint public lastHardWork; - address public pool; - constructor(address vault_, address pool_) StrategyStrictBase(vault_) { + address public gauge; + + bool public harvester; + + constructor(address vault_, address gauge_, bool harvester_) StrategyStrictBase(vault_) { vault = vault_; asset = IVault(vault_).asset(); - pool = pool_; + gauge = gauge_; + harvester = harvester_; + IERC20(asset).approve(gauge_, type(uint).max); } - function isReadyToHardWork() external pure returns (bool) { - // todo - return true; + function isReadyToHardWork() external view returns (bool) { + return IPearlGaugeV2(gauge).earned(address(this)) > 0; } - function doHardWork() external returns (uint earned, uint lost) {} - + function doHardWork() external /* returns (uint earned, uint lost)*/ { + // claim fees if available + // liquidate fee if available + + uint rtReward = _claim(); + + IController controller = IController(IVault(vault).controller()); + + if (harvester) { + IIFO ifo = IIFO(controller.ifo()); + IGauge multigauge = IGauge(controller.multigauge()); + (bool exchanged, uint got) = ifo.exchange(rtReward); + if (exchanged) { + multigauge.notifyRewardAmount(vault, controller.stgn(), got); + } else { + multigauge.notifyRewardAmount(vault, IPearlGaugeV2(gauge).rewardToken(), rtReward); + } + } else { + // todo Compounder CVR + } + } function investedAssets() public view override returns (uint) { - + return IPearlGaugeV2(gauge).balanceOf(address(this)); } - function _claim() internal override { - + function _claim() internal override returns (uint rtReward) { + IPearlGaugeV2 _gauge = IPearlGaugeV2(gauge); + IERC20 rt = IERC20(_gauge.rewardToken()); + uint oldBal = rt.balanceOf(address(this)); + IPearlGaugeV2(gauge).getReward(); + rtReward = rt.balanceOf(address(this)) - oldBal; } function _depositToPool(uint amount) internal override { - + IPearlGaugeV2(gauge).deposit(amount); } - function _emergencyExitFromPool() internal override {} - - function _withdrawFromPool(uint amount) internal override returns (uint investedAssetsUSD, uint assetPrice) { - + function _emergencyExitFromPool() internal override { + _withdrawAllFromPool(); + IERC20(asset).safeTransfer(vault, IERC20(asset).balanceOf(address(this))); } - function _withdrawAllFromPool() internal override returns (uint investedAssetsUSD, uint assetPrice) { + function _withdrawFromPool(uint amount) internal override { + IPearlGaugeV2(gauge).withdraw(amount); + IERC20(asset).safeTransfer(vault, amount); + } + function _withdrawAllFromPool() internal override { + _withdrawFromPool(IPearlGaugeV2(gauge).balanceOf(address(this))); } } diff --git a/src/PerfFeeTreasury.sol b/src/PerfFeeTreasury.sol index 1a335ea..35c2300 100644 --- a/src/PerfFeeTreasury.sol +++ b/src/PerfFeeTreasury.sol @@ -4,35 +4,34 @@ pragma solidity ^0.8.21; import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; contract PerfFeeTreasury { - using SafeERC20 for IERC20; - - address public governance; - address public pendingGovernance; - - constructor(address _governance) { - governance = _governance; - } - - modifier onlyGovernance() { - require(msg.sender == governance, "NOT_GOV"); - _; - } - - function offerOwnership(address newOwner) external onlyGovernance { - require(newOwner != address(0), "ZERO_ADDRESS"); - pendingGovernance = newOwner; - } - - function acceptOwnership() external { - require(msg.sender == pendingGovernance, "NOT_GOV"); - governance = pendingGovernance; - } - - function claim(address[] memory tokens) external onlyGovernance { - address _governance = governance; - for (uint i = 0; i < tokens.length; ++i) { - IERC20(tokens[i]).safeTransfer(_governance, IERC20(tokens[i]).balanceOf(address(this))); + using SafeERC20 for IERC20; + + address public governance; + address public pendingGovernance; + + constructor(address _governance) { + governance = _governance; } - } + modifier onlyGovernance() { + require(msg.sender == governance, "NOT_GOV"); + _; + } + + function offerOwnership(address newOwner) external onlyGovernance { + require(newOwner != address(0), "ZERO_ADDRESS"); + pendingGovernance = newOwner; + } + + function acceptOwnership() external { + require(msg.sender == pendingGovernance, "NOT_GOV"); + governance = pendingGovernance; + } + + function claim(address[] memory tokens) external onlyGovernance { + address _governance = governance; + for (uint i = 0; i < tokens.length; ++i) { + IERC20(tokens[i]).safeTransfer(_governance, IERC20(tokens[i]).balanceOf(address(this))); + } + } } diff --git a/src/Vault.sol b/src/Vault.sol index 61b9f5f..9dca859 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -11,206 +11,207 @@ import "./interfaces/IVault.sol"; /// @title ERC4626 tokenized vault implementation for Sturgeon strategies /// @author a17 contract Vault is ERC4626, ReentrancyGuard, IVault { - using SafeERC20 for IERC20; - using Math for uint; - - // ************************************************************* - // CONSTANTS - // ************************************************************* - - /// @dev Denominator for buffer calculation. 100% of the buffer amount. - uint constant public BUFFER_DENOMINATOR = 100_000; - - // ************************************************************* - // VARIABLES - // ************************************************************* - - /// @dev Connected strategy. Can not be changed. - IStrategyStrict public strategy; - /// @dev Percent of assets that will always stay in this vault. - uint public immutable buffer; - - // ************************************************************* - // EVENTS - // ************************************************************* - - event Invest(address splitter, uint amount); - - // ************************************************************* - // INIT - // ************************************************************* - - constructor( - IERC20 asset_, - string memory name_, - string memory symbol_, - uint buffer_ - ) - ERC20(name_, symbol_) - ERC4626(asset_) - { - // buffer is 5% max - require(buffer_ <= BUFFER_DENOMINATOR / 20, "!BUFFER"); - buffer = buffer_; - } - - function setStrategy(address strategy_) external { - require (address(strategy) == address(0), "Already"); - strategy = IStrategyStrict(strategy_); - } - - // ************************************************************* - // VIEWS - // ************************************************************* - - /// @dev Total amount of the underlying asset that is “managed” by Vault - function totalAssets() public view override (ERC4626, IERC4626) returns (uint) { - return IERC20(asset()).balanceOf(address(this)) + strategy.totalAssets(); - } - - /// @dev Amount of assets under control of strategy. - function strategyAssets() external view returns (uint) { - return strategy.totalAssets(); - } - - /// @dev Price of 1 full share - function sharePrice() external view returns (uint) { - uint units = 10 ** uint256(decimals()); - uint totalSupply_ = totalSupply(); - return totalSupply_ == 0 - ? units - : units * totalAssets() / totalSupply_; - } - - // ************************************************************* - // DEPOSIT LOGIC - // ************************************************************* - - function deposit(uint assets, address receiver) public override (ERC4626, IERC4626) nonReentrant returns (uint) { - uint shares = super.deposit(assets, receiver); - _afterDeposit(assets, shares); - return shares; - } - - function mint(uint shares, address receiver) public override (ERC4626, IERC4626) nonReentrant returns (uint) { - uint assets = super.mint(shares, receiver); - _afterDeposit(assets, shares); - return assets; - } - - /// @dev Calculate available to invest amount and send this amount to strategy - function _afterDeposit(uint /*assets*/, uint /*shares*/) internal { - IStrategyStrict _strategy = strategy; - IERC20 asset_ = IERC20(asset()); - - uint256 toInvest = _availableToInvest(_strategy, asset_); - // invest only when buffer is filled - if (toInvest > 0) { - asset_.safeTransfer(address(_strategy), toInvest); - _strategy.investAll(); - emit Invest(address(_strategy), toInvest); + using SafeERC20 for IERC20; + using Math for uint; + + // ************************************************************* + // CONSTANTS + // ************************************************************* + + /// @dev Denominator for buffer calculation. 100% of the buffer amount. + uint public constant BUFFER_DENOMINATOR = 100_000; + + // ************************************************************* + // VARIABLES + // ************************************************************* + + /// @dev Connected strategy. Can not be changed. + address public controller; + IStrategyStrict public strategy; + /// @dev Percent of assets that will always stay in this vault. + uint public immutable buffer; + + // ************************************************************* + // EVENTS + // ************************************************************* + + event Invest(address splitter, uint amount); + + // ************************************************************* + // INIT + // ************************************************************* + + constructor( + address controller_, + IERC20 asset_, + string memory name_, + string memory symbol_, + uint buffer_ + ) ERC20(name_, symbol_) ERC4626(asset_) { + // buffer is 5% max + require(buffer_ <= BUFFER_DENOMINATOR / 20, "!BUFFER"); + buffer = buffer_; + controller = controller_; } - } - - /// @notice Returns amount of assets ready to invest to the strategy - function _availableToInvest(IStrategyStrict _strategy, IERC20 asset_) internal view returns (uint) { - uint _buffer = buffer; - uint assetsInVault = asset_.balanceOf(address(this)); - uint assetsInStrategy = _strategy.totalAssets(); - uint wantInvestTotal = (assetsInVault + assetsInStrategy) - * (BUFFER_DENOMINATOR - _buffer) / BUFFER_DENOMINATOR; - if (assetsInStrategy >= wantInvestTotal) { - return 0; - } else { - uint remainingToInvest = wantInvestTotal - assetsInStrategy; - return remainingToInvest <= assetsInVault ? remainingToInvest : assetsInVault; + + function setStrategy(address strategy_) external { + require(address(strategy) == address(0), "Already"); + strategy = IStrategyStrict(strategy_); } - } + // ************************************************************* + // VIEWS + // ************************************************************* + + /// @dev Total amount of the underlying asset that is “managed” by Vault + function totalAssets() public view override(ERC4626, IERC4626) returns (uint) { + return IERC20(asset()).balanceOf(address(this)) + strategy.totalAssets(); + } - // ************************************************************* - // WITHDRAW LOGIC - // ************************************************************* + /// @dev Amount of assets under control of strategy. + function strategyAssets() external view returns (uint) { + return strategy.totalAssets(); + } - /** @dev See {IERC4626-withdraw}. */ - function withdraw(uint assets, address receiver, address owner) public override (ERC4626, IERC4626) nonReentrant returns (uint) { - uint256 maxAssets = maxWithdraw(owner); - if (assets > maxAssets) { - revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); + /// @dev Price of 1 full share + function sharePrice() external view returns (uint) { + uint units = 10 ** uint(decimals()); + uint totalSupply_ = totalSupply(); + return totalSupply_ == 0 ? units : units * totalAssets() / totalSupply_; } - uint256 shares = previewWithdraw(assets); + // ************************************************************* + // DEPOSIT LOGIC + // ************************************************************* - _beforeWithdraw(assets, shares); + function deposit(uint assets, address receiver) public override(ERC4626, IERC4626) nonReentrant returns (uint) { + uint shares = super.deposit(assets, receiver); + _afterDeposit(assets, shares); + return shares; + } - _withdraw(_msgSender(), receiver, owner, assets, shares); + function mint(uint shares, address receiver) public override(ERC4626, IERC4626) nonReentrant returns (uint) { + uint assets = super.mint(shares, receiver); + _afterDeposit(assets, shares); + return assets; + } - return shares; - } + /// @dev Calculate available to invest amount and send this amount to strategy + function _afterDeposit(uint, /*assets*/ uint /*shares*/ ) internal { + IStrategyStrict _strategy = strategy; + IERC20 asset_ = IERC20(asset()); + + uint toInvest = _availableToInvest(_strategy, asset_); + // invest only when buffer is filled + if (toInvest > 0) { + asset_.safeTransfer(address(_strategy), toInvest); + _strategy.investAll(); + emit Invest(address(_strategy), toInvest); + } + } - /** @dev See {IERC4626-redeem}. */ - function redeem(uint shares, address receiver, address owner) public override (ERC4626, IERC4626) nonReentrant returns (uint) { - uint256 maxShares = maxRedeem(owner); - if (shares > maxShares) { - revert ERC4626ExceededMaxRedeem(owner, shares, maxShares); + /// @notice Returns amount of assets ready to invest to the strategy + function _availableToInvest(IStrategyStrict _strategy, IERC20 asset_) internal view returns (uint) { + uint _buffer = buffer; + uint assetsInVault = asset_.balanceOf(address(this)); + uint assetsInStrategy = _strategy.totalAssets(); + uint wantInvestTotal = (assetsInVault + assetsInStrategy) * (BUFFER_DENOMINATOR - _buffer) / BUFFER_DENOMINATOR; + if (assetsInStrategy >= wantInvestTotal) { + return 0; + } else { + uint remainingToInvest = wantInvestTotal - assetsInStrategy; + return remainingToInvest <= assetsInVault ? remainingToInvest : assetsInVault; + } } - uint256 assets = previewRedeem(shares); - - require(assets != 0, "ZERO_ASSETS"); - _beforeWithdraw(assets, shares); - - _withdraw(_msgSender(), receiver, owner, assets, shares); - - return assets; - } - - /// @dev Withdraw all available shares for tx sender. - /// The revert is expected if the balance is higher than `maxRedeem` - /// It suppose to be used only on UI - for on-chain interactions withdraw concrete amount with properly checks. - function withdrawAll() external { - redeem(balanceOf(msg.sender), msg.sender, msg.sender); - } - - /// @dev Internal hook for getting necessary assets from strategy. - function _beforeWithdraw(uint assets, uint shares) internal { - uint balance = IERC20(asset()).balanceOf(address(this)); - // if not enough balance in the vault withdraw from strategies - if (balance < assets) { - _processWithdrawFromStrategy( - assets, - shares, - totalSupply(), - buffer, - strategy, - balance - ); + // ************************************************************* + // WITHDRAW LOGIC + // ************************************************************* + + /** + * @dev See {IERC4626-withdraw}. + */ + function withdraw( + uint assets, + address receiver, + address owner + ) public override(ERC4626, IERC4626) nonReentrant returns (uint) { + uint maxAssets = maxWithdraw(owner); + if (assets > maxAssets) { + revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); + } + + uint shares = previewWithdraw(assets); + + _beforeWithdraw(assets, shares); + + _withdraw(_msgSender(), receiver, owner, assets, shares); + + return shares; } - } - - /// @dev Do necessary calculation for withdrawing from strategy and move assets to vault. - function _processWithdrawFromStrategy( - uint assetsNeed, - uint shares, - uint totalSupply_, - uint _buffer, - IStrategyStrict _strategy, - uint assetsInVault - ) internal { - // withdraw everything from the strategy to accurately check the share value - if (shares == totalSupply_) { - _strategy.withdrawAllToVault(); - } else { - uint assetsInStrategy = _strategy.totalAssets(); - - // we should always have buffer amount inside the vault - // assume `assetsNeed` can not be higher than entire balance - uint expectedBuffer = (assetsInStrategy + assetsInVault - assetsNeed) * _buffer / BUFFER_DENOMINATOR; - - // this code should not be called if `assetsInVault` higher than `assetsNeed` - uint missing = Math.min(expectedBuffer + assetsNeed - assetsInVault, assetsInStrategy); - // if zero should be resolved on strategy side - _strategy.withdrawToVault(missing); + + /** + * @dev See {IERC4626-redeem}. + */ + function redeem( + uint shares, + address receiver, + address owner + ) public override(ERC4626, IERC4626) nonReentrant returns (uint) { + uint maxShares = maxRedeem(owner); + if (shares > maxShares) { + revert ERC4626ExceededMaxRedeem(owner, shares, maxShares); + } + + uint assets = previewRedeem(shares); + + require(assets != 0, "ZERO_ASSETS"); + _beforeWithdraw(assets, shares); + + _withdraw(_msgSender(), receiver, owner, assets, shares); + + return assets; + } + + /// @dev Withdraw all available shares for tx sender. + /// The revert is expected if the balance is higher than `maxRedeem` + /// It suppose to be used only on UI - for on-chain interactions withdraw concrete amount with properly checks. + function withdrawAll() external { + redeem(balanceOf(msg.sender), msg.sender, msg.sender); + } + + /// @dev Internal hook for getting necessary assets from strategy. + function _beforeWithdraw(uint assets, uint shares) internal { + uint balance = IERC20(asset()).balanceOf(address(this)); + // if not enough balance in the vault withdraw from strategies + if (balance < assets) { + _processWithdrawFromStrategy(assets, shares, totalSupply(), buffer, strategy, balance); + } + } + + /// @dev Do necessary calculation for withdrawing from strategy and move assets to vault. + function _processWithdrawFromStrategy( + uint assetsNeed, + uint shares, + uint totalSupply_, + uint _buffer, + IStrategyStrict _strategy, + uint assetsInVault + ) internal { + // withdraw everything from the strategy to accurately check the share value + if (shares == totalSupply_) { + _strategy.withdrawAllToVault(); + } else { + uint assetsInStrategy = _strategy.totalAssets(); + + // we should always have buffer amount inside the vault + // assume `assetsNeed` can not be higher than entire balance + uint expectedBuffer = (assetsInStrategy + assetsInVault - assetsNeed) * _buffer / BUFFER_DENOMINATOR; + + // this code should not be called if `assetsInVault` higher than `assetsNeed` + uint missing = Math.min(expectedBuffer + assetsNeed - assetsInVault, assetsInStrategy); + // if zero should be resolved on strategy side + _strategy.withdrawToVault(missing); + } } - } } diff --git a/src/VeDistributor.sol b/src/VeDistributor.sol index ce90833..c78e353 100644 --- a/src/VeDistributor.sol +++ b/src/VeDistributor.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.21; - import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import "openzeppelin-contracts/contracts/utils/math/Math.sol"; import "./interfaces/IVe.sol"; @@ -13,398 +12,367 @@ import "./base/Controllable.sol"; /// Based on Solidly contract. /// @author belbix contract VeDistributor is Controllable, IVeDistributor { - using SafeERC20 for IERC20; - - // for contract internal purposes, don't need to store in the interface - struct ClaimCalculationResult { - uint toDistribute; - uint userEpoch; - uint weekCursor; - uint maxUserEpoch; - bool success; - } - - // ************************************************************* - // CONSTANTS - // ************************************************************* - - /// @dev Version of this contract. Adjust manually on each code modification. - string public constant VE_DIST_VERSION = "1.0.1"; - uint internal constant WEEK = 7 * 86400; - - // ************************************************************* - // VARIABLES - // Keep names and ordering! - // Add only in the bottom. - // ************************************************************* - - /// @dev Voting escrow token address - IVe public ve; - /// @dev Token for ve rewards - address public override rewardToken; - - // --- CHECKPOINT - - /// @dev Cursor for the current epoch - uint public activePeriod; - /// @dev Tokens per week stored on checkpoint call. Predefined array size = max weeks size - uint[1000000000000000] public tokensPerWeek; - /// @dev Ve supply checkpoints. Predefined array size = max weeks size - uint[1000000000000000] public veSupply; - /// @dev Ve supply checkpoint time cursor - uint public timeCursor; - /// @dev Token balance updated on checkpoint/claim - uint public tokenLastBalance; - /// @dev Last checkpoint time - uint public lastTokenTime; - - // --- CLAIM - - /// @dev Timestamp when this contract was inited - uint public startTime; - /// @dev veID => week cursor stored on the claim action - mapping(uint => uint) public timeCursorOf; - /// @dev veID => epoch stored on the claim action - mapping(uint => uint) public userEpochOf; - - // ************************************************************* - // EVENTS - // ************************************************************* - - event CheckpointToken( - uint time, - uint tokens - ); - - event Claimed( - uint tokenId, - uint amount, - uint claimEpoch, - uint maxEpoch - ); - - // ************************************************************* - // INIT - // ************************************************************* - - /// @dev Proxy initialization. Call it after contract deploy. - function init( - address controller_, - address _ve, - address _rewardToken - ) external initializer { - __Controllable_init(controller_); - - uint _t = block.timestamp / WEEK * WEEK; - startTime = _t; - lastTokenTime = _t; - timeCursor = _t; - - rewardToken = _rewardToken; - ve = IVe(_ve); - - IERC20(_rewardToken).approve(_ve, type(uint).max); - } - - // ************************************************************* - // CHECKPOINT - // ************************************************************* - - function checkpoint() external override { - uint _period = activePeriod; - // only trigger if new week - if (block.timestamp >= _period + 1 weeks) { - // set new period rounded to weeks - activePeriod = block.timestamp / 1 weeks * 1 weeks; - // checkpoint token balance that was just minted in veDist - _checkpointToken(); - // checkpoint supply - _checkpointTotalSupply(); + using SafeERC20 for IERC20; + + // for contract internal purposes, don't need to store in the interface + struct ClaimCalculationResult { + uint toDistribute; + uint userEpoch; + uint weekCursor; + uint maxUserEpoch; + bool success; } - } - - /// @dev Update tokensPerWeek value - function _checkpointToken() internal { - uint tokenBalance = IERC20(rewardToken).balanceOf(address(this)); - uint toDistribute = tokenBalance - tokenLastBalance; - tokenLastBalance = tokenBalance; - - uint t = lastTokenTime; - uint sinceLast = block.timestamp - t; - lastTokenTime = block.timestamp; - uint thisWeek = t / WEEK * WEEK; - uint nextWeek = 0; - - // checkpoint should be called at least once per 20 weeks - for (uint i = 0; i < 20; i++) { - nextWeek = thisWeek + WEEK; - if (block.timestamp < nextWeek) { - tokensPerWeek[thisWeek] += adjustToDistribute(toDistribute, block.timestamp, t, sinceLast); - break; - } else { - tokensPerWeek[thisWeek] += adjustToDistribute(toDistribute, nextWeek, t, sinceLast); - } - t = nextWeek; - thisWeek = nextWeek; + + // ************************************************************* + // CONSTANTS + // ************************************************************* + + /// @dev Version of this contract. Adjust manually on each code modification. + string public constant VE_DIST_VERSION = "1.0.1"; + uint internal constant WEEK = 7 * 86400; + + // ************************************************************* + // VARIABLES + // Keep names and ordering! + // Add only in the bottom. + // ************************************************************* + + /// @dev Voting escrow token address + IVe public ve; + /// @dev Token for ve rewards + address public override rewardToken; + + // --- CHECKPOINT + + /// @dev Cursor for the current epoch + uint public activePeriod; + /// @dev Tokens per week stored on checkpoint call. Predefined array size = max weeks size + uint[1000000000000000] public tokensPerWeek; + /// @dev Ve supply checkpoints. Predefined array size = max weeks size + uint[1000000000000000] public veSupply; + /// @dev Ve supply checkpoint time cursor + uint public timeCursor; + /// @dev Token balance updated on checkpoint/claim + uint public tokenLastBalance; + /// @dev Last checkpoint time + uint public lastTokenTime; + + // --- CLAIM + + /// @dev Timestamp when this contract was inited + uint public startTime; + /// @dev veID => week cursor stored on the claim action + mapping(uint => uint) public timeCursorOf; + /// @dev veID => epoch stored on the claim action + mapping(uint => uint) public userEpochOf; + + // ************************************************************* + // EVENTS + // ************************************************************* + + event CheckpointToken(uint time, uint tokens); + + event Claimed(uint tokenId, uint amount, uint claimEpoch, uint maxEpoch); + + // ************************************************************* + // INIT + // ************************************************************* + + /// @dev Proxy initialization. Call it after contract deploy. + function init(address controller_, address _ve, address _rewardToken) external initializer { + __Controllable_init(controller_); + + uint _t = block.timestamp / WEEK * WEEK; + startTime = _t; + lastTokenTime = _t; + timeCursor = _t; + + rewardToken = _rewardToken; + ve = IVe(_ve); + + IERC20(_rewardToken).approve(_ve, type(uint).max); } - emit CheckpointToken(block.timestamp, toDistribute); - } - - /// @dev Adjust value based on time since last update - function adjustToDistribute( - uint toDistribute, - uint t0, - uint t1, - uint sinceLast - ) public pure returns (uint) { - if (t0 <= t1 || t0 - t1 == 0 || sinceLast == 0) { - return toDistribute; + + // ************************************************************* + // CHECKPOINT + // ************************************************************* + + function checkpoint() external override { + uint _period = activePeriod; + // only trigger if new week + if (block.timestamp >= _period + 1 weeks) { + // set new period rounded to weeks + activePeriod = block.timestamp / 1 weeks * 1 weeks; + // checkpoint token balance that was just minted in veDist + _checkpointToken(); + // checkpoint supply + _checkpointTotalSupply(); + } } - return toDistribute * (t0 - t1) / sinceLast; - } - - /// @dev Search in the loop given timestamp through ve points history. - /// Return minimal possible epoch. - function findTimestampEpoch(IVe _ve, uint _timestamp) public view returns (uint) { - uint _min = 0; - uint _max = _ve.epoch(); - for (uint i = 0; i < 128; i++) { - if (_min >= _max) break; - uint _mid = (_min + _max + 2) / 2; - IVe.Point memory pt = _ve.pointHistory(_mid); - if (pt.ts <= _timestamp) { - _min = _mid; - } else { - _max = _mid - 1; - } + + /// @dev Update tokensPerWeek value + function _checkpointToken() internal { + uint tokenBalance = IERC20(rewardToken).balanceOf(address(this)); + uint toDistribute = tokenBalance - tokenLastBalance; + tokenLastBalance = tokenBalance; + + uint t = lastTokenTime; + uint sinceLast = block.timestamp - t; + lastTokenTime = block.timestamp; + uint thisWeek = t / WEEK * WEEK; + uint nextWeek = 0; + + // checkpoint should be called at least once per 20 weeks + for (uint i = 0; i < 20; i++) { + nextWeek = thisWeek + WEEK; + if (block.timestamp < nextWeek) { + tokensPerWeek[thisWeek] += adjustToDistribute(toDistribute, block.timestamp, t, sinceLast); + break; + } else { + tokensPerWeek[thisWeek] += adjustToDistribute(toDistribute, nextWeek, t, sinceLast); + } + t = nextWeek; + thisWeek = nextWeek; + } + emit CheckpointToken(block.timestamp, toDistribute); } - return _min; - } - - /// @dev Search in the loop given timestamp through ve user points history. - /// Return minimal possible epoch. - function findTimestampUserEpoch( - IVe _ve, - uint tokenId, - uint _timestamp, - uint maxUserEpoch - ) public view returns (uint) { - uint _min = 0; - uint _max = maxUserEpoch; - for (uint i = 0; i < 128; i++) { - if (_min >= _max) break; - uint _mid = (_min + _max + 2) / 2; - IVe.Point memory pt = _ve.userPointHistory(tokenId, _mid); - if (pt.ts <= _timestamp) { - _min = _mid; - } else { - _max = _mid - 1; - } + + /// @dev Adjust value based on time since last update + function adjustToDistribute(uint toDistribute, uint t0, uint t1, uint sinceLast) public pure returns (uint) { + if (t0 <= t1 || t0 - t1 == 0 || sinceLast == 0) { + return toDistribute; + } + return toDistribute * (t0 - t1) / sinceLast; } - return _min; - } - - /// @dev Return ve power at given timestamp - function veForAt(uint _tokenId, uint _timestamp) external view returns (uint) { - IVe _ve = ve; - uint maxUserEpoch = _ve.userPointEpoch(_tokenId); - uint epoch = findTimestampUserEpoch(_ve, _tokenId, _timestamp, maxUserEpoch); - IVe.Point memory pt = _ve.userPointHistory(_tokenId, epoch); - return uint(int256(_positiveInt128(pt.bias - pt.slope * (int128(int256(_timestamp - pt.ts)))))); - } - - /// @dev Call ve checkpoint and write veSupply at the current timeCursor - function checkpointTotalSupply() external override { - _checkpointTotalSupply(); - } - - function _checkpointTotalSupply() internal { - IVe _ve = ve; - uint t = timeCursor; - uint roundedTimestamp = block.timestamp / WEEK * WEEK; - _ve.checkpoint(); - - // assume will be called more frequently than 20 weeks - for (uint i = 0; i < 20; i++) { - if (t > roundedTimestamp) { - break; - } else { - uint epoch = findTimestampEpoch(_ve, t); - IVe.Point memory pt = _ve.pointHistory(epoch); - veSupply[t] = adjustVeSupply(t, pt.ts, pt.bias, pt.slope); - } - t += WEEK; + + /// @dev Search in the loop given timestamp through ve points history. + /// Return minimal possible epoch. + function findTimestampEpoch(IVe _ve, uint _timestamp) public view returns (uint) { + uint _min = 0; + uint _max = _ve.epoch(); + for (uint i = 0; i < 128; i++) { + if (_min >= _max) break; + uint _mid = (_min + _max + 2) / 2; + IVe.Point memory pt = _ve.pointHistory(_mid); + if (pt.ts <= _timestamp) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + return _min; } - timeCursor = t; - } - /// @dev Calculate ve supply based on bias and slop for the given timestamp - function adjustVeSupply(uint t, uint ptTs, int128 ptBias, int128 ptSlope) public pure returns (uint) { - if (t < ptTs) { - return 0; + /// @dev Search in the loop given timestamp through ve user points history. + /// Return minimal possible epoch. + function findTimestampUserEpoch( + IVe _ve, + uint tokenId, + uint _timestamp, + uint maxUserEpoch + ) public view returns (uint) { + uint _min = 0; + uint _max = maxUserEpoch; + for (uint i = 0; i < 128; i++) { + if (_min >= _max) break; + uint _mid = (_min + _max + 2) / 2; + IVe.Point memory pt = _ve.userPointHistory(tokenId, _mid); + if (pt.ts <= _timestamp) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + return _min; } - int128 dt = int128(int256(t - ptTs)); - if (ptBias < ptSlope * dt) { - return 0; + + /// @dev Return ve power at given timestamp + function veForAt(uint _tokenId, uint _timestamp) external view returns (uint) { + IVe _ve = ve; + uint maxUserEpoch = _ve.userPointEpoch(_tokenId); + uint epoch = findTimestampUserEpoch(_ve, _tokenId, _timestamp, maxUserEpoch); + IVe.Point memory pt = _ve.userPointHistory(_tokenId, epoch); + return uint(int(_positiveInt128(pt.bias - pt.slope * (int128(int(_timestamp - pt.ts)))))); } - return uint(int256(_positiveInt128(ptBias - ptSlope * dt))); - } - - // ************************************************************* - // CLAIM - // ************************************************************* - - /// @dev Return available to claim earned amount - function claimable(uint _tokenId) external view returns (uint) { - uint _lastTokenTime = lastTokenTime / WEEK * WEEK; - ClaimCalculationResult memory result = _calculateClaim(_tokenId, ve, _lastTokenTime); - return result.toDistribute; - } - - /// @dev Claim rewards for given veID - function claim(uint _tokenId) external override returns (uint) { - IVe _ve = ve; - if (block.timestamp >= timeCursor) _checkpointTotalSupply(); - uint _lastTokenTime = lastTokenTime; - _lastTokenTime = _lastTokenTime / WEEK * WEEK; - uint amount = _claim(_tokenId, _ve, _lastTokenTime); - if (amount != 0) { - address owner = _ve.ownerOf(_tokenId); - IERC20(rewardToken).safeTransfer(owner, amount); - tokenLastBalance -= amount; + + /// @dev Call ve checkpoint and write veSupply at the current timeCursor + function checkpointTotalSupply() external override { + _checkpointTotalSupply(); } - return amount; - } - - /// @dev Claim rewards for given veIDs - function claimMany(uint[] memory _tokenIds) external returns (bool) { - if (block.timestamp >= timeCursor) _checkpointTotalSupply(); - uint _lastTokenTime = lastTokenTime; - _lastTokenTime = _lastTokenTime / WEEK * WEEK; - IVe _votingEscrow = ve; - uint total = 0; - - for (uint i = 0; i < _tokenIds.length; i++) { - uint _tokenId = _tokenIds[i]; - if (_tokenId == 0) break; - uint amount = _claim(_tokenId, _votingEscrow, _lastTokenTime); - if (amount != 0) { - address owner = _votingEscrow.ownerOf(_tokenId); - IERC20(rewardToken).safeTransfer(owner, amount); - total += amount; - } + + function _checkpointTotalSupply() internal { + IVe _ve = ve; + uint t = timeCursor; + uint roundedTimestamp = block.timestamp / WEEK * WEEK; + _ve.checkpoint(); + + // assume will be called more frequently than 20 weeks + for (uint i = 0; i < 20; i++) { + if (t > roundedTimestamp) { + break; + } else { + uint epoch = findTimestampEpoch(_ve, t); + IVe.Point memory pt = _ve.pointHistory(epoch); + veSupply[t] = adjustVeSupply(t, pt.ts, pt.bias, pt.slope); + } + t += WEEK; + } + timeCursor = t; } - if (total != 0) { - tokenLastBalance -= total; + + /// @dev Calculate ve supply based on bias and slop for the given timestamp + function adjustVeSupply(uint t, uint ptTs, int128 ptBias, int128 ptSlope) public pure returns (uint) { + if (t < ptTs) { + return 0; + } + int128 dt = int128(int(t - ptTs)); + if (ptBias < ptSlope * dt) { + return 0; + } + return uint(int(_positiveInt128(ptBias - ptSlope * dt))); } - return true; - } + // ************************************************************* + // CLAIM + // ************************************************************* - function _claim(uint _tokenId, IVe _ve, uint _lastTokenTime) internal returns (uint) { - ClaimCalculationResult memory result = _calculateClaim(_tokenId, _ve, _lastTokenTime); - if (result.success) { - userEpochOf[_tokenId] = result.userEpoch; - timeCursorOf[_tokenId] = result.weekCursor; - emit Claimed(_tokenId, result.toDistribute, result.userEpoch, result.maxUserEpoch); + /// @dev Return available to claim earned amount + function claimable(uint _tokenId) external view returns (uint) { + uint _lastTokenTime = lastTokenTime / WEEK * WEEK; + ClaimCalculationResult memory result = _calculateClaim(_tokenId, ve, _lastTokenTime); + return result.toDistribute; } - return result.toDistribute; - } - - function _calculateClaim( - uint _tokenId, - IVe _ve, - uint _lastTokenTime - ) internal view returns (ClaimCalculationResult memory) { - uint userEpoch; - uint maxUserEpoch = _ve.userPointEpoch(_tokenId); - uint _startTime = startTime; - - if (maxUserEpoch == 0) { - return ClaimCalculationResult(0, 0, 0, 0, false); - } - - uint weekCursor = timeCursorOf[_tokenId]; - if (weekCursor == 0) { - userEpoch = findTimestampUserEpoch(_ve, _tokenId, _startTime, maxUserEpoch); - } else { - userEpoch = userEpochOf[_tokenId]; + /// @dev Claim rewards for given veID + function claim(uint _tokenId) external override returns (uint) { + IVe _ve = ve; + if (block.timestamp >= timeCursor) _checkpointTotalSupply(); + uint _lastTokenTime = lastTokenTime; + _lastTokenTime = _lastTokenTime / WEEK * WEEK; + uint amount = _claim(_tokenId, _ve, _lastTokenTime); + if (amount != 0) { + address owner = _ve.ownerOf(_tokenId); + IERC20(rewardToken).safeTransfer(owner, amount); + tokenLastBalance -= amount; + } + return amount; } - if (userEpoch == 0) userEpoch = 1; + /// @dev Claim rewards for given veIDs + function claimMany(uint[] memory _tokenIds) external returns (bool) { + if (block.timestamp >= timeCursor) _checkpointTotalSupply(); + uint _lastTokenTime = lastTokenTime; + _lastTokenTime = _lastTokenTime / WEEK * WEEK; + IVe _votingEscrow = ve; + uint total = 0; + + for (uint i = 0; i < _tokenIds.length; i++) { + uint _tokenId = _tokenIds[i]; + if (_tokenId == 0) break; + uint amount = _claim(_tokenId, _votingEscrow, _lastTokenTime); + if (amount != 0) { + address owner = _votingEscrow.ownerOf(_tokenId); + IERC20(rewardToken).safeTransfer(owner, amount); + total += amount; + } + } + if (total != 0) { + tokenLastBalance -= total; + } - IVe.Point memory userPoint = _ve.userPointHistory(_tokenId, userEpoch); - if (weekCursor == 0) { - weekCursor = (userPoint.ts + WEEK - 1) / WEEK * WEEK; + return true; } - if (weekCursor >= lastTokenTime) { - return ClaimCalculationResult(0, 0, 0, 0, false); - } - if (weekCursor < _startTime) { - weekCursor = _startTime; + + function _claim(uint _tokenId, IVe _ve, uint _lastTokenTime) internal returns (uint) { + ClaimCalculationResult memory result = _calculateClaim(_tokenId, _ve, _lastTokenTime); + if (result.success) { + userEpochOf[_tokenId] = result.userEpoch; + timeCursorOf[_tokenId] = result.weekCursor; + emit Claimed(_tokenId, result.toDistribute, result.userEpoch, result.maxUserEpoch); + } + return result.toDistribute; } - return calculateToDistribute( - _tokenId, - weekCursor, - _lastTokenTime, - userPoint, - userEpoch, - maxUserEpoch, - _ve - ); - } - - function calculateToDistribute( - uint _tokenId, - uint weekCursor, - uint _lastTokenTime, - IVe.Point memory userPoint, - uint userEpoch, - uint maxUserEpoch, - IVe _ve - ) public view returns (ClaimCalculationResult memory) { - IVe.Point memory oldUserPoint; - uint toDistribute; - for (uint i = 0; i < 50; i++) { - if (weekCursor >= _lastTokenTime) { - break; - } - if (weekCursor >= userPoint.ts && userEpoch <= maxUserEpoch) { - userEpoch += 1; - oldUserPoint = userPoint; - if (userEpoch > maxUserEpoch) { - userPoint = IVe.Point(0, 0, 0, 0); + function _calculateClaim( + uint _tokenId, + IVe _ve, + uint _lastTokenTime + ) internal view returns (ClaimCalculationResult memory) { + uint userEpoch; + uint maxUserEpoch = _ve.userPointEpoch(_tokenId); + uint _startTime = startTime; + + if (maxUserEpoch == 0) { + return ClaimCalculationResult(0, 0, 0, 0, false); + } + + uint weekCursor = timeCursorOf[_tokenId]; + + if (weekCursor == 0) { + userEpoch = findTimestampUserEpoch(_ve, _tokenId, _startTime, maxUserEpoch); } else { - userPoint = _ve.userPointHistory(_tokenId, userEpoch); + userEpoch = userEpochOf[_tokenId]; + } + + if (userEpoch == 0) userEpoch = 1; + + IVe.Point memory userPoint = _ve.userPointHistory(_tokenId, userEpoch); + if (weekCursor == 0) { + weekCursor = (userPoint.ts + WEEK - 1) / WEEK * WEEK; + } + if (weekCursor >= lastTokenTime) { + return ClaimCalculationResult(0, 0, 0, 0, false); } - } else { - int128 dt = int128(int256(weekCursor - oldUserPoint.ts)); - uint balanceOf = uint(int256(_positiveInt128(oldUserPoint.bias - dt * oldUserPoint.slope))); - if (balanceOf == 0 && userEpoch > maxUserEpoch) { - break; + if (weekCursor < _startTime) { + weekCursor = _startTime; + } + + return calculateToDistribute(_tokenId, weekCursor, _lastTokenTime, userPoint, userEpoch, maxUserEpoch, _ve); + } + + function calculateToDistribute( + uint _tokenId, + uint weekCursor, + uint _lastTokenTime, + IVe.Point memory userPoint, + uint userEpoch, + uint maxUserEpoch, + IVe _ve + ) public view returns (ClaimCalculationResult memory) { + IVe.Point memory oldUserPoint; + uint toDistribute; + for (uint i = 0; i < 50; i++) { + if (weekCursor >= _lastTokenTime) { + break; + } + if (weekCursor >= userPoint.ts && userEpoch <= maxUserEpoch) { + userEpoch += 1; + oldUserPoint = userPoint; + if (userEpoch > maxUserEpoch) { + userPoint = IVe.Point(0, 0, 0, 0); + } else { + userPoint = _ve.userPointHistory(_tokenId, userEpoch); + } + } else { + int128 dt = int128(int(weekCursor - oldUserPoint.ts)); + uint balanceOf = uint(int(_positiveInt128(oldUserPoint.bias - dt * oldUserPoint.slope))); + if (balanceOf == 0 && userEpoch > maxUserEpoch) { + break; + } + toDistribute += balanceOf * tokensPerWeek[weekCursor] / veSupply[weekCursor]; + weekCursor += WEEK; + } } - toDistribute += balanceOf * tokensPerWeek[weekCursor] / veSupply[weekCursor]; - weekCursor += WEEK; - } + return + ClaimCalculationResult(toDistribute, Math.min(maxUserEpoch, userEpoch - 1), weekCursor, maxUserEpoch, true); } - return ClaimCalculationResult( - toDistribute, - Math.min(maxUserEpoch, userEpoch - 1), - weekCursor, - maxUserEpoch, - true - ); - } - - function _positiveInt128(int128 value) internal pure returns (int128) { - return value < 0 ? int128(0) : value; - } - - /// @dev Block timestamp rounded to weeks - function timestamp() external view returns (uint) { - return block.timestamp / WEEK * WEEK; - } + function _positiveInt128(int128 value) internal pure returns (int128) { + return value < 0 ? int128(0) : value; + } + + /// @dev Block timestamp rounded to weeks + function timestamp() external view returns (uint) { + return block.timestamp / WEEK * WEEK; + } } diff --git a/src/VeSTGN.sol b/src/VeSTGN.sol index 5454fb2..7e89a2f 100644 --- a/src/VeSTGN.sol +++ b/src/VeSTGN.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.21; - import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import "openzeppelin-contracts/contracts/token/ERC721/IERC721Receiver.sol"; @@ -16,1142 +15,1126 @@ import "./lib/VeSTGNLib.sol"; /// Based on Curve/Solidly contract. /// @author belbix contract VeSTGN is Controllable, ReentrancyGuard, IVe { - using SafeERC20 for IERC20; - using Math for uint; - - // Only for internal usage - struct DepositInfo { - address stakingToken; - uint tokenId; - uint value; - uint unlockTime; - uint lockedAmount; - uint lockedDerivedAmount; - uint lockedEnd; - DepositType depositType; - } - - // Only for internal usage - struct CheckpointInfo { - uint tokenId; - uint oldDerivedAmount; - uint newDerivedAmount; - uint oldEnd; - uint newEnd; - bool isAlwaysMaxLock; - } - - enum TimeLockType { - UNKNOWN, - ADD_TOKEN, - WHITELIST_TRANSFER - } - - // ************************************************************* - // CONSTANTS - // ************************************************************* - - /// @dev Version of this contract. Adjust manually on each code modification. - string public constant VE_VERSION = "1.3.0"; - uint internal constant WEEK = 1 weeks; - uint internal constant MAX_TIME = 16 weeks; - uint public constant GOV_ACTION_TIME_LOCK = 18 hours; - - string constant public override name = "veSTGN"; - string constant public override symbol = "veSTGN"; - - /// @dev ERC165 interface ID of ERC165 - bytes4 internal constant _ERC165_INTERFACE_ID = 0x01ffc9a7; - /// @dev ERC165 interface ID of ERC721 - bytes4 internal constant _ERC721_INTERFACE_ID = 0x80ac58cd; - /// @dev ERC165 interface ID of ERC721Metadata - bytes4 internal constant _ERC721_METADATA_INTERFACE_ID = 0x5b5e139f; - - // ************************************************************* - // VARIABLES - // Keep names and ordering! - // Add only in the bottom. - // ************************************************************* - - /// @dev Underlying tokens info - address[] public override tokens; - /// @dev token => weight - mapping(address => uint) public tokenWeights; - /// @dev token => is allowed for deposits - mapping(address => bool) public isValidToken; - /// @dev Current count of token - uint public tokenId; - /// @dev veId => stakingToken => Locked amount - mapping(uint => mapping(address => uint)) public override lockedAmounts; - /// @dev veId => Amount based on weights aka power - mapping(uint => uint) public override lockedDerivedAmount; - /// @dev veId => Lock end timestamp - mapping(uint => uint) internal _lockedEndReal; - - // --- CHECKPOINTS LOGIC - - /// @dev Epoch counter. Update each week. - uint public override epoch; - /// @dev epoch -> unsigned point - mapping(uint => Point) internal _pointHistory; - /// @dev user -> Point[userEpoch] - mapping(uint => Point[1000000000]) internal _userPointHistory; - /// @dev veId -> Personal epoch counter - mapping(uint => uint) public override userPointEpoch; - /// @dev time -> signed slope change - mapping(uint => int128) public slopeChanges; - - // --- STATISTICS - - /// @dev veId -> Block number when last time NFT owner changed - mapping(uint => uint) public ownershipChange; - /// @dev Mapping from NFT ID to the address that owns it. - mapping(uint => address) internal _idToOwner; - /// @dev Mapping from NFT ID to approved address. - mapping(uint => address) internal _idToApprovals; - /// @dev Mapping from owner address to count of his tokens. - mapping(address => uint) internal _ownerToNFTokenCount; - /// @dev Mapping from owner address to mapping of index to tokenIds - mapping(address => mapping(uint => uint)) internal _ownerToNFTokenIdList; - /// @dev Mapping from NFT ID to index of owner - mapping(uint => uint) public tokenToOwnerIndex; - /// @dev Mapping from owner address to mapping of operator addresses. - mapping(address => mapping(address => bool)) public ownerToOperators; - - /// @dev Mapping of interface id to bool about whether or not it's supported - mapping(bytes4 => bool) internal _supportedInterfaces; - - // --- PERMISSIONS - - /// @dev Whitelisted contracts will be able to transfer NFTs - mapping(address => bool) public isWhitelistedTransfer; - /// @dev Time-locks for governance actions. Zero means not announced and should not processed. - mapping(TimeLockType => uint) public govActionTimeLock; - /// @dev underlying token => true if we can stake token to some place, false if paused - mapping(address => bool) internal tokenFarmingStatus; - - // --- OTHER - mapping(uint => bool) public isAlwaysMaxLock; - uint public additionalTotalSupply; - - // ************************************************************* - // EVENTS - // ************************************************************* - - event Deposit( - address indexed stakingToken, - address indexed provider, - uint tokenId, - uint value, - uint indexed locktime, - DepositType depositType, - uint ts - ); - event Withdraw(address indexed stakingToken, address indexed provider, uint tokenId, uint value, uint ts); - event Merged(address indexed stakingToken, address indexed provider, uint from, uint to); - event Split(uint parentTokenId, uint newTokenId, uint percent); - event TransferWhitelisted(address value); - event StakingTokenAdded(address value, uint weight); - event GovActionAnnounced(uint _type, uint timeToExecute); - event AlwaysMaxLock(uint tokenId, bool status); - - // ************************************************************* - // INIT - // ************************************************************* - - /// @dev Proxy initialization. Call it after contract deploy. - /// @param token_ Underlying ERC20 token - /// @param controller_ Central contract of the protocol - function init(address token_, uint weight, address controller_) external initializer { - __Controllable_init(controller_); - - // the first token should have 18 decimals - require(IERC20Metadata(token_).decimals() == uint8(18)); - _addToken(token_, weight); - - _pointHistory[0].blk = block.number; - _pointHistory[0].ts = block.timestamp; - - _supportedInterfaces[_ERC165_INTERFACE_ID] = true; - _supportedInterfaces[_ERC721_INTERFACE_ID] = true; - _supportedInterfaces[_ERC721_METADATA_INTERFACE_ID] = true; - - // mint-ish - emit Transfer(address(0), address(this), 0); - // burn-ish - emit Transfer(address(this), address(0), 0); - } - - // ************************************************************* - // GOVERNANCE ACTIONS - // ************************************************************* - - function announceAction(TimeLockType _type) external { - require(isGovernance(msg.sender), "FORBIDDEN"); - require(govActionTimeLock[_type] == 0 && _type != TimeLockType.UNKNOWN, "WRONG_INPUT"); - - govActionTimeLock[_type] = block.timestamp + GOV_ACTION_TIME_LOCK; - emit GovActionAnnounced(uint(_type), block.timestamp + GOV_ACTION_TIME_LOCK); - } - - /// @dev Whitelist address for transfers. Removing from whitelist should be forbidden. - function whitelistTransferFor(address value) external { - require(isGovernance(msg.sender), "FORBIDDEN"); - require(value != address(0), "WRONG_INPUT"); - uint timeLock = govActionTimeLock[TimeLockType.WHITELIST_TRANSFER]; - require(timeLock != 0 && timeLock < block.timestamp, "TIME_LOCK"); - - isWhitelistedTransfer[value] = true; - govActionTimeLock[TimeLockType.WHITELIST_TRANSFER] = 0; - - emit TransferWhitelisted(value); - } - - function addToken(address token, uint weight) external { - require(isGovernance(msg.sender), "FORBIDDEN"); - uint timeLock = govActionTimeLock[TimeLockType.ADD_TOKEN]; - require(timeLock != 0 && timeLock < block.timestamp, "TIME_LOCK"); - - _addToken(token, weight); - govActionTimeLock[TimeLockType.ADD_TOKEN] = 0; - } - - function _addToken(address token, uint weight) internal { - require(token != address(0) && weight != 0, "WRONG_INPUT"); - - uint length = tokens.length; - for (uint i; i < length; ++i) { - require(token != tokens[i], "WRONG_INPUT"); + using SafeERC20 for IERC20; + using Math for uint; + + // Only for internal usage + struct DepositInfo { + address stakingToken; + uint tokenId; + uint value; + uint unlockTime; + uint lockedAmount; + uint lockedDerivedAmount; + uint lockedEnd; + DepositType depositType; + } + + // Only for internal usage + struct CheckpointInfo { + uint tokenId; + uint oldDerivedAmount; + uint newDerivedAmount; + uint oldEnd; + uint newEnd; + bool isAlwaysMaxLock; + } + + enum TimeLockType { + UNKNOWN, + ADD_TOKEN, + WHITELIST_TRANSFER + } + + // ************************************************************* + // CONSTANTS + // ************************************************************* + + /// @dev Version of this contract. Adjust manually on each code modification. + string public constant VE_VERSION = "1.3.0"; + uint internal constant WEEK = 1 weeks; + uint internal constant MAX_TIME = 16 weeks; + uint public constant GOV_ACTION_TIME_LOCK = 18 hours; + + string public constant override name = "veSTGN"; + string public constant override symbol = "veSTGN"; + + /// @dev ERC165 interface ID of ERC165 + bytes4 internal constant _ERC165_INTERFACE_ID = 0x01ffc9a7; + /// @dev ERC165 interface ID of ERC721 + bytes4 internal constant _ERC721_INTERFACE_ID = 0x80ac58cd; + /// @dev ERC165 interface ID of ERC721Metadata + bytes4 internal constant _ERC721_METADATA_INTERFACE_ID = 0x5b5e139f; + + // ************************************************************* + // VARIABLES + // Keep names and ordering! + // Add only in the bottom. + // ************************************************************* + + /// @dev Underlying tokens info + address[] public override tokens; + /// @dev token => weight + mapping(address => uint) public tokenWeights; + /// @dev token => is allowed for deposits + mapping(address => bool) public isValidToken; + /// @dev Current count of token + uint public tokenId; + /// @dev veId => stakingToken => Locked amount + mapping(uint => mapping(address => uint)) public override lockedAmounts; + /// @dev veId => Amount based on weights aka power + mapping(uint => uint) public override lockedDerivedAmount; + /// @dev veId => Lock end timestamp + mapping(uint => uint) internal _lockedEndReal; + + // --- CHECKPOINTS LOGIC + + /// @dev Epoch counter. Update each week. + uint public override epoch; + /// @dev epoch -> unsigned point + mapping(uint => Point) internal _pointHistory; + /// @dev user -> Point[userEpoch] + mapping(uint => Point[1000000000]) internal _userPointHistory; + /// @dev veId -> Personal epoch counter + mapping(uint => uint) public override userPointEpoch; + /// @dev time -> signed slope change + mapping(uint => int128) public slopeChanges; + + // --- STATISTICS + + /// @dev veId -> Block number when last time NFT owner changed + mapping(uint => uint) public ownershipChange; + /// @dev Mapping from NFT ID to the address that owns it. + mapping(uint => address) internal _idToOwner; + /// @dev Mapping from NFT ID to approved address. + mapping(uint => address) internal _idToApprovals; + /// @dev Mapping from owner address to count of his tokens. + mapping(address => uint) internal _ownerToNFTokenCount; + /// @dev Mapping from owner address to mapping of index to tokenIds + mapping(address => mapping(uint => uint)) internal _ownerToNFTokenIdList; + /// @dev Mapping from NFT ID to index of owner + mapping(uint => uint) public tokenToOwnerIndex; + /// @dev Mapping from owner address to mapping of operator addresses. + mapping(address => mapping(address => bool)) public ownerToOperators; + + /// @dev Mapping of interface id to bool about whether or not it's supported + mapping(bytes4 => bool) internal _supportedInterfaces; + + // --- PERMISSIONS + + /// @dev Whitelisted contracts will be able to transfer NFTs + mapping(address => bool) public isWhitelistedTransfer; + /// @dev Time-locks for governance actions. Zero means not announced and should not processed. + mapping(TimeLockType => uint) public govActionTimeLock; + /// @dev underlying token => true if we can stake token to some place, false if paused + mapping(address => bool) internal tokenFarmingStatus; + + // --- OTHER + mapping(uint => bool) public isAlwaysMaxLock; + uint public additionalTotalSupply; + + // ************************************************************* + // EVENTS + // ************************************************************* + + event Deposit( + address indexed stakingToken, + address indexed provider, + uint tokenId, + uint value, + uint indexed locktime, + DepositType depositType, + uint ts + ); + event Withdraw(address indexed stakingToken, address indexed provider, uint tokenId, uint value, uint ts); + event Merged(address indexed stakingToken, address indexed provider, uint from, uint to); + event Split(uint parentTokenId, uint newTokenId, uint percent); + event TransferWhitelisted(address value); + event StakingTokenAdded(address value, uint weight); + event GovActionAnnounced(uint _type, uint timeToExecute); + event AlwaysMaxLock(uint tokenId, bool status); + + // ************************************************************* + // INIT + // ************************************************************* + + /// @dev Proxy initialization. Call it after contract deploy. + /// @param token_ Underlying ERC20 token + /// @param controller_ Central contract of the protocol + function init(address token_, uint weight, address controller_) external initializer { + __Controllable_init(controller_); + + // the first token should have 18 decimals + require(IERC20Metadata(token_).decimals() == uint8(18)); + _addToken(token_, weight); + + _pointHistory[0].blk = block.number; + _pointHistory[0].ts = block.timestamp; + + _supportedInterfaces[_ERC165_INTERFACE_ID] = true; + _supportedInterfaces[_ERC721_INTERFACE_ID] = true; + _supportedInterfaces[_ERC721_METADATA_INTERFACE_ID] = true; + + // mint-ish + emit Transfer(address(0), address(this), 0); + // burn-ish + emit Transfer(address(this), address(0), 0); + } + + // ************************************************************* + // GOVERNANCE ACTIONS + // ************************************************************* + + function announceAction(TimeLockType _type) external { + require(isGovernance(msg.sender), "FORBIDDEN"); + require(govActionTimeLock[_type] == 0 && _type != TimeLockType.UNKNOWN, "WRONG_INPUT"); + + govActionTimeLock[_type] = block.timestamp + GOV_ACTION_TIME_LOCK; + emit GovActionAnnounced(uint(_type), block.timestamp + GOV_ACTION_TIME_LOCK); + } + + /// @dev Whitelist address for transfers. Removing from whitelist should be forbidden. + function whitelistTransferFor(address value) external { + require(isGovernance(msg.sender), "FORBIDDEN"); + require(value != address(0), "WRONG_INPUT"); + uint timeLock = govActionTimeLock[TimeLockType.WHITELIST_TRANSFER]; + require(timeLock != 0 && timeLock < block.timestamp, "TIME_LOCK"); + + isWhitelistedTransfer[value] = true; + govActionTimeLock[TimeLockType.WHITELIST_TRANSFER] = 0; + + emit TransferWhitelisted(value); + } + + function addToken(address token, uint weight) external { + require(isGovernance(msg.sender), "FORBIDDEN"); + uint timeLock = govActionTimeLock[TimeLockType.ADD_TOKEN]; + require(timeLock != 0 && timeLock < block.timestamp, "TIME_LOCK"); + + _addToken(token, weight); + govActionTimeLock[TimeLockType.ADD_TOKEN] = 0; + } + + function _addToken(address token, uint weight) internal { + require(token != address(0) && weight != 0, "WRONG_INPUT"); + + uint length = tokens.length; + for (uint i; i < length; ++i) { + require(token != tokens[i], "WRONG_INPUT"); + } + + tokens.push(token); + tokenWeights[token] = weight; + isValidToken[token] = true; + + emit StakingTokenAdded(token, weight); + } + + function changeTokenFarmingAllowanceStatus(address _token, bool status) external { + require(isGovernance(msg.sender), "FORBIDDEN"); + require(tokenFarmingStatus[_token] != status); + tokenFarmingStatus[_token] = status; + } + + // ************************************************************* + // VIEWS + // ************************************************************* + + function lockedEnd(uint _tokenId) public view override returns (uint) { + if (isAlwaysMaxLock[_tokenId]) { + return (block.timestamp + MAX_TIME) / WEEK * WEEK; + } else { + return _lockedEndReal[_tokenId]; + } + } + + /// @dev Return length of staking tokens. + function tokensLength() external view returns (uint) { + return tokens.length; } - tokens.push(token); - tokenWeights[token] = weight; - isValidToken[token] = true; + /// @dev Current block timestamp + function blockTimestamp() external view returns (uint) { + return block.timestamp; + } + + /// @dev Interface identification is specified in ERC-165. + /// @param _interfaceID Id of the interface + function supportsInterface(bytes4 _interfaceID) public view returns (bool) { + return _supportedInterfaces[_interfaceID]; + } + + /// @notice Get the most recently recorded rate of voting power decrease for `_tokenId` + /// @param _tokenId token of the NFT + /// @return Value of the slope + function getLastUserSlope(uint _tokenId) external view returns (int128) { + uint uEpoch = userPointEpoch[_tokenId]; + return _userPointHistory[_tokenId][uEpoch].slope; + } + + /// @notice Get the timestamp for checkpoint `_idx` for `_tokenId` + /// @param _tokenId token of the NFT + /// @param _idx User epoch number + /// @return Epoch time of the checkpoint + function userPointHistoryTs(uint _tokenId, uint _idx) external view returns (uint) { + return _userPointHistory[_tokenId][_idx].ts; + } + + /// @dev Returns the number of NFTs owned by `_owner`. + /// Throws if `_owner` is the zero address. NFTs assigned to the zero address are considered invalid. + /// @param _owner Address for whom to query the balance. + function _balance(address _owner) internal view returns (uint) { + return _ownerToNFTokenCount[_owner]; + } + + /// @dev Returns the number of NFTs owned by `_owner`. + /// Throws if `_owner` is the zero address. NFTs assigned to the zero address are considered invalid. + /// @param _owner Address for whom to query the balance. + function balanceOf(address _owner) external view override returns (uint) { + return _balance(_owner); + } + + /// @dev Returns the address of the owner of the NFT. + /// @param _tokenId The identifier for an NFT. + function ownerOf(uint _tokenId) public view override returns (address) { + return _idToOwner[_tokenId]; + } + + /// @dev Get the approved address for a single NFT. + /// @param _tokenId ID of the NFT to query the approval of. + function getApproved(uint _tokenId) external view override returns (address) { + return _idToApprovals[_tokenId]; + } + + /// @dev Checks if `_operator` is an approved operator for `_owner`. + /// @param _owner The address that owns the NFTs. + /// @param _operator The address that acts on behalf of the owner. + function isApprovedForAll(address _owner, address _operator) external view override returns (bool) { + return (ownerToOperators[_owner])[_operator]; + } - emit StakingTokenAdded(token, weight); - } + /// @dev Get token by index + function tokenOfOwnerByIndex(address _owner, uint _tokenIndex) external view returns (uint) { + return _ownerToNFTokenIdList[_owner][_tokenIndex]; + } - function changeTokenFarmingAllowanceStatus(address _token, bool status) external { - require(isGovernance(msg.sender), "FORBIDDEN"); - require(tokenFarmingStatus[_token] != status); - tokenFarmingStatus[_token] = status; - } + /// @dev Returns whether the given spender can transfer a given token ID + /// @param _spender address of the spender to query + /// @param _tokenId uint ID of the token to be transferred + /// @return bool whether the msg.sender is approved for the given token ID, + /// is an operator of the owner, or is the owner of the token + function isApprovedOrOwner(address _spender, uint _tokenId) public view override returns (bool) { + address owner = _idToOwner[_tokenId]; + bool spenderIsOwner = owner == _spender; + bool spenderIsApproved = _spender == _idToApprovals[_tokenId]; + bool spenderIsApprovedForAll = (ownerToOperators[owner])[_spender]; + return spenderIsOwner || spenderIsApproved || spenderIsApprovedForAll; + } - // ************************************************************* - // VIEWS - // ************************************************************* + function balanceOfNFT(uint _tokenId) public view override returns (uint) { + // flash NFT protection + if (ownershipChange[_tokenId] == block.number) { + return 0; + } + return _balanceOfNFT(_tokenId, block.timestamp); + } - function lockedEnd(uint _tokenId) public view override returns (uint) { - if (isAlwaysMaxLock[_tokenId]) { - return (block.timestamp + MAX_TIME) / WEEK * WEEK; - } else { - return _lockedEndReal[_tokenId]; + function balanceOfNFTAt(uint _tokenId, uint _t) external view returns (uint) { + return _balanceOfNFT(_tokenId, _t); } - } - - /// @dev Return length of staking tokens. - function tokensLength() external view returns (uint) { - return tokens.length; - } - - /// @dev Current block timestamp - function blockTimestamp() external view returns (uint) { - return block.timestamp; - } - - /// @dev Interface identification is specified in ERC-165. - /// @param _interfaceID Id of the interface - function supportsInterface(bytes4 _interfaceID) public view returns (bool) { - return _supportedInterfaces[_interfaceID]; - } - - /// @notice Get the most recently recorded rate of voting power decrease for `_tokenId` - /// @param _tokenId token of the NFT - /// @return Value of the slope - function getLastUserSlope(uint _tokenId) external view returns (int128) { - uint uEpoch = userPointEpoch[_tokenId]; - return _userPointHistory[_tokenId][uEpoch].slope; - } - - /// @notice Get the timestamp for checkpoint `_idx` for `_tokenId` - /// @param _tokenId token of the NFT - /// @param _idx User epoch number - /// @return Epoch time of the checkpoint - function userPointHistoryTs(uint _tokenId, uint _idx) external view returns (uint) { - return _userPointHistory[_tokenId][_idx].ts; - } - - /// @dev Returns the number of NFTs owned by `_owner`. - /// Throws if `_owner` is the zero address. NFTs assigned to the zero address are considered invalid. - /// @param _owner Address for whom to query the balance. - function _balance(address _owner) internal view returns (uint) { - return _ownerToNFTokenCount[_owner]; - } - - /// @dev Returns the number of NFTs owned by `_owner`. - /// Throws if `_owner` is the zero address. NFTs assigned to the zero address are considered invalid. - /// @param _owner Address for whom to query the balance. - function balanceOf(address _owner) external view override returns (uint) { - return _balance(_owner); - } - - /// @dev Returns the address of the owner of the NFT. - /// @param _tokenId The identifier for an NFT. - function ownerOf(uint _tokenId) public view override returns (address) { - return _idToOwner[_tokenId]; - } - - /// @dev Get the approved address for a single NFT. - /// @param _tokenId ID of the NFT to query the approval of. - function getApproved(uint _tokenId) external view override returns (address) { - return _idToApprovals[_tokenId]; - } - - /// @dev Checks if `_operator` is an approved operator for `_owner`. - /// @param _owner The address that owns the NFTs. - /// @param _operator The address that acts on behalf of the owner. - function isApprovedForAll(address _owner, address _operator) external view override returns (bool) { - return (ownerToOperators[_owner])[_operator]; - } - - /// @dev Get token by index - function tokenOfOwnerByIndex(address _owner, uint _tokenIndex) external view returns (uint) { - return _ownerToNFTokenIdList[_owner][_tokenIndex]; - } - - /// @dev Returns whether the given spender can transfer a given token ID - /// @param _spender address of the spender to query - /// @param _tokenId uint ID of the token to be transferred - /// @return bool whether the msg.sender is approved for the given token ID, - /// is an operator of the owner, or is the owner of the token - function isApprovedOrOwner(address _spender, uint _tokenId) public view override returns (bool) { - address owner = _idToOwner[_tokenId]; - bool spenderIsOwner = owner == _spender; - bool spenderIsApproved = _spender == _idToApprovals[_tokenId]; - bool spenderIsApprovedForAll = (ownerToOperators[owner])[_spender]; - return spenderIsOwner || spenderIsApproved || spenderIsApprovedForAll; - } - - function balanceOfNFT(uint _tokenId) public view override returns (uint) { - // flash NFT protection - if (ownershipChange[_tokenId] == block.number) { - return 0; + + function totalSupply() external view returns (uint) { + return totalSupplyAtT(block.timestamp); } - return _balanceOfNFT(_tokenId, block.timestamp); - } - - function balanceOfNFTAt(uint _tokenId, uint _t) external view returns (uint) { - return _balanceOfNFT(_tokenId, _t); - } - - function totalSupply() external view returns (uint) { - return totalSupplyAtT(block.timestamp); - } - - function balanceOfAtNFT(uint _tokenId, uint _block) external view returns (uint) { - return _balanceOfAtNFT(_tokenId, _block); - } - - function userPointHistory(uint _tokenId, uint _loc) external view override returns (Point memory) { - return _userPointHistory[_tokenId][_loc]; - } - - function pointHistory(uint _loc) external view override returns (Point memory) { - return _pointHistory[_loc]; - } - - // ************************************************************* - // NFT LOGIC - // ************************************************************* - - /// @dev Add a NFT to an index mapping to a given address - /// @param _to address of the receiver - /// @param _tokenId uint ID Of the token to be added - function _addTokenToOwnerList(address _to, uint _tokenId) internal { - uint currentCount = _balance(_to); - - _ownerToNFTokenIdList[_to][currentCount] = _tokenId; - tokenToOwnerIndex[_tokenId] = currentCount; - } - - /// @dev Remove a NFT from an index mapping to a given address - /// @param _from address of the sender - /// @param _tokenId uint ID Of the token to be removed - function _removeTokenFromOwnerList(address _from, uint _tokenId) internal { - // Delete - uint currentCount = _balance(_from) - 1; - uint currentIndex = tokenToOwnerIndex[_tokenId]; - - if (currentCount == currentIndex) { - // update ownerToNFTokenIdList - _ownerToNFTokenIdList[_from][currentCount] = 0; - // update tokenToOwnerIndex - tokenToOwnerIndex[_tokenId] = 0; - } else { - uint lastTokenId = _ownerToNFTokenIdList[_from][currentCount]; - - // Add - // update ownerToNFTokenIdList - _ownerToNFTokenIdList[_from][currentIndex] = lastTokenId; - // update tokenToOwnerIndex - tokenToOwnerIndex[lastTokenId] = currentIndex; - - // Delete - // update ownerToNFTokenIdList - _ownerToNFTokenIdList[_from][currentCount] = 0; - // update tokenToOwnerIndex - tokenToOwnerIndex[_tokenId] = 0; + + function balanceOfAtNFT(uint _tokenId, uint _block) external view returns (uint) { + return _balanceOfAtNFT(_tokenId, _block); } - } - - /// @dev Add a NFT to a given address - function _addTokenTo(address _to, uint _tokenId) internal { - // assume always call on new tokenId or after _removeTokenFrom() call - // Change the owner - _idToOwner[_tokenId] = _to; - // Update owner token index tracking - _addTokenToOwnerList(_to, _tokenId); - // Change count tracking - _ownerToNFTokenCount[_to] += 1; - } - - /// @dev Remove a NFT from a given address - /// Throws if `_from` is not the current owner. - function _removeTokenFrom(address _from, uint _tokenId) internal { - require(_idToOwner[_tokenId] == _from, "NOT_OWNER"); - // Change the owner - _idToOwner[_tokenId] = address(0); - // Update owner token index tracking - _removeTokenFromOwnerList(_from, _tokenId); - // Change count tracking - _ownerToNFTokenCount[_from] -= 1; - } - - /// @dev Execute transfer of a NFT. - /// Throws unless `msg.sender` is the current owner, an authorized operator, or the approved - /// address for this NFT. (NOTE: `msg.sender` not allowed in internal function so pass `_sender`.) - /// Throws if `_to` is the zero address. - /// Throws if `_from` is not the current owner. - /// Throws if `_tokenId` is not a valid NFT. - function _transferFrom( - address _from, - address _to, - uint _tokenId, - address _sender - ) internal { - require(isApprovedOrOwner(_sender, _tokenId), "NOT_OWNER"); - require(_to != address(0), "WRONG_INPUT"); - // from address will be checked in _removeTokenFrom() - - if (_idToApprovals[_tokenId] != address(0)) { - // Reset approvals - _idToApprovals[_tokenId] = address(0); + + function userPointHistory(uint _tokenId, uint _loc) external view override returns (Point memory) { + return _userPointHistory[_tokenId][_loc]; } - _removeTokenFrom(_from, _tokenId); - _addTokenTo(_to, _tokenId); - // Set the block of ownership transfer (for Flash NFT protection) - ownershipChange[_tokenId] = block.number; - // Log the transfer - emit Transfer(_from, _to, _tokenId); - } - - /// @dev Transfers forbidden for veSTGN - function transferFrom( - address, - address, - uint - ) external pure override { - revert("FORBIDDEN"); - // _transferFrom(_from, _to, _tokenId, msg.sender); - } - - function _isContract(address account) internal view returns (bool) { - // This method relies on extcodesize, which returns 0 for contracts in - // construction, since the code is only stored at the end of the - // constructor execution. - uint size; - assembly { - size := extcodesize(account) + + function pointHistory(uint _loc) external view override returns (Point memory) { + return _pointHistory[_loc]; } - return size > 0; - } - - /// @dev Transfers the ownership of an NFT from one address to another address. - /// Throws unless `msg.sender` is the current owner, an authorized operator, or the - /// approved address for this NFT. - /// Throws if `_from` is not the current owner. - /// Throws if `_to` is the zero address. - /// Throws if `_tokenId` is not a valid NFT. - /// If `_to` is a smart contract, it calls `onERC721Received` on `_to` and throws if - /// the return value is not `bytes4(keccak256("onERC721Received(address,address,uint,bytes)"))`. - /// @param _from The current owner of the NFT. - /// @param _to The new owner. - /// @param _tokenId The NFT to transfer. - /// @param _data Additional data with no specified format, sent in call to `_to`. - function safeTransferFrom( - address _from, - address _to, - uint _tokenId, - bytes memory _data - ) public override { - require(isWhitelistedTransfer[_to] || isWhitelistedTransfer[_from], "FORBIDDEN"); - - _transferFrom(_from, _to, _tokenId, msg.sender); - require(_checkOnERC721Received(_from, _to, _tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); - } - - /// @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address. - /// The call is not executed if the target address is not a contract. - /// - /// @param _from address representing the previous owner of the given token ID - /// @param _to target address that will receive the tokens - /// @param _tokenId uint256 ID of the token to be transferred - /// @param _data bytes optional data to send along with the call - /// @return bool whether the call correctly returned the expected magic value - /// - function _checkOnERC721Received( - address _from, - address _to, - uint256 _tokenId, - bytes memory _data - ) private returns (bool) { - if (_isContract(_to)) { - try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4 retval) { - return retval == IERC721Receiver.onERC721Received.selector; - } catch (bytes memory reason) { - if (reason.length == 0) { - revert("ERC721: transfer to non ERC721Receiver implementer"); + + // ************************************************************* + // NFT LOGIC + // ************************************************************* + + /// @dev Add a NFT to an index mapping to a given address + /// @param _to address of the receiver + /// @param _tokenId uint ID Of the token to be added + function _addTokenToOwnerList(address _to, uint _tokenId) internal { + uint currentCount = _balance(_to); + + _ownerToNFTokenIdList[_to][currentCount] = _tokenId; + tokenToOwnerIndex[_tokenId] = currentCount; + } + + /// @dev Remove a NFT from an index mapping to a given address + /// @param _from address of the sender + /// @param _tokenId uint ID Of the token to be removed + function _removeTokenFromOwnerList(address _from, uint _tokenId) internal { + // Delete + uint currentCount = _balance(_from) - 1; + uint currentIndex = tokenToOwnerIndex[_tokenId]; + + if (currentCount == currentIndex) { + // update ownerToNFTokenIdList + _ownerToNFTokenIdList[_from][currentCount] = 0; + // update tokenToOwnerIndex + tokenToOwnerIndex[_tokenId] = 0; } else { - /// @solidity memory-safe-assembly - assembly { - revert(add(32, reason), mload(reason)) - } + uint lastTokenId = _ownerToNFTokenIdList[_from][currentCount]; + + // Add + // update ownerToNFTokenIdList + _ownerToNFTokenIdList[_from][currentIndex] = lastTokenId; + // update tokenToOwnerIndex + tokenToOwnerIndex[lastTokenId] = currentIndex; + + // Delete + // update ownerToNFTokenIdList + _ownerToNFTokenIdList[_from][currentCount] = 0; + // update tokenToOwnerIndex + tokenToOwnerIndex[_tokenId] = 0; } - } - } else { - return true; } - } - - /// @dev Transfers the ownership of an NFT from one address to another address. - /// Throws unless `msg.sender` is the current owner, an authorized operator, or the - /// approved address for this NFT. - /// Throws if `_from` is not the current owner. - /// Throws if `_to` is the zero address. - /// Throws if `_tokenId` is not a valid NFT. - /// If `_to` is a smart contract, it calls `onERC721Received` on `_to` and throws if - /// the return value is not `bytes4(keccak256("onERC721Received(address,address,uint,bytes)"))`. - /// @param _from The current owner of the NFT. - /// @param _to The new owner. - /// @param _tokenId The NFT to transfer. - function safeTransferFrom( - address _from, - address _to, - uint _tokenId - ) external override { - safeTransferFrom(_from, _to, _tokenId, ""); - } - - /// @dev Set or reaffirm the approved address for an NFT. The zero address indicates there is no approved address. - /// Throws unless `msg.sender` is the current NFT owner, or an authorized operator of the current owner. - /// Throws if `_tokenId` is not a valid NFT. (NOTE: This is not written the EIP) - /// Throws if `_approved` is the current owner. (NOTE: This is not written the EIP) - /// @param _approved Address to be approved for the given NFT ID. - /// @param _tokenId ID of the token to be approved. - function approve(address _approved, uint _tokenId) public override { - address owner = _idToOwner[_tokenId]; - // Throws if `_tokenId` is not a valid NFT - require(owner != address(0), "WRONG_INPUT"); - // Throws if `_approved` is the current owner - require(_approved != owner, "IDENTICAL_ADDRESS"); - // Check requirements - bool senderIsOwner = (owner == msg.sender); - bool senderIsApprovedForAll = (ownerToOperators[owner])[msg.sender]; - require(senderIsOwner || senderIsApprovedForAll, "NOT_OWNER"); - // Set the approval - _idToApprovals[_tokenId] = _approved; - emit Approval(owner, _approved, _tokenId); - } - - /// @dev Enables or disables approval for a third party ("operator") to manage all of - /// `msg.sender`'s assets. It also emits the ApprovalForAll event. - /// Throws if `_operator` is the `msg.sender`. (NOTE: This is not written the EIP) - /// @notice This works even if sender doesn't own any tokens at the time. - /// @param _operator Address to add to the set of authorized operators. - /// @param _approved True if the operators is approved, false to revoke approval. - function setApprovalForAll(address _operator, bool _approved) external override { - // Throws if `_operator` is the `msg.sender` - require(_operator != msg.sender, "IDENTICAL_ADDRESS"); - ownerToOperators[msg.sender][_operator] = _approved; - emit ApprovalForAll(msg.sender, _operator, _approved); - } - - /// @dev Function to mint tokens - /// Throws if `_to` is zero address. - /// Throws if `_tokenId` is owned by someone. - /// @param _to The address that will receive the minted tokens. - /// @param _tokenId The token id to mint. - /// @return A boolean that indicates if the operation was successful. - function _mint(address _to, uint _tokenId) internal returns (bool) { - // Throws if `_to` is zero address - require(_to != address(0), "WRONG_INPUT"); - _addTokenTo(_to, _tokenId); - require(_checkOnERC721Received(address(0), _to, _tokenId, ''), "ERC721: transfer to non ERC721Receiver implementer"); - emit Transfer(address(0), _to, _tokenId); - return true; - } - - // ************************************************************* - // DEPOSIT/WITHDRAW LOGIC - // ************************************************************* - - /// @notice Deposit and lock tokens for a user - function _depositFor(DepositInfo memory info) internal { - - uint newLockedDerivedAmount = info.lockedDerivedAmount; - if (info.value != 0) { - - // calculate new amounts - uint newAmount = info.lockedAmount + info.value; - newLockedDerivedAmount = VeSTGNLib.calculateDerivedAmount( - info.lockedAmount, - info.lockedDerivedAmount, - newAmount, - tokenWeights[info.stakingToken], - IERC20Metadata(info.stakingToken).decimals() - ); - // update chain info - lockedAmounts[info.tokenId][info.stakingToken] = newAmount; - _updateLockedDerivedAmount(info.tokenId, newLockedDerivedAmount); + + /// @dev Add a NFT to a given address + function _addTokenTo(address _to, uint _tokenId) internal { + // assume always call on new tokenId or after _removeTokenFrom() call + // Change the owner + _idToOwner[_tokenId] = _to; + // Update owner token index tracking + _addTokenToOwnerList(_to, _tokenId); + // Change count tracking + _ownerToNFTokenCount[_to] += 1; + } + + /// @dev Remove a NFT from a given address + /// Throws if `_from` is not the current owner. + function _removeTokenFrom(address _from, uint _tokenId) internal { + require(_idToOwner[_tokenId] == _from, "NOT_OWNER"); + // Change the owner + _idToOwner[_tokenId] = address(0); + // Update owner token index tracking + _removeTokenFromOwnerList(_from, _tokenId); + // Change count tracking + _ownerToNFTokenCount[_from] -= 1; } - // Adding to existing lock, or if a lock is expired - creating a new one - uint newLockedEnd = info.lockedEnd; - if (info.unlockTime != 0) { - _lockedEndReal[info.tokenId] = info.unlockTime; - newLockedEnd = info.unlockTime; + /// @dev Execute transfer of a NFT. + /// Throws unless `msg.sender` is the current owner, an authorized operator, or the approved + /// address for this NFT. (NOTE: `msg.sender` not allowed in internal function so pass `_sender`.) + /// Throws if `_to` is the zero address. + /// Throws if `_from` is not the current owner. + /// Throws if `_tokenId` is not a valid NFT. + function _transferFrom(address _from, address _to, uint _tokenId, address _sender) internal { + require(isApprovedOrOwner(_sender, _tokenId), "NOT_OWNER"); + require(_to != address(0), "WRONG_INPUT"); + // from address will be checked in _removeTokenFrom() + + if (_idToApprovals[_tokenId] != address(0)) { + // Reset approvals + _idToApprovals[_tokenId] = address(0); + } + _removeTokenFrom(_from, _tokenId); + _addTokenTo(_to, _tokenId); + // Set the block of ownership transfer (for Flash NFT protection) + ownershipChange[_tokenId] = block.number; + // Log the transfer + emit Transfer(_from, _to, _tokenId); } - // update checkpoint - _checkpoint(CheckpointInfo( - info.tokenId, - info.lockedDerivedAmount, - newLockedDerivedAmount, - info.lockedEnd, - newLockedEnd, - isAlwaysMaxLock[info.tokenId] - )); - - // move tokens to this contract, if necessary - emit Deposit(info.stakingToken, msg.sender, info.tokenId, info.value, newLockedEnd, info.depositType, block.timestamp); - } - - function _lockInfo(address stakingToken, uint veId) internal view returns ( - uint _lockedAmount, - uint _lockedDerivedAmount, - uint _lockedEnd - ) { - _lockedAmount = lockedAmounts[veId][stakingToken]; - _lockedDerivedAmount = lockedDerivedAmount[veId]; - _lockedEnd = lockedEnd(veId); - } - - function _incrementTokenIdAndGet() internal returns (uint){ - uint current = tokenId; - tokenId = current + 1; - return current + 1; - } - - /// @dev Setup always max lock. If true given tokenId will be always counted with max possible lock and can not be withdrawn. - /// When deactivated setup a new counter with max lock duration and use all common logic. - /*function setAlwaysMaxLock(uint _tokenId, bool status) external { + /// @dev Transfers forbidden for veSTGN + function transferFrom(address, address, uint) external pure override { + revert("FORBIDDEN"); + // _transferFrom(_from, _to, _tokenId, msg.sender); + } + + function _isContract(address account) internal view returns (bool) { + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + uint size; + assembly { + size := extcodesize(account) + } + return size > 0; + } + + /// @dev Transfers the ownership of an NFT from one address to another address. + /// Throws unless `msg.sender` is the current owner, an authorized operator, or the + /// approved address for this NFT. + /// Throws if `_from` is not the current owner. + /// Throws if `_to` is the zero address. + /// Throws if `_tokenId` is not a valid NFT. + /// If `_to` is a smart contract, it calls `onERC721Received` on `_to` and throws if + /// the return value is not `bytes4(keccak256("onERC721Received(address,address,uint,bytes)"))`. + /// @param _from The current owner of the NFT. + /// @param _to The new owner. + /// @param _tokenId The NFT to transfer. + /// @param _data Additional data with no specified format, sent in call to `_to`. + function safeTransferFrom(address _from, address _to, uint _tokenId, bytes memory _data) public override { + require(isWhitelistedTransfer[_to] || isWhitelistedTransfer[_from], "FORBIDDEN"); + + _transferFrom(_from, _to, _tokenId, msg.sender); + require( + _checkOnERC721Received(_from, _to, _tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer" + ); + } + + /// @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address. + /// The call is not executed if the target address is not a contract. + /// + /// @param _from address representing the previous owner of the given token ID + /// @param _to target address that will receive the tokens + /// @param _tokenId uint256 ID of the token to be transferred + /// @param _data bytes optional data to send along with the call + /// @return bool whether the call correctly returned the expected magic value + /// + function _checkOnERC721Received( + address _from, + address _to, + uint _tokenId, + bytes memory _data + ) private returns (bool) { + if (_isContract(_to)) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) returns (bytes4 retval) { + return retval == IERC721Receiver.onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert("ERC721: transfer to non ERC721Receiver implementer"); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + /// @dev Transfers the ownership of an NFT from one address to another address. + /// Throws unless `msg.sender` is the current owner, an authorized operator, or the + /// approved address for this NFT. + /// Throws if `_from` is not the current owner. + /// Throws if `_to` is the zero address. + /// Throws if `_tokenId` is not a valid NFT. + /// If `_to` is a smart contract, it calls `onERC721Received` on `_to` and throws if + /// the return value is not `bytes4(keccak256("onERC721Received(address,address,uint,bytes)"))`. + /// @param _from The current owner of the NFT. + /// @param _to The new owner. + /// @param _tokenId The NFT to transfer. + function safeTransferFrom(address _from, address _to, uint _tokenId) external override { + safeTransferFrom(_from, _to, _tokenId, ""); + } + + /// @dev Set or reaffirm the approved address for an NFT. The zero address indicates there is no approved address. + /// Throws unless `msg.sender` is the current NFT owner, or an authorized operator of the current owner. + /// Throws if `_tokenId` is not a valid NFT. (NOTE: This is not written the EIP) + /// Throws if `_approved` is the current owner. (NOTE: This is not written the EIP) + /// @param _approved Address to be approved for the given NFT ID. + /// @param _tokenId ID of the token to be approved. + function approve(address _approved, uint _tokenId) public override { + address owner = _idToOwner[_tokenId]; + // Throws if `_tokenId` is not a valid NFT + require(owner != address(0), "WRONG_INPUT"); + // Throws if `_approved` is the current owner + require(_approved != owner, "IDENTICAL_ADDRESS"); + // Check requirements + bool senderIsOwner = (owner == msg.sender); + bool senderIsApprovedForAll = (ownerToOperators[owner])[msg.sender]; + require(senderIsOwner || senderIsApprovedForAll, "NOT_OWNER"); + // Set the approval + _idToApprovals[_tokenId] = _approved; + emit Approval(owner, _approved, _tokenId); + } + + /// @dev Enables or disables approval for a third party ("operator") to manage all of + /// `msg.sender`'s assets. It also emits the ApprovalForAll event. + /// Throws if `_operator` is the `msg.sender`. (NOTE: This is not written the EIP) + /// @notice This works even if sender doesn't own any tokens at the time. + /// @param _operator Address to add to the set of authorized operators. + /// @param _approved True if the operators is approved, false to revoke approval. + function setApprovalForAll(address _operator, bool _approved) external override { + // Throws if `_operator` is the `msg.sender` + require(_operator != msg.sender, "IDENTICAL_ADDRESS"); + ownerToOperators[msg.sender][_operator] = _approved; + emit ApprovalForAll(msg.sender, _operator, _approved); + } + + /// @dev Function to mint tokens + /// Throws if `_to` is zero address. + /// Throws if `_tokenId` is owned by someone. + /// @param _to The address that will receive the minted tokens. + /// @param _tokenId The token id to mint. + /// @return A boolean that indicates if the operation was successful. + function _mint(address _to, uint _tokenId) internal returns (bool) { + // Throws if `_to` is zero address + require(_to != address(0), "WRONG_INPUT"); + _addTokenTo(_to, _tokenId); + require( + _checkOnERC721Received(address(0), _to, _tokenId, ""), "ERC721: transfer to non ERC721Receiver implementer" + ); + emit Transfer(address(0), _to, _tokenId); + return true; + } + + // ************************************************************* + // DEPOSIT/WITHDRAW LOGIC + // ************************************************************* + + /// @notice Deposit and lock tokens for a user + function _depositFor(DepositInfo memory info) internal { + uint newLockedDerivedAmount = info.lockedDerivedAmount; + if (info.value != 0) { + // calculate new amounts + uint newAmount = info.lockedAmount + info.value; + newLockedDerivedAmount = VeSTGNLib.calculateDerivedAmount( + info.lockedAmount, + info.lockedDerivedAmount, + newAmount, + tokenWeights[info.stakingToken], + IERC20Metadata(info.stakingToken).decimals() + ); + // update chain info + lockedAmounts[info.tokenId][info.stakingToken] = newAmount; + _updateLockedDerivedAmount(info.tokenId, newLockedDerivedAmount); + } + + // Adding to existing lock, or if a lock is expired - creating a new one + uint newLockedEnd = info.lockedEnd; + if (info.unlockTime != 0) { + _lockedEndReal[info.tokenId] = info.unlockTime; + newLockedEnd = info.unlockTime; + } + + // update checkpoint + _checkpoint( + CheckpointInfo( + info.tokenId, + info.lockedDerivedAmount, + newLockedDerivedAmount, + info.lockedEnd, + newLockedEnd, + isAlwaysMaxLock[info.tokenId] + ) + ); + + // move tokens to this contract, if necessary + emit Deposit( + info.stakingToken, msg.sender, info.tokenId, info.value, newLockedEnd, info.depositType, block.timestamp + ); + } + + function _lockInfo( + address stakingToken, + uint veId + ) internal view returns (uint _lockedAmount, uint _lockedDerivedAmount, uint _lockedEnd) { + _lockedAmount = lockedAmounts[veId][stakingToken]; + _lockedDerivedAmount = lockedDerivedAmount[veId]; + _lockedEnd = lockedEnd(veId); + } + + function _incrementTokenIdAndGet() internal returns (uint) { + uint current = tokenId; + tokenId = current + 1; + return current + 1; + } + + /// @dev Setup always max lock. If true given tokenId will be always counted with max possible lock and can not be withdrawn. + /// When deactivated setup a new counter with max lock duration and use all common logic. + /*function setAlwaysMaxLock(uint _tokenId, bool status) external { require(isApprovedOrOwner(msg.sender, _tokenId), "NOT_OWNER"); require(status != isAlwaysMaxLock[_tokenId], "WRONG_INPUT"); _setAlwaysMaxLock(_tokenId, status); - }*/ - - function _setAlwaysMaxLock(uint _tokenId, bool status) internal { - - // need to setup first, it will be checked later - isAlwaysMaxLock[_tokenId] = status; - - uint _derivedAmount = lockedDerivedAmount[_tokenId]; - uint maxLockDuration = (block.timestamp + MAX_TIME) / WEEK * WEEK; - - // the idea is exclude nft from checkpoint calculations when max lock activated and count the balance as is - if (status) { - // need to increase additional total supply for properly calculation - additionalTotalSupply += _derivedAmount; - - // set checkpoints to zero - _checkpoint(CheckpointInfo( - _tokenId, - _derivedAmount, - 0, - maxLockDuration, - maxLockDuration, - false // need to use false for this fake update - )); - } else { - // remove from additional supply - require(additionalTotalSupply >= _derivedAmount, "WRONG_SUPPLY"); - additionalTotalSupply -= _derivedAmount; - // if we disable need to set real lock end to max value - _lockedEndReal[_tokenId] = maxLockDuration; - // and activate real checkpoints + total supply - _checkpoint(CheckpointInfo( - _tokenId, - 0, // it was setup to zero when we set always max lock - _derivedAmount, - maxLockDuration, - maxLockDuration, - false - )); + }*/ + + function _setAlwaysMaxLock(uint _tokenId, bool status) internal { + // need to setup first, it will be checked later + isAlwaysMaxLock[_tokenId] = status; + + uint _derivedAmount = lockedDerivedAmount[_tokenId]; + uint maxLockDuration = (block.timestamp + MAX_TIME) / WEEK * WEEK; + + // the idea is exclude nft from checkpoint calculations when max lock activated and count the balance as is + if (status) { + // need to increase additional total supply for properly calculation + additionalTotalSupply += _derivedAmount; + + // set checkpoints to zero + _checkpoint( + CheckpointInfo( + _tokenId, + _derivedAmount, + 0, + maxLockDuration, + maxLockDuration, + false // need to use false for this fake update + ) + ); + } else { + // remove from additional supply + require(additionalTotalSupply >= _derivedAmount, "WRONG_SUPPLY"); + additionalTotalSupply -= _derivedAmount; + // if we disable need to set real lock end to max value + _lockedEndReal[_tokenId] = maxLockDuration; + // and activate real checkpoints + total supply + _checkpoint( + CheckpointInfo( + _tokenId, + 0, // it was setup to zero when we set always max lock + _derivedAmount, + maxLockDuration, + maxLockDuration, + false + ) + ); + } + + emit AlwaysMaxLock(_tokenId, status); } - emit AlwaysMaxLock(_tokenId, status); - } + function _updateLockedDerivedAmount(uint _tokenId, uint amount) internal { + uint cur = lockedDerivedAmount[_tokenId]; + if (cur == amount) { + // if did not change do nothing + return; + } - function _updateLockedDerivedAmount(uint _tokenId, uint amount) internal { - uint cur = lockedDerivedAmount[_tokenId]; - if (cur == amount) { - // if did not change do nothing - return; + if (isAlwaysMaxLock[_tokenId]) { + if (cur > amount) { + additionalTotalSupply -= (cur - amount); + } else if (cur < amount) { + additionalTotalSupply += amount - cur; + } + } + + lockedDerivedAmount[_tokenId] = amount; } - if (isAlwaysMaxLock[_tokenId]) { - if (cur > amount) { - additionalTotalSupply -= (cur - amount); - } else if (cur < amount) { - additionalTotalSupply += amount - cur; - } + /// @notice Deposit `_value` tokens for `_to` and lock for `_lock_duration` + /// @param _token Token for deposit. Should be whitelisted in this contract. + /// @param _value Amount to deposit + /// @param _lockDuration Number of seconds to lock tokens for (rounded down to nearest week) + /// @param _to Address to deposit + function _createLock(address _token, uint _value, uint _lockDuration, address _to) internal returns (uint) { + require(_value > 0, "WRONG_INPUT"); + // Lock time is rounded down to weeks + uint unlockTime = (block.timestamp + _lockDuration) / WEEK * WEEK; + require(unlockTime > block.timestamp, "LOW_LOCK_PERIOD"); + require(unlockTime <= block.timestamp + MAX_TIME, "HIGH_LOCK_PERIOD"); + require(isValidToken[_token], "INVALID_TOKEN"); + + uint _tokenId = _incrementTokenIdAndGet(); + _mint(_to, _tokenId); + + _depositFor( + DepositInfo({ + stakingToken: _token, + tokenId: _tokenId, + value: _value, + unlockTime: unlockTime, + lockedAmount: 0, + lockedDerivedAmount: 0, + lockedEnd: 0, + depositType: DepositType.CREATE_LOCK_TYPE + }) + ); + return _tokenId; } - lockedDerivedAmount[_tokenId] = amount; - } - - /// @notice Deposit `_value` tokens for `_to` and lock for `_lock_duration` - /// @param _token Token for deposit. Should be whitelisted in this contract. - /// @param _value Amount to deposit - /// @param _lockDuration Number of seconds to lock tokens for (rounded down to nearest week) - /// @param _to Address to deposit - function _createLock(address _token, uint _value, uint _lockDuration, address _to) internal returns (uint) { - require(_value > 0, "WRONG_INPUT"); - // Lock time is rounded down to weeks - uint unlockTime = (block.timestamp + _lockDuration) / WEEK * WEEK; - require(unlockTime > block.timestamp, "LOW_LOCK_PERIOD"); - require(unlockTime <= block.timestamp + MAX_TIME, "HIGH_LOCK_PERIOD"); - require(isValidToken[_token], "INVALID_TOKEN"); - - uint _tokenId = _incrementTokenIdAndGet(); - _mint(_to, _tokenId); - - _depositFor(DepositInfo({ - stakingToken: _token, - tokenId: _tokenId, - value: _value, - unlockTime: unlockTime, - lockedAmount: 0, - lockedDerivedAmount: 0, - lockedEnd: 0, - depositType: DepositType.CREATE_LOCK_TYPE - })); - return _tokenId; - } - - /// @notice Deposit `_value` tokens for `_to` and lock for `_lock_duration` - /// @param _token Token for deposit. Should be whitelisted in this contract. - /// @param _value Amount to deposit - /// @param _lockDuration Number of seconds to lock tokens for (rounded down to nearest week) - /// @param _to Address to deposit - function createLockFor(address _token, uint _value, uint _lockDuration, address _to) - external nonReentrant override returns (uint) { - return _createLock(_token, _value, _lockDuration, _to); - } - - /// @notice Deposit `_value` tokens for `msg.sender` and lock for `_lock_duration` - /// @param _value Amount to deposit - /// @param _lockDuration Number of seconds to lock tokens for (rounded down to nearest week) - function createLock(address _token, uint _value, uint _lockDuration) external nonReentrant returns (uint) { - return _createLock(_token, _value, _lockDuration, msg.sender); - } - - /// @notice Deposit `_value` additional tokens for `_tokenId` without modifying the unlock time - /// @dev Anyone (even a smart contract) can deposit for someone else, but - /// cannot extend their locktime and deposit for a brand new user - /// @param _token Token for deposit. Should be whitelisted in this contract. - /// @param _tokenId ve token ID - /// @param _value Amount of tokens to deposit and add to the lock - function increaseAmount(address _token, uint _tokenId, uint _value) external nonReentrant override { - require(_value > 0, "WRONG_INPUT"); - (uint _lockedAmount, uint _lockedDerivedAmount, uint _lockedEnd) = _lockInfo(_token, _tokenId); - - require(_lockedDerivedAmount > 0, "NFT_WITHOUT_POWER"); - require(_lockedEnd > block.timestamp, "EXPIRED"); - require(isValidToken[_token], "INVALID_TOKEN"); - - _depositFor(DepositInfo({ - stakingToken: _token, - tokenId: _tokenId, - value: _value, - unlockTime: 0, - lockedAmount: _lockedAmount, - lockedDerivedAmount: _lockedDerivedAmount, - lockedEnd: _lockedEnd, - depositType: DepositType.INCREASE_LOCK_AMOUNT - })); - } - - /// @notice Extend the unlock time for `_tokenId` - /// @param _tokenId ve token ID - /// @param _lockDuration New number of seconds until tokens unlock - function increaseUnlockTime(uint _tokenId, uint _lockDuration) external nonReentrant returns ( - uint power, - uint unlockDate - ) { - uint _lockedDerivedAmount = lockedDerivedAmount[_tokenId]; - uint _lockedEnd = _lockedEndReal[_tokenId]; - // Lock time is rounded down to weeks - uint unlockTime = (block.timestamp + _lockDuration) / WEEK * WEEK; - require(!isAlwaysMaxLock[_tokenId], "ALWAYS_MAX_LOCK"); - require(_lockedDerivedAmount > 0, "NFT_WITHOUT_POWER"); - require(_lockedEnd > block.timestamp, "EXPIRED"); - require(unlockTime > _lockedEnd, "LOW_UNLOCK_TIME"); - require(unlockTime <= block.timestamp + MAX_TIME, "HIGH_LOCK_PERIOD"); - require(isApprovedOrOwner(msg.sender, _tokenId), "NOT_OWNER"); + /// @notice Deposit `_value` tokens for `_to` and lock for `_lock_duration` + /// @param _token Token for deposit. Should be whitelisted in this contract. + /// @param _value Amount to deposit + /// @param _lockDuration Number of seconds to lock tokens for (rounded down to nearest week) + /// @param _to Address to deposit + function createLockFor( + address _token, + uint _value, + uint _lockDuration, + address _to + ) external override nonReentrant returns (uint) { + return _createLock(_token, _value, _lockDuration, _to); + } - _depositFor(DepositInfo({ - stakingToken: address(0), - tokenId: _tokenId, - value: 0, - unlockTime: unlockTime, - lockedAmount: 0, - lockedDerivedAmount: _lockedDerivedAmount, - lockedEnd: _lockedEnd, - depositType: DepositType.INCREASE_UNLOCK_TIME - })); - - power = balanceOfNFT(_tokenId); - unlockDate = _lockedEndReal[_tokenId]; - } - - /// @dev Merge two NFTs union their balances and keep the biggest lock time. - function merge(uint _from, uint _to) external nonReentrant { - require(_from != _to, "IDENTICAL_ADDRESS"); - require(!isAlwaysMaxLock[_from] && !isAlwaysMaxLock[_to], "ALWAYS_MAX_LOCK"); - require(isApprovedOrOwner(msg.sender, _from) && isApprovedOrOwner(msg.sender, _to), "NOT_OWNER"); - - uint lockedEndFrom = lockedEnd(_from); - uint lockedEndTo = lockedEnd(_to); - require(lockedEndFrom > block.timestamp && lockedEndTo > block.timestamp, "EXPIRED"); - uint end = lockedEndFrom >= lockedEndTo ? lockedEndFrom : lockedEndTo; - uint oldDerivedAmount = lockedDerivedAmount[_from]; - - uint length = tokens.length; - // we should use the old one for properly calculate checkpoint for the new ve - uint newLockedEndTo = lockedEndTo; - for (uint i; i < length; i++) { - address stakingToken = tokens[i]; - uint _lockedAmountFrom = lockedAmounts[_from][stakingToken]; - if (_lockedAmountFrom == 0) { - continue; - } - lockedAmounts[_from][stakingToken] = 0; - - _depositFor(DepositInfo({ - stakingToken: stakingToken, - tokenId: _to, - value: _lockedAmountFrom, - unlockTime: end, - lockedAmount: lockedAmounts[_to][stakingToken], - lockedDerivedAmount: lockedDerivedAmount[_to], - lockedEnd: newLockedEndTo, - depositType: DepositType.MERGE_TYPE - })); - - // set new lock time to the current end lock - newLockedEndTo = end; - - emit Merged(stakingToken, msg.sender, _from, _to); + /// @notice Deposit `_value` tokens for `msg.sender` and lock for `_lock_duration` + /// @param _value Amount to deposit + /// @param _lockDuration Number of seconds to lock tokens for (rounded down to nearest week) + function createLock(address _token, uint _value, uint _lockDuration) external nonReentrant returns (uint) { + return _createLock(_token, _value, _lockDuration, msg.sender); } - _updateLockedDerivedAmount(_from, 0); - _lockedEndReal[_from] = 0; - - // update checkpoint - _checkpoint(CheckpointInfo( - _from, - oldDerivedAmount, - 0, - lockedEndFrom, - lockedEndFrom, - isAlwaysMaxLock[_from] - )); - - _burn(_from); - } - - /// @dev Split given veNFT. A new NFT will have a given percent of underlying tokens. - /// @param _tokenId ve token ID - /// @param percent percent of underlying tokens for new NFT with denominator 1e18 (1-(100e18-1)). - function split(uint _tokenId, uint percent) external nonReentrant { - require(!isAlwaysMaxLock[_tokenId], "ALWAYS_MAX_LOCK"); - require(isApprovedOrOwner(msg.sender, _tokenId), "NOT_OWNER"); - require(percent != 0 && percent < 100e18, "WRONG_INPUT"); - - uint _lockedDerivedAmount = lockedDerivedAmount[_tokenId]; - uint oldLockedDerivedAmount = _lockedDerivedAmount; - uint _lockedEnd = lockedEnd(_tokenId); - - require(_lockedEnd > block.timestamp, "EXPIRED"); - - // crete new NFT - uint _newTokenId = _incrementTokenIdAndGet(); - _mint(msg.sender, _newTokenId); - - // migrate percent of locked tokens to the new NFT - uint length = tokens.length; - for (uint i; i < length; ++i) { - address stakingToken = tokens[i]; - uint _lockedAmount = lockedAmounts[_tokenId][stakingToken]; - if (_lockedAmount == 0) { - continue; - } - uint amountForNewNFT = _lockedAmount * percent / 100e18; - require(amountForNewNFT != 0, "LOW_PERCENT"); - - uint newLockedDerivedAmount = VeSTGNLib.calculateDerivedAmount( - _lockedAmount, - _lockedDerivedAmount, - _lockedAmount - amountForNewNFT, - tokenWeights[stakingToken], - IERC20Metadata(stakingToken).decimals() - ); - - _lockedDerivedAmount = newLockedDerivedAmount; - - lockedAmounts[_tokenId][stakingToken] = _lockedAmount - amountForNewNFT; - - // increase values for new NFT - _depositFor(DepositInfo({ - stakingToken: stakingToken, - tokenId: _newTokenId, - value: amountForNewNFT, - unlockTime: _lockedEnd, - lockedAmount: 0, - lockedDerivedAmount: lockedDerivedAmount[_newTokenId], - lockedEnd: _lockedEnd, - depositType: DepositType.MERGE_TYPE - })); + /// @notice Deposit `_value` additional tokens for `_tokenId` without modifying the unlock time + /// @dev Anyone (even a smart contract) can deposit for someone else, but + /// cannot extend their locktime and deposit for a brand new user + /// @param _token Token for deposit. Should be whitelisted in this contract. + /// @param _tokenId ve token ID + /// @param _value Amount of tokens to deposit and add to the lock + function increaseAmount(address _token, uint _tokenId, uint _value) external override nonReentrant { + require(_value > 0, "WRONG_INPUT"); + (uint _lockedAmount, uint _lockedDerivedAmount, uint _lockedEnd) = _lockInfo(_token, _tokenId); + + require(_lockedDerivedAmount > 0, "NFT_WITHOUT_POWER"); + require(_lockedEnd > block.timestamp, "EXPIRED"); + require(isValidToken[_token], "INVALID_TOKEN"); + + _depositFor( + DepositInfo({ + stakingToken: _token, + tokenId: _tokenId, + value: _value, + unlockTime: 0, + lockedAmount: _lockedAmount, + lockedDerivedAmount: _lockedDerivedAmount, + lockedEnd: _lockedEnd, + depositType: DepositType.INCREASE_LOCK_AMOUNT + }) + ); } - _updateLockedDerivedAmount(_tokenId, _lockedDerivedAmount); - - // update checkpoint - _checkpoint(CheckpointInfo( - _tokenId, - oldLockedDerivedAmount, - _lockedDerivedAmount, - _lockedEnd, - _lockedEnd, - isAlwaysMaxLock[_tokenId] - )); - - emit Split(_tokenId, _newTokenId, percent); - } - - /// @notice Withdraw all staking tokens for `_tokenId` - /// @dev Only possible if the lock has expired - function withdrawAll(uint _tokenId) external { - uint length = tokens.length; - for (uint i; i < length; ++i) { - address token = tokens[i]; - if (lockedAmounts[_tokenId][token] != 0) { - withdraw(token, _tokenId); - } + /// @notice Extend the unlock time for `_tokenId` + /// @param _tokenId ve token ID + /// @param _lockDuration New number of seconds until tokens unlock + function increaseUnlockTime( + uint _tokenId, + uint _lockDuration + ) external nonReentrant returns (uint power, uint unlockDate) { + uint _lockedDerivedAmount = lockedDerivedAmount[_tokenId]; + uint _lockedEnd = _lockedEndReal[_tokenId]; + // Lock time is rounded down to weeks + uint unlockTime = (block.timestamp + _lockDuration) / WEEK * WEEK; + require(!isAlwaysMaxLock[_tokenId], "ALWAYS_MAX_LOCK"); + require(_lockedDerivedAmount > 0, "NFT_WITHOUT_POWER"); + require(_lockedEnd > block.timestamp, "EXPIRED"); + require(unlockTime > _lockedEnd, "LOW_UNLOCK_TIME"); + require(unlockTime <= block.timestamp + MAX_TIME, "HIGH_LOCK_PERIOD"); + require(isApprovedOrOwner(msg.sender, _tokenId), "NOT_OWNER"); + + _depositFor( + DepositInfo({ + stakingToken: address(0), + tokenId: _tokenId, + value: 0, + unlockTime: unlockTime, + lockedAmount: 0, + lockedDerivedAmount: _lockedDerivedAmount, + lockedEnd: _lockedEnd, + depositType: DepositType.INCREASE_UNLOCK_TIME + }) + ); + + power = balanceOfNFT(_tokenId); + unlockDate = _lockedEndReal[_tokenId]; } - } - /// @notice Withdraw given staking token for `_tokenId` - /// @dev Only possible if the lock has expired - function withdraw(address stakingToken, uint _tokenId) public nonReentrant { - require(isApprovedOrOwner(msg.sender, _tokenId), "NOT_OWNER"); + /// @dev Merge two NFTs union their balances and keep the biggest lock time. + function merge(uint _from, uint _to) external nonReentrant { + require(_from != _to, "IDENTICAL_ADDRESS"); + require(!isAlwaysMaxLock[_from] && !isAlwaysMaxLock[_to], "ALWAYS_MAX_LOCK"); + require(isApprovedOrOwner(msg.sender, _from) && isApprovedOrOwner(msg.sender, _to), "NOT_OWNER"); + + uint lockedEndFrom = lockedEnd(_from); + uint lockedEndTo = lockedEnd(_to); + require(lockedEndFrom > block.timestamp && lockedEndTo > block.timestamp, "EXPIRED"); + uint end = lockedEndFrom >= lockedEndTo ? lockedEndFrom : lockedEndTo; + uint oldDerivedAmount = lockedDerivedAmount[_from]; + + uint length = tokens.length; + // we should use the old one for properly calculate checkpoint for the new ve + uint newLockedEndTo = lockedEndTo; + for (uint i; i < length; i++) { + address stakingToken = tokens[i]; + uint _lockedAmountFrom = lockedAmounts[_from][stakingToken]; + if (_lockedAmountFrom == 0) { + continue; + } + lockedAmounts[_from][stakingToken] = 0; + + _depositFor( + DepositInfo({ + stakingToken: stakingToken, + tokenId: _to, + value: _lockedAmountFrom, + unlockTime: end, + lockedAmount: lockedAmounts[_to][stakingToken], + lockedDerivedAmount: lockedDerivedAmount[_to], + lockedEnd: newLockedEndTo, + depositType: DepositType.MERGE_TYPE + }) + ); + + // set new lock time to the current end lock + newLockedEndTo = end; + + emit Merged(stakingToken, msg.sender, _from, _to); + } - (uint oldLockedAmount, uint oldLockedDerivedAmount, uint oldLockedEnd) = - _lockInfo(stakingToken, _tokenId); - require(block.timestamp >= oldLockedEnd, "NOT_EXPIRED"); - require(oldLockedAmount > 0, "ZERO_LOCKED"); - require(!isAlwaysMaxLock[_tokenId], "ALWAYS_MAX_LOCK"); + _updateLockedDerivedAmount(_from, 0); + _lockedEndReal[_from] = 0; + // update checkpoint + _checkpoint(CheckpointInfo(_from, oldDerivedAmount, 0, lockedEndFrom, lockedEndFrom, isAlwaysMaxLock[_from])); - uint newLockedDerivedAmount = VeSTGNLib.calculateDerivedAmount( - oldLockedAmount, - oldLockedDerivedAmount, - 0, - tokenWeights[stakingToken], - IERC20Metadata(stakingToken).decimals() - ); + _burn(_from); + } - // if no tokens set lock to zero - uint newLockEnd = oldLockedEnd; - if (newLockedDerivedAmount == 0) { - _lockedEndReal[_tokenId] = 0; - newLockEnd = 0; + /// @dev Split given veNFT. A new NFT will have a given percent of underlying tokens. + /// @param _tokenId ve token ID + /// @param percent percent of underlying tokens for new NFT with denominator 1e18 (1-(100e18-1)). + function split(uint _tokenId, uint percent) external nonReentrant { + require(!isAlwaysMaxLock[_tokenId], "ALWAYS_MAX_LOCK"); + require(isApprovedOrOwner(msg.sender, _tokenId), "NOT_OWNER"); + require(percent != 0 && percent < 100e18, "WRONG_INPUT"); + + uint _lockedDerivedAmount = lockedDerivedAmount[_tokenId]; + uint oldLockedDerivedAmount = _lockedDerivedAmount; + uint _lockedEnd = lockedEnd(_tokenId); + + require(_lockedEnd > block.timestamp, "EXPIRED"); + + // crete new NFT + uint _newTokenId = _incrementTokenIdAndGet(); + _mint(msg.sender, _newTokenId); + + // migrate percent of locked tokens to the new NFT + uint length = tokens.length; + for (uint i; i < length; ++i) { + address stakingToken = tokens[i]; + uint _lockedAmount = lockedAmounts[_tokenId][stakingToken]; + if (_lockedAmount == 0) { + continue; + } + uint amountForNewNFT = _lockedAmount * percent / 100e18; + require(amountForNewNFT != 0, "LOW_PERCENT"); + + uint newLockedDerivedAmount = VeSTGNLib.calculateDerivedAmount( + _lockedAmount, + _lockedDerivedAmount, + _lockedAmount - amountForNewNFT, + tokenWeights[stakingToken], + IERC20Metadata(stakingToken).decimals() + ); + + _lockedDerivedAmount = newLockedDerivedAmount; + + lockedAmounts[_tokenId][stakingToken] = _lockedAmount - amountForNewNFT; + + // increase values for new NFT + _depositFor( + DepositInfo({ + stakingToken: stakingToken, + tokenId: _newTokenId, + value: amountForNewNFT, + unlockTime: _lockedEnd, + lockedAmount: 0, + lockedDerivedAmount: lockedDerivedAmount[_newTokenId], + lockedEnd: _lockedEnd, + depositType: DepositType.MERGE_TYPE + }) + ); + } + + _updateLockedDerivedAmount(_tokenId, _lockedDerivedAmount); + + // update checkpoint + _checkpoint( + CheckpointInfo( + _tokenId, + oldLockedDerivedAmount, + _lockedDerivedAmount, + _lockedEnd, + _lockedEnd, + isAlwaysMaxLock[_tokenId] + ) + ); + + emit Split(_tokenId, _newTokenId, percent); } - // update derived amount - _updateLockedDerivedAmount(_tokenId, newLockedDerivedAmount); - - // set locked amount to zero, we will withdraw all - lockedAmounts[_tokenId][stakingToken] = 0; - - // update checkpoint - _checkpoint(CheckpointInfo( - _tokenId, - oldLockedDerivedAmount, - newLockedDerivedAmount, - oldLockedEnd, - newLockEnd, - false // already checked and can not be true - )); - - // Burn the NFT - if (newLockedDerivedAmount == 0) { - _burn(_tokenId); + /// @notice Withdraw all staking tokens for `_tokenId` + /// @dev Only possible if the lock has expired + function withdrawAll(uint _tokenId) external { + uint length = tokens.length; + for (uint i; i < length; ++i) { + address token = tokens[i]; + if (lockedAmounts[_tokenId][token] != 0) { + withdraw(token, _tokenId); + } + } } - emit Withdraw(stakingToken, msg.sender, _tokenId, oldLockedAmount, block.timestamp); - } - - ///////////////////////////////////////////////////////////////////////////////////// - // Attention! - // The following ERC20/minime-compatible methods are not real balanceOf and supply! - // They measure the weights for the purpose of voting, so they don't represent - // real coins. - ///////////////////////////////////////////////////////////////////////////////////// - - /// @notice Get the current voting power for `_tokenId` - /// @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility - /// @param _tokenId NFT for lock - /// @param _t Epoch time to return voting power at - /// @return User voting power - function _balanceOfNFT(uint _tokenId, uint _t) internal view returns (uint) { - // with max lock return balance as is - if (isAlwaysMaxLock[_tokenId]) { - return lockedDerivedAmount[_tokenId]; + /// @notice Withdraw given staking token for `_tokenId` + /// @dev Only possible if the lock has expired + function withdraw(address stakingToken, uint _tokenId) public nonReentrant { + require(isApprovedOrOwner(msg.sender, _tokenId), "NOT_OWNER"); + + (uint oldLockedAmount, uint oldLockedDerivedAmount, uint oldLockedEnd) = _lockInfo(stakingToken, _tokenId); + require(block.timestamp >= oldLockedEnd, "NOT_EXPIRED"); + require(oldLockedAmount > 0, "ZERO_LOCKED"); + require(!isAlwaysMaxLock[_tokenId], "ALWAYS_MAX_LOCK"); + + uint newLockedDerivedAmount = VeSTGNLib.calculateDerivedAmount( + oldLockedAmount, + oldLockedDerivedAmount, + 0, + tokenWeights[stakingToken], + IERC20Metadata(stakingToken).decimals() + ); + + // if no tokens set lock to zero + uint newLockEnd = oldLockedEnd; + if (newLockedDerivedAmount == 0) { + _lockedEndReal[_tokenId] = 0; + newLockEnd = 0; + } + + // update derived amount + _updateLockedDerivedAmount(_tokenId, newLockedDerivedAmount); + + // set locked amount to zero, we will withdraw all + lockedAmounts[_tokenId][stakingToken] = 0; + + // update checkpoint + _checkpoint( + CheckpointInfo( + _tokenId, + oldLockedDerivedAmount, + newLockedDerivedAmount, + oldLockedEnd, + newLockEnd, + false // already checked and can not be true + ) + ); + + // Burn the NFT + if (newLockedDerivedAmount == 0) { + _burn(_tokenId); + } + + emit Withdraw(stakingToken, msg.sender, _tokenId, oldLockedAmount, block.timestamp); } - uint _epoch = userPointEpoch[_tokenId]; - if (_epoch == 0) { - return 0; - } else { - Point memory lastPoint = _userPointHistory[_tokenId][_epoch]; - require(_t >= lastPoint.ts, "WRONG_INPUT"); - lastPoint.bias -= lastPoint.slope * int128(int256(_t) - int256(lastPoint.ts)); - if (lastPoint.bias < 0) { - lastPoint.bias = 0; - } - return uint(int256(lastPoint.bias)); + ///////////////////////////////////////////////////////////////////////////////////// + // Attention! + // The following ERC20/minime-compatible methods are not real balanceOf and supply! + // They measure the weights for the purpose of voting, so they don't represent + // real coins. + ///////////////////////////////////////////////////////////////////////////////////// + + /// @notice Get the current voting power for `_tokenId` + /// @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility + /// @param _tokenId NFT for lock + /// @param _t Epoch time to return voting power at + /// @return User voting power + function _balanceOfNFT(uint _tokenId, uint _t) internal view returns (uint) { + // with max lock return balance as is + if (isAlwaysMaxLock[_tokenId]) { + return lockedDerivedAmount[_tokenId]; + } + + uint _epoch = userPointEpoch[_tokenId]; + if (_epoch == 0) { + return 0; + } else { + Point memory lastPoint = _userPointHistory[_tokenId][_epoch]; + require(_t >= lastPoint.ts, "WRONG_INPUT"); + lastPoint.bias -= lastPoint.slope * int128(int(_t) - int(lastPoint.ts)); + if (lastPoint.bias < 0) { + lastPoint.bias = 0; + } + return uint(int(lastPoint.bias)); + } } - } - - /// @dev Returns current token URI metadata - /// @param _tokenId Token ID to fetch URI for. - function tokenURI(uint _tokenId) external view override returns (string memory) { - require(_idToOwner[_tokenId] != address(0), "TOKEN_NOT_EXIST"); - - uint _lockedEnd = lockedEnd(_tokenId); - return - VeSTGNLib.tokenURI( - _tokenId, - uint(int256(lockedDerivedAmount[_tokenId])), - block.timestamp < _lockedEnd ? _lockedEnd - block.timestamp : 0, - _balanceOfNFT(_tokenId, block.timestamp) - ); - } - - /// @notice Measure voting power of `_tokenId` at block height `_block` - /// @dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime - /// @param _tokenId User's wallet NFT - /// @param _block Block to calculate the voting power at - /// @return Voting power - function _balanceOfAtNFT(uint _tokenId, uint _block) internal view returns (uint) { - // for always max lock just return full derived amount - if (isAlwaysMaxLock[_tokenId]) { - return lockedDerivedAmount[_tokenId]; + + /// @dev Returns current token URI metadata + /// @param _tokenId Token ID to fetch URI for. + function tokenURI(uint _tokenId) external view override returns (string memory) { + require(_idToOwner[_tokenId] != address(0), "TOKEN_NOT_EXIST"); + + uint _lockedEnd = lockedEnd(_tokenId); + return VeSTGNLib.tokenURI( + _tokenId, + uint(int(lockedDerivedAmount[_tokenId])), + block.timestamp < _lockedEnd ? _lockedEnd - block.timestamp : 0, + _balanceOfNFT(_tokenId, block.timestamp) + ); } - return VeSTGNLib.balanceOfAtNFT( - _tokenId, - _block, - epoch, - userPointEpoch, - _userPointHistory, - _pointHistory - ); - } - - /// @notice Calculate total voting power - /// @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility - /// @return Total voting power - function totalSupplyAtT(uint t) public view returns (uint) { - uint _epoch = epoch; - Point memory lastPoint = _pointHistory[_epoch]; - return VeSTGNLib.supplyAt(lastPoint, t, slopeChanges) + additionalTotalSupply; - } - - /// @notice Calculate total voting power at some point in the past - /// @param _block Block to calculate the total voting power at - /// @return Total voting power at `_block` - function totalSupplyAt(uint _block) external view override returns (uint) { - return VeSTGNLib.totalSupplyAt( - _block, - epoch, - _pointHistory, - slopeChanges - ) + additionalTotalSupply; - } - - /// @notice Record global data to checkpoint - function checkpoint() external override { - _checkpoint(CheckpointInfo(0, 0, 0, 0, 0, false)); - } - - /// @notice Record global and per-user data to checkpoint - function _checkpoint(CheckpointInfo memory info) internal { - - // we do not need checkpoints for always max lock - if (info.isAlwaysMaxLock) { - return; + /// @notice Measure voting power of `_tokenId` at block height `_block` + /// @dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime + /// @param _tokenId User's wallet NFT + /// @param _block Block to calculate the voting power at + /// @return Voting power + function _balanceOfAtNFT(uint _tokenId, uint _block) internal view returns (uint) { + // for always max lock just return full derived amount + if (isAlwaysMaxLock[_tokenId]) { + return lockedDerivedAmount[_tokenId]; + } + + return VeSTGNLib.balanceOfAtNFT(_tokenId, _block, epoch, userPointEpoch, _userPointHistory, _pointHistory); } - uint _epoch = epoch; - uint newEpoch = VeSTGNLib.checkpoint( - info.tokenId, - info.oldDerivedAmount, - info.newDerivedAmount, - info.oldEnd, - info.newEnd, - _epoch, - slopeChanges, - userPointEpoch, - _userPointHistory, - _pointHistory - ); + /// @notice Calculate total voting power + /// @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility + /// @return Total voting power + function totalSupplyAtT(uint t) public view returns (uint) { + uint _epoch = epoch; + Point memory lastPoint = _pointHistory[_epoch]; + return VeSTGNLib.supplyAt(lastPoint, t, slopeChanges) + additionalTotalSupply; + } + + /// @notice Calculate total voting power at some point in the past + /// @param _block Block to calculate the total voting power at + /// @return Total voting power at `_block` + function totalSupplyAt(uint _block) external view override returns (uint) { + return VeSTGNLib.totalSupplyAt(_block, epoch, _pointHistory, slopeChanges) + additionalTotalSupply; + } - if (newEpoch != 0 && newEpoch != _epoch) { - epoch = newEpoch; + /// @notice Record global data to checkpoint + function checkpoint() external override { + _checkpoint(CheckpointInfo(0, 0, 0, 0, 0, false)); } - } - function _burn(uint _tokenId) internal { - address owner = ownerOf(_tokenId); - // Clear approval - approve(address(0), _tokenId); - // Remove token - _removeTokenFrom(owner, _tokenId); - emit Transfer(owner, address(0), _tokenId); - } + /// @notice Record global and per-user data to checkpoint + function _checkpoint(CheckpointInfo memory info) internal { + // we do not need checkpoints for always max lock + if (info.isAlwaysMaxLock) { + return; + } + + uint _epoch = epoch; + uint newEpoch = VeSTGNLib.checkpoint( + info.tokenId, + info.oldDerivedAmount, + info.newDerivedAmount, + info.oldEnd, + info.newEnd, + _epoch, + slopeChanges, + userPointEpoch, + _userPointHistory, + _pointHistory + ); + + if (newEpoch != 0 && newEpoch != _epoch) { + epoch = newEpoch; + } + } + function _burn(uint _tokenId) internal { + address owner = ownerOf(_tokenId); + // Clear approval + approve(address(0), _tokenId); + // Remove token + _removeTokenFrom(owner, _tokenId); + emit Transfer(owner, address(0), _tokenId); + } } diff --git a/src/Vesting.sol b/src/Vesting.sol index d53e3b0..9a1b278 100644 --- a/src/Vesting.sol +++ b/src/Vesting.sol @@ -5,63 +5,61 @@ import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import "./interfaces/IVesting.sol"; contract Vesting is IVesting { - using SafeERC20 for IERC20; - - /// @dev Token for vesting - IERC20 public token; - /// @dev Will start after the cliff - uint public vestingPeriod; - /// @dev Delay before the vesting - uint public cliffPeriod; - /// @dev Who will receive the tokens - address public claimant; - - uint public startTs; - uint public toDistribute; - - event Started(uint amount, uint time); - event Claimed(address claimer, uint amount); - - constructor() {} - - function setup(address token_, uint vestingPeriod_, uint cliffPeriod_, address claimant_) external { - require (token_ != address(0), "WRONG_INPUT"); - require (address(token) == address(0), "ALREADY"); - token = IERC20(token_); - vestingPeriod = vestingPeriod_; - cliffPeriod = cliffPeriod_; - claimant = claimant_; - } - - function start(uint amount) external { - require(startTs == 0, "Already started"); - - require(IERC20(token).balanceOf(address(this)) == amount, "Incorrect amount"); - - startTs = block.timestamp + cliffPeriod; - toDistribute = amount; - emit Started(amount, block.timestamp); - } - - function claim() external { - address _claimant = claimant; - require(_claimant == msg.sender, "Not claimant"); - require(startTs != 0, "Not started"); - - uint _startTs = startTs; - require(_startTs < block.timestamp, "Too early"); - - uint timeDiff = block.timestamp - _startTs; - uint toClaim = timeDiff * toDistribute / vestingPeriod; - uint balance = token.balanceOf(address(this)); - - toClaim = balance < toClaim ? balance : toClaim; - require(toClaim != 0, "Nothing to claim"); - token.safeTransfer(_claimant, toClaim); - - startTs = block.timestamp; - emit Claimed(_claimant, toClaim); - } - - + using SafeERC20 for IERC20; + + /// @dev Token for vesting + IERC20 public token; + /// @dev Will start after the cliff + uint public vestingPeriod; + /// @dev Delay before the vesting + uint public cliffPeriod; + /// @dev Who will receive the tokens + address public claimant; + + uint public startTs; + uint public toDistribute; + + event Started(uint amount, uint time); + event Claimed(address claimer, uint amount); + + constructor() {} + + function setup(address token_, uint vestingPeriod_, uint cliffPeriod_, address claimant_) external { + require(token_ != address(0), "WRONG_INPUT"); + require(address(token) == address(0), "ALREADY"); + token = IERC20(token_); + vestingPeriod = vestingPeriod_; + cliffPeriod = cliffPeriod_; + claimant = claimant_; + } + + function start(uint amount) external { + require(startTs == 0, "Already started"); + + require(IERC20(token).balanceOf(address(this)) == amount, "Incorrect amount"); + + startTs = block.timestamp + cliffPeriod; + toDistribute = amount; + emit Started(amount, block.timestamp); + } + + function claim() external { + address _claimant = claimant; + require(_claimant == msg.sender, "Not claimant"); + require(startTs != 0, "Not started"); + + uint _startTs = startTs; + require(_startTs < block.timestamp, "Too early"); + + uint timeDiff = block.timestamp - _startTs; + uint toClaim = timeDiff * toDistribute / vestingPeriod; + uint balance = token.balanceOf(address(this)); + + toClaim = balance < toClaim ? balance : toClaim; + require(toClaim != 0, "Nothing to claim"); + token.safeTransfer(_claimant, toClaim); + + startTs = block.timestamp; + emit Claimed(_claimant, toClaim); + } } diff --git a/src/base/Controllable.sol b/src/base/Controllable.sol index c16fc23..8355c1b 100644 --- a/src/base/Controllable.sol +++ b/src/base/Controllable.sol @@ -17,8 +17,8 @@ abstract contract Controllable is Initializable, IControllable { /// @dev Should be incremented when contract changed string public constant CONTROLLABLE_VERSION = "1.0.0"; - bytes32 internal constant _CONTROLLER_SLOT = bytes32(uint256(keccak256("eip1967.controllable.controller")) - 1); - bytes32 internal constant _CREATED_BLOCK_SLOT = bytes32(uint256(keccak256("eip1967.controllable.created_block")) - 1); + bytes32 internal constant _CONTROLLER_SLOT = bytes32(uint(keccak256("eip1967.controllable.controller")) - 1); + bytes32 internal constant _CREATED_BLOCK_SLOT = bytes32(uint(keccak256("eip1967.controllable.created_block")) - 1); event ContractInitialized(address controller, uint ts, uint block); @@ -40,12 +40,12 @@ abstract contract Controllable is Initializable, IControllable { } /// @dev Return true if given address is controller - function isController(address _value) public override view returns (bool) { + function isController(address _value) public view override returns (bool) { return _value == controller(); } /// @notice Return true if given address is setup as governance in Controller - function isGovernance(address _value) public override view returns (bool) { + function isGovernance(address _value) public view override returns (bool) { return IController(controller()).governance() == _value; } @@ -58,7 +58,7 @@ abstract contract Controllable is Initializable, IControllable { /// @notice Return creation block number /// @return Creation block number - function createdBlock() external override view returns (uint256) { + function createdBlock() external view override returns (uint) { return _CREATED_BLOCK_SLOT.getUint(); } diff --git a/src/base/StakelessMultiPoolBase.sol b/src/base/StakelessMultiPoolBase.sol index f17bc86..24f3347 100644 --- a/src/base/StakelessMultiPoolBase.sol +++ b/src/base/StakelessMultiPoolBase.sol @@ -11,351 +11,336 @@ import "./Controllable.sol"; /// Universal pool for different purposes, cover the most popular use cases. /// @author belbix abstract contract StakelessMultiPoolBase is ReentrancyGuard, IMultiPool, Controllable { - using SafeERC20 for IERC20; - - // ************************************************************* - // CONSTANTS - // ************************************************************* - - /// @dev Version of this contract. Adjust manually on each code modification. - string public constant MULTI_POOL_VERSION = "1.0.0"; - /// @dev Precision for internal calculations - uint internal constant _PRECISION = 10 ** 27; - /// @dev Max reward tokens per 1 staking token - uint internal constant _MAX_REWARD_TOKENS = 10; - - // ************************************************************* - // VARIABLES - // Keep names and ordering! - // Add only in the bottom and adjust __gap variable - // ************************************************************* - - /// @dev Rewards are released over this period - uint public duration; - /// @dev This token will be always allowed as reward - address public defaultRewardToken; - - /// @dev Staking token => Supply adjusted on derived balance logic. Use for rewards boost. - mapping(address => uint) public override derivedSupply; - /// @dev Staking token => Account => Staking token virtual balance. Can be adjusted regarding rewards boost logic. - mapping(address => mapping(address => uint)) public override derivedBalances; - /// @dev Staking token => Account => User virtual balance of staking token. - mapping(address => mapping(address => uint)) public override balanceOf; - /// @dev Staking token => Total amount of attached staking tokens - mapping(address => uint) public override totalSupply; - - /// @dev Staking token => Reward token => Reward rate with precision _PRECISION - mapping(address => mapping(address => uint)) public rewardRate; - /// @dev Staking token => Reward token => Reward finish period in timestamp. - mapping(address => mapping(address => uint)) public periodFinish; - /// @dev Staking token => Reward token => Last updated time for reward token for internal calculations. - mapping(address => mapping(address => uint)) public lastUpdateTime; - /// @dev Staking token => Reward token => Part of SNX pool logic. Internal snapshot of reward per token value. - mapping(address => mapping(address => uint)) public rewardPerTokenStored; - - /// @dev Staking token => Reward token => Account => amount. Already paid reward amount for snapshot calculation. - mapping(address => mapping(address => mapping(address => uint))) public userRewardPerTokenPaid; - /// @dev Staking token => Reward token => Account => amount. Snapshot of user's reward per token. - mapping(address => mapping(address => mapping(address => uint))) public rewards; - - /// @dev Allowed reward tokens for staking token - mapping(address => address[]) public override rewardTokens; - /// @dev Allowed reward tokens for staking token stored in map for fast check. - mapping(address => mapping(address => bool)) public override isRewardToken; - /// @notice account => recipient. All rewards for this account will receive recipient - mapping(address => address) public rewardsRedirect; - - /// @dev This empty reserved space is put in place to allow future versions to add new. - /// variables without shifting down storage in the inheritance chain. - /// Total gap == 50 - storage slots used. - uint[50 - 15] private __gap; - - // ************************************************************* - // EVENTS - // ************************************************************* - - event BalanceIncreased(address indexed token, address indexed account, uint amount); - event BalanceDecreased(address indexed token, address indexed account, uint amount); - event NotifyReward(address indexed from, address token, address indexed reward, uint amount); - event ClaimRewards(address indexed account, address token, address indexed reward, uint amount, address recepient); - - // ************************************************************* - // INIT - // ************************************************************* - - function __MultiPool_init( - address controller_, - address _defaultRewardToken, - uint _duration - ) internal onlyInitializing { - __Controllable_init(controller_); - defaultRewardToken = _defaultRewardToken; - require(_duration != 0, "wrong duration"); - duration = _duration; - } - - // ************************************************************* - // RESTRICTIONS - // ************************************************************* - - modifier onlyAllowedContracts() { - _requireGovOrIfo(); - _; - } - - // ************************************************************* - // VIEWS - // ************************************************************* - - /// @dev Should return true for whitelisted reward tokens - function isStakeToken(address token) public view override virtual returns (bool); - - /// @dev Length of rewards tokens array for given token - function rewardTokensLength(address token) external view override returns (uint) { - return rewardTokens[token].length; - } - - /// @dev Reward paid for token for the current period. - function rewardPerToken(address stakingToken, address rewardToken) public view returns (uint) { - uint _derivedSupply = derivedSupply[stakingToken]; - if (_derivedSupply == 0) { - return rewardPerTokenStored[stakingToken][rewardToken]; + using SafeERC20 for IERC20; + + // ************************************************************* + // CONSTANTS + // ************************************************************* + + /// @dev Version of this contract. Adjust manually on each code modification. + string public constant MULTI_POOL_VERSION = "1.0.0"; + /// @dev Precision for internal calculations + uint internal constant _PRECISION = 10 ** 27; + /// @dev Max reward tokens per 1 staking token + uint internal constant _MAX_REWARD_TOKENS = 10; + + // ************************************************************* + // VARIABLES + // Keep names and ordering! + // Add only in the bottom and adjust __gap variable + // ************************************************************* + + /// @dev Rewards are released over this period + uint public duration; + /// @dev This token will be always allowed as reward + address public defaultRewardToken; + + /// @dev Staking token => Supply adjusted on derived balance logic. Use for rewards boost. + mapping(address => uint) public override derivedSupply; + /// @dev Staking token => Account => Staking token virtual balance. Can be adjusted regarding rewards boost logic. + mapping(address => mapping(address => uint)) public override derivedBalances; + /// @dev Staking token => Account => User virtual balance of staking token. + mapping(address => mapping(address => uint)) public override balanceOf; + /// @dev Staking token => Total amount of attached staking tokens + mapping(address => uint) public override totalSupply; + + /// @dev Staking token => Reward token => Reward rate with precision _PRECISION + mapping(address => mapping(address => uint)) public rewardRate; + /// @dev Staking token => Reward token => Reward finish period in timestamp. + mapping(address => mapping(address => uint)) public periodFinish; + /// @dev Staking token => Reward token => Last updated time for reward token for internal calculations. + mapping(address => mapping(address => uint)) public lastUpdateTime; + /// @dev Staking token => Reward token => Part of SNX pool logic. Internal snapshot of reward per token value. + mapping(address => mapping(address => uint)) public rewardPerTokenStored; + + /// @dev Staking token => Reward token => Account => amount. Already paid reward amount for snapshot calculation. + mapping(address => mapping(address => mapping(address => uint))) public userRewardPerTokenPaid; + /// @dev Staking token => Reward token => Account => amount. Snapshot of user's reward per token. + mapping(address => mapping(address => mapping(address => uint))) public rewards; + + /// @dev Allowed reward tokens for staking token + mapping(address => address[]) public override rewardTokens; + /// @dev Allowed reward tokens for staking token stored in map for fast check. + mapping(address => mapping(address => bool)) public override isRewardToken; + /// @notice account => recipient. All rewards for this account will receive recipient + mapping(address => address) public rewardsRedirect; + + /// @dev This empty reserved space is put in place to allow future versions to add new. + /// variables without shifting down storage in the inheritance chain. + /// Total gap == 50 - storage slots used. + uint[50 - 15] private __gap; + + // ************************************************************* + // EVENTS + // ************************************************************* + + event BalanceIncreased(address indexed token, address indexed account, uint amount); + event BalanceDecreased(address indexed token, address indexed account, uint amount); + event NotifyReward(address indexed from, address token, address indexed reward, uint amount); + event ClaimRewards(address indexed account, address token, address indexed reward, uint amount, address recepient); + + // ************************************************************* + // INIT + // ************************************************************* + + function __MultiPool_init( + address controller_, + address _defaultRewardToken, + uint _duration + ) internal onlyInitializing { + __Controllable_init(controller_); + defaultRewardToken = _defaultRewardToken; + require(_duration != 0, "wrong duration"); + duration = _duration; } - return rewardPerTokenStored[stakingToken][rewardToken] - + - (lastTimeRewardApplicable(stakingToken, rewardToken) - lastUpdateTime[stakingToken][rewardToken]) - * rewardRate[stakingToken][rewardToken] - / _derivedSupply; - } - - /// @dev Returns the last time the reward was modified or periodFinish if the reward has ended - function lastTimeRewardApplicable(address stakingToken, address rewardToken) public view returns (uint) { - uint _periodFinish = periodFinish[stakingToken][rewardToken]; - return block.timestamp < _periodFinish ? block.timestamp : _periodFinish; - } - - /// @dev Balance of holder adjusted with specific rules for boost calculation. - /// Supposed to be implemented in a parent contract - /// Adjust user balance with some logic, like boost logic. - function derivedBalance(address stakingToken, address account) public view virtual override returns (uint) { - return balanceOf[stakingToken][account]; - } - - /// @dev Amount of reward tokens left for the current period - function left(address stakingToken, address rewardToken) public view override returns (uint) { - uint _periodFinish = periodFinish[stakingToken][rewardToken]; - if (block.timestamp >= _periodFinish) return 0; - uint _remaining = _periodFinish - block.timestamp; - return _remaining * rewardRate[stakingToken][rewardToken] / _PRECISION; - } - - /// @dev Approximate of earned rewards ready to claim - function earned(address stakingToken, address rewardToken, address account) public view override returns (uint) { - return derivedBalance(stakingToken, account) - * (rewardPerToken(stakingToken, rewardToken) - userRewardPerTokenPaid[stakingToken][rewardToken][account]) - / _PRECISION - + rewards[stakingToken][rewardToken][account]; - } - - // ************************************************************* - // OPERATOR ACTIONS - // ************************************************************* - - /// @dev Whitelist reward token for staking token. Only operator can do it. - function registerRewardToken(address stakeToken, address rewardToken) external override onlyAllowedContracts { - require(rewardTokens[stakeToken].length < _MAX_REWARD_TOKENS, "Too many reward tokens"); - require(!isRewardToken[stakeToken][rewardToken], "Already registered"); - isRewardToken[stakeToken][rewardToken] = true; - rewardTokens[stakeToken].push(rewardToken); - } - - /// @dev Remove from whitelist reward token for staking token. Only operator can do it. - /// We assume that the first token can not be removed. - function removeRewardToken(address stakeToken, address rewardToken) external override onlyAllowedContracts { - require(periodFinish[stakeToken][rewardToken] < block.timestamp, "Rewards not ended"); - require(isRewardToken[stakeToken][rewardToken], "Not reward token"); - - isRewardToken[stakeToken][rewardToken] = false; - uint length = rewardTokens[stakeToken].length; - uint i = 0; - for (; i < length; i++) { - address t = rewardTokens[stakeToken][i]; - if (t == rewardToken) { - break; - } + // ************************************************************* + // RESTRICTIONS + // ************************************************************* + + modifier onlyAllowedContracts() { + _requireGovOrIfo(); + _; + } + + // ************************************************************* + // VIEWS + // ************************************************************* + + /// @dev Should return true for whitelisted reward tokens + function isStakeToken(address token) public view virtual override returns (bool); + + /// @dev Length of rewards tokens array for given token + function rewardTokensLength(address token) external view override returns (uint) { + return rewardTokens[token].length; + } + + /// @dev Reward paid for token for the current period. + function rewardPerToken(address stakingToken, address rewardToken) public view returns (uint) { + uint _derivedSupply = derivedSupply[stakingToken]; + if (_derivedSupply == 0) { + return rewardPerTokenStored[stakingToken][rewardToken]; + } + + return rewardPerTokenStored[stakingToken][rewardToken] + + (lastTimeRewardApplicable(stakingToken, rewardToken) - lastUpdateTime[stakingToken][rewardToken]) + * rewardRate[stakingToken][rewardToken] / _derivedSupply; + } + + /// @dev Returns the last time the reward was modified or periodFinish if the reward has ended + function lastTimeRewardApplicable(address stakingToken, address rewardToken) public view returns (uint) { + uint _periodFinish = periodFinish[stakingToken][rewardToken]; + return block.timestamp < _periodFinish ? block.timestamp : _periodFinish; + } + + /// @dev Balance of holder adjusted with specific rules for boost calculation. + /// Supposed to be implemented in a parent contract + /// Adjust user balance with some logic, like boost logic. + function derivedBalance(address stakingToken, address account) public view virtual override returns (uint) { + return balanceOf[stakingToken][account]; + } + + /// @dev Amount of reward tokens left for the current period + function left(address stakingToken, address rewardToken) public view override returns (uint) { + uint _periodFinish = periodFinish[stakingToken][rewardToken]; + if (block.timestamp >= _periodFinish) return 0; + uint _remaining = _periodFinish - block.timestamp; + return _remaining * rewardRate[stakingToken][rewardToken] / _PRECISION; } - // if isRewardToken map and rewardTokens array changed accordingly the token always exist - rewardTokens[stakeToken][i] = rewardTokens[stakeToken][length - 1]; - rewardTokens[stakeToken].pop(); - } - - /// @dev Account or governance can setup a redirect of all rewards. - /// It needs for 3rd party contracts integrations. - function setRewardsRedirect(address account, address recipient) external { - require(msg.sender == account || isGovernance(msg.sender), "Not allowed"); - rewardsRedirect[account] = recipient; - } - - // ************************************************************* - // BALANCE - // ************************************************************* - - /// @dev Assume to be called when linked token balance changes. - function _registerBalanceIncreasing( - address stakingToken, - address account, - uint amount - ) internal virtual nonReentrant { - require(isStakeToken(stakingToken), "Staking token not allowed"); - require(amount > 0, "Zero amount"); - - _increaseBalance(stakingToken, account, amount); - emit BalanceIncreased(stakingToken, account, amount); - } - - function _increaseBalance( - address stakingToken, - address account, - uint amount - ) internal virtual { - _updateRewardForAllTokens(stakingToken, account); - totalSupply[stakingToken] += amount; - balanceOf[stakingToken][account] += amount; - _updateDerivedBalance(stakingToken, account); - } - - /// @dev Assume to be called when linked token balance changes. - function _registerBalanceDecreasing( - address stakingToken, - address account, - uint amount - ) internal nonReentrant virtual { - require(isStakeToken(stakingToken), "Staking token not allowed"); - _decreaseBalance(stakingToken, account, amount); - emit BalanceDecreased(stakingToken, account, amount); - } - - function _decreaseBalance( - address stakingToken, - address account, - uint amount - ) internal virtual { - _updateRewardForAllTokens(stakingToken, account); - totalSupply[stakingToken] -= amount; - balanceOf[stakingToken][account] -= amount; - _updateDerivedBalance(stakingToken, account); - } - - function _updateDerivedBalance(address stakingToken, address account) internal { - uint __derivedBalance = derivedBalances[stakingToken][account]; - derivedSupply[stakingToken] -= __derivedBalance; - __derivedBalance = derivedBalance(stakingToken, account); - derivedBalances[stakingToken][account] = __derivedBalance; - derivedSupply[stakingToken] += __derivedBalance; - } - - // ************************************************************* - // CLAIM - // ************************************************************* - - /// @dev Caller should implement restriction checks - function _getReward( - address stakingToken, - address account, - address[] memory rewardTokens_, - address recipient - ) internal nonReentrant virtual { - address newRecipient = rewardsRedirect[recipient]; - if (newRecipient != address(0)) { - recipient = newRecipient; + + /// @dev Approximate of earned rewards ready to claim + function earned(address stakingToken, address rewardToken, address account) public view override returns (uint) { + return derivedBalance(stakingToken, account) + * (rewardPerToken(stakingToken, rewardToken) - userRewardPerTokenPaid[stakingToken][rewardToken][account]) + / _PRECISION + rewards[stakingToken][rewardToken][account]; } - require(recipient == msg.sender, "Not allowed"); - _updateDerivedBalance(stakingToken, account); + // ************************************************************* + // OPERATOR ACTIONS + // ************************************************************* + + /// @dev Whitelist reward token for staking token. Only operator can do it. + function registerRewardToken(address stakeToken, address rewardToken) external override onlyAllowedContracts { + require(rewardTokens[stakeToken].length < _MAX_REWARD_TOKENS, "Too many reward tokens"); + require(!isRewardToken[stakeToken][rewardToken], "Already registered"); + isRewardToken[stakeToken][rewardToken] = true; + rewardTokens[stakeToken].push(rewardToken); + } - for (uint i = 0; i < rewardTokens_.length; i++) { - address rewardToken = rewardTokens_[i]; - _updateReward(stakingToken, rewardToken, account); + /// @dev Remove from whitelist reward token for staking token. Only operator can do it. + /// We assume that the first token can not be removed. + function removeRewardToken(address stakeToken, address rewardToken) external override onlyAllowedContracts { + require(periodFinish[stakeToken][rewardToken] < block.timestamp, "Rewards not ended"); + require(isRewardToken[stakeToken][rewardToken], "Not reward token"); + + isRewardToken[stakeToken][rewardToken] = false; + uint length = rewardTokens[stakeToken].length; + uint i = 0; + for (; i < length; i++) { + address t = rewardTokens[stakeToken][i]; + if (t == rewardToken) { + break; + } + } + // if isRewardToken map and rewardTokens array changed accordingly the token always exist + rewardTokens[stakeToken][i] = rewardTokens[stakeToken][length - 1]; + rewardTokens[stakeToken].pop(); + } - uint _reward = rewards[stakingToken][rewardToken][account]; - if (_reward > 0) { - rewards[stakingToken][rewardToken][account] = 0; - IERC20(rewardToken).safeTransfer(recipient, _reward); - } + /// @dev Account or governance can setup a redirect of all rewards. + /// It needs for 3rd party contracts integrations. + function setRewardsRedirect(address account, address recipient) external { + require(msg.sender == account || isGovernance(msg.sender), "Not allowed"); + rewardsRedirect[account] = recipient; + } + + // ************************************************************* + // BALANCE + // ************************************************************* + + /// @dev Assume to be called when linked token balance changes. + function _registerBalanceIncreasing( + address stakingToken, + address account, + uint amount + ) internal virtual nonReentrant { + require(isStakeToken(stakingToken), "Staking token not allowed"); + require(amount > 0, "Zero amount"); + + _increaseBalance(stakingToken, account, amount); + emit BalanceIncreased(stakingToken, account, amount); + } - emit ClaimRewards(account, stakingToken, rewardToken, _reward, recipient); + function _increaseBalance(address stakingToken, address account, uint amount) internal virtual { + _updateRewardForAllTokens(stakingToken, account); + totalSupply[stakingToken] += amount; + balanceOf[stakingToken][account] += amount; + _updateDerivedBalance(stakingToken, account); } - } - // ************************************************************* - // REWARDS CALCULATIONS - // ************************************************************* + /// @dev Assume to be called when linked token balance changes. + function _registerBalanceDecreasing( + address stakingToken, + address account, + uint amount + ) internal virtual nonReentrant { + require(isStakeToken(stakingToken), "Staking token not allowed"); + _decreaseBalance(stakingToken, account, amount); + emit BalanceDecreased(stakingToken, account, amount); + } - function _updateRewardForAllTokens(address stakingToken, address account) internal { - address[] memory rts = rewardTokens[stakingToken]; - uint length = rts.length; - for (uint i; i < length; i++) { - _updateReward(stakingToken, rts[i], account); + function _decreaseBalance(address stakingToken, address account, uint amount) internal virtual { + _updateRewardForAllTokens(stakingToken, account); + totalSupply[stakingToken] -= amount; + balanceOf[stakingToken][account] -= amount; + _updateDerivedBalance(stakingToken, account); } - _updateReward(stakingToken, defaultRewardToken, account); - } - - function _updateReward(address stakingToken, address rewardToken, address account) internal { - uint _rewardPerTokenStored = rewardPerToken(stakingToken, rewardToken); - rewardPerTokenStored[stakingToken][rewardToken] = _rewardPerTokenStored; - lastUpdateTime[stakingToken][rewardToken] = lastTimeRewardApplicable(stakingToken, rewardToken); - if (account != address(0)) { - rewards[stakingToken][rewardToken][account] = earned(stakingToken, rewardToken, account); - userRewardPerTokenPaid[stakingToken][rewardToken][account] = _rewardPerTokenStored; + + function _updateDerivedBalance(address stakingToken, address account) internal { + uint __derivedBalance = derivedBalances[stakingToken][account]; + derivedSupply[stakingToken] -= __derivedBalance; + __derivedBalance = derivedBalance(stakingToken, account); + derivedBalances[stakingToken][account] = __derivedBalance; + derivedSupply[stakingToken] += __derivedBalance; } - } - - // ************************************************************* - // NOTIFY - // ************************************************************* - - function _notifyRewardAmount( - address stakingToken, - address rewardToken, - uint amount, - bool transferRewards - ) internal virtual { - require(amount > 0, "Zero amount"); - require(defaultRewardToken == rewardToken || isRewardToken[stakingToken][rewardToken], "Token not allowed"); - - _updateReward(stakingToken, rewardToken, address(0)); - uint _duration = duration; - - if (transferRewards) { - uint balanceBefore = IERC20(rewardToken).balanceOf(address(this)); - IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), amount); - // refresh amount if token was taxable - amount = IERC20(rewardToken).balanceOf(address(this)) - balanceBefore; + + // ************************************************************* + // CLAIM + // ************************************************************* + + /// @dev Caller should implement restriction checks + function _getReward( + address stakingToken, + address account, + address[] memory rewardTokens_, + address recipient + ) internal virtual nonReentrant { + address newRecipient = rewardsRedirect[recipient]; + if (newRecipient != address(0)) { + recipient = newRecipient; + } + require(recipient == msg.sender, "Not allowed"); + + _updateDerivedBalance(stakingToken, account); + + for (uint i = 0; i < rewardTokens_.length; i++) { + address rewardToken = rewardTokens_[i]; + _updateReward(stakingToken, rewardToken, account); + + uint _reward = rewards[stakingToken][rewardToken][account]; + if (_reward > 0) { + rewards[stakingToken][rewardToken][account] = 0; + IERC20(rewardToken).safeTransfer(recipient, _reward); + } + + emit ClaimRewards(account, stakingToken, rewardToken, _reward, recipient); + } } - // if transferRewards=false need to wisely use it in implementation! - - if (block.timestamp >= periodFinish[stakingToken][rewardToken]) { - rewardRate[stakingToken][rewardToken] = amount * _PRECISION / _duration; - } else { - uint _remaining = periodFinish[stakingToken][rewardToken] - block.timestamp; - uint _left = _remaining * rewardRate[stakingToken][rewardToken]; - // rewards should not extend period infinity, only higher amount allowed - require(amount > _left / _PRECISION, "Amount should be higher than remaining rewards"); - rewardRate[stakingToken][rewardToken] = (amount * _PRECISION + _left) / _duration; + + // ************************************************************* + // REWARDS CALCULATIONS + // ************************************************************* + + function _updateRewardForAllTokens(address stakingToken, address account) internal { + address[] memory rts = rewardTokens[stakingToken]; + uint length = rts.length; + for (uint i; i < length; i++) { + _updateReward(stakingToken, rts[i], account); + } + _updateReward(stakingToken, defaultRewardToken, account); } - lastUpdateTime[stakingToken][rewardToken] = block.timestamp; - periodFinish[stakingToken][rewardToken] = block.timestamp + _duration; - emit NotifyReward(msg.sender, stakingToken, rewardToken, amount); - } + function _updateReward(address stakingToken, address rewardToken, address account) internal { + uint _rewardPerTokenStored = rewardPerToken(stakingToken, rewardToken); + rewardPerTokenStored[stakingToken][rewardToken] = _rewardPerTokenStored; + lastUpdateTime[stakingToken][rewardToken] = lastTimeRewardApplicable(stakingToken, rewardToken); + if (account != address(0)) { + rewards[stakingToken][rewardToken][account] = earned(stakingToken, rewardToken, account); + userRewardPerTokenPaid[stakingToken][rewardToken][account] = _rewardPerTokenStored; + } + } - function _requireGovOrIfo() internal view { - IController _controller = IController(controller()); - require( - msg.sender == _controller.governance() - || msg.sender == _controller.ifo() - , "Not allowed"); - } + // ************************************************************* + // NOTIFY + // ************************************************************* + + function _notifyRewardAmount( + address stakingToken, + address rewardToken, + uint amount, + bool transferRewards + ) internal virtual { + require(amount > 0, "Zero amount"); + require(defaultRewardToken == rewardToken || isRewardToken[stakingToken][rewardToken], "Token not allowed"); + + _updateReward(stakingToken, rewardToken, address(0)); + uint _duration = duration; + + if (transferRewards) { + uint balanceBefore = IERC20(rewardToken).balanceOf(address(this)); + IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), amount); + // refresh amount if token was taxable + amount = IERC20(rewardToken).balanceOf(address(this)) - balanceBefore; + } + // if transferRewards=false need to wisely use it in implementation! + + if (block.timestamp >= periodFinish[stakingToken][rewardToken]) { + rewardRate[stakingToken][rewardToken] = amount * _PRECISION / _duration; + } else { + uint _remaining = periodFinish[stakingToken][rewardToken] - block.timestamp; + uint _left = _remaining * rewardRate[stakingToken][rewardToken]; + // rewards should not extend period infinity, only higher amount allowed + require(amount > _left / _PRECISION, "Amount should be higher than remaining rewards"); + rewardRate[stakingToken][rewardToken] = (amount * _PRECISION + _left) / _duration; + } + + lastUpdateTime[stakingToken][rewardToken] = block.timestamp; + periodFinish[stakingToken][rewardToken] = block.timestamp + _duration; + emit NotifyReward(msg.sender, stakingToken, rewardToken, amount); + } + function _requireGovOrIfo() internal view { + IController _controller = IController(controller()); + require(msg.sender == _controller.governance() || msg.sender == _controller.ifo(), "Not allowed"); + } } diff --git a/src/base/StrategyStrictBase.sol b/src/base/StrategyStrictBase.sol index 54850ce..87d63a3 100644 --- a/src/base/StrategyStrictBase.sol +++ b/src/base/StrategyStrictBase.sol @@ -9,143 +9,141 @@ import "../interfaces/IStrategyStrict.sol"; /// @title Abstract contract for base strict strategy functionality /// @author AlehNat abstract contract StrategyStrictBase is IStrategyStrict { - using SafeERC20 for IERC20; - - // ************************************************************* - // CONSTANTS - // ************************************************************* - - - // ************************************************************* - // ERRORS - // ************************************************************* - - string internal constant WRONG_CONTROLLER = "SB: Wrong controller"; - string internal constant DENIED = "SB: Denied"; - string internal constant TOO_HIGH = "SB: Too high"; - string internal constant IMPACT_TOO_HIGH = "SB: Impact too high"; - string internal constant WRONG_AMOUNT = "SB: Wrong amount"; - string internal constant ALREADY_INITIALIZED = "SB: Already initialized"; - - // ************************************************************* - // VARIABLES - // Keep names and ordering! - // Add only in the bottom. - // ************************************************************* - - /// @dev Underlying asset - address public override asset; - /// @dev Linked vault - address public override vault; - /// @dev Percent of profit for autocompound inside this strategy. - uint public override compoundRatio; - - // ************************************************************* - // EVENTS - // ************************************************************* - - event WithdrawAllToVault(uint amount); - event WithdrawToVault(uint amount, uint sent, uint balance); - event EmergencyExit(address sender, uint amount); - event ManualClaim(address sender); - event InvestAll(uint balance); - event DepositToPool(uint amount); - event WithdrawFromPool(uint amount); - event WithdrawAllFromPool(uint amount); - event Claimed(address token, uint amount); - event CompoundRatioChanged(uint oldValue, uint newValue); - - // ************************************************************* - // INIT - // ************************************************************* - - constructor(address vault_) { - asset = IERC4626(vault_).asset(); - vault = vault_; - } - - // ************************************************************* - // VIEWS - // ************************************************************* - - /// @dev Total amount of underlying assets under control of this strategy. - function totalAssets() public view override returns (uint) { - return IERC20(asset).balanceOf(address(this)) + investedAssets(); - } - - // ************************************************************* - // DEPOSIT/WITHDRAW - // ************************************************************* - - /// @dev Stakes everything the strategy holds into the reward pool. - function investAll() external override { - require(msg.sender == vault, DENIED); - address _asset = asset; // gas saving - uint balance = IERC20(_asset).balanceOf(address(this)); - if (balance > 0) { - _depositToPool(balance); + using SafeERC20 for IERC20; + + // ************************************************************* + // CONSTANTS + // ************************************************************* + + // ************************************************************* + // ERRORS + // ************************************************************* + + string internal constant WRONG_CONTROLLER = "SB: Wrong controller"; + string internal constant DENIED = "SB: Denied"; + string internal constant TOO_HIGH = "SB: Too high"; + string internal constant IMPACT_TOO_HIGH = "SB: Impact too high"; + string internal constant WRONG_AMOUNT = "SB: Wrong amount"; + string internal constant ALREADY_INITIALIZED = "SB: Already initialized"; + + // ************************************************************* + // VARIABLES + // Keep names and ordering! + // Add only in the bottom. + // ************************************************************* + + /// @dev Underlying asset + address public override asset; + /// @dev Linked vault + address public override vault; + /// @dev Percent of profit for autocompound inside this strategy. + uint public override compoundRatio; + + // ************************************************************* + // EVENTS + // ************************************************************* + + event WithdrawAllToVault(uint amount); + event WithdrawToVault(uint amount, uint sent, uint balance); + event EmergencyExit(address sender, uint amount); + event ManualClaim(address sender); + event InvestAll(uint balance); + event DepositToPool(uint amount); + event WithdrawFromPool(uint amount); + event WithdrawAllFromPool(uint amount); + event Claimed(address token, uint amount); + event CompoundRatioChanged(uint oldValue, uint newValue); + + // ************************************************************* + // INIT + // ************************************************************* + + constructor(address vault_) { + asset = IERC4626(vault_).asset(); + vault = vault_; } - emit InvestAll(balance); - } - - /// @dev Withdraws all underlying assets to the vault - function withdrawAllToVault() external override { - address _vault = vault; - address _asset = asset; // gas saving - require(msg.sender == _vault, DENIED); - _withdrawAllFromPool(); - uint balance = IERC20(_asset).balanceOf(address(this)); - - if (balance != 0) { - IERC20(_asset).safeTransfer(_vault, balance); + + // ************************************************************* + // VIEWS + // ************************************************************* + + /// @dev Total amount of underlying assets under control of this strategy. + function totalAssets() public view override returns (uint) { + return IERC20(asset).balanceOf(address(this)) + investedAssets(); } - emit WithdrawAllToVault(balance); - } - - /// @dev Withdraws some assets to the vault - function withdrawToVault(uint amount) external override { - address _vault = vault; - address _asset = asset; // gas saving - require(msg.sender == _vault, DENIED); - uint balance = IERC20(_asset).balanceOf(address(this)); - if (amount > balance) { - _withdrawFromPool(amount - balance); - balance = IERC20(_asset).balanceOf(address(this)); + + // ************************************************************* + // DEPOSIT/WITHDRAW + // ************************************************************* + + /// @dev Stakes everything the strategy holds into the reward pool. + function investAll() external override { + require(msg.sender == vault, DENIED); + address _asset = asset; // gas saving + uint balance = IERC20(_asset).balanceOf(address(this)); + if (balance > 0) { + _depositToPool(balance); + } + emit InvestAll(balance); } - uint amountAdjusted = Math.min(amount, balance); - if (amountAdjusted != 0) { - IERC20(_asset).safeTransfer(_vault, amountAdjusted); + /// @dev Withdraws all underlying assets to the vault + function withdrawAllToVault() external override { + address _vault = vault; + address _asset = asset; // gas saving + require(msg.sender == _vault, DENIED); + _withdrawAllFromPool(); + uint balance = IERC20(_asset).balanceOf(address(this)); + + if (balance != 0) { + IERC20(_asset).safeTransfer(_vault, balance); + } + emit WithdrawAllToVault(balance); } - emit WithdrawToVault(amount, amountAdjusted, balance); - } - // ************************************************************* - // VIRTUAL - // These functions must be implemented in the strategy contract - // ************************************************************* + /// @dev Withdraws some assets to the vault + function withdrawToVault(uint amount) external override { + address _vault = vault; + address _asset = asset; // gas saving + require(msg.sender == _vault, DENIED); + uint balance = IERC20(_asset).balanceOf(address(this)); + if (amount > balance) { + _withdrawFromPool(amount - balance); + balance = IERC20(_asset).balanceOf(address(this)); + } + + uint amountAdjusted = Math.min(amount, balance); + if (amountAdjusted != 0) { + IERC20(_asset).safeTransfer(_vault, amountAdjusted); + } + emit WithdrawToVault(amount, amountAdjusted, balance); + } - /// @dev Amount of underlying assets invested to the pool. - function investedAssets() public view virtual returns (uint); + // ************************************************************* + // VIRTUAL + // These functions must be implemented in the strategy contract + // ************************************************************* - /// @dev Deposit given amount to the pool. - function _depositToPool(uint amount) internal virtual; + /// @dev Amount of underlying assets invested to the pool. + function investedAssets() public view virtual returns (uint); - /// @dev Withdraw given amount from the pool. - /// @return investedAssetsUSD Sum of USD value of each asset in the pool that was withdrawn, decimals of {asset}. - /// @return assetPrice Price of the strategy {asset}. - function _withdrawFromPool(uint amount) internal virtual returns (uint investedAssetsUSD, uint assetPrice); + /// @dev Deposit given amount to the pool. + function _depositToPool(uint amount) internal virtual; - /// @dev Withdraw all from the pool. - /// @return investedAssetsUSD Sum of USD value of each asset in the pool that was withdrawn, decimals of {asset}. - /// @return assetPrice Price of the strategy {asset}. - function _withdrawAllFromPool() internal virtual returns (uint investedAssetsUSD, uint assetPrice); + /// @dev Withdraw given amount from the pool. + //return investedAssetsUSD Sum of USD value of each asset in the pool that was withdrawn, decimals of {asset}. + //return assetPrice Price of the strategy {asset}. + function _withdrawFromPool(uint amount) internal virtual; /* returns (uint investedAssetsUSD, uint assetPrice)*/ - /// @dev If pool support emergency withdraw need to call it for emergencyExit() - /// Withdraw assets without impact checking. - function _emergencyExitFromPool() internal virtual; + /// @dev Withdraw all from the pool. + //return investedAssetsUSD Sum of USD value of each asset in the pool that was withdrawn, decimals of {asset}. + //return assetPrice Price of the strategy {asset}. + function _withdrawAllFromPool() internal virtual; /* returns (uint investedAssetsUSD, uint assetPrice)*/ - /// @dev Claim all possible rewards. - function _claim() internal virtual; + /// @dev If pool support emergency withdraw need to call it for emergencyExit() + /// Withdraw assets without impact checking. + function _emergencyExitFromPool() internal virtual; + /// @dev Claim all possible rewards. + function _claim() internal virtual returns (uint rtReward); } diff --git a/src/base/UpgradeableProxy.sol b/src/base/UpgradeableProxy.sol index 3739487..2338b82 100644 --- a/src/base/UpgradeableProxy.sol +++ b/src/base/UpgradeableProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.21; - abstract contract UpgradeableProxy { - /// @dev This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; @@ -54,26 +52,22 @@ abstract contract UpgradeableProxy { */ function _delegate(address implementation) internal virtual { assembly { - // Copy msg.data. We take full control of memory in this inline assembly - // block because it will not return to Solidity code. We overwrite the - // Solidity scratch pad at memory position 0. + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. calldatacopy(0, 0, calldatasize()) - // Call the implementation. - // out and outsize are 0 because we don't know the size yet. + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) - // Copy the returned data. + // Copy the returned data. returndatacopy(0, 0, returndatasize()) switch result // delegatecall returns 0 on error. - case 0 { - revert(0, returndatasize()) - } - default { - return (0, returndatasize()) - } + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } } } diff --git a/src/interfaces/IControllable.sol b/src/interfaces/IControllable.sol index d65a22d..abb2f3a 100644 --- a/src/interfaces/IControllable.sol +++ b/src/interfaces/IControllable.sol @@ -2,13 +2,11 @@ pragma solidity ^0.8.21; interface IControllable { - function isController(address _contract) external view returns (bool); function isGovernance(address _contract) external view returns (bool); - function createdBlock() external view returns (uint256); + function createdBlock() external view returns (uint); function controller() external view returns (address); - } diff --git a/src/interfaces/IController.sol b/src/interfaces/IController.sol index 130273e..4915563 100644 --- a/src/interfaces/IController.sol +++ b/src/interfaces/IController.sol @@ -2,37 +2,36 @@ pragma solidity ^0.8.21; interface IController { + // --- DEPENDENCY ADDRESSES + function governance() external view returns (address); - // --- DEPENDENCY ADDRESSES - function governance() external view returns (address); + function stgn() external view returns (address); -// function voter() external view returns (address); + /// @notice A dedicated solution for swap tokens via different chains. + function liquidator() external view returns (address); - /// @notice A dedicated solution for swap tokens via different chains. - function liquidator() external view returns (address); + function ifo() external view returns (address); - function ifo() external view returns (address); + // function investFund() external view returns (address); -// function investFund() external view returns (address); + /// @notice Proxy contract for distribute profit to ve holders. + function veDistributor() external view returns (address); - /// @notice Proxy contract for distribute profit to ve holders. - function veDistributor() external view returns (address); + function multigauge() external view returns (address); -// function platformVoter() external view returns (address); + // function platformVoter() external view returns (address); - // --- VAULTS + // --- VAULTS - function vaults(uint id) external view returns (address); + function vaults(uint id) external view returns (address); - function vaultsList() external view returns (address[] memory); + function vaultsList() external view returns (address[] memory); - function vaultsListLength() external view returns (uint); + function vaultsListLength() external view returns (uint); - function isValidVault(address _vault) external view returns (bool); - - // --- restrictions - - function isOperator(address _adr) external view returns (bool); + function isValidVault(address _vault) external view returns (bool); + // --- restrictions + function isOperator(address _adr) external view returns (bool); } diff --git a/src/interfaces/IGauge.sol b/src/interfaces/IGauge.sol index a1102a9..f3144e8 100644 --- a/src/interfaces/IGauge.sol +++ b/src/interfaces/IGauge.sol @@ -2,33 +2,21 @@ pragma solidity ^0.8.21; interface IGauge { + function veIds(address stakingToken, address account) external view returns (uint); - function veIds(address stakingToken, address account) external view returns (uint); + function getReward(address stakingToken, address account, address[] memory tokens) external; - function getReward( - address stakingToken, - address account, - address[] memory tokens - ) external; + function getAllRewards(address stakingToken, address account) external; - function getAllRewards( - address stakingToken, - address account - ) external; + function getAllRewardsForTokens(address[] memory stakingTokens, address account) external; - function getAllRewardsForTokens( - address[] memory stakingTokens, - address account - ) external; + function attachVe(address stakingToken, address account, uint veId) external; - function attachVe(address stakingToken, address account, uint veId) external; + function detachVe(address stakingToken, address account, uint veId) external; - function detachVe(address stakingToken, address account, uint veId) external; + function handleBalanceChange(address account) external; - function handleBalanceChange(address account) external; - - function notifyRewardAmount(address stakingToken, address token, uint amount) external; - - function addStakingToken(address token) external; + function notifyRewardAmount(address stakingToken, address token, uint amount) external; + function addStakingToken(address token) external; } diff --git a/src/interfaces/IIFO.sol b/src/interfaces/IIFO.sol new file mode 100644 index 0000000..8b1e7cb --- /dev/null +++ b/src/interfaces/IIFO.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.21; + +interface IIFO { + function exchange(uint amount) external returns (bool, uint); +} diff --git a/src/interfaces/IMultiPool.sol b/src/interfaces/IMultiPool.sol index c88c060..dddee92 100644 --- a/src/interfaces/IMultiPool.sol +++ b/src/interfaces/IMultiPool.sol @@ -2,31 +2,29 @@ pragma solidity ^0.8.21; interface IMultiPool { + function totalSupply(address stakingToken) external view returns (uint); - function totalSupply(address stakingToken) external view returns (uint); + function derivedSupply(address stakingToken) external view returns (uint); - function derivedSupply(address stakingToken) external view returns (uint); + function derivedBalances(address stakingToken, address account) external view returns (uint); - function derivedBalances(address stakingToken, address account) external view returns (uint); + function balanceOf(address stakingToken, address account) external view returns (uint); - function balanceOf(address stakingToken, address account) external view returns (uint); + function rewardTokens(address stakingToken, uint id) external view returns (address); - function rewardTokens(address stakingToken, uint id) external view returns (address); + function isRewardToken(address stakingToken, address token) external view returns (bool); - function isRewardToken(address stakingToken, address token) external view returns (bool); + function rewardTokensLength(address stakingToken) external view returns (uint); - function rewardTokensLength(address stakingToken) external view returns (uint); + function derivedBalance(address stakingToken, address account) external view returns (uint); - function derivedBalance(address stakingToken, address account) external view returns (uint); + function left(address stakingToken, address token) external view returns (uint); - function left(address stakingToken, address token) external view returns (uint); + function earned(address stakingToken, address token, address account) external view returns (uint); - function earned(address stakingToken, address token, address account) external view returns (uint); + function registerRewardToken(address stakingToken, address token) external; - function registerRewardToken(address stakingToken, address token) external; - - function removeRewardToken(address stakingToken, address token) external; - - function isStakeToken(address token) external view returns (bool); + function removeRewardToken(address stakingToken, address token) external; + function isStakeToken(address token) external view returns (bool); } diff --git a/src/interfaces/IPearlGaugeV2.sol b/src/interfaces/IPearlGaugeV2.sol new file mode 100644 index 0000000..4d636dd --- /dev/null +++ b/src/interfaces/IPearlGaugeV2.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +interface IPearlGaugeV2 { + function TOKEN() external view returns (address); + + function rewardToken() external view returns (address); + + function balanceOf(address account) external view returns (uint); + + ///@notice see earned rewards for user + function earned(address account) external view returns (uint); + + ///@notice deposit amount TOKEN + function deposit(uint amount) external; + + ///@notice withdraw a certain amount of TOKEN + function withdraw(uint amount) external; + + ///@notice User harvest function + function getReward() external; +} diff --git a/src/interfaces/IProxyControlled.sol b/src/interfaces/IProxyControlled.sol index 12fb9a4..593c594 100644 --- a/src/interfaces/IProxyControlled.sol +++ b/src/interfaces/IProxyControlled.sol @@ -2,11 +2,9 @@ pragma solidity ^0.8.21; interface IProxyControlled { + function initProxy(address _logic) external; - function initProxy(address _logic) external; - - function upgrade(address _newImplementation) external; - - function implementation() external view returns (address); + function upgrade(address _newImplementation) external; + function implementation() external view returns (address); } diff --git a/src/interfaces/IStrategyStrict.sol b/src/interfaces/IStrategyStrict.sol index 5fc8efd..d97d55d 100644 --- a/src/interfaces/IStrategyStrict.sol +++ b/src/interfaces/IStrategyStrict.sol @@ -2,26 +2,24 @@ pragma solidity ^0.8.21; interface IStrategyStrict { + function asset() external view returns (address); - function asset() external view returns (address); + function vault() external view returns (address); - function vault() external view returns (address); + function compoundRatio() external view returns (uint); - function compoundRatio() external view returns (uint); + function totalAssets() external view returns (uint); - function totalAssets() external view returns (uint); + function lastHardWork() external view returns (uint); - function lastHardWork() external view returns (uint); + /// @dev Usually, indicate that claimable rewards have reasonable amount. + function isReadyToHardWork() external view returns (bool); - /// @dev Usually, indicate that claimable rewards have reasonable amount. - function isReadyToHardWork() external view returns (bool); + function withdrawAllToVault() external; - function withdrawAllToVault() external; + function withdrawToVault(uint amount) external; - function withdrawToVault(uint amount) external; - - function investAll() external; - - function doHardWork() external returns (uint earned, uint lost); + function investAll() external; + function doHardWork() external; /* returns (uint earned, uint lost)*/ } diff --git a/src/interfaces/IVault.sol b/src/interfaces/IVault.sol index 329efef..a373d3a 100644 --- a/src/interfaces/IVault.sol +++ b/src/interfaces/IVault.sol @@ -6,5 +6,5 @@ import "./IStrategyStrict.sol"; interface IVault is IERC4626 { function strategy() external view returns (IStrategyStrict); - -} \ No newline at end of file + function controller() external view returns (address); +} diff --git a/src/interfaces/IVe.sol b/src/interfaces/IVe.sol index 3cb6f18..519ab49 100644 --- a/src/interfaces/IVe.sol +++ b/src/interfaces/IVe.sol @@ -4,50 +4,49 @@ pragma solidity ^0.8.21; import "openzeppelin-contracts/contracts/token/ERC721/extensions/IERC721Metadata.sol"; interface IVe is IERC721Metadata { + enum DepositType { + DEPOSIT_FOR_TYPE, + CREATE_LOCK_TYPE, + INCREASE_LOCK_AMOUNT, + INCREASE_UNLOCK_TIME, + MERGE_TYPE + } - enum DepositType { - DEPOSIT_FOR_TYPE, - CREATE_LOCK_TYPE, - INCREASE_LOCK_AMOUNT, - INCREASE_UNLOCK_TIME, - MERGE_TYPE - } + struct Point { + int128 bias; + int128 slope; // # -dweight / dt + uint ts; + uint blk; // block + } + /* We cannot really do block numbers per se b/c slope is per time, not per block + * and per block could be fairly bad b/c Ethereum changes blocktimes. + * What we can do is to extrapolate ***At functions */ - struct Point { - int128 bias; - int128 slope; // # -dweight / dt - uint ts; - uint blk; // block - } - /* We cannot really do block numbers per se b/c slope is per time, not per block - * and per block could be fairly bad b/c Ethereum changes blocktimes. - * What we can do is to extrapolate ***At functions */ + function lockedAmounts(uint veId, address stakingToken) external view returns (uint); - function lockedAmounts(uint veId, address stakingToken) external view returns (uint); + function lockedDerivedAmount(uint veId) external view returns (uint); - function lockedDerivedAmount(uint veId) external view returns (uint); + function lockedEnd(uint veId) external view returns (uint); - function lockedEnd(uint veId) external view returns (uint); + function tokens(uint idx) external view returns (address); - function tokens(uint idx) external view returns (address); + function balanceOfNFT(uint) external view returns (uint); - function balanceOfNFT(uint) external view returns (uint); + function isApprovedOrOwner(address, uint) external view returns (bool); - function isApprovedOrOwner(address, uint) external view returns (bool); + function createLockFor(address _token, uint _value, uint _lockDuration, address _to) external returns (uint); - function createLockFor(address _token, uint _value, uint _lockDuration, address _to) external returns (uint); + function userPointEpoch(uint tokenId) external view returns (uint); - function userPointEpoch(uint tokenId) external view returns (uint); + function epoch() external view returns (uint); - function epoch() external view returns (uint); + function userPointHistory(uint tokenId, uint loc) external view returns (Point memory); - function userPointHistory(uint tokenId, uint loc) external view returns (Point memory); + function pointHistory(uint loc) external view returns (Point memory); - function pointHistory(uint loc) external view returns (Point memory); + function checkpoint() external; - function checkpoint() external; + function increaseAmount(address _token, uint _tokenId, uint _value) external; - function increaseAmount(address _token, uint _tokenId, uint _value) external; - - function totalSupplyAt(uint _block) external view returns (uint); + function totalSupplyAt(uint _block) external view returns (uint); } diff --git a/src/interfaces/IVeDistributor.sol b/src/interfaces/IVeDistributor.sol index 8c55def..755d79c 100644 --- a/src/interfaces/IVeDistributor.sol +++ b/src/interfaces/IVeDistributor.sol @@ -2,13 +2,11 @@ pragma solidity ^0.8.21; interface IVeDistributor { + function rewardToken() external view returns (address); - function rewardToken() external view returns (address); + function checkpoint() external; - function checkpoint() external; - - function checkpointTotalSupply() external; - - function claim(uint _tokenId) external returns (uint); + function checkpointTotalSupply() external; + function claim(uint _tokenId) external returns (uint); } diff --git a/src/lib/SlotsLib.sol b/src/lib/SlotsLib.sol index 8bb8b41..824fc0a 100644 --- a/src/lib/SlotsLib.sol +++ b/src/lib/SlotsLib.sol @@ -4,122 +4,120 @@ pragma solidity ^0.8.21; /// @title Library for setting / getting slot variables (used in upgradable proxy contracts) /// @author bogdoslav library SlotsLib { + /// @notice Version of the contract + /// @dev Should be incremented when contract changed + string public constant SLOT_LIB_VERSION = "1.0.0"; - /// @notice Version of the contract - /// @dev Should be incremented when contract changed - string public constant SLOT_LIB_VERSION = "1.0.0"; + // ************* GETTERS ******************* - // ************* GETTERS ******************* - - /// @dev Gets a slot as bytes32 - function getBytes32(bytes32 slot) internal view returns (bytes32 result) { - assembly { - result := sload(slot) + /// @dev Gets a slot as bytes32 + function getBytes32(bytes32 slot) internal view returns (bytes32 result) { + assembly { + result := sload(slot) + } } - } - /// @dev Gets a slot as an address - function getAddress(bytes32 slot) internal view returns (address result) { - assembly { - result := sload(slot) + /// @dev Gets a slot as an address + function getAddress(bytes32 slot) internal view returns (address result) { + assembly { + result := sload(slot) + } } - } - /// @dev Gets a slot as uint256 - function getUint(bytes32 slot) internal view returns (uint result) { - assembly { - result := sload(slot) + /// @dev Gets a slot as uint256 + function getUint(bytes32 slot) internal view returns (uint result) { + assembly { + result := sload(slot) + } } - } - // ************* ARRAY GETTERS ******************* + // ************* ARRAY GETTERS ******************* - /// @dev Gets an array length - function arrayLength(bytes32 slot) internal view returns (uint result) { - assembly { - result := sload(slot) + /// @dev Gets an array length + function arrayLength(bytes32 slot) internal view returns (uint result) { + assembly { + result := sload(slot) + } } - } - - /// @dev Gets a slot array by index as address - /// @notice First slot is array length, elements ordered backward in memory - /// @notice This is unsafe, without checking array length. - function addressAt(bytes32 slot, uint index) internal view returns (address result) { - bytes32 pointer = bytes32(uint(slot) - 1 - index); - assembly { - result := sload(pointer) + + /// @dev Gets a slot array by index as address + /// @notice First slot is array length, elements ordered backward in memory + /// @notice This is unsafe, without checking array length. + function addressAt(bytes32 slot, uint index) internal view returns (address result) { + bytes32 pointer = bytes32(uint(slot) - 1 - index); + assembly { + result := sload(pointer) + } } - } - - /// @dev Gets a slot array by index as uint - /// @notice First slot is array length, elements ordered backward in memory - /// @notice This is unsafe, without checking array length. - function uintAt(bytes32 slot, uint index) internal view returns (uint result) { - bytes32 pointer = bytes32(uint(slot) - 1 - index); - assembly { - result := sload(pointer) + + /// @dev Gets a slot array by index as uint + /// @notice First slot is array length, elements ordered backward in memory + /// @notice This is unsafe, without checking array length. + function uintAt(bytes32 slot, uint index) internal view returns (uint result) { + bytes32 pointer = bytes32(uint(slot) - 1 - index); + assembly { + result := sload(pointer) + } } - } - // ************* SETTERS ******************* + // ************* SETTERS ******************* - /// @dev Sets a slot with bytes32 - /// @notice Check address for 0 at the setter - function set(bytes32 slot, bytes32 value) internal { - assembly { - sstore(slot, value) + /// @dev Sets a slot with bytes32 + /// @notice Check address for 0 at the setter + function set(bytes32 slot, bytes32 value) internal { + assembly { + sstore(slot, value) + } } - } - /// @dev Sets a slot with address - /// @notice Check address for 0 at the setter - function set(bytes32 slot, address value) internal { - assembly { - sstore(slot, value) + /// @dev Sets a slot with address + /// @notice Check address for 0 at the setter + function set(bytes32 slot, address value) internal { + assembly { + sstore(slot, value) + } } - } - /// @dev Sets a slot with uint - function set(bytes32 slot, uint value) internal { - assembly { - sstore(slot, value) + /// @dev Sets a slot with uint + function set(bytes32 slot, uint value) internal { + assembly { + sstore(slot, value) + } } - } - // ************* ARRAY SETTERS ******************* + // ************* ARRAY SETTERS ******************* - /// @dev Sets a slot array at index with address - /// @notice First slot is array length, elements ordered backward in memory - /// @notice This is unsafe, without checking array length. - function setAt(bytes32 slot, uint index, address value) internal { - bytes32 pointer = bytes32(uint(slot) - 1 - index); - assembly { - sstore(pointer, value) + /// @dev Sets a slot array at index with address + /// @notice First slot is array length, elements ordered backward in memory + /// @notice This is unsafe, without checking array length. + function setAt(bytes32 slot, uint index, address value) internal { + bytes32 pointer = bytes32(uint(slot) - 1 - index); + assembly { + sstore(pointer, value) + } } - } - - /// @dev Sets a slot array at index with uint - /// @notice First slot is array length, elements ordered backward in memory - /// @notice This is unsafe, without checking array length. - function setAt(bytes32 slot, uint index, uint value) internal { - bytes32 pointer = bytes32(uint(slot) - 1 - index); - assembly { - sstore(pointer, value) - } - } - /// @dev Sets an array length - function setLength(bytes32 slot, uint length) internal { - assembly { - sstore(slot, length) + /// @dev Sets a slot array at index with uint + /// @notice First slot is array length, elements ordered backward in memory + /// @notice This is unsafe, without checking array length. + function setAt(bytes32 slot, uint index, uint value) internal { + bytes32 pointer = bytes32(uint(slot) - 1 - index); + assembly { + sstore(pointer, value) + } } - } - /// @dev Pushes an address to the array - function push(bytes32 slot, address value) internal { - uint length = arrayLength(slot); - setAt(slot, length, value); - setLength(slot, length + 1); - } + /// @dev Sets an array length + function setLength(bytes32 slot, uint length) internal { + assembly { + sstore(slot, length) + } + } + /// @dev Pushes an address to the array + function push(bytes32 slot, address value) internal { + uint length = arrayLength(slot); + setAt(slot, length, value); + setLength(slot, length + 1); + } } diff --git a/src/lib/StringLib.sol b/src/lib/StringLib.sol index 9f40aeb..1188644 100644 --- a/src/lib/StringLib.sol +++ b/src/lib/StringLib.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.21; - library StringLib { function toAsciiString(address x) internal pure returns (string memory) { bytes memory s = new bytes(40); @@ -19,4 +18,4 @@ library StringLib { if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); else return bytes1(uint8(b) + 0x57); } -} \ No newline at end of file +} diff --git a/src/lib/VeSTGNLib.sol b/src/lib/VeSTGNLib.sol index c85dfc4..c7a2015 100644 --- a/src/lib/VeSTGNLib.sol +++ b/src/lib/VeSTGNLib.sol @@ -10,369 +10,422 @@ import "../interfaces/IVe.sol"; /// @author belbix /// @author a17 library VeSTGNLib { - using Math for uint; - - uint internal constant WEEK = 1 weeks; - uint internal constant MULTIPLIER = 1 ether; - int128 internal constant I_MAX_TIME = 16 weeks; - uint internal constant WEIGHT_DENOMINATOR = 100e18; - - // Only for internal usage - struct CheckpointInfo { - uint tokenId; - uint oldDerivedAmount; - uint newDerivedAmount; - uint oldEnd; - uint newEnd; - uint epoch; - IVe.Point uOld; - IVe.Point uNew; - int128 oldDSlope; - int128 newDSlope; - } - - //////////////////////////////////////////////////// - // MAIN LOGIC - //////////////////////////////////////////////////// - - function calculateDerivedAmount( - uint currentAmount, - uint oldDerivedAmount, - uint newAmount, - uint weight, - uint8 decimals - ) internal pure returns (uint) { - // subtract current derived balance - // rounded to UP for subtracting closer to 0 value - if (oldDerivedAmount != 0 && currentAmount != 0) { - currentAmount = currentAmount.mulDiv(1e18, 10 ** decimals, Math.Rounding.Ceil); - uint currentDerivedAmount = currentAmount.mulDiv(weight, WEIGHT_DENOMINATOR, Math.Rounding.Ceil); - if (oldDerivedAmount > currentDerivedAmount) { - oldDerivedAmount -= currentDerivedAmount; - } else { - // in case of wrong rounding better to set to zero than revert - oldDerivedAmount = 0; - } + using Math for uint; + + uint internal constant WEEK = 1 weeks; + uint internal constant MULTIPLIER = 1 ether; + int128 internal constant I_MAX_TIME = 16 weeks; + uint internal constant WEIGHT_DENOMINATOR = 100e18; + + // Only for internal usage + struct CheckpointInfo { + uint tokenId; + uint oldDerivedAmount; + uint newDerivedAmount; + uint oldEnd; + uint newEnd; + uint epoch; + IVe.Point uOld; + IVe.Point uNew; + int128 oldDSlope; + int128 newDSlope; } - // recalculate derived amount with new amount - // rounded to DOWN - // normalize decimals to 18 - newAmount = newAmount.mulDiv(1e18, 10 ** decimals, Math.Rounding.Floor); - // calculate the final amount based on the weight - newAmount = newAmount.mulDiv(weight, WEIGHT_DENOMINATOR, Math.Rounding.Floor); - return oldDerivedAmount + newAmount; - } - - /// @notice Binary search to estimate timestamp for block number - /// @param _block Block to find - /// @param maxEpoch Don't go beyond this epoch - /// @return Approximate timestamp for block - function findBlockEpoch(uint _block, uint maxEpoch, mapping(uint => IVe.Point) storage _pointHistory) public view returns (uint) { - // Binary search - uint _min = 0; - uint _max = maxEpoch; - for (uint i = 0; i < 128; ++i) { - // Will be always enough for 128-bit numbers - if (_min >= _max) { - break; - } - uint _mid = (_min + _max + 1) / 2; - if (_pointHistory[_mid].blk <= _block) { - _min = _mid; - } else { - _max = _mid - 1; - } - } - return _min; - } - - /// @notice Measure voting power of `_tokenId` at block height `_block` - /// @return Voting power - function balanceOfAtNFT( - uint _tokenId, - uint _block, - uint maxEpoch, - mapping(uint => uint) storage userPointEpoch, - mapping(uint => IVe.Point[1000000000]) storage _userPointHistory, - mapping(uint => IVe.Point) storage _pointHistory - ) external view returns (uint) { - // Copying and pasting totalSupply code because Vyper cannot pass by - // reference yet - require(_block <= block.number, "WRONG_INPUT"); - - // Binary search - uint _min = 0; - uint _max = userPointEpoch[_tokenId]; - for (uint i = 0; i < 128; ++i) { - // Will be always enough for 128-bit numbers - if (_min >= _max) { - break; - } - uint _mid = (_min + _max + 1) / 2; - if (_userPointHistory[_tokenId][_mid].blk <= _block) { - _min = _mid; - } else { - _max = _mid - 1; - } - } + //////////////////////////////////////////////////// + // MAIN LOGIC + //////////////////////////////////////////////////// + + function calculateDerivedAmount( + uint currentAmount, + uint oldDerivedAmount, + uint newAmount, + uint weight, + uint8 decimals + ) internal pure returns (uint) { + // subtract current derived balance + // rounded to UP for subtracting closer to 0 value + if (oldDerivedAmount != 0 && currentAmount != 0) { + currentAmount = currentAmount.mulDiv(1e18, 10 ** decimals, Math.Rounding.Ceil); + uint currentDerivedAmount = currentAmount.mulDiv(weight, WEIGHT_DENOMINATOR, Math.Rounding.Ceil); + if (oldDerivedAmount > currentDerivedAmount) { + oldDerivedAmount -= currentDerivedAmount; + } else { + // in case of wrong rounding better to set to zero than revert + oldDerivedAmount = 0; + } + } - IVe.Point memory uPoint = _userPointHistory[_tokenId][_min]; - - uint _epoch = findBlockEpoch(_block, maxEpoch, _pointHistory); - IVe.Point memory point0 = _pointHistory[_epoch]; - uint dBlock = 0; - uint dt = 0; - if (_epoch < maxEpoch) { - IVe.Point memory point1 = _pointHistory[_epoch + 1]; - dBlock = point1.blk - point0.blk; - dt = point1.ts - point0.ts; - } else { - dBlock = block.number - point0.blk; - dt = block.timestamp - point0.ts; - } - uint blockTime = point0.ts; - if (dBlock != 0 && _block > point0.blk) { - blockTime += (dt * (_block - point0.blk)) / dBlock; + // recalculate derived amount with new amount + // rounded to DOWN + // normalize decimals to 18 + newAmount = newAmount.mulDiv(1e18, 10 ** decimals, Math.Rounding.Floor); + // calculate the final amount based on the weight + newAmount = newAmount.mulDiv(weight, WEIGHT_DENOMINATOR, Math.Rounding.Floor); + return oldDerivedAmount + newAmount; } - uPoint.bias -= uPoint.slope * int128(int256(blockTime - uPoint.ts)); - return uint(uint128(_positiveInt128(uPoint.bias))); - } - - /// @notice Calculate total voting power at some point in the past - /// @param point The point (bias/slope) to start search from - /// @param t Time to calculate the total voting power at - /// @return Total voting power at that time - function supplyAt(IVe.Point memory point, uint t, mapping(uint => int128) storage slopeChanges) public view returns (uint) { - IVe.Point memory lastPoint = point; - uint ti = (lastPoint.ts / WEEK) * WEEK; - for (uint i = 0; i < 255; ++i) { - ti += WEEK; - int128 dSlope = 0; - if (ti > t) { - ti = t; - } else { - dSlope = slopeChanges[ti]; - } - lastPoint.bias -= lastPoint.slope * int128(int256(ti - lastPoint.ts)); - if (ti == t) { - break; - } - lastPoint.slope += dSlope; - lastPoint.ts = ti; - } - return uint(uint128(_positiveInt128(lastPoint.bias))); - } - - /// @notice Calculate total voting power at some point in the past - /// @param _block Block to calculate the total voting power at - /// @return Total voting power at `_block` - function totalSupplyAt( - uint _block, - uint _epoch, - mapping(uint => IVe.Point) storage _pointHistory, - mapping(uint => int128) storage slopeChanges - ) external view returns (uint) { - require(_block <= block.number, "WRONG_INPUT"); - - uint targetEpoch = findBlockEpoch(_block, _epoch, _pointHistory); - - IVe.Point memory point = _pointHistory[targetEpoch]; - // it is possible only for a block before the launch - // return 0 as more clear answer than revert - if (point.blk > _block) { - return 0; - } - uint dt = 0; - if (targetEpoch < _epoch) { - IVe.Point memory pointNext = _pointHistory[targetEpoch + 1]; - // next point block can not be the same or lower - dt = ((_block - point.blk) * (pointNext.ts - point.ts)) / (pointNext.blk - point.blk); - } else { - if (point.blk != block.number) { - dt = ((_block - point.blk) * (block.timestamp - point.ts)) / (block.number - point.blk); - } + /// @notice Binary search to estimate timestamp for block number + /// @param _block Block to find + /// @param maxEpoch Don't go beyond this epoch + /// @return Approximate timestamp for block + function findBlockEpoch( + uint _block, + uint maxEpoch, + mapping(uint => IVe.Point) storage _pointHistory + ) public view returns (uint) { + // Binary search + uint _min = 0; + uint _max = maxEpoch; + for (uint i = 0; i < 128; ++i) { + // Will be always enough for 128-bit numbers + if (_min >= _max) { + break; + } + uint _mid = (_min + _max + 1) / 2; + if (_pointHistory[_mid].blk <= _block) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + return _min; } - // Now dt contains info on how far are we beyond point - return supplyAt(point, point.ts + dt, slopeChanges); - } - - /// @notice Record global and per-user data to checkpoint - function checkpoint( - uint tokenId, - uint oldDerivedAmount, - uint newDerivedAmount, - uint oldEnd, - uint newEnd, - uint epoch, - mapping(uint => int128) storage slopeChanges, - mapping(uint => uint) storage userPointEpoch, - mapping(uint => IVe.Point[1000000000]) storage _userPointHistory, - mapping(uint => IVe.Point) storage _pointHistory - ) external returns (uint newEpoch) { - IVe.Point memory uOld; - IVe.Point memory uNew; - return _checkpoint( - CheckpointInfo({ - tokenId: tokenId, - oldDerivedAmount: oldDerivedAmount, - newDerivedAmount: newDerivedAmount, - oldEnd: oldEnd, - newEnd: newEnd, - epoch: epoch, - uOld: uOld, - uNew: uNew, - oldDSlope: 0, - newDSlope: 0 - }), - slopeChanges, - userPointEpoch, - _userPointHistory, - _pointHistory - ); - } - - function _checkpoint( - CheckpointInfo memory info, - mapping(uint => int128) storage slopeChanges, - mapping(uint => uint) storage userPointEpoch, - mapping(uint => IVe.Point[1000000000]) storage _userPointHistory, - mapping(uint => IVe.Point) storage _pointHistory - ) internal returns (uint newEpoch) { - - if (info.tokenId != 0) { - // Calculate slopes and biases - // Kept at zero when they have to - if (info.oldEnd > block.timestamp && info.oldDerivedAmount > 0) { - info.uOld.slope = int128(uint128(info.oldDerivedAmount)) / I_MAX_TIME; - info.uOld.bias = info.uOld.slope * int128(int256(info.oldEnd - block.timestamp)); - } - if (info.newEnd > block.timestamp && info.newDerivedAmount > 0) { - info.uNew.slope = int128(uint128(info.newDerivedAmount)) / I_MAX_TIME; - info.uNew.bias = info.uNew.slope * int128(int256(info.newEnd - block.timestamp)); - } - - // Read values of scheduled changes in the slope - // oldLocked.end can be in the past and in the future - // newLocked.end can ONLY by in the FUTURE unless everything expired: than zeros - info.oldDSlope = slopeChanges[info.oldEnd]; - if (info.newEnd != 0) { - if (info.newEnd == info.oldEnd) { - info.newDSlope = info.oldDSlope; + + /// @notice Measure voting power of `_tokenId` at block height `_block` + /// @return Voting power + function balanceOfAtNFT( + uint _tokenId, + uint _block, + uint maxEpoch, + mapping(uint => uint) storage userPointEpoch, + mapping(uint => IVe.Point[1000000000]) storage _userPointHistory, + mapping(uint => IVe.Point) storage _pointHistory + ) external view returns (uint) { + // Copying and pasting totalSupply code because Vyper cannot pass by + // reference yet + require(_block <= block.number, "WRONG_INPUT"); + + // Binary search + uint _min = 0; + uint _max = userPointEpoch[_tokenId]; + for (uint i = 0; i < 128; ++i) { + // Will be always enough for 128-bit numbers + if (_min >= _max) { + break; + } + uint _mid = (_min + _max + 1) / 2; + if (_userPointHistory[_tokenId][_mid].blk <= _block) { + _min = _mid; + } else { + _max = _mid - 1; + } + } + + IVe.Point memory uPoint = _userPointHistory[_tokenId][_min]; + + uint _epoch = findBlockEpoch(_block, maxEpoch, _pointHistory); + IVe.Point memory point0 = _pointHistory[_epoch]; + uint dBlock = 0; + uint dt = 0; + if (_epoch < maxEpoch) { + IVe.Point memory point1 = _pointHistory[_epoch + 1]; + dBlock = point1.blk - point0.blk; + dt = point1.ts - point0.ts; } else { - info.newDSlope = slopeChanges[info.newEnd]; + dBlock = block.number - point0.blk; + dt = block.timestamp - point0.ts; + } + uint blockTime = point0.ts; + if (dBlock != 0 && _block > point0.blk) { + blockTime += (dt * (_block - point0.blk)) / dBlock; } - } - } - IVe.Point memory lastPoint = IVe.Point({bias: 0, slope: 0, ts: block.timestamp, blk: block.number}); - if (info.epoch > 0) { - lastPoint = _pointHistory[info.epoch]; + uPoint.bias -= uPoint.slope * int128(int(blockTime - uPoint.ts)); + return uint(uint128(_positiveInt128(uPoint.bias))); } - uint lastCheckpoint = lastPoint.ts; - // initialLastPoint is used for extrapolation to calculate block number - // (approximately, for *At methods) and save them - // as we cannot figure that out exactly from inside the contract - IVe.Point memory initialLastPoint = lastPoint; - uint blockSlope = 0; - // dblock/dt - if (block.timestamp > lastPoint.ts) { - blockSlope = (MULTIPLIER * (block.number - lastPoint.blk)) / (block.timestamp - lastPoint.ts); + + /// @notice Calculate total voting power at some point in the past + /// @param point The point (bias/slope) to start search from + /// @param t Time to calculate the total voting power at + /// @return Total voting power at that time + function supplyAt( + IVe.Point memory point, + uint t, + mapping(uint => int128) storage slopeChanges + ) public view returns (uint) { + IVe.Point memory lastPoint = point; + uint ti = (lastPoint.ts / WEEK) * WEEK; + for (uint i = 0; i < 255; ++i) { + ti += WEEK; + int128 dSlope = 0; + if (ti > t) { + ti = t; + } else { + dSlope = slopeChanges[ti]; + } + lastPoint.bias -= lastPoint.slope * int128(int(ti - lastPoint.ts)); + if (ti == t) { + break; + } + lastPoint.slope += dSlope; + lastPoint.ts = ti; + } + return uint(uint128(_positiveInt128(lastPoint.bias))); } - // If last point is already recorded in this block, slope=0 - // But that's ok b/c we know the block in such case - - // Go over weeks to fill history and calculate what the current point is - { - uint ti = (lastCheckpoint / WEEK) * WEEK; - // Hopefully it won't happen that this won't get used in 5 years! - // If it does, users will be able to withdraw but vote weight will be broken - for (uint i = 0; i < 255; ++i) { - ti += WEEK; - int128 dSlope = 0; - if (ti > block.timestamp) { - ti = block.timestamp; - } else { - dSlope = slopeChanges[ti]; + + /// @notice Calculate total voting power at some point in the past + /// @param _block Block to calculate the total voting power at + /// @return Total voting power at `_block` + function totalSupplyAt( + uint _block, + uint _epoch, + mapping(uint => IVe.Point) storage _pointHistory, + mapping(uint => int128) storage slopeChanges + ) external view returns (uint) { + require(_block <= block.number, "WRONG_INPUT"); + + uint targetEpoch = findBlockEpoch(_block, _epoch, _pointHistory); + + IVe.Point memory point = _pointHistory[targetEpoch]; + // it is possible only for a block before the launch + // return 0 as more clear answer than revert + if (point.blk > _block) { + return 0; } - lastPoint.bias = _positiveInt128(lastPoint.bias - lastPoint.slope * int128(int256(ti - lastCheckpoint))); - lastPoint.slope = _positiveInt128(lastPoint.slope + dSlope); - lastCheckpoint = ti; - lastPoint.ts = ti; - lastPoint.blk = initialLastPoint.blk + (blockSlope * (ti - initialLastPoint.ts)) / MULTIPLIER; - info.epoch += 1; - if (ti == block.timestamp) { - lastPoint.blk = block.number; - break; + uint dt = 0; + if (targetEpoch < _epoch) { + IVe.Point memory pointNext = _pointHistory[targetEpoch + 1]; + // next point block can not be the same or lower + dt = ((_block - point.blk) * (pointNext.ts - point.ts)) / (pointNext.blk - point.blk); } else { - _pointHistory[info.epoch] = lastPoint; + if (point.blk != block.number) { + dt = ((_block - point.blk) * (block.timestamp - point.ts)) / (block.number - point.blk); + } } - } + // Now dt contains info on how far are we beyond point + return supplyAt(point, point.ts + dt, slopeChanges); } - newEpoch = info.epoch; - // Now pointHistory is filled until t=now - - if (info.tokenId != 0) { - // If last point was in this block, the slope change has been applied already - // But in such case we have 0 slope(s) - lastPoint.slope = _positiveInt128(lastPoint.slope + (info.uNew.slope - info.uOld.slope)); - lastPoint.bias = _positiveInt128(lastPoint.bias + (info.uNew.bias - info.uOld.bias)); + /// @notice Record global and per-user data to checkpoint + function checkpoint( + uint tokenId, + uint oldDerivedAmount, + uint newDerivedAmount, + uint oldEnd, + uint newEnd, + uint epoch, + mapping(uint => int128) storage slopeChanges, + mapping(uint => uint) storage userPointEpoch, + mapping(uint => IVe.Point[1000000000]) storage _userPointHistory, + mapping(uint => IVe.Point) storage _pointHistory + ) external returns (uint newEpoch) { + IVe.Point memory uOld; + IVe.Point memory uNew; + return _checkpoint( + CheckpointInfo({ + tokenId: tokenId, + oldDerivedAmount: oldDerivedAmount, + newDerivedAmount: newDerivedAmount, + oldEnd: oldEnd, + newEnd: newEnd, + epoch: epoch, + uOld: uOld, + uNew: uNew, + oldDSlope: 0, + newDSlope: 0 + }), + slopeChanges, + userPointEpoch, + _userPointHistory, + _pointHistory + ); } - // Record the changed point into history - _pointHistory[info.epoch] = lastPoint; - - if (info.tokenId != 0) { - // Schedule the slope changes (slope is going down) - // We subtract newUserSlope from [newLocked.end] - // and add old_user_slope to [old_locked.end] - if (info.oldEnd > block.timestamp) { - // old_dslope was - u_old.slope, so we cancel that - info.oldDSlope += info.uOld.slope; - if (info.newEnd == info.oldEnd) { - info.oldDSlope -= info.uNew.slope; - // It was a new deposit, not extension + function _checkpoint( + CheckpointInfo memory info, + mapping(uint => int128) storage slopeChanges, + mapping(uint => uint) storage userPointEpoch, + mapping(uint => IVe.Point[1000000000]) storage _userPointHistory, + mapping(uint => IVe.Point) storage _pointHistory + ) internal returns (uint newEpoch) { + if (info.tokenId != 0) { + // Calculate slopes and biases + // Kept at zero when they have to + if (info.oldEnd > block.timestamp && info.oldDerivedAmount > 0) { + info.uOld.slope = int128(uint128(info.oldDerivedAmount)) / I_MAX_TIME; + info.uOld.bias = info.uOld.slope * int128(int(info.oldEnd - block.timestamp)); + } + if (info.newEnd > block.timestamp && info.newDerivedAmount > 0) { + info.uNew.slope = int128(uint128(info.newDerivedAmount)) / I_MAX_TIME; + info.uNew.bias = info.uNew.slope * int128(int(info.newEnd - block.timestamp)); + } + + // Read values of scheduled changes in the slope + // oldLocked.end can be in the past and in the future + // newLocked.end can ONLY by in the FUTURE unless everything expired: than zeros + info.oldDSlope = slopeChanges[info.oldEnd]; + if (info.newEnd != 0) { + if (info.newEnd == info.oldEnd) { + info.newDSlope = info.oldDSlope; + } else { + info.newDSlope = slopeChanges[info.newEnd]; + } + } + } + + IVe.Point memory lastPoint = IVe.Point({bias: 0, slope: 0, ts: block.timestamp, blk: block.number}); + if (info.epoch > 0) { + lastPoint = _pointHistory[info.epoch]; + } + uint lastCheckpoint = lastPoint.ts; + // initialLastPoint is used for extrapolation to calculate block number + // (approximately, for *At methods) and save them + // as we cannot figure that out exactly from inside the contract + IVe.Point memory initialLastPoint = lastPoint; + uint blockSlope = 0; + // dblock/dt + if (block.timestamp > lastPoint.ts) { + blockSlope = (MULTIPLIER * (block.number - lastPoint.blk)) / (block.timestamp - lastPoint.ts); + } + // If last point is already recorded in this block, slope=0 + // But that's ok b/c we know the block in such case + + // Go over weeks to fill history and calculate what the current point is + { + uint ti = (lastCheckpoint / WEEK) * WEEK; + // Hopefully it won't happen that this won't get used in 5 years! + // If it does, users will be able to withdraw but vote weight will be broken + for (uint i = 0; i < 255; ++i) { + ti += WEEK; + int128 dSlope = 0; + if (ti > block.timestamp) { + ti = block.timestamp; + } else { + dSlope = slopeChanges[ti]; + } + lastPoint.bias = _positiveInt128(lastPoint.bias - lastPoint.slope * int128(int(ti - lastCheckpoint))); + lastPoint.slope = _positiveInt128(lastPoint.slope + dSlope); + lastCheckpoint = ti; + lastPoint.ts = ti; + lastPoint.blk = initialLastPoint.blk + (blockSlope * (ti - initialLastPoint.ts)) / MULTIPLIER; + info.epoch += 1; + if (ti == block.timestamp) { + lastPoint.blk = block.number; + break; + } else { + _pointHistory[info.epoch] = lastPoint; + } + } + } + + newEpoch = info.epoch; + // Now pointHistory is filled until t=now + + if (info.tokenId != 0) { + // If last point was in this block, the slope change has been applied already + // But in such case we have 0 slope(s) + lastPoint.slope = _positiveInt128(lastPoint.slope + (info.uNew.slope - info.uOld.slope)); + lastPoint.bias = _positiveInt128(lastPoint.bias + (info.uNew.bias - info.uOld.bias)); } - slopeChanges[info.oldEnd] = info.oldDSlope; - } - - if (info.newEnd > block.timestamp) { - if (info.newEnd > info.oldEnd) { - info.newDSlope -= info.uNew.slope; - // old slope disappeared at this point - slopeChanges[info.newEnd] = info.newDSlope; + + // Record the changed point into history + _pointHistory[info.epoch] = lastPoint; + + if (info.tokenId != 0) { + // Schedule the slope changes (slope is going down) + // We subtract newUserSlope from [newLocked.end] + // and add old_user_slope to [old_locked.end] + if (info.oldEnd > block.timestamp) { + // old_dslope was - u_old.slope, so we cancel that + info.oldDSlope += info.uOld.slope; + if (info.newEnd == info.oldEnd) { + info.oldDSlope -= info.uNew.slope; + // It was a new deposit, not extension + } + slopeChanges[info.oldEnd] = info.oldDSlope; + } + + if (info.newEnd > block.timestamp) { + if (info.newEnd > info.oldEnd) { + info.newDSlope -= info.uNew.slope; + // old slope disappeared at this point + slopeChanges[info.newEnd] = info.newDSlope; + } + // else: we recorded it already in oldDSlope + } + // Now handle user history + uint userEpoch = userPointEpoch[info.tokenId] + 1; + + userPointEpoch[info.tokenId] = userEpoch; + info.uNew.ts = block.timestamp; + info.uNew.blk = block.number; + _userPointHistory[info.tokenId][userEpoch] = info.uNew; } - // else: we recorded it already in oldDSlope - } - // Now handle user history - uint userEpoch = userPointEpoch[info.tokenId] + 1; - - userPointEpoch[info.tokenId] = userEpoch; - info.uNew.ts = block.timestamp; - info.uNew.blk = block.number; - _userPointHistory[info.tokenId][userEpoch] = info.uNew; } - } - - function _positiveInt128(int128 value) internal pure returns (int128) { - return value < 0 ? int128(0) : value; - } - - /// @dev Return SVG logo of veTETU. - function tokenURI(uint _tokenId, uint _balanceOf, uint untilEnd, uint _value) public pure returns (string memory output) { - output = ''; - output = string(abi.encodePacked(output, 'ID:', _u2s(_tokenId), '')); - output = string(abi.encodePacked(output, 'Balance:', _u2s(_balanceOf / 1e18), '')); - output = string(abi.encodePacked(output, 'Until unlock:', _u2s(untilEnd / 60 / 60 / 24), ' days')); - output = string(abi.encodePacked(output, 'Power:', _u2s(_value / 1e18), '')); - - string memory json = Base64.encode(bytes(string(abi.encodePacked('{"name": "veTETU #', Strings.toString(_tokenId), '", "description": "Locked TETU tokens", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}')))); - output = string(abi.encodePacked('data:application/json;base64,', json)); - } - - function _u2s(uint num) internal pure returns (string memory) { - return Strings.toString(num); - } + + function _positiveInt128(int128 value) internal pure returns (int128) { + return value < 0 ? int128(0) : value; + } + + /// @dev Return SVG logo of veTETU. + function tokenURI( + uint _tokenId, + uint _balanceOf, + uint untilEnd, + uint _value + ) public pure returns (string memory output) { + output = + ''; + output = string( + abi.encodePacked( + output, + 'ID:', + _u2s(_tokenId), + "" + ) + ); + output = string( + abi.encodePacked( + output, + 'Balance:', + _u2s(_balanceOf / 1e18), + "" + ) + ); + output = string( + abi.encodePacked( + output, + 'Until unlock:', + _u2s(untilEnd / 60 / 60 / 24), + " days" + ) + ); + output = string( + abi.encodePacked( + output, + 'Power:', + _u2s(_value / 1e18), + "" + ) + ); + + string memory json = Base64.encode( + bytes( + string( + abi.encodePacked( + '{"name": "veTETU #', + Strings.toString(_tokenId), + '", "description": "Locked TETU tokens", "image": "data:image/svg+xml;base64,', + Base64.encode(bytes(output)), + '"}' + ) + ) + ) + ); + output = string(abi.encodePacked("data:application/json;base64,", json)); + } + + function _u2s(uint num) internal pure returns (string memory) { + return Strings.toString(num); + } } diff --git a/test/Vault.t.sol b/test/Vault.t.sol index b9c5ed0..8521d24 100644 --- a/test/Vault.t.sol +++ b/test/Vault.t.sol @@ -7,12 +7,12 @@ import "../src/Vault.sol"; contract VaultTest is MockSetup { function test_vault() public { - Vault vault = new Vault(IERC20(tokenA), "Vault for MOCK_A", "xTokenA", 4_000); + Vault vault = new Vault(address(controller), IERC20(tokenA), "Vault for MOCK_A", "xTokenA", 4_000); MockStrategy strategy = new MockStrategy(address(vault), address(1)); vault.setStrategy(address(strategy)); deal(tokenA, address(this), 1e20); - IERC20(tokenA).approve(address (vault), 1e20); + IERC20(tokenA).approve(address(vault), 1e20); vault.mint(1e18, address(this)); assertEq(vault.balanceOf(address(this)), 1e18); vault.redeem(1e18, address(this), address(this)); @@ -29,8 +29,5 @@ contract VaultTest is MockSetup { assertEq(vault.sharePrice(), 1e18); assertEq(vault.totalAssets(), 0); assertEq(vault.strategyAssets(), 0); - } - - } diff --git a/test/Vesting.t.sol b/test/Vesting.t.sol index d531dad..8edb702 100644 --- a/test/Vesting.t.sol +++ b/test/Vesting.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.21; import "./setup/MockSetup.sol"; - contract VestingTest is MockSetup { function test_vesting() public { address[] memory vesting = ISTGN(controller.stgn()).vesting(); @@ -22,6 +21,5 @@ contract VestingTest is MockSetup { _vesting.claim(); vm.stopPrank(); } - } } diff --git a/test/mock/MockERC20.sol b/test/mock/MockERC20.sol index 359e5d1..f4428a8 100644 --- a/test/mock/MockERC20.sol +++ b/test/mock/MockERC20.sol @@ -9,11 +9,7 @@ contract MockERC20 is ERC20Permit { // add this to be excluded from coverage report function test() public {} - constructor( - string memory name_, - string memory symbol_, - uint8 decimals_ - ) ERC20(name_, symbol_) ERC20Permit(name_) { + constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) ERC20Permit(name_) { _decimals = decimals_; } diff --git a/test/mock/MockStrategy.sol b/test/mock/MockStrategy.sol index 2c1721e..dec6e9c 100644 --- a/test/mock/MockStrategy.sol +++ b/test/mock/MockStrategy.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.21; import "../../src/base/StrategyStrictBase.sol"; import "../../src/interfaces/IVault.sol"; - contract MockStrategy is StrategyStrictBase { uint public lastHardWork; address public pool; @@ -20,28 +19,17 @@ contract MockStrategy is StrategyStrictBase { return true; } - function doHardWork() external returns (uint earned, uint lost) {} - - - function investedAssets() public view override returns (uint) { - - } - - function _claim() internal override { + function doHardWork() external /* returns (uint earned, uint lost)*/ {} - } + function investedAssets() public view override returns (uint) {} - function _depositToPool(uint amount) internal override { + function _claim() internal virtual override returns (uint rtReward) {} - } + function _depositToPool(uint amount) internal override {} function _emergencyExitFromPool() internal override {} - function _withdrawFromPool(uint amount) internal override returns (uint investedAssetsUSD, uint assetPrice) { - - } - - function _withdrawAllFromPool() internal override returns (uint investedAssetsUSD, uint assetPrice) { + function _withdrawFromPool(uint amount) internal override /* returns (uint investedAssetsUSD, uint assetPrice)*/ {} - } + function _withdrawAllFromPool() internal override /* returns (uint investedAssetsUSD, uint assetPrice)*/ {} } diff --git a/test/setup/MockSetup.sol b/test/setup/MockSetup.sol index 7043401..b2a0178 100644 --- a/test/setup/MockSetup.sol +++ b/test/setup/MockSetup.sol @@ -27,7 +27,6 @@ abstract contract MockSetup is Test { } function _init() public returns (Controller) { - address[] memory vestingClaimant = new address[](2); uint[] memory vestingAmount = new uint[](2); vestingClaimant[0] = address(1); @@ -35,14 +34,19 @@ abstract contract MockSetup is Test { vestingAmount[0] = 1e24; vestingAmount[1] = 2e24; - Controller _c = Controller(DeployLib.deployPlatform(DeployLib.DeployParams({ - governance: address(this), - ifoRate: 12e17, - vestingClaimant: vestingClaimant, - vestingAmount: vestingAmount, - vestingPeriod: 365 days, - vestingCliff: 30 days - }))); + Controller _c = Controller( + DeployLib.deployPlatform( + DeployLib.DeployParams({ + governance: address(this), + ifoRate: 12e17, + vestingClaimant: vestingClaimant, + vestingAmount: vestingAmount, + vestingPeriod: 365 days, + vestingCliff: 30 days, + rewardToken: tokenC + }) + ) + ); return _c; } diff --git a/test/setup/PolygonSetip.sol b/test/setup/PolygonSetip.sol new file mode 100644 index 0000000..2643c6a --- /dev/null +++ b/test/setup/PolygonSetip.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import {Test, console} from "forge-std/Test.sol"; +import "../../src/Controller.sol"; +import "../../chains/PolygonLib.sol"; + +abstract contract PolygonSetup is Test { + Controller public controller; + + constructor() { + PolygonLib.runDeploy(true); + } +}