diff --git a/architecture_diagram.svg b/architecture_diagram.svg index 7931c53a..370fc6c5 100644 --- a/architecture_diagram.svg +++ b/architecture_diagram.svg @@ -1,4 +1,4 @@ -PolicyholderPolic...buy, update, cancel policybuy, update, cancel policysubmit claimsubmit claimPaclasPaclasAave ProductAave ProductTreasuryTreasuryClaimsEscrowClaimsEscrowPolicyManagerPolicyManagerroute, refund premiumsroute, refund premiumscreate, update, burn policycreate, update, burn policyAaveAavevalidate positionvalidate positionassess claimassess claimVaultVaultback riskback riskCapital ProviderCapit...ProductsProductsDeFi EcosystemDeFi EcosystemCurveCurveYearnYearnCurve ProductCurve ProductYearn ProductYearn Productinvestinvestverify signatureverify signatureroute premiumsroute premiumscreate claimcreate claimwithdraw payoutwithdraw payoutCpFarmCpFarmprovide capitalprovide capitalreceive premiumsreceive premiumsfarm rewardsfarm rewardsSOLACE-ETH Liquidity PoolSOLACE-ETH Liquidity...Liquidity ProviderLiqui...SolaceEthLpFarmSolaceEthLpFarmprovide liquidityprovide liquidityreceive swap feesreceive swap feesfarm rewardsfarm rewardsFarmControllerFarmControllerOptionsFarmingOptionsFarmingFarmerFarmerSOLACESOLACEreceive SOLACEreceive SOLACEDAODAOgoverngoverncreate optioncreate optioncalculate rewardscalculate rewardsexercise optionexercise optionViewer does not support full SVG 1.1 \ No newline at end of file +PolicyholderPolic...buy, update, cancel policybuy, update, cancel policysubmit claimsubmit claimPaclasPaclasAave ProductAave ProductTreasuryTreasuryClaimsEscrowClaimsEscrowPolicyManagerPolicyManagerroute, refund premiumsroute, refund premiumscreate, update, burn policycreate, update, burn policyAaveAavevalidate positionvalidate positionassess claimassess claimVaultVaultback riskback riskCapital ProviderCapit...ProductsProductsDeFi EcosystemDeFi EcosystemCurveCurveYearnYearnCurve ProductCurve ProductYearn ProductYearn Productinvestinvestverify signatureverify signatureroute premiumsroute premiumscreate claimcreate claimwithdraw payoutwithdraw payoutCpFarmCpFarmprovide capitalprovide capitalreceive premiumsreceive premiumsfarm rewardsfarm rewardsSOLACE-ETH Liquidity PoolSOLACE-ETH Liquidity...Liquidity ProviderLiqui...SolaceEthLpFarmSolaceEthLpFarmprovide liquidityprovide liquidityreceive swap feesreceive swap feesfarm rewardsfarm rewardsFarmControllerFarmControllerOptionsFarmingOptionsFarmingFarmerFarmerSOLACESOLACEreceive SOLACEreceive SOLACEDAODAOgoverngovernSptFarmSptFarmstake policies, farm rewardsstake policies, farm rewar...create optioncreate optioncalculate rewardscalculate rewardsexercise optionexercise optionViewer does not support full SVG 1.1 \ No newline at end of file diff --git a/contracts/CpFarm.sol b/contracts/CpFarm.sol index bd06e9d0..9a2ee762 100644 --- a/contracts/CpFarm.sol +++ b/contracts/CpFarm.sol @@ -14,7 +14,7 @@ import "./interface/ICpFarm.sol"; /** * @title CpFarm * @author solace.fi - * @notice Rewards [**Capital Providers**](/docs/user-guides/capital-provider/cp-role-guide) in [**SOLACE**](./SOLACE) for providing capital in the [`Vault`](./Vault). + * @notice Rewards [**Capital Providers**](/docs/user-guides/capital-provider/cp-role-guide) in [`Policy Manager`](./PolicyManager) for providing capital in the [`Vault`](./Vault). * * Over the course of `startTime` to `endTime`, the farm distributes `rewardPerSecond` [**SOLACE**](./SOLACE) to all farmers split relative to the amount of [**SCP**](./Vault) they have deposited. * diff --git a/contracts/SptFarm.sol b/contracts/SptFarm.sol new file mode 100644 index 00000000..9a866072 --- /dev/null +++ b/contracts/SptFarm.sol @@ -0,0 +1,537 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.6; + +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "./Governable.sol"; +import "./interface/IRegistry.sol"; +import "./interface/IPolicyManager.sol"; +import "./interface/ISptFarm.sol"; + + +/** + * @title ISptFarm + * @author solace.fi + * @notice Rewards [**Policyholders**](/docs/protocol/policy-holder) in [**Options**](../OptionFarming) for staking their [**Policies**](./PolicyManager). + * + * Over the course of `startTime` to `endTime`, the farm distributes `rewardPerSecond` [**Options**](../OptionFarming) to all farmers split relative to the amount of [**SCP**](../Vault) they have deposited. + * + * Users can become [**Capital Providers**](/docs/user-guides/capital-provider/cp-role-guide) by depositing **ETH** into the [`Vault`](../Vault), receiving [**SCP**](../Vault) in the process. [**Capital Providers**](/docs/user-guides/capital-provider/cp-role-guide) can then deposit their [**SCP**](../Vault) via [`depositCp()`](#depositcp) or [`depositCpSigned()`](#depositcpsigned). Alternatively users can bypass the [`Vault`](../Vault) and stake their **ETH** via [`depositEth()`](#depositeth). + * + * Users can withdraw their rewards via [`withdrawRewards()`](#withdrawrewards). + * + * Users can withdraw their [**SCP**](../Vault) via [`withdrawCp()`](#withdrawcp). + * + * Note that transferring in **ETH** will mint you shares, but transferring in **WETH** or [**SCP**](../Vault) will not. These must be deposited via functions in this contract. Misplaced funds cannot be rescued. + */ +contract SptFarm is ISptFarm, ReentrancyGuard, Governable { + using EnumerableSet for EnumerableSet.UintSet; + + /// @notice A unique enumerator that identifies the farm type. + uint256 internal constant _farmType = 3; + /// @notice PolicyManager contract. + IPolicyManager internal _policyManager; + /// @notice FarmController contract. + IFarmController internal _controller; + /// @notice Amount of SOLACE distributed per seconds. + uint256 internal _rewardPerSecond; + /// @notice When the farm will start. + uint256 internal _startTime; + /// @notice When the farm will end. + uint256 internal _endTime; + /// @notice Last time rewards were distributed or farm was updated. + uint256 internal _lastRewardTime; + /// @notice Accumulated rewards per share, times 1e12. + uint256 internal _accRewardPerShare; + /// @notice Value of policys staked by all farmers. + uint256 internal _valueStaked; + + // Info of each user. + struct UserInfo { + uint256 value; // Value of user provided policys. + uint256 rewardDebt; // Reward debt. See explanation below. + uint256 unpaidRewards; // Rewards that have not been paid. + // + // We do some fancy math here. Basically, any point in time, the amount of reward token + // entitled to a user but is pending to be distributed is: + // + // pending reward = (user.value * _accRewardPerShare) - user.rewardDebt + user.unpaidRewards + // + // Whenever a user deposits or withdraws policies to a farm. Here's what happens: + // 1. The farm's `accRewardPerShare` and `lastRewardTime` gets updated. + // 2. Users pending rewards accumulate in `unpaidRewards`. + // 3. User's `value` gets updated. + // 4. User's `rewardDebt` gets updated. + } + + /// @notice Information about each farmer. + /// @dev user address => user info + mapping(address => UserInfo) internal _userInfo; + + // list of tokens deposited by user + mapping(address => EnumerableSet.UintSet) internal _userDeposited; + + struct PolicyInfo { + address depositor; + uint256 value; + } + + // policy id => policy info + mapping(uint256 => PolicyInfo) internal _policyInfo; + + /** + * @notice Constructs the SptFarm. + * @param governance_ The address of the [governor](/docs/protocol/governance). + * @param registry_ Address of the [`Registry`](./Registry) contract. + * @param startTime_ When farming will begin. + * @param endTime_ When farming will end. + */ + constructor( + address governance_, + address registry_, + uint256 startTime_, + uint256 endTime_ + ) Governable(governance_) { + require(registry_ != address(0x0), "zero address registry"); + IRegistry registry = IRegistry(registry_); + address controller_ = registry.farmController(); + require(controller_ != address(0x0), "zero address controller"); + _controller = IFarmController(controller_); + address policyManager_ = registry.policyManager(); + require(policyManager_ != address(0x0), "zero address policymanager"); + _policyManager = IPolicyManager(policyManager_); + require(startTime_ <= endTime_, "invalid window"); + _startTime = startTime_; + _endTime = endTime_; + _lastRewardTime = Math.max(block.timestamp, startTime_); + } + + /*************************************** + VIEW FUNCTIONS + ***************************************/ + + /// @notice A unique enumerator that identifies the farm type. + function farmType() external pure override returns (uint256 farmType_) { + return _farmType; + } + + /// @notice [`PolicyManager`](./PolicyManager) contract. + function policyManager() external view override returns (address policyManager_) { + return address(_policyManager); + } + + /** + * @notice Returns the count of [**policies**](./PolicyManager) that a user has deposited onto the farm. + * @param user The user to check count for. + * @return count The count of deposited [**policies**](./PolicyManager). + */ + function countDeposited(address user) external view override returns (uint256 count) { + return _userDeposited[user].length(); + } + + /** + * @notice Returns the list of [**policies**](./PolicyManager) that a user has deposited onto the farm and their values. + * @param user The user to list deposited policies. + * @return policyIDs The list of deposited policies. + * @return policyValues The values of the policies. + */ + function listDeposited(address user) external view override returns (uint256[] memory policyIDs, uint256[] memory policyValues) { + uint256 length = _userDeposited[user].length(); + policyIDs = new uint256[](length); + policyValues = new uint256[](length); + for(uint256 i = 0; i < length; ++i) { + uint256 policyID = _userDeposited[user].at(i); + policyIDs[i] = policyID; + policyValues[i] = _policyInfo[policyID].value; + } + return (policyIDs, policyValues); + } + + /** + * @notice Returns the ID of a [**Policies**](./PolicyManager) that a user has deposited onto a farm and its value. + * @param user The user to get policyID for. + * @param index The farm-based index of the policy. + * @return policyID The ID of the deposited [**policy**](./PolicyManager). + * @return policyValue The value of the [**policy**](./PolicyManager). + */ + function getDeposited(address user, uint256 index) external view override returns (uint256 policyID, uint256 policyValue) { + policyID = _userDeposited[user].at(index); + policyValue = _policyInfo[policyID].value; + return (policyID, policyValue); + } + + /// @notice FarmController contract. + function farmController() external view override returns (address controller_) { + return address(_controller); + } + + /// @notice Amount of SOLACE distributed per second. + function rewardPerSecond() external view override returns (uint256) { + return _rewardPerSecond; + } + + /// @notice When the farm will start. + function startTime() external view override returns (uint256 timestamp) { + return _startTime; + } + + /// @notice When the farm will end. + function endTime() external view override returns (uint256 timestamp) { + return _endTime; + } + + /// @notice Last time rewards were distributed or farm was updated. + function lastRewardTime() external view override returns (uint256 timestamp) { + return _lastRewardTime; + } + + /// @notice Accumulated rewards per share, times 1e12. + function accRewardPerShare() external view override returns (uint256 acc) { + return _accRewardPerShare; + } + + /// @notice The value of [**policies**](./PolicyManager) a user deposited. + function userStaked(address user) external view override returns (uint256 amount) { + return _userInfo[user].value; + } + + /// @notice Value of [**policies**](./PolicyManager) staked by all farmers. + function valueStaked() external view override returns (uint256 amount) { + return _valueStaked; + } + + /// @notice Information about a deposited policy. + function policyInfo(uint256 policyID) external view override returns (address depositor, uint256 value) { + PolicyInfo storage policyInfo_ = _policyInfo[policyID]; + return (policyInfo_.depositor, policyInfo_.value); + } + + /** + * @notice Calculates the accumulated balance of [**SOLACE**](./SOLACE) for specified user. + * @param user The user for whom unclaimed rewards will be shown. + * @return reward Total amount of withdrawable rewards. + */ + function pendingRewards(address user) external view override returns (uint256 reward) { + // get farmer information + UserInfo storage userInfo_ = _userInfo[user]; + // math + uint256 accRewardPerShare_ = _accRewardPerShare; + if (block.timestamp > _lastRewardTime && _valueStaked != 0) { + uint256 tokenReward = getRewardAmountDistributed(_lastRewardTime, block.timestamp); + accRewardPerShare_ += tokenReward * 1e12 / _valueStaked; + } + return userInfo_.value * accRewardPerShare_ / 1e12 - userInfo_.rewardDebt + userInfo_.unpaidRewards; + } + + /** + * @notice Calculates the reward amount distributed between two timestamps. + * @param from The start of the period to measure rewards for. + * @param to The end of the period to measure rewards for. + * @return amount The reward amount distributed in the given period. + */ + function getRewardAmountDistributed(uint256 from, uint256 to) public view override returns (uint256 amount) { + // validate window + from = Math.max(from, _startTime); + to = Math.min(to, _endTime); + // no reward for negative window + if (from > to) return 0; + return (to - from) * _rewardPerSecond; + } + + /*************************************** + MUTATOR FUNCTIONS + ***************************************/ + + /** + * @notice Deposit a [**policy**](./PolicyManager). + * User must `ERC721.approve()` or `ERC721.setApprovalForAll()` first. + * @param policyID The ID of the policy to deposit. + */ + function depositPolicy(uint256 policyID) external override { + // pull policy + _policyManager.transferFrom(msg.sender, address(this), policyID); + // accounting + _deposit(msg.sender, policyID); + } + + /** + * @notice Deposit a [**policy**](./PolicyManager) using permit. + * @param depositor The depositing user. + * @param policyID The ID of the policy to deposit. + * @param deadline Time the transaction must go through before. + * @param v secp256k1 signature + * @param r secp256k1 signature + * @param s secp256k1 signature + */ + function depositPolicySigned(address depositor, uint256 policyID, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external override { + // permit + _policyManager.permit(address(this), policyID, deadline, v, r, s); + // pull policy + _policyManager.transferFrom(depositor, address(this), policyID); + // accounting + _deposit(depositor, policyID); + } + + /** + * @notice Deposit multiple [**policies**](./PolicyManager). + * User must `ERC721.approve()` or `ERC721.setApprovalForAll()` first. + * @param policyIDs The IDs of the policies to deposit. + */ + function depositPolicyMulti(uint256[] memory policyIDs) external override { + for(uint256 i = 0; i < policyIDs.length; i++) { + uint256 policyID = policyIDs[i]; + // pull policy + _policyManager.transferFrom(msg.sender, address(this), policyID); + // accounting + _deposit(msg.sender, policyID); + } + } + + /** + * @notice Deposit multiple [**policies**](./PolicyManager) using permit. + * @param depositors The depositing users. + * @param policyIDs The IDs of the policies to deposit. + * @param deadlines Times the transactions must go through before. + * @param vs secp256k1 signatures + * @param rs secp256k1 signatures + * @param ss secp256k1 signatures + */ + function depositPolicySignedMulti(address[] memory depositors, uint256[] memory policyIDs, uint256[] memory deadlines, uint8[] memory vs, bytes32[] memory rs, bytes32[] memory ss) external override { + require(depositors.length == policyIDs.length && depositors.length == deadlines.length && depositors.length == vs.length && depositors.length == rs.length && depositors.length == ss.length, "length mismatch"); + for(uint256 i = 0; i < policyIDs.length; i++) { + uint256 policyID = policyIDs[i]; + // permit + _policyManager.permit(address(this), policyID, deadlines[i], vs[i], rs[i], ss[i]); + // pull policy + _policyManager.transferFrom(depositors[i], address(this), policyID); + // accounting + _deposit(depositors[i], policyID); + } + } + + /** + * @notice Performs the internal accounting for a deposit. + * @param depositor The depositing user. + * @param policyID The ID of the policy to deposit. + */ + function _deposit(address depositor, uint256 policyID) internal { + // get policy + (/* address policyholder */, /* address product */, uint256 coverAmount, uint40 expirationBlock, uint24 price, /* bytes calldata positionDescription */) = _policyManager.getPolicyInfo(policyID); + require(expirationBlock > block.number, "policy is expired"); + // harvest and update farm + _harvest(depositor); + // get farmer information + UserInfo storage user = _userInfo[depositor]; + // record position + uint256 policyValue = coverAmount * uint256(price); // a multiple of premium per block + PolicyInfo memory policyInfo_ = PolicyInfo({ + depositor: depositor, + value: policyValue + }); + _policyInfo[policyID] = policyInfo_; + // accounting + user.value += policyValue; + _valueStaked += policyValue; + user.rewardDebt = user.value * _accRewardPerShare / 1e12; + _userDeposited[depositor].add(policyID); + // emit event + emit PolicyDeposited(depositor, policyID); + } + + /** + * @notice Withdraw a [**policy**](./PolicyManager). + * Can only withdraw policies you deposited. + * @param policyID The ID of the policy to withdraw. + */ + function withdrawPolicy(uint256 policyID) external override { + // harvest and update farm + _harvest(msg.sender); + // get farmer information + UserInfo storage user = _userInfo[msg.sender]; + // get policy info + PolicyInfo memory policyInfo_ = _policyInfo[policyID]; + // cannot withdraw a policy you didnt deposit + require(policyInfo_.depositor == msg.sender, "not your policy"); + // accounting + user.value -= policyInfo_.value; + _valueStaked -= policyInfo_.value; + user.rewardDebt = user.value * _accRewardPerShare / 1e12; + // delete policy info + delete _policyInfo[policyID]; + // return staked policy + _userDeposited[msg.sender].remove(policyID); + _policyManager.safeTransferFrom(address(this), msg.sender, policyID); + // emit event + emit PolicyWithdrawn(msg.sender, policyID); + } + + /** + * @notice Withdraw multiple [**policies**](./PolicyManager). + * Can only withdraw policies you deposited. + * @param policyIDs The IDs of the policies to withdraw. + */ + function withdrawPolicyMulti(uint256[] memory policyIDs) external override { + // harvest and update farm + _harvest(msg.sender); + // get farmer information + UserInfo storage user = _userInfo[msg.sender]; + uint256 userValue_ = user.value; + uint256 valueStaked_ = _valueStaked; + for(uint256 i = 0; i < policyIDs.length; i++) { + uint256 policyID = policyIDs[i]; + // get policy info + PolicyInfo memory policyInfo_ = _policyInfo[policyID]; + // cannot withdraw a policy you didnt deposit + require(policyInfo_.depositor == msg.sender, "not your policy"); + // accounting + userValue_ -= policyInfo_.value; + valueStaked_ -= policyInfo_.value; + // delete policy info + delete _policyInfo[policyID]; + // return staked policy + _userDeposited[msg.sender].remove(policyID); + _policyManager.safeTransferFrom(address(this), msg.sender, policyID); + // emit event + emit PolicyWithdrawn(msg.sender, policyID); + } + // accounting + user.value = userValue_; + _valueStaked = valueStaked_; + user.rewardDebt = user.value * _accRewardPerShare / 1e12; + } + + /** + * @notice Burns expired policies. + * @param policyIDs The list of expired policies. + */ + function updateActivePolicies(uint256[] calldata policyIDs) external override { + // update farm + updateFarm(); + // for each policy to burn + for(uint256 i = 0; i < policyIDs.length; i++) { + uint256 policyID = policyIDs[i]; + // get policy info + PolicyInfo memory policyInfo_ = _policyInfo[policyID]; + // if policy is on the farm and policy is expired or burnt + if(policyInfo_.depositor != address(0x0) && !_policyManager.policyIsActive(policyID)) { + // get farmer information + UserInfo storage user = _userInfo[policyInfo_.depositor]; + // accounting + user.value -= policyInfo_.value; + _valueStaked -= policyInfo_.value; + user.rewardDebt = user.value * _accRewardPerShare / 1e12; + // delete policy info + delete _policyInfo[policyID]; + // remove staked policy + _userDeposited[policyInfo_.depositor].remove(policyID); + // emit event + emit PolicyWithdrawn(address(0x0), policyID); + } + } + // policymanager needs to do its own accounting + _policyManager.updateActivePolicies(policyIDs); + } + + /** + * @notice Updates farm information to be up to date to the current time. + */ + function updateFarm() public override { + // dont update needlessly + if (block.timestamp <= _lastRewardTime) return; + if (_valueStaked == 0) { + _lastRewardTime = Math.min(block.timestamp, _endTime); + return; + } + // update math + uint256 tokenReward = getRewardAmountDistributed(_lastRewardTime, block.timestamp); + _accRewardPerShare += tokenReward * 1e12 / _valueStaked; + _lastRewardTime = Math.min(block.timestamp, _endTime); + } + + /** + * @notice Update farm and accumulate a user's rewards. + * @param user User to process rewards for. + */ + function _harvest(address user) internal { + // update farm + updateFarm(); + // get farmer information + UserInfo storage userInfo_ = _userInfo[user]; + // accumulate unpaid rewards + userInfo_.unpaidRewards = userInfo_.value * _accRewardPerShare / 1e12 - userInfo_.rewardDebt + userInfo_.unpaidRewards; + } + + /*************************************** + OPTIONS MINING FUNCTIONS + ***************************************/ + + /** + * @notice Converts the senders unpaid rewards into an [`Option`](./OptionsFarming). + * @return optionID The ID of the newly minted [`Option`](./OptionsFarming). + */ + function withdrawRewards() external override nonReentrant returns (uint256 optionID) { + // update farm + _harvest(msg.sender); + // get farmer information + UserInfo storage userInfo_ = _userInfo[msg.sender]; + // math + userInfo_.rewardDebt = userInfo_.value * _accRewardPerShare / 1e12; + uint256 unpaidRewards = userInfo_.unpaidRewards; + userInfo_.unpaidRewards = 0; + optionID = _controller.createOption(msg.sender, unpaidRewards); + return optionID; + } + + /** + * @notice Withdraw a users rewards without unstaking their policys. + * Can only be called by [`FarmController`](./FarmController). + * @param user User to withdraw rewards for. + * @return rewardAmount The amount of rewards the user earned on this farm. + */ + function withdrawRewardsForUser(address user) external override nonReentrant returns (uint256 rewardAmount) { + require(msg.sender == address(_controller), "!farmcontroller"); + // update farm + _harvest(user); + // get farmer information + UserInfo storage userInfo_ = _userInfo[user]; + // math + userInfo_.rewardDebt = userInfo_.value * _accRewardPerShare / 1e12; + rewardAmount = userInfo_.unpaidRewards; + userInfo_.unpaidRewards = 0; + return rewardAmount; + } + + /*************************************** + GOVERNANCE FUNCTIONS + ***************************************/ + + /** + * @notice Sets the amount of [**SOLACE**](./SOLACE) to distribute per second. + * Only affects future rewards. + * Can only be called by [`FarmController`](./FarmController). + * @param rewardPerSecond_ Amount to distribute per second. + */ + function setRewards(uint256 rewardPerSecond_) external override { + // can only be called by FarmController contract + require(msg.sender == address(_controller), "!farmcontroller"); + // update + updateFarm(); + // accounting + _rewardPerSecond = rewardPerSecond_; + emit RewardsSet(rewardPerSecond_); + } + + /** + * @notice Sets the farm's end time. Used to extend the duration. + * Can only be called by the current [**governor**](/docs/protocol/governance). + * @param endTime_ The new end time. + */ + function setEnd(uint256 endTime_) external override onlyGovernance { + // accounting + _endTime = endTime_; + // update + updateFarm(); + emit FarmEndSet(endTime_); + } +} diff --git a/contracts/interface/ICpFarm.sol b/contracts/interface/ICpFarm.sol index 4082e827..40ece275 100644 --- a/contracts/interface/ICpFarm.sol +++ b/contracts/interface/ICpFarm.sol @@ -8,9 +8,9 @@ import "./IFarm.sol"; /** * @title CpFarm * @author solace.fi - * @notice Rewards [**Capital Providers**](/docs/user-guides/capital-provider/cp-role-guide) in [**SOLACE**](../SOLACE) for providing capital in the [`Vault`](../Vault). + * @notice Rewards [**Capital Providers**](/docs/user-guides/capital-provider/cp-role-guide) in [**Options**](../OptionFarming) for providing capital in the [`Vault`](../Vault). * - * Over the course of `startTime` to `endTime`, the farm distributes `rewardPerSecond` [**SOLACE**](../SOLACE) to all farmers split relative to the amount of [**SCP**](../Vault) they have deposited. + * Over the course of `startTime` to `endTime`, the farm distributes `rewardPerSecond` [**Options**](../OptionFarming) to all farmers split relative to the amount of [**SCP**](../Vault) they have deposited. * * Users can become [**Capital Providers**](/docs/user-guides/capital-provider/cp-role-guide) by depositing **ETH** into the [`Vault`](../Vault), receiving [**SCP**](../Vault) in the process. [**Capital Providers**](/docs/user-guides/capital-provider/cp-role-guide) can then deposit their [**SCP**](../Vault) via [`depositCp()`](#depositcp) or [`depositCpSigned()`](#depositcpsigned). Alternatively users can bypass the [`Vault`](../Vault) and stake their **ETH** via [`depositEth()`](#depositeth). * diff --git a/contracts/interface/ISptFarm.sol b/contracts/interface/ISptFarm.sol new file mode 100644 index 00000000..78f0cbd6 --- /dev/null +++ b/contracts/interface/ISptFarm.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.6; + +import "./IPolicyManager.sol"; +import "./IFarm.sol"; + + +/** + * @title ISptFarm + * @author solace.fi + * @notice Rewards [**Policyholders**](/docs/protocol/policy-holder) in [**Options**](../OptionFarming) for staking their [**Policies**](./PolicyManager). + * + * Over the course of `startTime` to `endTime`, the farm distributes `rewardPerSecond` [**Options**](../OptionFarming) to all farmers split relative to the value of the policies they have deposited. + * + * Note that you should deposit your policies via [`depositPolicy()`](#depositpolicy) or [`depositPolicySigned()`](#depositpolicysigned). Raw `ERC721.transfer()` will not be recognized. + */ +interface ISptFarm is IFarm { + + /*************************************** + EVENTS + ***************************************/ + + // Emitted when a policy is deposited onto the farm. + event PolicyDeposited(address indexed user, uint256 policyID); + // Emitted when a policy is withdrawn from the farm. + event PolicyWithdrawn(address indexed user, uint256 policyID); + /// @notice Emitted when rewardPerSecond is changed. + event RewardsSet(uint256 rewardPerSecond); + /// @notice Emitted when the end time is changed. + event FarmEndSet(uint256 endTime); + + /*************************************** + VIEW FUNCTIONS + ***************************************/ + + /// @notice + function policyManager() external view returns (address policyManager_); + + /// @notice Last time rewards were distributed or farm was updated. + function lastRewardTime() external view returns (uint256 timestamp); + + /// @notice Accumulated rewards per share, times 1e12. + function accRewardPerShare() external view returns (uint256 acc); + + /// @notice Value of policies a user deposited. + function userStaked(address user) external view returns (uint256 amount); + + /// @notice Value of policies deposited by all farmers. + function valueStaked() external view returns (uint256 amount); + + /// @notice Information about a deposited policy. + function policyInfo(uint256 policyID) external view returns (address depositor, uint256 value); + + /** + * @notice Returns the count of [**policies**](./PolicyManager) that a user has deposited onto the farm. + * @param user The user to check count for. + * @return count The count of deposited [**policies**](./PolicyManager). + */ + function countDeposited(address user) external view returns (uint256 count); + + /** + * @notice Returns the list of [**policies**](./PolicyManager) that a user has deposited onto the farm and their values. + * @param user The user to list deposited policies. + * @return policyIDs The list of deposited policies. + * @return policyValues The values of the policies. + */ + function listDeposited(address user) external view returns (uint256[] memory policyIDs, uint256[] memory policyValues); + + /** + * @notice Returns the ID of a [**Policies**](./PolicyManager) that a user has deposited onto a farm and its value. + * @param user The user to get policyID for. + * @param index The farm-based index of the token. + * @return policyID The ID of the deposited [**policy**](./PolicyManager). + * @return policyValue The value of the [**policy**](./PolicyManager). + */ + function getDeposited(address user, uint256 index) external view returns (uint256 policyID, uint256 policyValue); + + /*************************************** + MUTATOR FUNCTIONS + ***************************************/ + + /** + * @notice Deposit a [**policy**](./PolicyManager). + * User must `ERC721.approve()` or `ERC721.setApprovalForAll()` first. + * @param policyID The ID of the policy to deposit. + */ + function depositPolicy(uint256 policyID) external; + + /** + * @notice Deposit a [**policy**](./PolicyManager) using permit. + * @param depositor The depositing user. + * @param policyID The ID of the policy to deposit. + * @param deadline Time the transaction must go through before. + * @param v secp256k1 signature + * @param r secp256k1 signature + * @param s secp256k1 signature + */ + function depositPolicySigned(address depositor, uint256 policyID, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; + + /** + * @notice Deposit multiple [**policies**](./PolicyManager). + * User must `ERC721.approve()` or `ERC721.setApprovalForAll()` first. + * @param policyIDs The IDs of the policies to deposit. + */ + function depositPolicyMulti(uint256[] memory policyIDs) external; + + /** + * @notice Deposit multiple [**policies**](./PolicyManager) using permit. + * @param depositors The depositing users. + * @param policyIDs The IDs of the policies to deposit. + * @param deadlines Times the transactions must go through before. + * @param vs secp256k1 signatures + * @param rs secp256k1 signatures + * @param ss secp256k1 signatures + */ + function depositPolicySignedMulti(address[] memory depositors, uint256[] memory policyIDs, uint256[] memory deadlines, uint8[] memory vs, bytes32[] memory rs, bytes32[] memory ss) external; + + /** + * @notice Withdraw a [**policy**](./PolicyManager). + * Can only withdraw policies you deposited. + * @param policyID The ID of the policy to withdraw. + */ + function withdrawPolicy(uint256 policyID) external; + + /** + * @notice Withdraw multiple [**policies**](./PolicyManager). + * Can only withdraw policies you deposited. + * @param policyIDs The IDs of the policies to withdraw. + */ + function withdrawPolicyMulti(uint256[] memory policyIDs) external; + + /** + * @notice Burns expired policies. + * @param policyIDs The list of expired policies. + */ + function updateActivePolicies(uint256[] calldata policyIDs) external; +} diff --git a/hardhat.config.ts b/hardhat.config.ts index c4a37f11..531f8160 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -13,9 +13,9 @@ import { config as dotenv_config } from "dotenv"; dotenv_config(); const USE_PROCESSED_FILES = process.env.USE_PROCESSED_FILES === "true"; -const mainnet_fork = { url: process.env.MAINNET_URL || '', blockNumber: 13463370 }; -const rinkeby_fork = { url: process.env.RINKEBY_URL || '', blockNumber: 9487521 }; -const kovan_fork = { url: process.env.KOVAN_URL || '', blockNumber: 26927369 }; +const mainnet_fork = { url: process.env.MAINNET_URL || '', blockNumber: 13529321 }; +const rinkeby_fork = { url: process.env.RINKEBY_URL || '', blockNumber: 9565570 }; +const kovan_fork = { url: process.env.KOVAN_URL || '', blockNumber: 28087080 }; const no_fork = { url: '', blockNumber: 0 }; const forking = ( process.env.FORK_NETWORK === "mainnet" ? mainnet_fork diff --git a/scripts/deploy-kovan.ts b/scripts/deploy-kovan.ts index 1c886574..e6384add 100644 --- a/scripts/deploy-kovan.ts +++ b/scripts/deploy-kovan.ts @@ -12,7 +12,7 @@ import { create2Contract } from "./create2Contract"; import { logContractAddress } from "./utils"; import { import_artifacts, ArtifactImports } from "./../test/utilities/artifact_importer"; -import { Deployer, Registry, Weth9, Vault, ClaimsEscrow, Treasury, PolicyManager, PolicyDescriptor, RiskManager, OptionsFarming, FarmController, CpFarm, Solace, AaveV2Product, WaaveProduct } from "../typechain"; +import { Deployer, Registry, Weth9, Vault, ClaimsEscrow, Treasury, PolicyManager, PolicyDescriptor, RiskManager, OptionsFarming, FarmController, CpFarm, SptFarm, Solace, AaveV2Product, WaaveProduct } from "../typechain"; const DEPLOYER_CONTRACT_ADDRESS = "0x501aCe4732E4A80CC1bc5cd081BEe7f88ff694EF"; const REGISTRY_ADDRESS = "0x501aCEE3310d98881c827d4357C970F23a30AD29"; @@ -26,6 +26,7 @@ const RISK_MANAGER_ADDRESS = "0x501ACe9eE0AB4D2D4204Bcf3bE6eE13Fd6337804"; const OPTIONS_FARMING_ADDRESS = "0x501ACEB9772d1EfE5F8eA46FE5004fAd039e067A"; const FARM_CONTROLLER_ADDRESS = "0x501aCEDD1a697654d5F53514FF09eDECD3ca6D95"; const CP_FARM_ADDRESS = "0x501ACeb4D4C2CB7E4b07b53fbe644f3e51D25A3e"; +const SPT_FARM_ADDRESS = "0x501acE7644A3482F7358BE05454278cF2c699581"; const SOLACE_ADDRESS = "0x501ACe4A8d42bA427B67d0CaD1AB11e25AeA65Ab"; const AAVE_PRODUCT_ADDRESS = "0x501ace153Ff22348076FdD236b774F6eb2d55EfB"; @@ -44,9 +45,13 @@ const minPeriod = 6450; // this is about 1 day const maxPeriod = 2354250; // this is about 1 year from https://ycharts.com/indicators/ethereum_blocks_per_day const price = 11044; // 2.60%/yr // farm params -const startTime = 1634515200; // Oct 17, 2021 -const endTime = 1666051200; // Oct 17, 2022 -const solacePerSecond = BN.from("1157407407407407400"); // 100K per day +const solacePerSecond = BN.from("1157407407407407400"); // 100K per day across all farms +const cpFarmStartTime = 1634515200; // Oct 17, 2021 +const cpFarmEndTime = 1666051200; // Oct 17, 2022 +const cpFarmAllocPoints = 90000; +const sptFarmStartTime = 1635897600; // Nov 3, 2021 +const sptFarmEndTime = 1667449200; // Nov 3, 2022 +const sptFarmAllocPoints = 10000; let artifacts: ArtifactImports; let deployerContract: Deployer; @@ -62,6 +67,7 @@ let riskManager: RiskManager; let optionsFarming: OptionsFarming; let farmController: FarmController; let cpFarm: CpFarm; +let sptFarm: SptFarm; let solace: Solace; let aaveProduct: AaveV2Product; @@ -97,6 +103,7 @@ async function main() { await deployOptionsFarming(); await deployFarmController(); await deployCpFarm(); + await deploySptFarm(); await deploySOLACE(); // products await deployAaveV2Product(); @@ -309,19 +316,37 @@ async function deployCpFarm() { cpFarm = (await ethers.getContractAt(artifacts.CpFarm.abi, CP_FARM_ADDRESS)) as CpFarm; } else { console.log("Deploying CpFarm"); - var res = await create2Contract(deployer,artifacts.CpFarm,[signerAddress,registry.address,startTime,endTime], {}, "", deployerContract.address); + var res = await create2Contract(deployer,artifacts.CpFarm,[signerAddress,registry.address,cpFarmStartTime,cpFarmEndTime], {}, "", deployerContract.address); cpFarm = (await ethers.getContractAt(artifacts.CpFarm.abi, res.address)) as CpFarm; transactions.push({"description": "Deploy CpFarm", "to": deployerContract.address, "gasLimit": res.gasUsed}); console.log(`Deployed CpFarm to ${cpFarm.address}`); } if((await farmController.farmIndices(cpFarm.address)).eq(0) && await farmController.governance() == signerAddress) { console.log("Registering CpFarm in FarmController"); - let tx = await farmController.connect(deployer).registerFarm(cpFarm.address, 50); + let tx = await farmController.connect(deployer).registerFarm(cpFarm.address, cpFarmAllocPoints); let receipt = await tx.wait(); transactions.push({"description": "Register CpFarm in FarmController", "to": farmController.address, "gasLimit": receipt.gasUsed.toString()}); } } +async function deploySptFarm() { + if(!!SPT_FARM_ADDRESS) { + sptFarm = (await ethers.getContractAt(artifacts.SptFarm.abi, SPT_FARM_ADDRESS)) as SptFarm; + } else { + console.log("Deploying SptFarm"); + var res = await create2Contract(deployer,artifacts.SptFarm,[signerAddress,registry.address,sptFarmStartTime,sptFarmEndTime], {}, "", deployerContract.address); + sptFarm = (await ethers.getContractAt(artifacts.SptFarm.abi, res.address)) as SptFarm; + transactions.push({"description": "Deploy SptFarm", "to": deployerContract.address, "gasLimit": res.gasUsed}); + console.log(`Deployed SptFarm to ${sptFarm.address}`); + } + if((await farmController.farmIndices(sptFarm.address)).eq(0) && await farmController.governance() == signerAddress) { + console.log("Registering SptFarm in FarmController"); + let tx = await farmController.connect(deployer).registerFarm(sptFarm.address, sptFarmAllocPoints); + let receipt = await tx.wait(); + transactions.push({"description": "Register SptFarm in FarmController", "to": farmController.address, "gasLimit": receipt.gasUsed.toString()}); + } +} + async function deploySOLACE() { if(!!SOLACE_ADDRESS) { solace = (await ethers.getContractAt(artifacts.SOLACE.abi, SOLACE_ADDRESS)) as Solace; @@ -427,6 +452,7 @@ async function logAddresses() { logContractAddress("OptionsFarming", optionsFarming.address); logContractAddress("FarmController", farmController.address); logContractAddress("CpFarm", cpFarm.address); + logContractAddress("SptFarm", sptFarm.address); logContractAddress("SOLACE", solace.address); logContractAddress("AaveV2Product", aaveProduct.address); diff --git a/scripts/deploy-mainnet.ts b/scripts/deploy-mainnet.ts index 69c3025b..bd091a6a 100644 --- a/scripts/deploy-mainnet.ts +++ b/scripts/deploy-mainnet.ts @@ -12,7 +12,7 @@ import { create2Contract } from "./create2Contract"; import { logContractAddress } from "./utils"; import { import_artifacts, ArtifactImports } from "./../test/utilities/artifact_importer"; -import { Deployer, Registry, Weth9, Vault, ClaimsEscrow, Treasury, PolicyManager, PolicyDescriptor, RiskManager, OptionsFarming, FarmController, CpFarm, Solace } from "../typechain"; +import { Deployer, Registry, Weth9, Vault, ClaimsEscrow, Treasury, PolicyManager, PolicyDescriptor, RiskManager, OptionsFarming, FarmController, CpFarm, SptFarm, Solace } from "../typechain"; const DEPLOYER_CONTRACT_ADDRESS = "0x501aCe4732E4A80CC1bc5cd081BEe7f88ff694EF"; const REGISTRY_ADDRESS = "0x501aCEE3310d98881c827d4357C970F23a30AD29"; @@ -26,22 +26,20 @@ const RISK_MANAGER_ADDRESS = "0x501ACe9eE0AB4D2D4204Bcf3bE6eE13Fd6337804"; const OPTIONS_FARMING_ADDRESS = "0x501ACEB9772d1EfE5F8eA46FE5004fAd039e067A"; const FARM_CONTROLLER_ADDRESS = "0x501aCEDD1a697654d5F53514FF09eDECD3ca6D95"; const CP_FARM_ADDRESS = "0x501ACeb4D4C2CB7E4b07b53fbe644f3e51D25A3e"; +const SPT_FARM_ADDRESS = "0x501acE7644A3482F7358BE05454278cF2c699581"; const SOLACE_ADDRESS = ""; const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; -const COMPTROLLER_ADDRESS = "0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B"; -const AAVE_DATA_PROVIDER = "0x057835Ad21a177dbdd3090bB1CAE03EaCF78Fc6d"; -const UNISWAP_ROUTER_ADDRESS = "0xE592427A0AEce92De3Edee1F18E0157C05861564"; const SINGLETON_FACTORY_ADDRESS = "0xce0042B868300000d44A59004Da54A005ffdcf9f"; -// product params -const minPeriod = 6450; // this is about 1 day -const maxPeriod = 2354250; // this is about 1 year from https://ycharts.com/indicators/ethereum_blocks_per_day -const price = 11044; // 2.60%/yr // farm params -const startTime = 1634515200; // Oct 17, 2021 -const endTime = 1666051200; // Oct 17, 2022 -const solacePerSecond = BN.from("1157407407407407400"); // 100K per day +const solacePerSecond = BN.from("1157407407407407400"); // 100K per day across all farms +const cpFarmStartTime = 1634515200; // Oct 17, 2021 +const cpFarmEndTime = 1666051200; // Oct 17, 2022 +const cpFarmAllocPoints = 90000; +const sptFarmStartTime = 1635897600; // Nov 3, 2021 +const sptFarmEndTime = 1667449200; // Nov 3, 2022 +const sptFarmAllocPoints = 10000; let artifacts: ArtifactImports; let deployerContract: Deployer; @@ -57,6 +55,7 @@ let riskManager: RiskManager; let optionsFarming: OptionsFarming; let farmController: FarmController; let cpFarm: CpFarm; +let sptFarm: SptFarm; let solace: Solace; let signerAddress: string; @@ -90,6 +89,7 @@ async function main() { await deployOptionsFarming(); await deployFarmController(); await deployCpFarm(); + await deploySptFarm(); //await deploySOLACE(); writeFileSync("stash/transactions/deployTransactionsMainnet.json", JSON.stringify(transactions, undefined, ' ')); @@ -337,14 +337,14 @@ async function deployCpFarm() { cpFarm = (await ethers.getContractAt(artifacts.CpFarm.abi, CP_FARM_ADDRESS)) as CpFarm; } else { console.log("Deploying CpFarm"); - var res = await create2Contract(deployer,artifacts.CpFarm,[signerAddress,registry.address,startTime,endTime], {}, "", deployerContract.address); + var res = await create2Contract(deployer,artifacts.CpFarm,[signerAddress,registry.address,cpFarmStartTime,cpFarmEndTime], {}, "", deployerContract.address); cpFarm = (await ethers.getContractAt(artifacts.CpFarm.abi, res.address)) as CpFarm; transactions.push({"description": "Deploy CpFarm", "to": deployerContract.address, "gasLimit": res.gasUsed}); console.log(`Deployed CpFarm to ${cpFarm.address}`); } if((await farmController.farmIndices(cpFarm.address)).eq(0) && await farmController.governance() == signerAddress) { console.log("Registering CpFarm in FarmController"); - let tx = await farmController.connect(deployer).registerFarm(cpFarm.address, 50); + let tx = await farmController.connect(deployer).registerFarm(cpFarm.address, cpFarmAllocPoints); let receipt = await tx.wait(); transactions.push({"description": "Register CpFarm in FarmController", "to": farmController.address, "gasLimit": receipt.gasUsed.toString()}); } @@ -355,6 +355,31 @@ async function deployCpFarm() { } } +async function deploySptFarm() { + if(!!SPT_FARM_ADDRESS) { + sptFarm = (await ethers.getContractAt(artifacts.SptFarm.abi, SPT_FARM_ADDRESS)) as SptFarm; + } else { + console.log("Deploying SptFarm"); + var res = await create2Contract(deployer,artifacts.SptFarm,[signerAddress,registry.address,sptFarmStartTime,sptFarmEndTime], {nonce: 74}, "", deployerContract.address); + sptFarm = (await ethers.getContractAt(artifacts.SptFarm.abi, res.address)) as SptFarm; + transactions.push({"description": "Deploy SptFarm", "to": deployerContract.address, "gasLimit": res.gasUsed}); + console.log(`Deployed SptFarm to ${sptFarm.address}`); + } + /* + if((await farmController.farmIndices(sptFarm.address)).eq(0) && await farmController.governance() == signerAddress) { + console.log("Registering SptFarm in FarmController"); + let tx = await farmController.connect(deployer).registerFarm(sptFarm.address, sptFarmAllocPoints); + let receipt = await tx.wait(); + transactions.push({"description": "Register SptFarm in FarmController", "to": farmController.address, "gasLimit": receipt.gasUsed.toString()}); + } + */ + if(await sptFarm.governance() === signerAddress && await sptFarm.pendingGovernance() !== multisigAddress) { + console.log(`sptFarm.setPendingGovernance(${multisigAddress})`) + let tx = await sptFarm.connect(deployer).setPendingGovernance(multisigAddress); + await tx.wait(); + } +} + async function deploySOLACE() { if(!!SOLACE_ADDRESS) { solace = (await ethers.getContractAt(artifacts.SOLACE.abi, SOLACE_ADDRESS)) as Solace; diff --git a/scripts/deploy-rinkeby.ts b/scripts/deploy-rinkeby.ts index 53bb2abd..d0bc055d 100644 --- a/scripts/deploy-rinkeby.ts +++ b/scripts/deploy-rinkeby.ts @@ -12,7 +12,7 @@ import { create2Contract } from "./create2Contract"; import { logContractAddress } from "./utils"; import { import_artifacts, ArtifactImports } from "./../test/utilities/artifact_importer"; -import { Deployer, Registry, Weth9, Vault, ClaimsEscrow, Treasury, PolicyManager, PolicyDescriptor, RiskManager, OptionsFarming, FarmController, CpFarm, Solace, CompoundProductRinkeby, WaaveProduct, LiquityProduct } from "../typechain"; +import { Deployer, Registry, Weth9, Vault, ClaimsEscrow, Treasury, PolicyManager, PolicyDescriptor, RiskManager, OptionsFarming, FarmController, CpFarm, SptFarm, Solace, CompoundProductRinkeby, WaaveProduct, LiquityProduct } from "../typechain"; const DEPLOYER_CONTRACT_ADDRESS = "0x501aCe4732E4A80CC1bc5cd081BEe7f88ff694EF"; const REGISTRY_ADDRESS = "0x501aCEE3310d98881c827d4357C970F23a30AD29"; @@ -26,6 +26,7 @@ const RISK_MANAGER_ADDRESS = "0x501ACe9eE0AB4D2D4204Bcf3bE6eE13Fd6337804"; const OPTIONS_FARMING_ADDRESS = "0x501ACEB9772d1EfE5F8eA46FE5004fAd039e067A"; const FARM_CONTROLLER_ADDRESS = "0x501aCEDD1a697654d5F53514FF09eDECD3ca6D95"; const CP_FARM_ADDRESS = "0x501ACeb4D4C2CB7E4b07b53fbe644f3e51D25A3e"; +const SPT_FARM_ADDRESS = "0x501acE7644A3482F7358BE05454278cF2c699581"; const SOLACE_ADDRESS = "0x501ACe4A8d42bA427B67d0CaD1AB11e25AeA65Ab"; const COMPOUND_PRODUCT_ADDRESS = "0x501AcE207C72f084B172816e1CB8EC2A90bdaAE8"; @@ -46,9 +47,13 @@ const minPeriod = 6450; // this is about 1 day const maxPeriod = 2354250; // this is about 1 year from https://ycharts.com/indicators/ethereum_blocks_per_day const price = 11044; // 2.60%/yr // farm params -const startTime = 1634515200; // Oct 17, 2021 -const endTime = 1666051200; // Oct 17, 2022 -const solacePerSecond = BN.from("1157407407407407400"); // 100K per day +const solacePerSecond = BN.from("1157407407407407400"); // 100K per day across all farms +const cpFarmStartTime = 1634515200; // Oct 17, 2021 +const cpFarmEndTime = 1666051200; // Oct 17, 2022 +const cpFarmAllocPoints = 90000; +const sptFarmStartTime = 1635897600; // Nov 3, 2021 +const sptFarmEndTime = 1667449200; // Nov 3, 2022 +const sptFarmAllocPoints = 10000; let artifacts: ArtifactImports; let deployerContract: Deployer; @@ -64,6 +69,7 @@ let riskManager: RiskManager; let optionsFarming: OptionsFarming; let farmController: FarmController; let cpFarm: CpFarm; +let sptFarm: SptFarm; let solace: Solace; let compoundProduct: CompoundProductRinkeby; @@ -100,6 +106,7 @@ async function main() { await deployOptionsFarming(); await deployFarmController(); await deployCpFarm(); + await deploySptFarm(); await deploySOLACE(); // products await deployCompoundProduct(); @@ -313,19 +320,37 @@ async function deployCpFarm() { cpFarm = (await ethers.getContractAt(artifacts.CpFarm.abi, CP_FARM_ADDRESS)) as CpFarm; } else { console.log("Deploying CpFarm"); - var res = await create2Contract(deployer,artifacts.CpFarm,[signerAddress,registry.address,startTime,endTime], {}, "", deployerContract.address); + var res = await create2Contract(deployer,artifacts.CpFarm,[signerAddress,registry.address,cpFarmStartTime,cpFarmEndTime], {}, "", deployerContract.address); cpFarm = (await ethers.getContractAt(artifacts.CpFarm.abi, res.address)) as CpFarm; transactions.push({"description": "Deploy CpFarm", "to": deployerContract.address, "gasLimit": res.gasUsed}); console.log(`Deployed CpFarm to ${cpFarm.address}`); } if((await farmController.farmIndices(cpFarm.address)).eq(0) && await farmController.governance() == signerAddress) { console.log("Registering CpFarm in FarmController"); - let tx = await farmController.connect(deployer).registerFarm(cpFarm.address, 50); + let tx = await farmController.connect(deployer).registerFarm(cpFarm.address, cpFarmAllocPoints); let receipt = await tx.wait(); transactions.push({"description": "Register CpFarm in FarmController", "to": farmController.address, "gasLimit": receipt.gasUsed.toString()}); } } +async function deploySptFarm() { + if(!!SPT_FARM_ADDRESS) { + sptFarm = (await ethers.getContractAt(artifacts.SptFarm.abi, SPT_FARM_ADDRESS)) as SptFarm; + } else { + console.log("Deploying SptFarm"); + var res = await create2Contract(deployer,artifacts.SptFarm,[signerAddress,registry.address,sptFarmStartTime,sptFarmEndTime], {}, "", deployerContract.address); + sptFarm = (await ethers.getContractAt(artifacts.SptFarm.abi, res.address)) as SptFarm; + transactions.push({"description": "Deploy SptFarm", "to": deployerContract.address, "gasLimit": res.gasUsed}); + console.log(`Deployed SptFarm to ${sptFarm.address}`); + } + if((await farmController.farmIndices(sptFarm.address)).eq(0) && await farmController.governance() == signerAddress) { + console.log("Registering SptFarm in FarmController"); + let tx = await farmController.connect(deployer).registerFarm(sptFarm.address, sptFarmAllocPoints); + let receipt = await tx.wait(); + transactions.push({"description": "Register SptFarm in FarmController", "to": farmController.address, "gasLimit": receipt.gasUsed.toString()}); + } +} + async function deploySOLACE() { if(!!SOLACE_ADDRESS) { solace = (await ethers.getContractAt(artifacts.SOLACE.abi, SOLACE_ADDRESS)) as Solace; @@ -454,6 +479,7 @@ async function logAddresses() { logContractAddress("OptionsFarming", optionsFarming.address); logContractAddress("FarmController", farmController.address); logContractAddress("CpFarm", cpFarm.address); + logContractAddress("SptFarm", sptFarm.address); logContractAddress("SOLACE", solace.address); logContractAddress("CompoundProduct", compoundProduct.address); diff --git a/test/CpFarm.test.ts b/test/CpFarm.test.ts index c4e21ab3..351995a7 100644 --- a/test/CpFarm.test.ts +++ b/test/CpFarm.test.ts @@ -752,7 +752,7 @@ describe("CpFarm", function () { userSolace: BN; farmCp: BN; farmStake: BN; - farmControllerSolace: BN; + optionsFarmingSolace: BN; vaultAssets: BN; } @@ -766,7 +766,7 @@ describe("CpFarm", function () { userSolace: await solace.balanceOf(user.address), farmCp: await vault.balanceOf(farm.address), farmStake: await farm.valueStaked(), - farmControllerSolace: await solace.balanceOf(farmController.address), + optionsFarmingSolace: await solace.balanceOf(optionsFarming.address), vaultAssets: await vault.totalAssets() }; } @@ -781,7 +781,7 @@ describe("CpFarm", function () { userSolace: balances1.userSolace.sub(balances2.userSolace), farmCp: balances1.farmCp.sub(balances2.farmCp), farmStake: balances1.farmStake.sub(balances2.farmStake), - farmControllerSolace: balances1.farmControllerSolace.sub(balances2.farmControllerSolace), + optionsFarmingSolace: balances1.optionsFarmingSolace.sub(balances2.optionsFarmingSolace), vaultAssets: balances1.vaultAssets.sub(balances2.vaultAssets) }; } diff --git a/test/SptFarm.test.ts b/test/SptFarm.test.ts new file mode 100644 index 00000000..a751269c --- /dev/null +++ b/test/SptFarm.test.ts @@ -0,0 +1,989 @@ +import { ethers, waffle, upgrades } from "hardhat"; +const { deployContract, solidity } = waffle; +import { MockProvider } from "ethereum-waffle"; +const provider: MockProvider = waffle.provider; +import { Transaction, BigNumber as BN, Contract, constants, BigNumberish, Wallet } from "ethers"; +import chai from "chai"; +const { expect } = chai; +chai.use(solidity); + +import { encodePriceSqrt, FeeAmount, TICK_SPACINGS, getMaxTick, getMinTick } from "./utilities/uniswap"; +import { bnAddSub, bnMulDiv, expectClose } from "./utilities/math"; +import { getPermitErc721EnhancedSignature } from "./utilities/getPermitNFTSignature"; + +import { import_artifacts, ArtifactImports } from "./utilities/artifact_importer"; +import { Solace, FarmController, OptionsFarming, SptFarm, PolicyManager, RiskManager, Registry, MockProduct, Weth9, Treasury } from "../typechain"; +import { burnBlocks } from "./utilities/time"; + +// contracts +let solace: Solace; +let farmController: FarmController; +let optionsFarming: OptionsFarming; +let farm1: SptFarm; +let weth: Weth9; +let registry: Registry; +let treasury: Treasury; +let policyManager: PolicyManager; +let riskManager: RiskManager; +let product: MockProduct; + +// uniswap contracts +let uniswapFactory: Contract; +let uniswapRouter: Contract; +let lpToken: Contract; + +// pools +let solaceEthPool: Contract; + +// vars +let solacePerSecond = BN.from("100000000000000000000"); // 100 e18 +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const ONE_ETHER = BN.from("1000000000000000000"); +const TEN_ETHER = BN.from("10000000000000000000"); +const FIFTY_THOUSAND_ETHER = BN.from("50000000000000000000000"); +const ONE_MILLION_ETHER = BN.from("1000000000000000000000000"); +const ONE_YEAR = 31536000; // in seconds +let timestamp: number; +let initTime: number; +let startTime: number; +let endTime: number; +let sptFarmType = 3; +const price = 10000; +const duration = 1000; +const chainID = 31337; +const deadline = constants.MaxUint256; + +describe("SptFarm", function () { + const [deployer, governor, farmer1, farmer2, trader, coveredPlatform] = provider.getWallets(); + let artifacts: ArtifactImports; + + before(async function () { + artifacts = await import_artifacts(); + await deployer.sendTransaction({to:deployer.address}); // for some reason this helps solidity-coverage + + weth = (await deployContract(deployer, artifacts.WETH)) as Weth9; + + // deploy uniswap contracts + uniswapFactory = (await deployContract(deployer, artifacts.UniswapV3Factory)) as Contract; + lpToken = (await deployContract(deployer, artifacts.NonfungiblePositionManager, [uniswapFactory.address, weth.address, ZERO_ADDRESS])) as Contract; + uniswapRouter = (await deployContract(deployer, artifacts.SwapRouter, [uniswapFactory.address, weth.address])) as Contract; + + // deploy solace contracts + registry = (await deployContract(deployer, artifacts.Registry, [governor.address])) as Registry; + weth = (await deployContract(deployer, artifacts.WETH)) as Weth9; + await registry.connect(governor).setWeth(weth.address); + treasury = (await deployContract(deployer, artifacts.Treasury, [governor.address, registry.address])) as Treasury; + await registry.connect(governor).setTreasury(treasury.address); + policyManager = (await deployContract(deployer, artifacts.PolicyManager, [governor.address])) as PolicyManager; + await registry.connect(governor).setPolicyManager(policyManager.address); + riskManager = (await deployContract(deployer, artifacts.RiskManager, [governor.address, registry.address])) as RiskManager; + await registry.connect(governor).setRiskManager(riskManager.address); + solace = (await deployContract(deployer, artifacts.SOLACE, [governor.address])) as Solace; + await registry.connect(governor).setSolace(solace.address); + optionsFarming = (await deployContract(deployer, artifacts.OptionsFarming, [governor.address])) as OptionsFarming; + await registry.connect(governor).setOptionsFarming(optionsFarming.address); + farmController = (await deployContract(deployer, artifacts.FarmController, [governor.address, optionsFarming.address, solacePerSecond])) as FarmController; + await registry.connect(governor).setFarmController(farmController.address); + await optionsFarming.connect(governor).setFarmController(farmController.address); + + // add products + product = (await deployContract(deployer, artifacts.MockProduct, [deployer.address, policyManager.address, registry.address, coveredPlatform.address, 0, 100000000000, price])) as MockProduct; + await policyManager.connect(governor).addProduct(product.address); + await riskManager.connect(governor).addProduct(product.address, 1, price, 1); + + // transfer tokens + await solace.connect(governor).addMinter(governor.address); + await solace.connect(governor).mint(governor.address, ONE_MILLION_ETHER); + await solace.connect(governor).mint(trader.address, FIFTY_THOUSAND_ETHER); + await solace.connect(trader).approve(uniswapRouter.address, constants.MaxUint256); + await solace.connect(trader).approve(lpToken.address, constants.MaxUint256); + await weth.connect(farmer1).deposit({ value: TEN_ETHER }); + await weth.connect(farmer2).deposit({ value: TEN_ETHER }); + await weth.connect(trader).deposit({ value: TEN_ETHER }); + await weth.connect(trader).approve(uniswapRouter.address, constants.MaxUint256); + await weth.connect(trader).approve(lpToken.address, constants.MaxUint256); + + // create pools + // 50,000 solace = 1 eth (or 1 solace = 8 cents @ 1 eth = $4000) + let solaceIsToken0 = BN.from(solace.address).lt(BN.from(weth.address)); + let amount0 = solaceIsToken0 ? FIFTY_THOUSAND_ETHER : ONE_ETHER; + let amount1 = solaceIsToken0 ? ONE_ETHER : FIFTY_THOUSAND_ETHER; + let sqrtPrice = encodePriceSqrt(amount1, amount0); + solaceEthPool = await createPool(weth, solace, FeeAmount.MEDIUM, sqrtPrice); + await mintLpToken(trader, solace, weth, FeeAmount.MEDIUM, amount0, amount1); + }); + + describe("deployment", function () { + it("reverts if zero registry", async function () { + await expect(deployContract(deployer, artifacts.SptFarm, [governor.address, ZERO_ADDRESS, 1, 2])).to.be.revertedWith("zero address registry"); + }); + it("reverts if zero controller", async function () { + let registry2 = (await deployContract(deployer, artifacts.Registry, [governor.address])) as Registry; + await expect(deployContract(deployer, artifacts.SptFarm, [governor.address, registry2.address, 1, 2])).to.be.revertedWith("zero address controller"); + }); + it("reverts if zero policymanager", async function () { + let registry2 = (await deployContract(deployer, artifacts.Registry, [governor.address])) as Registry; + await registry2.connect(governor).setFarmController(farmController.address); + await expect(deployContract(deployer, artifacts.SptFarm, [governor.address, registry2.address, 1, 2])).to.be.revertedWith("zero address policymanager"); + }); + it("reverts if invalid window", async function () { + let registry2 = (await deployContract(deployer, artifacts.Registry, [governor.address])) as Registry; + await registry2.connect(governor).setFarmController(farmController.address); + await registry2.connect(governor).setPolicyManager(policyManager.address); + await expect(deployContract(deployer, artifacts.SptFarm, [governor.address, registry2.address, 4, 3])).to.be.revertedWith("invalid window"); + }); + it("deploys successfully", async function () { + let registry2 = (await deployContract(deployer, artifacts.Registry, [governor.address])) as Registry; + await registry2.connect(governor).setFarmController(farmController.address); + await registry2.connect(governor).setPolicyManager(policyManager.address); + await deployContract(deployer, artifacts.SptFarm, [governor.address, registry2.address, 1, 2]); + }); + }); + + describe("farm creation", function () { + before(async function () { + // get referrence timestamp + await provider.send("evm_mine", []); + initTime = (await provider.getBlock('latest')).timestamp; + startTime = initTime; + endTime = initTime + ONE_YEAR; + }); + it("can create farms", async function () { + farm1 = (await deployContract(deployer, artifacts.SptFarm, [governor.address, registry.address, startTime, endTime])) as SptFarm; + }); + it("returns farm information", async function () { + expect(await farm1.farmController()).to.equal(farmController.address); + expect(await farm1.policyManager()).to.equal(policyManager.address); + expect(await farm1.farmType()).to.equal(sptFarmType); + expect(await farm1.startTime()).to.equal(startTime); + expect(await farm1.endTime()).to.equal(endTime); + expect(await farm1.rewardPerSecond()).to.equal(0); + expect(await farm1.valueStaked()).to.equal(0); + expect(await farm1.lastRewardTime()).to.be.closeTo(BN.from(initTime), 5); + expect(await farm1.accRewardPerShare()).to.equal(0); + }); + }); + + describe("governance", function () { + it("starts with the correct governor", async function () { + expect(await farm1.governance()).to.equal(governor.address); + }); + it("rejects setting new governance by non governor", async function () { + await expect(farm1.connect(farmer1).setPendingGovernance(farmer1.address)).to.be.revertedWith("!governance"); + }); + it("can set new governance", async function () { + let tx = await farm1.connect(governor).setPendingGovernance(deployer.address); + expect(tx).to.emit(farm1, "GovernancePending").withArgs(deployer.address); + expect(await farm1.governance()).to.equal(governor.address); + expect(await farm1.pendingGovernance()).to.equal(deployer.address); + }); + it("rejects governance transfer by non governor", async function () { + await expect(farm1.connect(farmer1).acceptGovernance()).to.be.revertedWith("!pending governance"); + }); + it("can transfer governance", async function () { + // set + let tx = await farm1.connect(deployer).acceptGovernance(); + await expect(tx).to.emit(farm1, "GovernanceTransferred").withArgs(governor.address, deployer.address); + expect(await farm1.governance()).to.equal(deployer.address); + expect(await farm1.pendingGovernance()).to.equal(ZERO_ADDRESS); + + await farm1.connect(deployer).setPendingGovernance(governor.address); + await farm1.connect(governor).acceptGovernance(); + }); + }); + + describe("deposit and withdraw", function () { + let policyID1: BN, policyID2: BN, policyID3: BN, policyID4: BN, policyID5: BN; + let coverAmount1 = ONE_ETHER; + let coverAmount2 = ONE_ETHER.mul(4); + let coverAmount3 = ONE_ETHER.mul(2); + let coverAmount4 = ONE_ETHER.mul(9); + let coverAmount5 = ONE_ETHER.mul(25); + let policyValue1 = coverAmount1.mul(price); + let policyValue2 = coverAmount2.mul(price); + let policyValue3 = coverAmount3.mul(price); + let policyValue4 = coverAmount4.mul(price); + let policyValue5 = coverAmount5.mul(price); + let policyValue12 = policyValue1.add(policyValue2); + let policyValue13 = policyValue1.add(policyValue3); + let policyValue123 = policyValue1.add(policyValue2).add(policyValue3); + + before(async function () { + await product.connect(farmer1)._buyPolicy(farmer1.address, coverAmount1, duration, ZERO_ADDRESS); + policyID1 = await policyManager.totalPolicyCount(); + await product.connect(farmer2)._buyPolicy(farmer2.address, coverAmount2, duration, ZERO_ADDRESS); + policyID2 = await policyManager.totalPolicyCount(); + await product.connect(farmer1)._buyPolicy(farmer1.address, coverAmount3, duration, ZERO_ADDRESS); + policyID3 = await policyManager.totalPolicyCount(); + await product.connect(farmer1)._buyPolicy(farmer1.address, coverAmount4, duration, ZERO_ADDRESS); + policyID4 = await policyManager.totalPolicyCount(); + await product.connect(farmer1)._buyPolicy(farmer1.address, coverAmount5, duration, ZERO_ADDRESS); + policyID5 = await policyManager.totalPolicyCount(); + }); + it("can deposit", async function () { + // empty + let balances1a = await getBalances(farmer1, farm1); + expect(await farm1.countDeposited(farmer1.address)).to.equal(0); + expect(await farm1.listDeposited(farmer1.address)).to.deep.equal([[], []]); + // farmer 1, deposit policy 1 + expect((await farm1.policyInfo(policyID1)).depositor).to.equal(ZERO_ADDRESS); + await policyManager.connect(farmer1).approve(farm1.address, policyID1); + let tx1 = await farm1.connect(farmer1).depositPolicy(policyID1); + await expect(tx1).to.emit(farm1, "PolicyDeposited").withArgs(farmer1.address, policyID1); + let balances1b = await getBalances(farmer1, farm1); + let balancesDiff1 = getBalancesDiff(balances1b, balances1a); + expect(balancesDiff1.farmSpt).eq(1); + expect(balancesDiff1.userSpt).eq(-1); + expect(balancesDiff1.userStaked).eq(policyValue1); + expect(balancesDiff1.farmStake).eq(policyValue1); + expect(balances1b.userStaked).eq(policyValue1); + expect(balances1b.farmStake).eq(policyValue1); + expect(balances1b.farmSpt).eq(1); + expect(await farm1.countDeposited(farmer1.address)).to.equal(1); + expect(await farm1.getDeposited(farmer1.address, 0)).to.deep.equal([policyID1, policyValue1]); + expect(await farm1.listDeposited(farmer1.address)).to.deep.equal([[policyID1], [policyValue1]]); + let policyInfo = await farm1.policyInfo(policyID1); + expect(policyInfo.depositor).eq(farmer1.address); + expect(policyInfo.value).eq(policyValue1); + // farmer 2, deposit policy 2 + let balances2a = await getBalances(farmer2, farm1); + await policyManager.connect(farmer2).approve(farm1.address, policyID2); + let tx2 = await farm1.connect(farmer2).depositPolicy(policyID2); + await expect(tx2).to.emit(farm1, "PolicyDeposited").withArgs(farmer2.address, policyID2); + let balances2b = await getBalances(farmer2, farm1); + let balancesDiff2 = getBalancesDiff(balances2b, balances2a); + expect(balancesDiff2.farmSpt).eq(1); + expect(balancesDiff2.userSpt).eq(-1); + expect(balancesDiff2.userStaked).eq(policyValue2); + expect(balancesDiff2.farmStake).eq(policyValue2); + expect(balances2b.userStaked).eq(policyValue2); + expect(balances2b.farmStake).eq(policyValue12); + expect(balances2b.farmSpt).eq(2); + expect(await farm1.countDeposited(farmer2.address)).to.equal(1); + expect(await farm1.getDeposited(farmer2.address, 0)).to.deep.equal([policyID2, policyValue2]); + expect(await farm1.listDeposited(farmer2.address)).to.deep.equal([[policyID2], [policyValue2]]); + policyInfo = await farm1.policyInfo(policyID2); + expect(policyInfo.depositor).eq(farmer2.address); + expect(policyInfo.value).eq(policyValue2); + // farmer 1, deposit policy 3 + let balances3a = await getBalances(farmer1, farm1); + await policyManager.connect(farmer1).approve(farm1.address, policyID3); + let tx3 = await farm1.connect(farmer1).depositPolicy(policyID3); + await expect(tx3).to.emit(farm1, "PolicyDeposited").withArgs(farmer1.address, policyID3); + let balances3b = await getBalances(farmer1, farm1); + let balancesDiff3 = getBalancesDiff(balances3b, balances3a); + expect(balancesDiff3.farmSpt).eq(1); + expect(balancesDiff3.userSpt).eq(-1); + expect(balancesDiff3.userStaked).eq(policyValue3); + expect(balancesDiff3.farmStake).eq(policyValue3); + expect(balances3b.userStaked).eq(policyValue13); + expect(balances3b.farmStake).eq(policyValue123); + expect(balances3b.farmSpt).eq(3); + expect(await farm1.countDeposited(farmer1.address)).to.equal(2); + expect(await farm1.getDeposited(farmer1.address, 1)).to.deep.equal([policyID3, policyValue3]); + expect(await farm1.listDeposited(farmer1.address)).to.deep.equal([ + [policyID1, policyID3], + [policyValue1, policyValue3], + ]); + policyInfo = await farm1.policyInfo(policyID3); + expect(policyInfo.depositor).eq(farmer1.address); + expect(policyInfo.value).eq(policyValue3); + }); + it("can deposit via permit", async function () { + let balances4a = await getBalances(farmer1, farm1); + const { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm1.address, policyID4, deadline); + let tx1 = await farm1.connect(farmer1).depositPolicySigned(farmer1.address, policyID4, deadline, v, r, s); + await expect(tx1).to.emit(farm1, "PolicyDeposited").withArgs(farmer1.address, policyID4); + let balances4b = await getBalances(farmer1, farm1); + let balancesDiff4 = getBalancesDiff(balances4b, balances4a); + expect(balancesDiff4.farmSpt).eq(1); + expect(balancesDiff4.userSpt).eq(-1); + expect(balancesDiff4.userStaked).eq(policyValue4); + expect(balancesDiff4.farmStake).eq(policyValue4); + expect(balances4b.farmSpt).eq(4); + expect(await farm1.countDeposited(farmer1.address)).to.equal(3); + expect(await farm1.getDeposited(farmer1.address, 2)).to.deep.equal([policyID4, policyValue4]); + expect(await farm1.listDeposited(farmer1.address)).to.deep.equal([ + [policyID1, policyID3, policyID4], + [policyValue1, policyValue3, policyValue4], + ]); + let policyInfo = await farm1.policyInfo(policyID4); + expect(policyInfo.depositor).eq(farmer1.address); + expect(policyInfo.value).eq(policyValue4); + }); + it("cannot deposit when lacking funds", async function () { + // non existant token + let policyID = (await policyManager.totalSupply()).add(2); + await expect(farm1.connect(farmer1).depositPolicy(policyID)).to.be.reverted; + // deposit without approval + await expect(farm1.connect(farmer1).depositPolicy(policyID5)).to.be.reverted; + // deposit someone elses token + await expect(farm1.connect(farmer2).depositPolicy(policyID)).to.be.reverted; + await policyManager.connect(farmer1).approve(farm1.address, policyID5); + await expect(farm1.connect(farmer2).depositPolicy(policyID5)).to.be.reverted; + // expired policy + await product.connect(farmer1)._buyPolicy(farmer1.address, coverAmount1, 0, ZERO_ADDRESS); + policyID = await policyManager.totalPolicyCount(); + await policyManager.connect(farmer1).approve(farm1.address, policyID); + await expect(farm1.connect(farmer1).depositPolicy(policyID)).to.be.revertedWith("policy is expired"); + const { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm1.address, policyID, deadline); + await expect(farm1.connect(farmer1).depositPolicySigned(farmer1.address, policyID, deadline, v, r, s)).to.be.revertedWith("policy is expired"); + }); + it("cannot withdraw another user's rewards", async function () { + await expect(farm1.connect(farmer1).withdrawRewardsForUser(farmer2.address)).to.be.revertedWith("!farmcontroller"); + }); + it("can withdraw policies", async function () { + // farmer 1, partial withdraw + let balances1a = await getBalances(farmer1, farm1); + let tx1 = await farm1.connect(farmer1).withdrawPolicy(policyID1); + await expect(tx1).to.emit(farm1, "PolicyWithdrawn").withArgs(farmer1.address, policyID1); + let balances1b = await getBalances(farmer1, farm1); + let balancesDiff1 = getBalancesDiff(balances1b, balances1a); + expect(balancesDiff1.farmSpt).eq(-1); + expect(balancesDiff1.userSpt).eq(1); + expect(balancesDiff1.userStaked).eq(policyValue1.mul(-1)); + expect(balancesDiff1.farmStake).eq(policyValue1.mul(-1)); + let policyInfo = await farm1.policyInfo(policyID1); + expect(policyInfo.depositor).eq(ZERO_ADDRESS); + expect(policyInfo.value).eq(0); + }); + it("cannot overwithdraw", async function () { + // withdraw without deposit / double withdraw + await expect(farm1.connect(farmer1).withdrawPolicy(policyID1)).to.be.reverted; + // deposit one and withdraw another + await policyManager.connect(farmer1).approve(farm1.address, policyID1); + await farm1.connect(farmer1).depositPolicy(policyID1); + await expect(farm1.connect(farmer1).withdrawPolicy(policyID5)).to.be.reverted; + // withdraw a token someone else deposited + await expect(farm1.connect(farmer1).withdrawPolicy(policyID2)).to.be.reverted; + }); + it("can deposit multi", async function () { + // create more policies + await product.connect(farmer1)._buyPolicy(farmer1.address, coverAmount1, duration, ZERO_ADDRESS); + policyID1 = await policyManager.totalPolicyCount(); + await product.connect(farmer2)._buyPolicy(farmer2.address, coverAmount2, duration, ZERO_ADDRESS); + policyID2 = await policyManager.totalPolicyCount(); + await product.connect(farmer1)._buyPolicy(farmer1.address, coverAmount3, duration, ZERO_ADDRESS); + policyID3 = await policyManager.totalPolicyCount(); + await product.connect(farmer2)._buyPolicy(farmer2.address, coverAmount4, duration, ZERO_ADDRESS); + policyID4 = await policyManager.totalPolicyCount(); + await product.connect(farmer1)._buyPolicy(farmer1.address, coverAmount5, duration, ZERO_ADDRESS); + policyID5 = await policyManager.totalPolicyCount(); + // deposit + let balances1a = await getBalances(farmer1, farm1); + await policyManager.connect(farmer1).approve(farm1.address, policyID1); + await policyManager.connect(farmer1).approve(farm1.address, policyID3); + let tx1 = await farm1.connect(farmer1).depositPolicyMulti([policyID1, policyID3]); + await expect(tx1).to.emit(farm1, "PolicyDeposited").withArgs(farmer1.address, policyID1); + await expect(tx1).to.emit(farm1, "PolicyDeposited").withArgs(farmer1.address, policyID3); + let balances1b = await getBalances(farmer1, farm1); + let balancesDiff1 = getBalancesDiff(balances1b, balances1a); + expect(balancesDiff1.farmSpt).eq(2); + expect(balancesDiff1.userSpt).eq(-2); + expect(balancesDiff1.userStaked).eq(policyValue1.add(policyValue3)); + expect(balancesDiff1.farmStake).eq(policyValue1.add(policyValue3)); + let policyInfo = await farm1.policyInfo(policyID1); + expect(policyInfo.depositor).eq(farmer1.address); + expect(policyInfo.value).eq(policyValue1); + policyInfo = await farm1.policyInfo(policyID3); + expect(policyInfo.depositor).eq(farmer1.address); + expect(policyInfo.value).eq(policyValue3); + // revert if no approval + await expect(farm1.connect(farmer1).depositPolicyMulti([policyID5])).to.be.revertedWith("ERC721: transfer caller is not owner nor approved"); + // revert double deposit + await expect(farm1.connect(farmer1).depositPolicyMulti([policyID1])).to.be.revertedWith("ERC721: transfer of token that is not own"); + // revert not your token + await expect(farm1.connect(farmer1).depositPolicyMulti([policyID4])).to.be.revertedWith("ERC721: transfer caller is not owner nor approved"); + // revert non existent token + await expect(farm1.connect(farmer1).depositPolicyMulti([999])).to.be.revertedWith("ERC721: operator query for nonexistent token"); + // empty + await farm1.connect(farmer1).depositPolicyMulti([]); + // withdraw policies + await farm1.connect(farmer1).withdrawPolicy(policyID1); + await farm1.connect(farmer1).withdrawPolicy(policyID3); + }); + it("can deposit multi signed", async function () { + // create signatures + let farmers = [farmer1, farmer2, farmer1]; + let farmerAddresses = [farmer1.address, farmer2.address, farmer1.address]; + let policyIDs = [policyID1, policyID2, policyID3]; + let deadlines = [deadline, deadline, deadline]; + let vs = [], rs = [], ss = []; + for(var i = 0; i < policyIDs.length; ++i) { + const { v, r, s } = await getPermitErc721EnhancedSignature(farmers[i], policyManager, farm1.address, policyIDs[i], deadline); + vs.push(v); + rs.push(r); + ss.push(s); + } + // deposit + let balances1a = await getBalances(farmer1, farm1); + let balances2a = await getBalances(farmer2, farm1); + let tx1 = await farm1.connect(farmer1).depositPolicySignedMulti(farmerAddresses, policyIDs, deadlines, vs, rs, ss); + await expect(tx1).to.emit(farm1, "PolicyDeposited").withArgs(farmer1.address, policyID1); + await expect(tx1).to.emit(farm1, "PolicyDeposited").withArgs(farmer2.address, policyID2); + await expect(tx1).to.emit(farm1, "PolicyDeposited").withArgs(farmer1.address, policyID3); + let balances1b = await getBalances(farmer1, farm1); + let balances2b = await getBalances(farmer2, farm1); + let balancesDiff1 = getBalancesDiff(balances1b, balances1a); + let balancesDiff2 = getBalancesDiff(balances2b, balances2a); + expect(balancesDiff1.farmSpt).eq(3); + expect(balancesDiff1.userSpt).eq(-2); + expect(balancesDiff2.userSpt).eq(-1); + expect(balancesDiff1.userStaked).eq(policyValue1.add(policyValue3)); + expect(balancesDiff1.farmStake).eq(policyValue1.add(policyValue2).add(policyValue3)); + expect(balancesDiff2.userStaked).eq(policyValue2); + let policyInfo = await farm1.policyInfo(policyID1); + expect(policyInfo.depositor).eq(farmer1.address); + expect(policyInfo.value).eq(policyValue1); + policyInfo = await farm1.policyInfo(policyID2); + expect(policyInfo.depositor).eq(farmer2.address); + expect(policyInfo.value).eq(policyValue2); + policyInfo = await farm1.policyInfo(policyID3); + expect(policyInfo.depositor).eq(farmer1.address); + expect(policyInfo.value).eq(policyValue3); + // revert length mismatch + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer1.address, farmer2.address], [policyID5], [deadline], [vs[0]], [rs[0]], [ss[0]])).to.be.revertedWith("length mismatch"); + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer1.address], [policyID5, policyID4], [deadline], [vs[0]], [rs[0]], [ss[0]])).to.be.revertedWith("length mismatch"); + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer1.address], [policyID5], [deadline, deadline], [vs[0]], [rs[0]], [ss[0]])).to.be.revertedWith("length mismatch"); + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer1.address], [policyID5], [deadline], [vs[0], vs[1]], [rs[0]], [ss[0]])).to.be.revertedWith("length mismatch"); + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer1.address], [policyID5], [deadline], [vs[0]], [rs[0], rs[1]], [ss[0]])).to.be.revertedWith("length mismatch"); + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer1.address], [policyID5], [deadline], [vs[0]], [rs[0]], [ss[0], ss[1]])).to.be.revertedWith("length mismatch"); + // revert double deposit + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer1.address], [policyID1], [deadline], [vs[0]], [rs[0]], [ss[0]])).to.be.revertedWith("cannot permit to self"); + // revert not your token + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer1.address], [policyID4], [deadline], [vs[0]], [rs[0]], [ss[0]])).to.be.revertedWith("unauthorized"); + // revert non existent token + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer1.address], [999], [deadline], [vs[0]], [rs[0]], [ss[0]])).to.be.revertedWith("query for nonexistent token"); + // revert invalid signature + const { v, r, s } = await getPermitErc721EnhancedSignature(farmer2, policyManager, farm1.address, policyID4, deadline); + await expect(farm1.connect(farmer1).depositPolicySignedMulti([farmer2.address], [policyID4], [deadline], [12], [r], [r])).to.be.revertedWith("invalid signature"); + // empty + await farm1.connect(farmer1).depositPolicySignedMulti([], [], [], [], [], []); + }); + it("can withdraw multi", async function () { + let balances1a = await getBalances(farmer1, farm1); + let tx1 = await farm1.connect(farmer1).withdrawPolicyMulti([policyID1, policyID3]); + await expect(tx1).to.emit(farm1, "PolicyWithdrawn").withArgs(farmer1.address, policyID1); + await expect(tx1).to.emit(farm1, "PolicyWithdrawn").withArgs(farmer1.address, policyID3); + let balances1b = await getBalances(farmer1, farm1); + let balancesDiff1 = getBalancesDiff(balances1b, balances1a); + expect(balancesDiff1.farmSpt).eq(-2); + expect(balancesDiff1.userSpt).eq(2); + expect(balancesDiff1.userStaked).eq(policyValue1.add(policyValue3).mul(-1)); + expect(balancesDiff1.farmStake).eq(policyValue1.add(policyValue3).mul(-1)); + let policyInfo = await farm1.policyInfo(policyID1); + expect(policyInfo.depositor).eq(ZERO_ADDRESS); + expect(policyInfo.value).eq(0); + policyInfo = await farm1.policyInfo(policyID3); + expect(policyInfo.depositor).eq(ZERO_ADDRESS); + expect(policyInfo.value).eq(0); + // withdraw without deposit / double withdraw + await expect(farm1.connect(farmer1).withdrawPolicyMulti([policyID1])).to.be.reverted; + // deposit one and withdraw another + await policyManager.connect(farmer1).approve(farm1.address, policyID1); + await farm1.connect(farmer1).depositPolicy(policyID1); + await expect(farm1.connect(farmer1).withdrawPolicyMulti([policyID5])).to.be.reverted; + // withdraw a token someone else deposited + await expect(farm1.connect(farmer1).withdrawPolicyMulti([policyID2])).to.be.reverted; + // withdraw non existant token + await expect(farm1.connect(farmer1).withdrawPolicyMulti([999])).to.be.reverted; + // empty + await farm1.connect(farmer1).withdrawPolicyMulti([]); + }); + }); + + describe("updates", async function () { + let farm2: SptFarm; + before(async function () { + // get referrence timestamp + await provider.send("evm_mine", []); + initTime = (await provider.getBlock('latest')).timestamp; + startTime = initTime + 10; + endTime = initTime + 100; + farm2 = (await deployContract(deployer, artifacts.SptFarm, [governor.address, registry.address, startTime, endTime])) as SptFarm; + }); + it("can update a single farm", async function () { + // init + expect(await farm2.lastRewardTime()).to.equal(startTime); + // update before start + await farm2.updateFarm(); + expect(await farm2.lastRewardTime()).to.equal(startTime); + // update after start + timestamp = startTime + 10; + await provider.send("evm_setNextBlockTimestamp", [timestamp]); + await farm2.updateFarm(); + expect(await farm2.lastRewardTime()).to.equal(timestamp); + // update after end + timestamp = endTime + 10; + await provider.send("evm_setNextBlockTimestamp", [timestamp]); + await farm2.updateFarm(); + expect(await farm2.lastRewardTime()).to.equal(endTime); + }); + it("rejects set end by non governor", async function () { + await expect(farm2.connect(farmer1).setEnd(1)).to.be.revertedWith("!governance"); + }); + it("can set end", async function () { + endTime += 50; + await farm2.connect(governor).setEnd(endTime); + expect(await farm2.endTime()).to.equal(endTime); + expect(await farm2.lastRewardTime()).to.be.closeTo(BN.from(timestamp), 5); + // update before new end + await farm2.updateFarm(); + expect(await farm2.lastRewardTime()).to.be.closeTo(BN.from(timestamp), 5); + // update after new end + timestamp = endTime + 10; + await provider.send("evm_setNextBlockTimestamp", [timestamp]); + await farm2.updateFarm(); + expect(await farm2.lastRewardTime()).to.equal(endTime); + }); + }); + + describe("rewards", function () { + let farmID: BN; + let farm: SptFarm; + let allocPoints = BN.from("1"); + // start with 1:4 ownership, switch to 1:19 + let policyID1: BN, policyID2: BN, policyID3: BN; + let coverAmount1 = ONE_ETHER.mul(10); + let coverAmount2 = ONE_ETHER.mul(40); + let coverAmount3 = ONE_ETHER.mul(150); + let policyValue1 = coverAmount1.mul(price); + let policyValue2 = coverAmount2.mul(price); + let policyValue3 = coverAmount3.mul(price); + let policyValue12 = policyValue1.add(policyValue2); + let policyValue23 = policyValue2.add(policyValue3); + let policyValue123 = policyValue1.add(policyValue2).add(policyValue3); + // reward math variables + let pendingReward1: BN; + let pendingReward2: BN; + let expectedReward1: BN; + let expectedReward2: BN; + let receivedReward2: BN; + + before(async function () { + await solace.connect(governor).mint(optionsFarming.address, ONE_MILLION_ETHER); + await optionsFarming.connect(governor).setSolace(solace.address); + let solaceIsToken0 = BN.from(solace.address).lt(BN.from(weth.address)); + await optionsFarming.connect(governor).setSolaceEthPool(solaceEthPool.address, solaceIsToken0, 0); + }); + + beforeEach(async function () { + await solace.connect(farmer1).transfer(governor.address, await solace.balanceOf(farmer1.address)); + await solace.connect(farmer2).transfer(governor.address, await solace.balanceOf(farmer2.address)); + // get referrence timestamp + await provider.send("evm_mine", []); + initTime = (await provider.getBlock('latest')).timestamp; + startTime = initTime + 200; + endTime = initTime + 1000; + farm = (await deployContract(deployer, artifacts.SptFarm, [governor.address, registry.address, startTime, endTime])) as SptFarm; + await farmController.connect(governor).registerFarm(farm.address, allocPoints); + farmID = await farmController.numFarms(); + + await product.connect(farmer1)._buyPolicy(farmer1.address, coverAmount1, duration, ZERO_ADDRESS); + policyID1 = await policyManager.totalPolicyCount(); + await product.connect(farmer2)._buyPolicy(farmer2.address, coverAmount2, duration, ZERO_ADDRESS); + policyID2 = await policyManager.totalPolicyCount(); + await product.connect(farmer1)._buyPolicy(farmer2.address, coverAmount3, duration, ZERO_ADDRESS); + policyID3 = await policyManager.totalPolicyCount(); + }); + + afterEach(async function () { + await farmController.connect(governor).setAllocPoints(farmID, 0); // remember to deallocate dead farms + expect(await farmController.totalAllocPoints()).to.equal(0); + }); + + it("provides rewards to only farmer", async function () { + // deposit before start + const { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm.address, policyID1, deadline); + await farm.connect(farmer1).depositPolicySigned(farmer1.address, policyID1, deadline, v, r, s); + expect(await farm.pendingRewards(farmer1.address)).to.equal(0); + timestamp = startTime + 100; + await provider.send("evm_setNextBlockTimestamp", [timestamp]); + await provider.send("evm_mine", []); + // potential withdraw + pendingReward1 = await farm.pendingRewards(farmer1.address); + expectedReward1 = solacePerSecond.mul(100); + expect(pendingReward1).to.eq(expectedReward1); + }); + it("fairly provides rewards to all farmers", async function () { + // only farmer 1 + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm.address, policyID1, deadline); + await farm.connect(farmer1).depositPolicySigned(farmer1.address, policyID1, deadline, v, r, s); + timestamp = startTime + 100; + await provider.send("evm_setNextBlockTimestamp", [timestamp]); + // add farmer 2 + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer2, policyManager, farm.address, policyID2, deadline); + await farm.connect(farmer2).depositPolicySigned(farmer2.address, policyID2, deadline, v, r, s); + timestamp += 100; + await provider.send("evm_setNextBlockTimestamp", [timestamp]); + await provider.send("evm_mine", []); + // check farmer 1 rewards + pendingReward1 = BN.from(await farm.pendingRewards(farmer1.address)); + expectedReward1 = bnAddSub([ + bnMulDiv([solacePerSecond, 100, policyValue1], [policyValue1]), // 100% ownership for 100 seconds + bnMulDiv([solacePerSecond, 100, policyValue1], [policyValue12]), // 20% ownership for 100 seconds + ]); + expect(pendingReward1).to.eq(expectedReward1); + // check farmer 2 rewards + pendingReward2 = BN.from(await farm.pendingRewards(farmer2.address)); + expectedReward2 = bnMulDiv([solacePerSecond, 100, policyValue2], [policyValue12]), // 80% ownership for 100 seconds + expect(pendingReward2).to.eq(expectedReward2); + // farmer 2 deposit more + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer2, policyManager, farm.address, policyID3, deadline); + await farm.connect(farmer2).depositPolicySigned(farmer2.address, policyID3, deadline, v, r, s); + timestamp += 200; + await provider.send("evm_setNextBlockTimestamp", [timestamp]); + await provider.send("evm_mine", []); + // check farmer 1 rewards + pendingReward1 = BN.from(await farm.pendingRewards(farmer1.address)); + expectedReward1 = expectedReward1.add( + bnMulDiv([solacePerSecond, 200, policyValue1], [policyValue123]), // 5% ownership for 200 seconds + ); + expectClose(pendingReward1, expectedReward1, solacePerSecond); + // check farmer 2 rewards + pendingReward2 = BN.from(await farm.pendingRewards(farmer2.address)); + expectedReward2 = expectedReward2.add( + bnMulDiv([solacePerSecond, 200, policyValue23], [policyValue123]), // 95% ownership for 200 seconds + ); + expectClose(pendingReward2, expectedReward2, solacePerSecond); + }); + it("does not distribute rewards before farm start", async function () { + const { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm.address, policyID1, deadline); + await farm.connect(farmer1).depositPolicySigned(farmer1.address, policyID1, deadline, v, r, s); + await provider.send("evm_setNextBlockTimestamp", [startTime]); + await provider.send("evm_mine", []); + expect(await farm.pendingRewards(farmer1.address)).to.equal(0); + }); + it("does not distribute rewards after farm end", async function () { + const { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm.address, policyID1, deadline); + await farm.connect(farmer1).depositPolicySigned(farmer1.address, policyID1, deadline, v, r, s); + await provider.send("evm_setNextBlockTimestamp", [endTime]); + await provider.send("evm_mine", []); + let pendingReward1 = await farm.pendingRewards(farmer1.address); + await provider.send("evm_setNextBlockTimestamp", [endTime+1000]); + await provider.send("evm_mine", []); + let pendingReward2 = await farm.pendingRewards(farmer1.address); + expect(pendingReward1).to.equal(pendingReward2); + }); + it("allows farmers to cash out after farm end", async function () { + // deposit before start + const { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm.address, policyID1, deadline); + await farm.connect(farmer1).depositPolicySigned(farmer1.address, policyID1, deadline, v, r, s); + expect(await farm.pendingRewards(farmer1.address)).to.equal(0); + await provider.send("evm_setNextBlockTimestamp", [endTime+1000]); + await provider.send("evm_mine", []); + let pendingRewards = await farm.pendingRewards(farmer1.address); + let expectedRewards = bnMulDiv([solacePerSecond, 800]); // 100% ownership for 800 seconds + expect(pendingRewards).to.equal(expectedRewards); + let tx = await farm.connect(farmer1).withdrawRewards(); + let optionID = await optionsFarming.numOptions(); + expect(tx).to.emit(optionsFarming, "OptionCreated").withArgs(optionID); + let option = await optionsFarming.getOption(optionID); + expect(option.rewardAmount).to.equal(pendingRewards); + let expectedStrikePrice = await optionsFarming.calculateStrikePrice(pendingRewards); + expect(option.strikePrice).to.equal(expectedStrikePrice); + let balancesBefore = await getBalances(farmer1, farm); + await optionsFarming.connect(farmer1).exerciseOption(optionID, {value: option.strikePrice}); + let balancesAfter = await getBalances(farmer1, farm); + let balancesDiff = getBalancesDiff(balancesAfter, balancesBefore); + expect(balancesDiff.userSolace).to.equal(pendingRewards); + // double withdraw rewards + pendingRewards = await farm.pendingRewards(farmer1.address); + expect(pendingRewards).to.equal(0); + await expect(farm.connect(farmer1).withdrawRewards()).to.be.revertedWith("no zero value options"); + // withdraw stake + let stake = await farm.userStaked(farmer1.address); + await farm.connect(farmer1).withdrawPolicy(policyID1); + expect(await farm.userStaked(farmer1.address)).to.equal(0); + pendingRewards = await farm.pendingRewards(farmer1.address); + expect(pendingRewards).to.equal(0); + let numOptions = await optionsFarming.numOptions(); + await expect(farm.connect(farmer1).withdrawRewards()).to.be.revertedWith("no zero value options"); + expect(await optionsFarming.numOptions()).to.eq(numOptions); + }); + it("allows farmers to cash out before farm end", async function () { + // deposit before start + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm.address, policyID1, deadline); + await farm.connect(farmer1).depositPolicySigned(farmer1.address, policyID1, deadline, v, r, s); + expect(await farm.pendingRewards(farmer1.address)).to.equal(0); + await provider.send("evm_setNextBlockTimestamp", [startTime+100]); + await provider.send("evm_mine", []); + let pendingRewards = await farm.pendingRewards(farmer1.address); + let expectedRewards = bnMulDiv([solacePerSecond, 100]); // 100% ownership for 100 seconds + expect(pendingRewards).to.equal(expectedRewards); + let tx = await farm.connect(farmer1).withdrawRewards(); + let optionID = await optionsFarming.numOptions(); + expect(tx).to.emit(optionsFarming, "OptionCreated").withArgs(optionID); + expect(await farm.userStaked(farmer1.address)).to.equal(policyValue1); + let option = await optionsFarming.getOption(optionID); + expectedRewards = bnMulDiv([solacePerSecond, 101]); // 100% ownership for 101 seconds + expect(option.rewardAmount).to.equal(expectedRewards); + let expectedStrikePrice = await optionsFarming.calculateStrikePrice(expectedRewards); + expect(option.strikePrice).to.equal(expectedStrikePrice); + let balancesBefore = await getBalances(farmer1, farm); + await optionsFarming.connect(farmer1).exerciseOption(optionID, {value: option.strikePrice}); + let balancesAfter = await getBalances(farmer1, farm); + let balancesDiff = getBalancesDiff(balancesAfter, balancesBefore); + expect(balancesDiff.userSolace).to.equal(expectedRewards); + // double withdraw rewards + await provider.send("evm_setNextBlockTimestamp", [startTime+300]); + await provider.send("evm_mine", []); + pendingRewards = await farm.pendingRewards(farmer1.address); + expectedRewards = bnMulDiv([solacePerSecond, 199]); // 100% ownership for 199 more seconds + expect(pendingRewards).to.equal(expectedRewards); + tx = await farm.connect(farmer1).withdrawRewards(); + optionID = await optionsFarming.numOptions(); + expect(tx).to.emit(optionsFarming, "OptionCreated").withArgs(optionID); + option = await optionsFarming.getOption(optionID); + expectedRewards = bnMulDiv([solacePerSecond, 200]); // 100% ownership for 200 more seconds + expect(option.rewardAmount).to.equal(expectedRewards); + expectedStrikePrice = await optionsFarming.calculateStrikePrice(expectedRewards); + expect(option.strikePrice).to.equal(expectedStrikePrice); + // increase balance + await policyManager.connect(farmer2).transfer(farmer1.address, policyID2); + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm.address, policyID2, deadline); + await farm.connect(farmer1).depositPolicySigned(farmer1.address, policyID2, deadline, v, r, s); + expect(await farm.userStaked(farmer1.address)).to.equal(policyValue12); + await provider.send("evm_setNextBlockTimestamp", [startTime+325]); + await provider.send("evm_mine", []); + pendingRewards = await farm.pendingRewards(farmer1.address); + expectedRewards = bnMulDiv([solacePerSecond, 24]); // 100% ownership for 24 more seconds + tx = await farm.connect(farmer1).withdrawRewards(); + optionID = await optionsFarming.numOptions(); + expect(tx).to.emit(optionsFarming, "OptionCreated").withArgs(optionID); + option = await optionsFarming.getOption(optionID); + expectedRewards = bnMulDiv([solacePerSecond, 25]); // 100% ownership for 25 more seconds + expect(option.rewardAmount).to.equal(expectedRewards); + expectedStrikePrice = await optionsFarming.calculateStrikePrice(expectedRewards); + expect(option.strikePrice).to.equal(expectedStrikePrice); + // decrease balance + await farm.connect(farmer1).withdrawPolicy(policyID1); + expect(await farm.userStaked(farmer1.address)).to.equal(policyValue2); + await provider.send("evm_setNextBlockTimestamp", [startTime+345]); + await provider.send("evm_mine", []); + pendingRewards = await farm.pendingRewards(farmer1.address); + expectedRewards = bnMulDiv([solacePerSecond, 19]); // 100% ownership for 19 more seconds + tx = await farm.connect(farmer1).withdrawRewards(); + optionID = await optionsFarming.numOptions(); + expect(tx).to.emit(optionsFarming, "OptionCreated").withArgs(optionID); + option = await optionsFarming.getOption(optionID); + expectedRewards = bnMulDiv([solacePerSecond, 20]); // 100% ownership for 20 more seconds + expect(option.rewardAmount).to.equal(expectedRewards); + expectedStrikePrice = await optionsFarming.calculateStrikePrice(expectedRewards); + expect(option.strikePrice).to.equal(expectedStrikePrice); + // withdraw stake + await provider.send("evm_setNextBlockTimestamp", [startTime+374]); + await provider.send("evm_mine", []); + let stake = await farm.userStaked(farmer1.address); + expect(stake).to.equal(policyValue2); + await farm.connect(farmer1).withdrawPolicy(policyID2); + expect(await farm.userStaked(farmer1.address)).to.equal(0); + pendingRewards = await farm.pendingRewards(farmer1.address); + expectedRewards = bnMulDiv([solacePerSecond, 29]); // 100% ownership for 29 seconds + expect(pendingRewards).to.equal(expectedRewards); + tx = await farm.connect(farmer1).withdrawRewards(); + optionID = await optionsFarming.numOptions(); + expect(tx).to.emit(optionsFarming, "OptionCreated").withArgs(optionID); + option = await optionsFarming.getOption(optionID); + expect(option.rewardAmount).to.equal(expectedRewards); + expectedStrikePrice = await optionsFarming.calculateStrikePrice(expectedRewards); + expect(option.strikePrice).to.equal(expectedStrikePrice); + }); + it("non farmers cannot cash out", async function () { + let pendingRewards = await farmController.pendingRewards(deployer.address); + expect(pendingRewards).to.equal(0); + await expect(farm.connect(deployer).withdrawRewards()).to.be.revertedWith("no zero value options"); + }); + }); + + describe("updateActivePolicies", function () { + before(async function () { + product = (await deployContract(deployer, artifacts.MockProduct, [deployer.address, policyManager.address, registry.address, coveredPlatform.address, 0, 100000000000, price])) as MockProduct; + await policyManager.connect(governor).addProduct(product.address); + await riskManager.connect(governor).addProduct(product.address, 1, price, 1); + }); + it("can update no policies", async function () { + await farm1.updateActivePolicies([]); + }); + it("can update active policies", async function () { + // policy 1 expires + let balances1 = await getBalances(farmer1, farm1); + await product.connect(farmer1)._buyPolicy(farmer1.address, 0b000001, 110, ZERO_ADDRESS); + let policyID1 = await policyManager.totalPolicyCount(); + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm1.address, policyID1, deadline); + await farm1.connect(farmer1).depositPolicySigned(farmer1.address, policyID1, deadline, v, r, s); + // policy 2 expires + await product.connect(farmer1)._buyPolicy(farmer1.address, 0b000010, 120, ZERO_ADDRESS); + let policyID2 = await policyManager.totalPolicyCount(); + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm1.address, policyID2, deadline); + await farm1.connect(farmer1).depositPolicySigned(farmer1.address, policyID2, deadline, v, r, s); + // policy 3 expires but is not updated + await product.connect(farmer1)._buyPolicy(farmer1.address, 0b000100, 130, ZERO_ADDRESS); + let policyID3 = await policyManager.totalPolicyCount(); + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm1.address, policyID3, deadline); + await farm1.connect(farmer1).depositPolicySigned(farmer1.address, policyID3, deadline, v, r, s); + // policy 4 does not expire + await product.connect(farmer1)._buyPolicy(farmer1.address, 0b001000, 200, ZERO_ADDRESS); + let policyID4 = await policyManager.totalPolicyCount(); + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm1.address, policyID4, deadline); + await farm1.connect(farmer1).depositPolicySigned(farmer1.address, policyID4, deadline, v, r, s); + // policy 5 is canceled + await product.connect(farmer1)._buyPolicy(farmer1.address, 0b010000, 300, ZERO_ADDRESS); + let policyID5 = await policyManager.totalPolicyCount(); + var { v, r, s } = await getPermitErc721EnhancedSignature(farmer1, policyManager, farm1.address, policyID5, deadline); + await farm1.connect(farmer1).depositPolicySigned(farmer1.address, policyID5, deadline, v, r, s); + // policy 6 is expired but was never staked + await product.connect(farmer1)._buyPolicy(farmer1.address, 0b100000, 120, ZERO_ADDRESS); + let policyID6 = await policyManager.totalPolicyCount(); + // pass time + await burnBlocks(150); + expect(await product.activeCoverAmount()).to.equal(0b111111); + await farm1.connect(farmer1).withdrawPolicy(policyID5); + await product.connect(farmer1).cancelPolicy(policyID5); + let balances2 = await getBalances(farmer1, farm1); + expect(await product.activeCoverAmount()).to.equal(0b101111); + // update policies + await farm1.updateActivePolicies([policyID1, policyID2, policyID4, policyID5, policyID6, 999]); + expect(await policyManager.exists(policyID1)).to.be.false; + expect(await policyManager.exists(policyID2)).to.be.false; + expect(await policyManager.exists(policyID3)).to.be.true; + expect(await policyManager.exists(policyID4)).to.be.true; + expect(await policyManager.exists(policyID5)).to.be.false; + expect(await policyManager.exists(policyID6)).to.be.false; + expect(await product.activeCoverAmount()).to.equal(0b001100); + let balances3 = await getBalances(farmer1, farm1); + let balancesDiff13 = getBalancesDiff(balances3, balances1); + expect(balancesDiff13.farmSpt).eq(2); + expect(balancesDiff13.userSpt).eq(0); + expect(balancesDiff13.userStaked).eq(0b001100*price); + expect(balancesDiff13.farmStake).eq(0b001100*price); + let balancesDiff23 = getBalancesDiff(balances3, balances2); + expect(balancesDiff23.farmSpt).eq(-2); + expect(balancesDiff23.userSpt).eq(-1); + expect(balancesDiff23.userStaked).eq(-0b000011*price); + expect(balancesDiff23.farmStake).eq(-0b000011*price); + }); + }); + + describe("edge cases", function () { + let farm4: SptFarm; + before(async function () { + farm4 = (await deployContract(deployer, artifacts.SptFarm, [governor.address, registry.address, 0, 1000])) as SptFarm; + }); + it("can setRewards", async function () { + await farmController.connect(governor).setRewardPerSecond(solacePerSecond); + }); + it("rejects setRewards by non farmController", async function () { + await expect(farm4.connect(governor).setRewards(ONE_MILLION_ETHER)).to.be.revertedWith("!farmcontroller"); + await expect(farm4.connect(farmer1).setRewards(ONE_MILLION_ETHER)).to.be.revertedWith("!farmcontroller"); + }); + it("can getRewardAmountDistributed", async function () { + await farmController.connect(governor).registerFarm(farm4.address, 1); + let rewardPerSecond = await farm4.rewardPerSecond(); + expect(await farm4.getRewardAmountDistributed(20, 30)).to.equal(rewardPerSecond.mul(10)); + expect(await farm4.getRewardAmountDistributed(30, 20)).to.equal(0); + }); + it("can withdraw rewards via farmcontroller", async function () { + expect(await farm1.farmController()).to.eq(farmController.address); + expect(await optionsFarming.farmController()).to.eq(farmController.address); + await farmController.connect(farmer1).farmOptionMulti(); + }); + }); + + // helper functions + + // uniswap requires tokens to be in order + function sortTokens(tokenA: string, tokenB: string) { + return BN.from(tokenA).lt(BN.from(tokenB)) ? [tokenA, tokenB] : [tokenB, tokenA]; + } + + // creates, initializes, and returns a pool + async function createPool(tokenA: Contract, tokenB: Contract, fee: FeeAmount, sqrtPrice: BigNumberish = encodePriceSqrt(1,1)) { + let [token0, token1] = sortTokens(tokenA.address, tokenB.address); + let pool: Contract; + let tx = await uniswapFactory.createPool(token0, token1, fee); + let events = (await tx.wait()).events; + expect(events && events.length > 0 && events[0].args && events[0].args.pool); + if (events && events.length > 0 && events[0].args && events[0].args.pool) { + let poolAddress = events[0].args.pool; + pool = await ethers.getContractAt(artifacts.UniswapV3Pool.abi, poolAddress); + } else { + pool = new Contract(ZERO_ADDRESS, artifacts.UniswapV3Pool.abi) as Contract; + expect(true).to.equal(false); + } + expect(pool).to.exist; + if (pool) { + await pool.connect(governor).initialize(sqrtPrice); + } + return pool; + } + + interface Balances { + userEth: BN; + userWeth: BN; + userSpt: BN; + userStaked: BN; + userPendingRewards: BN; + userSolace: BN; + farmSpt: BN; + farmStake: BN; + optionsFarmingSolace: BN; + } + + async function getBalances(user: Wallet, farm: SptFarm): Promise { + return { + userEth: await user.getBalance(), + userWeth: await weth.balanceOf(user.address), + userSpt: await policyManager.balanceOf(user.address), + userStaked: await farm.userStaked(user.address), + userPendingRewards: await farm.pendingRewards(user.address), + userSolace: await solace.balanceOf(user.address), + farmSpt: await policyManager.balanceOf(farm.address), + farmStake: await farm.valueStaked(), + optionsFarmingSolace: await solace.balanceOf(optionsFarming.address) + }; + } + + function getBalancesDiff(balances1: Balances, balances2: Balances): Balances { + return { + userEth: balances1.userEth.sub(balances2.userEth), + userWeth: balances1.userWeth.sub(balances2.userWeth), + userSpt: balances1.userSpt.sub(balances2.userSpt), + userStaked: balances1.userStaked.sub(balances2.userStaked), + userPendingRewards: balances1.userPendingRewards.sub(balances2.userPendingRewards), + userSolace: balances1.userSolace.sub(balances2.userSolace), + farmSpt: balances1.farmSpt.sub(balances2.farmSpt), + farmStake: balances1.farmStake.sub(balances2.farmStake), + optionsFarmingSolace: balances1.optionsFarmingSolace.sub(balances2.optionsFarmingSolace) + }; + } + + // mints an lp token by provIDing liquidity + async function mintLpToken( + liquidityProvider: Wallet, + tokenA: Contract, + tokenB: Contract, + fee: FeeAmount, + amount0: BigNumberish, + amount1: BigNumberish, + tickLower: BigNumberish = getMinTick(TICK_SPACINGS[fee]), + tickUpper: BigNumberish = getMaxTick(TICK_SPACINGS[fee]) + ) { + let [token0, token1] = sortTokens(tokenA.address, tokenB.address); + await lpToken.connect(liquidityProvider).mint({ + token0: token0, + token1: token1, + tickLower: tickLower, + tickUpper: tickUpper, + fee: fee, + recipient: liquidityProvider.address, + amount0Desired: amount0, + amount1Desired: amount1, + amount0Min: 0, + amount1Min: 0, + deadline: constants.MaxUint256, + }); + let tokenID = await lpToken.totalSupply(); + return tokenID; + } +}); diff --git a/test/utilities/artifact_importer.ts b/test/utilities/artifact_importer.ts index 62f922b8..c17039be 100644 --- a/test/utilities/artifact_importer.ts +++ b/test/utilities/artifact_importer.ts @@ -32,6 +32,7 @@ export async function import_artifacts() { artifacts.CpFarm = await tryImport(`${artifact_dir}/CpFarm.sol/CpFarm.json`); artifacts.LpAppraisor = await tryImport(`${artifact_dir}/LpAppraisor.sol/LpAppraisor.json`); artifacts.SolaceEthLpFarm = await tryImport(`${artifact_dir}/SolaceEthLpFarm.sol/SolaceEthLpFarm.json`); + artifacts.SptFarm = await tryImport(`${artifact_dir}/SptFarm.sol/SptFarm.json`); // products artifacts.BaseProduct = await tryImport(`${artifact_dir}/products/BaseProduct.sol/BaseProduct.json`); artifacts.MockProduct = await tryImport(`${artifact_dir}/mocks/MockProduct.sol/MockProduct.json`);