diff --git a/contracts/interfaces/IStakingPool.sol b/contracts/interfaces/IStakingPool.sol index a96b79eff2..e4ffa9cf11 100644 --- a/contracts/interfaces/IStakingPool.sol +++ b/contracts/interfaces/IStakingPool.sol @@ -95,19 +95,16 @@ interface IStakingPool is IERC721 { function operatorTransfer(address from, address to, uint[] calldata tokenIds) external; - function updateTranches() external; + function updateTranches(bool updateUntilCurrentTimestamp) external; function allocateStake( CoverRequest calldata request ) external returns (uint allocatedAmount, uint premium, uint rewardsInNXM); function deallocateStake( - uint productId, - uint start, - uint period, - uint amount, - uint premium, - uint globalRewardsRatio + CoverRequest memory request, + uint coverStartTime, + uint premium ) external; function burnStake(uint productId, uint start, uint period, uint amount) external; diff --git a/contracts/libraries/Math.sol b/contracts/libraries/Math.sol index bd6014d5b4..4565549343 100644 --- a/contracts/libraries/Math.sol +++ b/contracts/libraries/Math.sol @@ -7,10 +7,6 @@ pragma solidity ^0.8.9; */ library Math { - function divRound(uint a, uint b) internal pure returns (uint) { - return (a + b / 2) / b; - } - function min(uint a, uint b) internal pure returns (uint) { return a < b ? a : b; } @@ -19,6 +15,14 @@ library Math { return a > b ? a : b; } + function divRound(uint a, uint b) internal pure returns (uint) { + return (a + b / 2) / b; + } + + function divCeil(uint a, uint b) internal pure returns (uint) { + return (a + b - 1) / b; + } + // babylonian method function sqrt(uint y) internal pure returns (uint) { diff --git a/contracts/libraries/UncheckedMath.sol b/contracts/libraries/UncheckedMath.sol new file mode 100644 index 0000000000..9631727865 --- /dev/null +++ b/contracts/libraries/UncheckedMath.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.9; + +/** + * @dev Simple library that defines basic math functions that allow overflow + */ +library UncheckedMath { + + function uncheckedAdd(uint a, uint b) internal pure returns (uint) { + unchecked { return a + b; } + } + + function uncheckedSub(uint a, uint b) internal pure returns (uint) { + unchecked { return a - b; } + } + + function uncheckedMul(uint a, uint b) internal pure returns (uint) { + unchecked { return a * b; } + } + + function uncheckedDiv(uint a, uint b) internal pure returns (uint) { + unchecked { return a / b; } + } + +} diff --git a/contracts/mocks/Cover/CoverMockStakingPool.sol b/contracts/mocks/Cover/CoverMockStakingPool.sol index 298b6bfbc4..2f8b893eba 100644 --- a/contracts/mocks/Cover/CoverMockStakingPool.sol +++ b/contracts/mocks/Cover/CoverMockStakingPool.sol @@ -92,12 +92,9 @@ contract CoverMockStakingPool is IStakingPool, ERC721 { } function deallocateStake( - uint /* productId */, - uint /* start */, - uint /* period */, - uint /* amount */, - uint /* premium */, - uint /* globalRewardsRatio */ + CoverRequest memory /*request*/, + uint /*coverStartTime*/, + uint /*premium*/ ) external { } @@ -121,7 +118,7 @@ contract CoverMockStakingPool is IStakingPool, ERC721 { } } - function updateTranches() external { + function updateTranches(bool) external { totalSupply = totalSupply; revert("CoverMockStakingPool: not callable"); } diff --git a/contracts/modules/cover/Cover.sol b/contracts/modules/cover/Cover.sol index c6190f563a..246fc9ca06 100644 --- a/contracts/modules/cover/Cover.sol +++ b/contracts/modules/cover/Cover.sol @@ -341,48 +341,17 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard { "Cover: payment asset deprecated" ); - uint refundInCoverAsset = 0; + uint refundInCoverAsset; - if (lastCoverSegment.start + lastCoverSegment.period > uint32(block.timestamp)) { // not expired - uint32 remainingPeriod = lastCoverSegment.start + lastCoverSegment.period - uint32(block.timestamp); - - { - uint originalPoolAllocationsCount = coverSegmentAllocations[coverId][lastCoverSegmentIndex].length; - - // Rollback previous cover - for (uint i = 0; i < originalPoolAllocationsCount; i++) { - - PoolAllocation memory allocation = coverSegmentAllocations[coverId][lastCoverSegmentIndex][i]; - - stakingPool(allocation.poolId).deallocateStake( - cover.productId, - lastCoverSegment.start, - lastCoverSegment.period, - allocation.coverAmountInNXM, - allocation.premiumInNXM, - lastCoverSegment.globalRewardsRatio - ); - - coverSegmentAllocations[coverId][lastCoverSegmentIndex][i].premiumInNXM - *= (lastCoverSegment.period - remainingPeriod) / lastCoverSegment.period; - - // Compute NXM rewards to be rolled back - uint deallocatedRewardsInNXM = allocation.premiumInNXM - * remainingPeriod / lastCoverSegment.period - * lastCoverSegment.globalRewardsRatio / REWARD_DENOMINATOR; - - tokenController().burnStakingPoolNXMRewards(deallocatedRewardsInNXM, allocation.poolId); - } - } - - refundInCoverAsset = lastCoverSegment.priceRatio - * lastCoverSegment.amount - / PRICE_DENOMINATOR - * remainingPeriod - / MAX_COVER_PERIOD; - - // Edit the last cover segment so it ends at the current block - lastCoverSegment.period = lastCoverSegment.period - remainingPeriod; + // if the last segment is not expired + if (lastCoverSegment.start + lastCoverSegment.period > uint32(block.timestamp)) { + refundInCoverAsset = deallocateCapacity( + coverId, + lastCoverSegmentIndex, + product, + cover, + lastCoverSegment + ); } (uint totalPremiumInNXM, uint totalCoveredAmountInPayoutAsset) = _buyCover(buyCoverParams, coverId, poolAllocations); @@ -398,6 +367,63 @@ contract Cover is ICover, MasterAwareV2, IStakingPoolBeacon, ReentrancyGuard { emit CoverEdited(coverId, cover.productId, lastCoverSegmentIndex + 1, msg.sender); } + function deallocateCapacity( + uint coverId, + uint lastCoverSegmentIndex, + Product memory product, + CoverData memory cover, + CoverSegment memory lastCoverSegment + ) internal returns (uint refundInCoverAsset) { + + uint32 remainingPeriod = lastCoverSegment.start + lastCoverSegment.period - uint32(block.timestamp); + { + uint originalPoolAllocationsCount = coverSegmentAllocations[coverId][lastCoverSegmentIndex].length; + + // Rollback previous cover + for (uint i = 0; i < originalPoolAllocationsCount; i++) { + + PoolAllocation memory allocation = coverSegmentAllocations[coverId][lastCoverSegmentIndex][i]; + CoverRequest memory request = CoverRequest( + coverId, + cover.productId, + allocation.coverAmountInNXM, + lastCoverSegment.period, + _productTypes[product.productType].gracePeriodInDays * 1 days, + // TODO globalCapacityRatio and capacityReductionRatio need to be stored at cover buy + globalCapacityRatio, + product.capacityReductionRatio, + lastCoverSegment.globalRewardsRatio + ); + + stakingPool(allocation.poolId).deallocateStake( + request, + lastCoverSegment.start, + allocation.premiumInNXM + ); + + coverSegmentAllocations[coverId][lastCoverSegmentIndex][i].premiumInNXM + *= (lastCoverSegment.period - remainingPeriod) / lastCoverSegment.period; + + // Compute NXM rewards to be rolled back + uint deallocatedRewardsInNXM = allocation.premiumInNXM + * remainingPeriod / lastCoverSegment.period + * lastCoverSegment.globalRewardsRatio / REWARD_DENOMINATOR; + + tokenController().burnStakingPoolNXMRewards(deallocatedRewardsInNXM, allocation.poolId); + } + } + + refundInCoverAsset = lastCoverSegment.priceRatio + * lastCoverSegment.amount + / PRICE_DENOMINATOR + * remainingPeriod + / MAX_COVER_PERIOD; + + // TODO: write to storage instead of memory + // Edit the last cover segment so it ends at the current block + lastCoverSegment.period = lastCoverSegment.period - remainingPeriod; + } + function handlePaymentAndRefund( BuyCoverParams memory buyCoverParams, uint totalPremiumInNXM, diff --git a/contracts/modules/staking/StakingPool.sol b/contracts/modules/staking/StakingPool.sol index 67d9a26a35..7f35f92cf1 100644 --- a/contracts/modules/staking/StakingPool.sol +++ b/contracts/modules/staking/StakingPool.sol @@ -11,6 +11,7 @@ import "../../interfaces/ICover.sol"; import "../../interfaces/ITokenController.sol"; import "../../interfaces/INXMToken.sol"; import "../../libraries/Math.sol"; +import "../../libraries/UncheckedMath.sol"; import "../../libraries/SafeUintCast.sol"; import "./StakingTypesLib.sol"; @@ -22,10 +23,11 @@ import "./StakingTypesLib.sol"; // on cover expiration we deallocate the capacity and it becomes available again contract StakingPool is IStakingPool, ERC721 { - using SafeUintCast for uint; using StakingTypesLib for CoverAmountGroup; using StakingTypesLib for CoverAmount; using StakingTypesLib for BucketTrancheGroup; + using SafeUintCast for uint; + using UncheckedMath for uint; /* storage */ @@ -172,8 +174,9 @@ contract StakingPool is IStakingPool, ERC721 { } } - - function updateTranches() public { + // updateUntilCurrentTimestamp forces rewards update until current timestamp not just until + // bucket/tranche expiry timestamps. Must be true when changing shares or reward per second. + function updateTranches(bool updateUntilCurrentTimestamp) public { uint _firstActiveBucketId = firstActiveBucketId; uint _firstActiveTrancheId = firstActiveTrancheId; @@ -181,15 +184,22 @@ contract StakingPool is IStakingPool, ERC721 { uint currentBucketId = block.timestamp / BUCKET_DURATION; uint currentTrancheId = block.timestamp / TRANCHE_DURATION; - // populate if the pool is new + // skip if the pool is new if (_firstActiveBucketId == 0) { - firstActiveBucketId = currentBucketId; - firstActiveTrancheId = currentTrancheId; return; } - if (_firstActiveBucketId == currentBucketId) { - return; + // if a force update was not requested + if (!updateUntilCurrentTimestamp) { + + bool canExpireBuckets = _firstActiveBucketId < currentBucketId; + bool canExpireTranches = _firstActiveTrancheId < currentTrancheId; + + // and if there's nothing to expire + if (!canExpireBuckets && !canExpireTranches) { + // we can exit + return; + } } // SLOAD @@ -200,59 +210,85 @@ contract StakingPool is IStakingPool, ERC721 { uint _accNxmPerRewardsShare = accNxmPerRewardsShare; uint _lastAccNxmUpdate = lastAccNxmUpdate; - while (_firstActiveBucketId < currentBucketId) { - - ++_firstActiveBucketId; - uint bucketEndTime = _firstActiveBucketId * BUCKET_DURATION; - uint elapsed = bucketEndTime - _lastAccNxmUpdate; - - // todo: should be allowed to overflow? - // todo: handle division by zero - _accNxmPerRewardsShare += elapsed * _rewardPerSecond / _rewardsSharesSupply; - _lastAccNxmUpdate = bucketEndTime; - // TODO: use _firstActiveBucketId before incrementing it? - _rewardPerSecond -= rewardBuckets[_firstActiveBucketId].rewardPerSecondCut; - - // should we expire a tranche? - // FIXME: this doesn't work with the new bucket size - if ( - bucketEndTime % TRANCHE_DURATION != 0 || - _firstActiveTrancheId == currentTrancheId - ) { - continue; + // exit early if we already updated in the current block + if (_lastAccNxmUpdate == block.timestamp) { + return; + } + + if (_rewardsSharesSupply == 0) { + // nothing to do + firstActiveBucketId = currentBucketId; + firstActiveTrancheId = currentTrancheId; + lastAccNxmUpdate = block.timestamp; + return; + } + + while (_firstActiveBucketId < currentBucketId || _firstActiveTrancheId < currentTrancheId) { + + // what expires first, the bucket or the tranche? + bool bucketExpiresFirst; + { + uint nextBucketStart = (_firstActiveBucketId + 1) * BUCKET_DURATION; + uint nextTrancheStart = (_firstActiveTrancheId + 1) * TRANCHE_DURATION; + bucketExpiresFirst = nextBucketStart <= nextTrancheStart; } - // todo: handle _firstActiveTrancheId = 0 case + if (bucketExpiresFirst) { - // SLOAD - Tranche memory expiringTranche = tranches[_firstActiveTrancheId]; + // expire a bucket + // each bucket contains a reward reduction - we subtract it when the bucket *starts*! - // todo: handle division by zero - uint expiredStake = _activeStake * expiringTranche.stakeShares / _stakeSharesSupply; + ++_firstActiveBucketId; + uint bucketStartTime = _firstActiveBucketId * BUCKET_DURATION; + uint elapsed = bucketStartTime - _lastAccNxmUpdate; - // the tranche is expired now so we decrease the stake and share supply - _activeStake -= expiredStake; - _stakeSharesSupply -= expiringTranche.stakeShares; - _rewardsSharesSupply -= expiringTranche.rewardsShares; + uint newAccNxmPerRewardsShare = elapsed * _rewardPerSecond / _rewardsSharesSupply; + _accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare); - // todo: update nft 0 + _rewardPerSecond -= rewardBuckets[_firstActiveBucketId].rewardPerSecondCut; + _lastAccNxmUpdate = bucketStartTime; - // SSTORE - delete tranches[_firstActiveTrancheId]; - expiredTranches[_firstActiveTrancheId] = ExpiredTranche( - _accNxmPerRewardsShare, // accNxmPerRewardShareAtExpiry - // TODO: should this be before or after active stake reduction? - _activeStake, // stakeAmountAtExpiry - _stakeSharesSupply // stakeShareSupplyAtExpiry - ); + continue; + } + + // expire a tranche + // each tranche contains shares - we expire them when the tranche *ends* + // TODO: check if we have to expire the tranche + { + uint trancheEndTime = (_firstActiveTrancheId + 1) * TRANCHE_DURATION; + uint elapsed = trancheEndTime - _lastAccNxmUpdate; + uint newAccNxmPerRewardsShare = elapsed * _rewardPerSecond / _rewardsSharesSupply; + _accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare); + _lastAccNxmUpdate = trancheEndTime; + + // SSTORE + expiredTranches[_firstActiveTrancheId] = ExpiredTranche( + _accNxmPerRewardsShare, // accNxmPerRewardShareAtExpiry + _activeStake, // stakeAmountAtExpiry + _stakeSharesSupply // stakeShareSupplyAtExpiry + ); + + // SLOAD and then SSTORE zero to get the gas refund + Tranche memory expiringTranche = tranches[_firstActiveTrancheId]; + delete tranches[_firstActiveTrancheId]; + + // the tranche is expired now so we decrease the stake and the shares supply + uint expiredStake = _activeStake * expiringTranche.stakeShares / _stakeSharesSupply; + _activeStake -= expiredStake; + _stakeSharesSupply -= expiringTranche.stakeShares; + _rewardsSharesSupply -= expiringTranche.rewardsShares; - // advance to the next tranche - _firstActiveTrancheId++; + // advance to the next tranche + _firstActiveTrancheId++; + } + + // end while } - { + if (updateUntilCurrentTimestamp) { uint elapsed = block.timestamp - _lastAccNxmUpdate; - _accNxmPerRewardsShare += elapsed * _rewardPerSecond / _rewardsSharesSupply; + uint newAccNxmPerRewardsShare = elapsed * _rewardPerSecond / _rewardsSharesSupply; + _accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare); _lastAccNxmUpdate = block.timestamp; } @@ -273,7 +309,7 @@ contract StakingPool is IStakingPool, ERC721 { require(msg.sender == manager(), "StakingPool: The pool is private"); } - updateTranches(); + updateTranches(true); // storage reads uint _activeStake = activeStake; @@ -332,7 +368,7 @@ contract StakingPool is IStakingPool, ERC721 { // if we're increasing an existing deposit if (deposit.lastAccNxmPerRewardShare != 0) { - uint newEarningsPerShare = _accNxmPerRewardsShare - deposit.lastAccNxmPerRewardShare; + uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(deposit.lastAccNxmPerRewardShare); deposit.pendingRewards += newEarningsPerShare * deposit.rewardsShares; } @@ -354,8 +390,7 @@ contract StakingPool is IStakingPool, ERC721 { newRewardsShares += newFeeRewardShares; // calculate rewards until now - uint newRewardPerShare = _accNxmPerRewardsShare - feeDeposit.lastAccNxmPerRewardShare; - + uint newRewardPerShare = _accNxmPerRewardsShare.uncheckedSub(feeDeposit.lastAccNxmPerRewardShare); feeDeposit.pendingRewards += newRewardPerShare * feeDeposit.rewardsShares; feeDeposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare; feeDeposit.rewardsShares += newFeeRewardShares; @@ -444,10 +479,10 @@ contract StakingPool is IStakingPool, ERC721 { uint managerLockedInGovernanceUntil = nxm.isLockedForMV(manager()); - updateTranches(); + // pass false as it does not modify the share supply nor the reward per second + updateTranches(false); uint _accNxmPerRewardsShare = accNxmPerRewardsShare; - uint _firstActiveTrancheId = block.timestamp / TRANCHE_DURATION; for (uint i = 0; i < params.length; i++) { @@ -483,12 +518,12 @@ contract StakingPool is IStakingPool, ERC721 { if (params[i].withdrawRewards) { // if the tranche is expired, use the accumulator value saved at expiration time - uint accNxmPerRewardShareInUse = trancheId < _firstActiveTrancheId + uint accNxmPerRewardShareToUse = trancheId < _firstActiveTrancheId ? expiredTranches[trancheId].accNxmPerRewardShareAtExpiry : _accNxmPerRewardsShare; // calculate reward since checkpoint - uint newRewardPerShare = accNxmPerRewardShareInUse - deposit.lastAccNxmPerRewardShare; + uint newRewardPerShare = accNxmPerRewardShareToUse.uncheckedSub(deposit.lastAccNxmPerRewardShare); rewardsToWithdraw += newRewardPerShare * deposit.rewardsShares + deposit.pendingRewards; // save checkpoint @@ -513,7 +548,8 @@ contract StakingPool is IStakingPool, ERC721 { CoverRequest calldata request ) external onlyCoverContract returns (uint allocatedAmount, uint premium, uint rewardsInNXM) { - updateTranches(); + // passing true because we change the reward per second + updateTranches(true); // process expirations uint gracePeriodExpiration = block.timestamp + request.period + request.gracePeriod; @@ -560,20 +596,21 @@ contract StakingPool is IStakingPool, ERC721 { } } - storeAllocatedCapacities( + updateAllocatedCapacities( request.productId, firstTrancheIdToUse, trancheCount, trancheAllocatedCapacities ); - storeExpiringCoverAmounts( + updateExpiringCoverAmounts( request.coverId, request.productId, firstTrancheIdToUse, trancheCount, gracePeriodExpiration / BUCKET_DURATION + 1, - coverTrancheAllocation + coverTrancheAllocation, + true // isAllocation ); } @@ -590,8 +627,7 @@ contract StakingPool is IStakingPool, ERC721 { require(request.rewardRatio <= REWARDS_DENOMINATOR, "StakingPool: reward ratio exceeds denominator"); rewards = premium * request.rewardRatio / REWARDS_DENOMINATOR; - uint expireAtBucket = (block.timestamp + request.period + BUCKET_DURATION - 1) / BUCKET_DURATION; - // divCeil = fn(a, b) => (a + b - 1) / b + uint expireAtBucket = Math.divCeil(block.timestamp + request.period, BUCKET_DURATION); uint _rewardPerSecond = rewards / (expireAtBucket * BUCKET_DURATION - block.timestamp); // 1 SLOAD + 1 SSTORE @@ -601,6 +637,90 @@ contract StakingPool is IStakingPool, ERC721 { return (allocatedAmount, premium, rewards); } + function deallocateStake( + CoverRequest memory request, + uint coverStartTime, + uint premium + ) external onlyCoverContract { + + updateTranches(true); + + uint gracePeriodExpiration = coverStartTime + request.period + request.gracePeriod; + uint firstTrancheIdToUse = gracePeriodExpiration / TRANCHE_DURATION; + uint trancheCount = (coverStartTime / TRANCHE_DURATION + MAX_ACTIVE_TRANCHES) - firstTrancheIdToUse + 1; + + ( + uint[] memory trancheAllocatedCapacities, + /*uint totalAllocatedCapacity*/ + ) = getAllocatedCapacities( + request.productId, + firstTrancheIdToUse, + trancheCount + ); + + uint packedCoverTrancheAllocation = coverTrancheAllocations[request.coverId]; + + { + uint[] memory coverTrancheAllocation = new uint[](trancheCount); + + for (uint i = 0; i < trancheCount; i++) { + uint amountPerTranche = uint32(packedCoverTrancheAllocation >> (i * 8)); + trancheAllocatedCapacities[i] -= amountPerTranche; + coverTrancheAllocation[i] = amountPerTranche; + } + + updateAllocatedCapacities( + request.productId, + firstTrancheIdToUse, + trancheCount, + trancheAllocatedCapacities + ); + + updateExpiringCoverAmounts( + request.coverId, + request.productId, + firstTrancheIdToUse, + trancheCount, + gracePeriodExpiration / BUCKET_DURATION + 1, + coverTrancheAllocation, + false // isAllocation + ); + } + + { + require(request.rewardRatio <= REWARDS_DENOMINATOR, "StakingPool: reward ratio exceeds denominator"); + + uint rewards = premium * request.rewardRatio / REWARDS_DENOMINATOR; + uint expireAtBucket = Math.divCeil(coverStartTime + request.period, BUCKET_DURATION); + uint _rewardPerSecond = rewards / (expireAtBucket * BUCKET_DURATION - coverStartTime); + + // 1 SLOAD + 1 SSTORE + rewardBuckets[expireAtBucket].rewardPerSecondCut -= _rewardPerSecond; + } + } + + function calculatePremium( + uint productId, + uint allocatedStake, + uint usableStake, + uint newAllocation, + uint period + ) public returns (uint) { + + uint premium; + + // silence compiler warnings + allocatedStake; + usableStake; + newAllocation; + period; + block.timestamp; + uint96 nextPrice = 0; + products[productId].lastPrice = nextPrice; + + return premium; + } + function getStoredActiveCoverAmounts( uint productId, uint firstTrancheId, @@ -744,7 +864,7 @@ contract StakingPool is IStakingPool, ERC721 { return (totalCapacities, totalCapacity); } - function storeAllocatedCapacities( + function updateAllocatedCapacities( uint productId, uint firstTrancheId, uint trancheCount, @@ -781,13 +901,14 @@ contract StakingPool is IStakingPool, ERC721 { } } - function storeExpiringCoverAmounts( + function updateExpiringCoverAmounts( uint coverId, uint productId, uint firstTrancheId, uint trancheCount, uint targetBucketId, - uint[] memory coverTrancheAllocation + uint[] memory coverTrancheAllocation, + bool isAllocation ) internal { uint firstGroupId = firstTrancheId / BUCKET_TRANCHE_GROUP_SIZE; @@ -812,16 +933,25 @@ contract StakingPool is IStakingPool, ERC721 { uint32 expiringAmount = bucketTrancheGroups[trancheGroupId].getItemAt(trancheIndexInGroup); uint32 trancheAllocation = coverTrancheAllocation[i].toUint32(); - packedCoverTrancheAllocation |= trancheAllocation << uint32(i * 32); + if (isAllocation) { + expiringAmount += trancheAllocation; + packedCoverTrancheAllocation |= trancheAllocation << uint32(i * 32); + } else { + expiringAmount -= trancheAllocation; + } // setItemAt does not mutate so we have to reassign it bucketTrancheGroups[trancheGroupId] = bucketTrancheGroups[trancheGroupId].setItemAt( trancheIndexInGroup, - expiringAmount + trancheAllocation + expiringAmount ); } - coverTrancheAllocations[coverId] = packedCoverTrancheAllocation; + if (isAllocation) { + coverTrancheAllocations[coverId] = packedCoverTrancheAllocation; + } else { + delete coverTrancheAllocations[coverId]; + } for (uint i = 0; i < groupCount; i++) { expiringCoverBuckets[productId][targetBucketId][firstGroupId + i] = bucketTrancheGroups[i]; @@ -900,11 +1030,12 @@ contract StakingPool is IStakingPool, ERC721 { return; // Done! Skip the rest of the function. } - // If the initial tranche is still active, move all the shares and pending rewards to the + // if the initial tranche is still active, move all the shares and pending rewards to the // newly chosen tranche and its coresponding deopsit. - // First make sure tranches are up to date in terms of accumulated NXM rewards. - updateTranches(); + // first make sure tranches are up to date in terms of accumulated NXM rewards. + // passing true because we mint reward shares + updateTranches(true); Deposit memory initialDeposit = deposits[tokenId][initialTrancheId]; @@ -943,9 +1074,8 @@ contract StakingPool is IStakingPool, ERC721 { uint rewardsToCarry; uint _accNxmPerRewardsShare = accNxmPerRewardsShare; { - uint newEarningsPerShare = _accNxmPerRewardsShare - initialDeposit.lastAccNxmPerRewardShare; - rewardsToCarry = newEarningsPerShare * initialDeposit.rewardsShares - + initialDeposit.pendingRewards; + uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(initialDeposit.lastAccNxmPerRewardShare); + rewardsToCarry = newEarningsPerShare * initialDeposit.rewardsShares + initialDeposit.pendingRewards; } Deposit memory updatedDeposit = deposits[tokenId][newTrancheId]; @@ -953,7 +1083,7 @@ contract StakingPool is IStakingPool, ERC721 { // If a deposit lasting until the new tranche's end date already exists, calculate its pending // rewards before carrying over the rewards from the inital deposit. if (updatedDeposit.lastAccNxmPerRewardShare != 0) { - uint newEarningsPerShare = _accNxmPerRewardsShare - updatedDeposit.lastAccNxmPerRewardShare; + uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(updatedDeposit.lastAccNxmPerRewardShare); updatedDeposit.pendingRewards += newEarningsPerShare * updatedDeposit.rewardsShares; } @@ -984,44 +1114,6 @@ contract StakingPool is IStakingPool, ERC721 { rewardsSharesSupply += newRewardsShares; } - function calculatePremium( - uint productId, - uint allocatedStake, - uint usableStake, - uint newAllocation, - uint period - ) public returns (uint) { - - // silence compiler warnings - allocatedStake; - usableStake; - newAllocation; - period; - block.timestamp; - uint96 nextPrice = 0; - products[productId].lastPrice = nextPrice; - - return 0; - } - - function deallocateStake( - uint productId, - uint start, - uint period, - uint amount, - uint premium, - uint /*globalRewardsRatio*/ - ) external onlyCoverContract { - - // silence compiler warnings - productId; - start; - period; - amount; - premium; - activeStake = activeStake; - } - // O(1) function burnStake(uint productId, uint start, uint period, uint amount) external onlyCoverContract { @@ -1030,9 +1122,10 @@ contract StakingPool is IStakingPool, ERC721 { period; // TODO: free up the stake used by the corresponding cover - // TODO: check if it's worth restricting the burn to 99% of the active stake + // TODO: block the pool if we perform 100% of the stake - updateTranches(); + // passing false because neither the amount of shares nor the reward per second are changed + updateTranches(false); uint _activeStake = activeStake; activeStake = _activeStake > amount ? _activeStake - amount : 0; @@ -1114,9 +1207,10 @@ contract StakingPool is IStakingPool, ERC721 { uint oldFee = poolFee; poolFee = uint8(newFee); - updateTranches(); + // passing true because the amount of rewards shares changes + updateTranches(true); - uint fromTrancheId = firstActiveTrancheId; + uint fromTrancheId = block.timestamp / TRANCHE_DURATION; uint toTrancheId = fromTrancheId + MAX_ACTIVE_TRANCHES; uint _accNxmPerRewardsShare = accNxmPerRewardsShare; @@ -1130,7 +1224,7 @@ contract StakingPool is IStakingPool, ERC721 { } // update pending reward and reward shares - uint newRewardPerRewardsShare = _accNxmPerRewardsShare - feeDeposit.lastAccNxmPerRewardShare; + uint newRewardPerRewardsShare = _accNxmPerRewardsShare.uncheckedSub(feeDeposit.lastAccNxmPerRewardShare); feeDeposit.pendingRewards += newRewardPerRewardsShare * feeDeposit.rewardsShares; // TODO: would using tranche.rewardsShares give a better precision? feeDeposit.rewardsShares = feeDeposit.rewardsShares * newFee / oldFee;