From c08ccbc27ae8d55e622181161654760b4a1c032d Mon Sep 17 00:00:00 2001 From: Matthias Zimmermann Date: Tue, 10 Sep 2024 23:07:13 +0000 Subject: [PATCH] refactor/cleanup Staking/StakingStore --- contracts/registry/RegistryAuthorization.sol | 5 +- contracts/staking/IStaking.sol | 3 +- contracts/staking/Staking.sol | 346 +++------- contracts/staking/StakingLib.sol | 29 +- contracts/staking/StakingStore.sol | 669 +++++++++++-------- contracts/type/ChainId.sol | 19 +- contracts/type/Seconds.sol | 9 + test/staking/Staking.t.sol | 13 +- test/staking/StakingProtocolTarget.t.sol | 3 +- 9 files changed, 524 insertions(+), 572 deletions(-) diff --git a/contracts/registry/RegistryAuthorization.sol b/contracts/registry/RegistryAuthorization.sol index 1c5c0b8e0..94287c259 100644 --- a/contracts/registry/RegistryAuthorization.sol +++ b/contracts/registry/RegistryAuthorization.sol @@ -299,10 +299,11 @@ contract RegistryAuthorization _authorize(functions, StakingStore.increaseTotalValueLocked.selector, "increaseTotalValueLocked"); _authorize(functions, StakingStore.decreaseTotalValueLocked.selector, "decreaseTotalValueLocked"); _authorize(functions, StakingStore.createStake.selector, "createStake"); + _authorize(functions, StakingStore.stake.selector, "stake"); + _authorize(functions, StakingStore.unstake.selector, "unstake"); _authorize(functions, StakingStore.updateRewards.selector, "updateRewards"); - _authorize(functions, StakingStore.increaseStakes.selector, "increaseStakes"); - _authorize(functions, StakingStore.decreaseStakes.selector, "decreaseStakes"); _authorize(functions, StakingStore.restakeRewards.selector, "restakeRewards"); + _authorize(functions, StakingStore.claimRewards.selector, "claimRewards"); } } diff --git a/contracts/staking/IStaking.sol b/contracts/staking/IStaking.sol index 55d0f7779..761bf094d 100644 --- a/contracts/staking/IStaking.sol +++ b/contracts/staking/IStaking.sol @@ -239,9 +239,8 @@ interface IStaking is function stake(NftId stakeNftId, Amount dipAmount) external returns (Amount newStakeBalance); /// @dev Pays the specified DIP amount to the holder of the stake NFT ID. - /// If dipAmount is set to Amount.max() all stakes and rewards are transferred to the stake holder. /// permissioned: only staking service may call this function. - function unstake(NftId stakeNftId) external returns (Amount unstakedAmount, Amount rewardsClaimedAmount); + function unstake(NftId stakeNftId) external returns (Amount unstakedAmount); /// @dev restakes the dips to a new target. /// the sum of the staked dips and the accumulated rewards will be restaked. diff --git a/contracts/staking/Staking.sol b/contracts/staking/Staking.sol index abbe76af3..762ca177e 100644 --- a/contracts/staking/Staking.sol +++ b/contracts/staking/Staking.sol @@ -14,17 +14,13 @@ import {Blocknumber} from "../type/Blocknumber.sol"; import {ChainId, ChainIdLib} from "../type/ChainId.sol"; import {Component} from "../shared/Component.sol"; import {IComponent} from "../shared/IComponent.sol"; -import {IComponentService} from "../shared/IComponentService.sol"; import {NftId} from "../type/NftId.sol"; -import {ObjectType, COMPONENT, STAKE, STAKING, TARGET} from "../type/ObjectType.sol"; +import {ObjectType, PROTOCOL, STAKE, STAKING, TARGET} from "../type/ObjectType.sol"; import {Seconds, SecondsLib} from "../type/Seconds.sol"; import {Registerable} from "../shared/Registerable.sol"; -import {ReleaseRegistry} from "../registry/ReleaseRegistry.sol"; import {StakingLib} from "./StakingLib.sol"; import {StakingReader} from "./StakingReader.sol"; import {StakingStore} from "./StakingStore.sol"; -import {TargetManagerLib} from "./TargetManagerLib.sol"; -import {Timestamp} from "../type/Timestamp.sol"; import {TokenHandler} from "../shared/TokenHandler.sol"; import {TokenHandlerDeployerLib} from "../shared/TokenHandlerDeployerLib.sol"; import {TokenRegistry} from "../registry/TokenRegistry.sol"; @@ -32,6 +28,7 @@ import {UFixed} from "../type/UFixed.sol"; import {Version, VersionLib, VersionPart, VersionPartLib} from "../type/Version.sol"; import {Versionable} from "../upgradeability/Versionable.sol"; + contract Staking is Component, Versionable, @@ -52,12 +49,6 @@ contract Staking is } - modifier onlyStake(NftId stakeNftId) { - _checkTypeAndOwner(stakeNftId, STAKE(), false); - _; - } - - modifier onlyStakeOwner(NftId stakeNftId) { _checkTypeAndOwner(stakeNftId, STAKE(), true); _; @@ -69,12 +60,6 @@ contract Staking is _; } - - modifier onlyTargetOwner(NftId targetNftId) { - _checkTypeAndOwner(targetNftId, TARGET(), true); - _; - } - //--- contract intitialization ------------------------------------------- function initializeTokenHandler() @@ -96,7 +81,6 @@ contract Staking is //--- staking owner functions -------------------------------------------// - // TODO also make sure that protocol rewards can be refilled and withdrawn /// @inheritdoc IStaking function setProtocolLockingPeriod(Seconds newLockingPeriod) @@ -105,13 +89,13 @@ contract Staking is restricted() onlyOwner() { - NftId protocolNftId = getRegistry().getProtocolNftId(); + StakingStorage storage $ = _getStakingStorage(); ( Seconds oldLockingPeriod, Blocknumber lastUpdatedIn - ) = _getStakingStorage()._store.setLockingPeriod(protocolNftId, newLockingPeriod); + ) = $._store.setLockingPeriod($._protocolNftId, newLockingPeriod); - emit LogStakingProtocolLockingPeriodSet(protocolNftId, newLockingPeriod, oldLockingPeriod, lastUpdatedIn); + emit LogStakingProtocolLockingPeriodSet($._protocolNftId, newLockingPeriod, oldLockingPeriod, lastUpdatedIn); } @@ -122,13 +106,13 @@ contract Staking is restricted() onlyOwner() { - NftId protocolNftId = getRegistry().getProtocolNftId(); + StakingStorage storage $ = _getStakingStorage(); ( UFixed oldRewardRate, Blocknumber lastUpdatedIn - ) = _getStakingStorage()._store.setRewardRate(protocolNftId, newRewardRate); + ) = $._store.setRewardRate($._protocolNftId, newRewardRate); - emit LogStakingProtocolRewardRateSet(protocolNftId, newRewardRate, oldRewardRate, lastUpdatedIn); + emit LogStakingProtocolRewardRateSet($._protocolNftId, newRewardRate, oldRewardRate, lastUpdatedIn); } @@ -159,21 +143,12 @@ contract Staking is restricted() onlyOwner() { - // checks - if (!ReleaseRegistry(getRegistry().getReleaseRegistryAddress()).isActiveRelease(release)) { - revert ErrorStakingReleaseNotActive(release); - } - - address stakingServiceAddress = getRegistry().getServiceAddress(STAKING(), release); - if (stakingServiceAddress == address(0)) { - revert ErrorStakingServiceNotFound(release); - } - // effects - address oldStakingService = address(_getStakingStorage()._stakingService); - _getStakingStorage()._stakingService = IStakingService(stakingServiceAddress); + StakingStorage storage $ = _getStakingStorage(); + address oldStakingService = address($._stakingService); + $._stakingService = StakingLib.checkAndGetStakingService(getRegistry(), release); - emit LogStakingStakingServiceSet(stakingServiceAddress, release, oldStakingService); + emit LogStakingStakingServiceSet(address($._stakingService), release, oldStakingService); } @@ -217,13 +192,14 @@ contract Staking is restricted() onlyOwner() { + StakingStorage storage $ = _getStakingStorage(); Amount oldAllowanceAmount = AmountLib.toAmount( token.allowance( address(this), - address(_getStakingStorage()._tokenHandler))); + address($._tokenHandler))); // staking token handler approval via its own implementation in staking service - IComponentService(_getServiceAddress(STAKING())).approveTokenHandler( + $._stakingService.approveTokenHandler( token, amount); @@ -254,27 +230,30 @@ contract Staking is onlyTarget(targetNftId) returns (Amount newBalance) { - // special case 1: protocol target: staking owner is recipient + address transferTo; + + // case 1: protocol target: staking owner is recipient if (targetNftId == getRegistry().getProtocolNftId()) { // verify that the caller is the staking owner - if (msg.sender != getOwner()) { + transferTo = getOwner(); + if (msg.sender != transferTo) { revert ErrorStakingNotStakingOwner(); } - return _withdrawRewardReserves(targetNftId, dipAmount, getOwner()); - } - - // special case 2: off-chain targets - // TODO decide how to handle and implement + // case 2: same chain target: target owner is recipient + } else if (ChainIdLib.isCurrentChain(targetNftId)) { + // verify that the caller is the target owner + transferTo = getRegistry().ownerOf(targetNftId); + if (msg.sender != transferTo) { + revert ErrorStakingNotNftOwner(targetNftId); + } - // default: on-chain target owner is recipient - address targetOwner = getRegistry().ownerOf(targetNftId); - // verify that the caller is the target owner - if (msg.sender != targetOwner) { - revert ErrorStakingNotNftOwner(targetNftId); + // case 3: cross-chain target: TODO decide how to handle and implement + } else { + revert("Cross-chain target not supported"); } - return _withdrawRewardReserves(targetNftId, dipAmount, targetOwner); + newBalance = _withdrawRewardReserves(targetNftId, dipAmount, transferTo); } @@ -286,7 +265,6 @@ contract Staking is onlyTarget(targetNftId) returns (Amount newBalance) { - address fundingBy = msg.sender; _refillRewardReserves(targetNftId, dipAmount, transferFrom); } @@ -299,14 +277,11 @@ contract Staking is onlyTarget(targetNftId) returns (Amount newBalance) { - // special case 1: protocol target: staking owner is recipient + // check that service does not withdraw from protocol target if (targetNftId == getRegistry().getProtocolNftId()) { - return _withdrawRewardReserves(targetNftId, dipAmount, transferTo); + revert ErrorStakingTargetTypeNotSupported(targetNftId, PROTOCOL()); } - // special case 2: off-chain targets - // TODO decide how to handle and implement - // default: on-chain target owner is recipient address targetOwner = getRegistry().ownerOf(targetNftId); return _withdrawRewardReserves(targetNftId, dipAmount, targetOwner); @@ -359,6 +334,7 @@ contract Staking is onlyTarget(targetNftId) { (UFixed oldRewardRate,) = _getStakingStorage()._store.setRewardRate(targetNftId, rewardRate); + emit LogStakingTargetRewardRateSet(targetNftId, rewardRate, oldRewardRate); } @@ -371,31 +347,10 @@ contract Staking is onlyTarget(targetNftId) { _getStakingStorage()._store.setMaxStakedAmount(targetNftId, maxStakedAmount); + emit LogStakingTargetMaxStakedAmountSet(targetNftId, maxStakedAmount); } - // TODO cleanup - // /// @inheritdoc IStaking - // function refillRewardReservesByService(NftId targetNftId, Amount dipAmount, address from) - // external - // virtual - // restricted() - // returns (Amount newBalance) - // { - // _refillRewardReserves(targetNftId, dipAmount, from); - // } - - - // /// @inheritdoc IStaking - // function withdrawRewardReservesByService(NftId targetNftId, Amount dipAmount, address to) - // external - // virtual - // restricted() - // returns (Amount newBalance) - // { - // _withdrawRewardReserves(targetNftId, dipAmount, to); - // } - /// @inheritdoc IStaking function addTargetToken(NftId targetNftId, address token) @@ -478,16 +433,15 @@ contract Staking is onlyTarget(targetNftId) returns (NftId stakeNftId) { - StakingStorage storage $ = _getStakingStorage(); - // effects (includes further checks in service) + StakingStorage storage $ = _getStakingStorage(); stakeNftId = $._stakingService.createStakeObject(targetNftId, stakeOwner); - Timestamp lockedUntil = $._store.createStake(stakeNftId, targetNftId, stakeAmount); - - emit LogStakingStakeCreated(stakeNftId, targetNftId, stakeAmount, lockedUntil, stakeOwner); + $._store.createStake(stakeNftId, targetNftId, stakeOwner, stakeAmount); // interactions - $._stakingService.pullDipToken(stakeAmount, stakeOwner); + if (stakeAmount.gtz()) { + $._stakingService.pullDipToken(stakeAmount, stakeOwner); + } } @@ -503,42 +457,15 @@ contract Staking is returns (Amount newStakeBalance) { StakingStorage storage $ = _getStakingStorage(); - - // update rewards for stake (add rewards since last update) - ( - Amount rewardIncreaseAmount, - Seconds targetLockingPeriod, - Amount stakeBalance, - Amount rewardBalance, - Timestamp lockedUntil - ) = _updateRewards($, stakeNftId); - - // no additional locking duration if no additional stakes - if (stakeAmount.eqz()) { - targetLockingPeriod = SecondsLib.zero(); - } - - // increase stakes and restake rewards - bool restakeRewards = true; - if (restakeRewards && rewardBalance.gtz()) { - emit LogStakingRewardsRestaked(stakeNftId, rewardBalance + rewardBalance, stakeBalance, AmountLib.zero(), lockedUntil); - } - - ( - stakeBalance, - rewardBalance, - lockedUntil - ) = $._store.increaseStakes( - stakeNftId, - stakeAmount, - targetLockingPeriod, - restakeRewards); - - // collect staked DIP token by staking service + $._store.stake( + stakeNftId, + true, // update rewards + true, // restake rewards + SecondsLib.max(), // max additional locking duration + stakeAmount); + + // collect staked DIP token via staking service if (stakeAmount.gtz()) { - emit LogStakingStaked(stakeNftId, stakeAmount, stakeBalance, rewardBalance, lockedUntil); - - // interactions address stakeOwner = getRegistry().ownerOf(stakeNftId); $._stakingService.pullDipToken(stakeAmount, stakeOwner); } @@ -549,33 +476,21 @@ contract Staking is function unstake(NftId stakeNftId) external virtual - restricted() // only staking service + restricted() onlyStakeOwner(stakeNftId) - returns ( - Amount unstakedAmount, - Amount rewardsClaimedAmount - ) + returns (Amount unstakedAmount) { StakingStorage storage $ = _getStakingStorage(); - bool restakeRewards = true; - Timestamp lockedUntil; - - ( - unstakedAmount, - rewardsClaimedAmount, - lockedUntil - ) = _unstakeAll( - $, - stakeNftId, - restakeRewards); // restake rewards - - // collect staked DIP token by staking service - Amount collectedAmount = unstakedAmount + rewardsClaimedAmount; - if (collectedAmount.gtz()) { - - // interactions + unstakedAmount = $._store.unstake( + stakeNftId, + true, // update rewards + true, // restake rewards + AmountLib.max()); // unstake up to this amount + + // transfer unstaked DIP token via staking service + if (unstakedAmount.gtz()) { address stakeOwner = getRegistry().ownerOf(stakeNftId); - $._stakingService.pushDipToken(collectedAmount, stakeOwner); + $._stakingService.pushDipToken(unstakedAmount, stakeOwner); } } @@ -596,20 +511,20 @@ contract Staking is ) { StakingStorage storage $ = _getStakingStorage(); - address stakeOwner = msg.sender; - ( - Amount unstakedAmount, - Amount rewardsClaimedAmount, - ) = _unstakeAll($, stakeNftId, true); // restake rewards + // step 1: unstake as much as possible + newStakedAmount = $._store.unstake( + stakeNftId, + true, // update rewards + true, // restake rewards + AmountLib.max()); // unstake up to this amount + // step 2: create new stake with full unstaked amount + address stakeOwner = getRegistry().ownerOf(stakeNftId); newStakeNftId = $._stakingService.createStakeObject(newTargetNftId, stakeOwner); - newStakedAmount = unstakedAmount + rewardsClaimedAmount; - $._store.createStake( - newStakeNftId, - newTargetNftId, - newStakedAmount); + $._store.createStake(newStakeNftId, newTargetNftId, stakeOwner, newStakedAmount); + // logging emit LogStakingStakeRestaked(newStakeNftId, newTargetNftId, newStakedAmount, stakeOwner, stakeNftId); } @@ -617,47 +532,32 @@ contract Staking is function updateRewards(NftId stakeNftId) external virtual - restricted() // only staking service - onlyStake(stakeNftId) + restricted() + onlyStakeOwner(stakeNftId) returns (Amount newRewardAmount) { - _updateRewards( - _getStakingStorage(), - stakeNftId); + StakingStorage storage $ = _getStakingStorage(); + $._store.updateRewards(stakeNftId); } function claimRewards(NftId stakeNftId) external virtual - restricted() // only staking service - onlyStake(stakeNftId) + restricted() + onlyStakeOwner(stakeNftId) returns ( - Amount rewardsClaimedAmount + Amount claimedAmount ) { StakingStorage storage $ = _getStakingStorage(); - - // update rewards since last update - _updateRewards($, stakeNftId); - - ( - Amount restakedRewardAmount, - Amount unstakedAmount, - Amount claimedAmount, - Amount stakedBalance, - Amount rewardBalance, - Timestamp lockedUntil - ) = $._store.decreaseStakes( - stakeNftId, - AmountLib.zero(), // unstake dip amount - AmountLib.max(), // unstake reward amount - false); // restake rewards + claimedAmount = $._store.claimRewards( + stakeNftId, + true, + AmountLib.max()); // collect staked DIP token by staking service if (claimedAmount.gtz()) { - emit LogStakingRewardsClaimed(stakeNftId, claimedAmount, stakedBalance, rewardBalance, lockedUntil); - // interactions address stakeOwner = getRegistry().ownerOf(stakeNftId); $._stakingService.pushDipToken(claimedAmount, stakeOwner); @@ -740,80 +640,6 @@ contract Staking is } - function _updateRewards( - StakingStorage storage $, - NftId stakeNftId - ) - internal - virtual - returns ( - Amount rewardIncreaseAmount, - Seconds targetLockingPeriod, - Amount stakeBalance, - Amount rewardBalance, - Timestamp lockedUntil - ) - { - ( - rewardIncreaseAmount, - targetLockingPeriod, - stakeBalance, - rewardBalance, - lockedUntil - ) = $._store.updateRewards(stakeNftId); - - if (rewardIncreaseAmount.gtz()) { - emit LogStakingStakeRewardsUpdated(stakeNftId, rewardIncreaseAmount, stakeBalance, rewardBalance, lockedUntil); - } - } - - - function _unstakeAll( - StakingStorage storage $, - NftId stakeNftId, - bool restakeRewards - ) - internal - virtual - returns ( - Amount unstakedAmount, - Amount claimedAmount, - Timestamp lockedUntil - ) - { - // additional checks (most checks are done prior to calling this function) - if ($._store.isStakeLocked(stakeNftId)) { - revert ErrorStakingStakeLocked(stakeNftId, $._store.getStakeInfo(stakeNftId).lockedUntil); - } - - // update rewards since last update - (,,,, lockedUntil) = _updateRewards($, stakeNftId); - Amount restakedRewardAmount; - Amount stakeBalance; - Amount rewardBalance; - - ( - restakedRewardAmount, - unstakedAmount, - claimedAmount, - stakeBalance, - rewardBalance, - ) = $._store.decreaseStakes( - stakeNftId, - AmountLib.max(), // unstake all stakes - AmountLib.max(), // claim all rewards - restakeRewards); - - if (restakedRewardAmount.gtz()) { - emit LogStakingRewardsRestaked(stakeNftId, restakedRewardAmount, stakeBalance + unstakedAmount, rewardBalance + claimedAmount, lockedUntil); - } - - if (unstakedAmount.gtz() || claimedAmount.gtz()) { - emit LogStakingUnstaked(stakeNftId, unstakedAmount + claimedAmount, stakeBalance, rewardBalance, lockedUntil); - } - } - - function _addToken( StakingStorage storage $, ChainId chainId, @@ -824,8 +650,6 @@ contract Staking is { if ($._store.getTokenInfo(chainId, token).lastUpdateIn.eqz()) { $._store.addToken(chainId, token); - - emit LogStakingTokenAdded(chainId, token); } } @@ -868,17 +692,16 @@ contract Staking is // Protocol target is created in the StakingStore constructor. // This allows setting up the protocol target before the full // staking authorization setup is in place. - _checkAndLogProtocolTargetCreation(); + _checkAndLogProtocolTargetCreation($); _registerInterface(type(IStaking).interfaceId); } - function _checkAndLogProtocolTargetCreation() + function _checkAndLogProtocolTargetCreation(StakingStorage storage $) internal virtual { - StakingStorage storage $ = _getStakingStorage(); TargetInfo memory protocolInfo = $._store.getTargetInfo($._protocolNftId); if (protocolInfo.lastUpdateIn.eqz()) { @@ -893,13 +716,14 @@ contract Staking is internal view { + StakingStorage storage $ = _getStakingStorage(); if (expectedObjectType == STAKE()) { - if (!_getStakingStorage()._store.exists(nftId)) { + if (!$._store.exists(nftId)) { revert ErrorStakingNotStake(nftId); } } else { if (expectedObjectType == TARGET()) { - if (!_getStakingStorage()._store.getTargetSet().exists(nftId)) { + if (!$._store.getTargetSet().exists(nftId)) { revert ErrorStakingNotTarget(nftId); } } diff --git a/contracts/staking/StakingLib.sol b/contracts/staking/StakingLib.sol index cc3349c45..9d0367200 100644 --- a/contracts/staking/StakingLib.sol +++ b/contracts/staking/StakingLib.sol @@ -3,13 +3,17 @@ pragma solidity ^0.8.20; import {IRegistry} from "../registry/IRegistry.sol"; import {IStaking} from "./IStaking.sol"; +import {IStakingService} from "./IStakingService.sol"; -import {Amount, AmountLib} from "../type/Amount.sol"; +import {Amount} from "../type/Amount.sol"; import {NftId} from "../type/NftId.sol"; +import {ReleaseRegistry} from "../registry/ReleaseRegistry.sol"; import {Seconds, SecondsLib} from "../type/Seconds.sol"; import {StakingReader} from "./StakingReader.sol"; +import {STAKING} from "../type/ObjectType.sol"; import {Timestamp, TimestampLib} from "../type/Timestamp.sol"; import {UFixed, UFixedLib} from "../type/UFixed.sol"; +import {VersionPart} from "../type/Version.sol"; library StakingLib { @@ -28,6 +32,7 @@ library StakingLib { return _checkCreateParameters(stakingReader, targetNftId, dipAmount); } + function _checkCreateParameters( StakingReader stakingReader, NftId targetNftId, @@ -69,6 +74,7 @@ library StakingLib { lockingPeriod = info.lockingPeriod; } + function checkUnstakeParameters( StakingReader stakingReader, NftId stakeNftId @@ -104,6 +110,27 @@ library StakingLib { } + function checkAndGetStakingService( + IRegistry registry, + VersionPart release + ) + public + view + returns (IStakingService stakingService) + { + if (!ReleaseRegistry(registry.getReleaseRegistryAddress()).isActiveRelease(release)) { + revert IStaking.ErrorStakingReleaseNotActive(release); + } + + address stakingServiceAddress = registry.getServiceAddress(STAKING(), release); + if (stakingServiceAddress == address(0)) { + revert IStaking.ErrorStakingServiceNotFound(release); + } + + return IStakingService(stakingServiceAddress); + } + + function checkDipAmount( StakingReader stakingReader, NftId targetNftId, diff --git a/contracts/staking/StakingStore.sol b/contracts/staking/StakingStore.sol index f72d4c065..3b9789435 100644 --- a/contracts/staking/StakingStore.sol +++ b/contracts/staking/StakingStore.sol @@ -10,18 +10,16 @@ import {Amount, AmountLib} from "../type/Amount.sol"; import {ChainId, ChainIdLib} from "../type/ChainId.sol"; import {Blocknumber, BlocknumberLib} from "../type/Blocknumber.sol"; import {KeyValueStore} from "../shared/KeyValueStore.sol"; -import {KEEP_STATE} from "../type/StateId.sol"; import {NftId, NftIdLib} from "../type/NftId.sol"; import {NftIdSet} from "../shared/NftIdSet.sol"; import {ObjectType} from "../type/ObjectType.sol"; -import {PROTOCOL, STAKE, TARGET} from "../type/ObjectType.sol"; +import {PROTOCOL} from "../type/ObjectType.sol"; import {Seconds, SecondsLib} from "../type/Seconds.sol"; import {StakingLib} from "./StakingLib.sol"; import {StakingLifecycle} from "./StakingLifecycle.sol"; import {StakingReader} from "./StakingReader.sol"; import {TargetManagerLib} from "./TargetManagerLib.sol"; import {Timestamp, TimestampLib} from "../type/Timestamp.sol"; -import {TokenRegistry} from "../registry/TokenRegistry.sol"; import {UFixed, UFixedLib} from "../type/UFixed.sol"; @@ -65,7 +63,7 @@ contract StakingStore is // targets mapping(NftId targetNftId => IStaking.TargetInfo) private _targetInfo; mapping(NftId targetNftId => mapping(address token => IStaking.TvlInfo)) private _tvlInfo; - mapping(NftId targetNftId => address [] token) _targetToken; + mapping(NftId targetNftId => address [] token) private _targetToken; // staking rate mapping(ChainId chainId => mapping(address token => IStaking.TokenInfo)) private _tokenInfo; @@ -103,7 +101,6 @@ contract StakingStore is restricted() // token registry via staking { // checks - IStaking.TokenInfo storage info = _tokenInfo[chainId][token]; // check token is not yet registered @@ -113,6 +110,9 @@ contract StakingStore is info.stakingRate = UFixedLib.zero(); info.lastUpdateIn = BlocknumberLib.current(); + + // logging + emit IStaking.LogStakingTokenAdded(chainId, token); } @@ -219,60 +219,6 @@ contract StakingStore is } - // TODO move to private functions - function _verifyAndUpdateTarget(NftId targetNftId) - private - returns ( - IStaking.TargetInfo storage targetInfo, - Blocknumber lastUpdatedIn - ) - { - // checks - targetInfo = _getAndVerifyTarget(targetNftId); - lastUpdatedIn = targetInfo.lastUpdateIn; - targetInfo.lastUpdateIn = BlocknumberLib.current(); - } - - - // TODO move to private functions - function _createTarget( - NftId targetNftId, - ObjectType objectType, - Seconds lockingPeriod, - UFixed rewardRate, - bool checkParameters - ) - private - { - // checks - if (checkParameters) { - TargetManagerLib.checkTargetParameters( - _registry, - _reader, - targetNftId, - objectType, - lockingPeriod, - rewardRate); - } - - // effects - IStaking.TargetInfo storage targetInfo = _targetInfo[targetNftId]; - targetInfo.stakedAmount = AmountLib.zero(); - targetInfo.rewardAmount = AmountLib.zero(); - targetInfo.reserveAmount = AmountLib.zero(); - targetInfo.maxStakedAmount = AmountLib.max(); - - targetInfo.objectType = objectType; - targetInfo.lockingPeriod = lockingPeriod; - targetInfo.rewardRate = rewardRate; - targetInfo.chainId = ChainIdLib.fromNftId(targetNftId); - targetInfo.lastUpdateIn = BlocknumberLib.current(); - - // add new target to target set - _targetNftIdSet.add(targetNftId); - } - - function addTargetToken( NftId targetNftId, address token @@ -358,48 +304,6 @@ contract StakingStore is } - function _spendRewardReserves( - NftId targetNftId, - IStaking.TargetInfo storage targetInfo, - Amount dipAmount - ) - private - { - Blocknumber lastUpdateIn = _decreaseReserves(targetNftId, targetInfo, dipAmount); - - // logging - emit IStaking.LogStakingRewardReservesSpent( - targetNftId, - dipAmount, - targetInfo.reserveAmount, - lastUpdateIn); - } - - - function _decreaseReserves( - NftId targetNftId, - IStaking.TargetInfo storage targetInfo, - Amount dipAmount - ) - private - returns ( Blocknumber lastUpdateIn) - { - lastUpdateIn = targetInfo.lastUpdateIn; - - // check if reserves are sufficient - if (dipAmount > targetInfo.reserveAmount) { - revert ErrorStakingStoreRewardReservesInsufficient( - targetNftId, - targetInfo.reserveAmount, - dipAmount); - } - - // effects - targetInfo.reserveAmount = targetInfo.reserveAmount - dipAmount; - targetInfo.lastUpdateIn = BlocknumberLib.current(); - } - - //--- tvl specific functions -------------------------------------// function increaseTotalValueLocked( @@ -444,269 +348,138 @@ contract StakingStore is function createStake( NftId stakeNftId, NftId targetNftId, - Amount stakedAmount + address stakeOwner, + Amount stakeAmount ) external restricted() returns (Timestamp lockedUntil) { // checks - lockedUntil = StakingLib.checkCreateParameters( - _reader, - targetNftId, - stakedAmount); - IStaking.StakeInfo storage stakeInfo = _stakeInfo[stakeNftId]; if (stakeInfo.lastUpdateIn.gtz()) { revert ErrorStakingStoreStakeBalanceAlreadyInitialized(stakeNftId); } IStaking.TargetInfo storage targetInfo = _getAndVerifyTarget(targetNftId); - _checkMaxStakedAmount(targetNftId, targetInfo, stakedAmount); + _checkMaxStakedAmount(targetNftId, targetInfo, stakeAmount); // effects - // update target - targetInfo.stakedAmount = targetInfo.stakedAmount + stakedAmount; - targetInfo.lastUpdateIn = BlocknumberLib.current(); - - // update stake stakeInfo.targetNftId = targetNftId; - stakeInfo.stakedAmount = stakedAmount; + stakeInfo.stakedAmount = AmountLib.zero(); stakeInfo.rewardAmount = AmountLib.zero(); - stakeInfo.lockedUntil = lockedUntil; - stakeInfo.lastUpdateAt = TimestampLib.current(); - stakeInfo.lastUpdateIn = BlocknumberLib.current(); + stakeInfo.lockedUntil = TimestampLib.current(); + _setStakeLastUpdatesToCurrent(stakeInfo); + + // logging for creation of empty stake + emit IStaking.LogStakingStakeCreated(stakeNftId, stakeInfo.targetNftId, stakeInfo.stakedAmount, stakeInfo.lockedUntil, stakeOwner); + + // process stake amount + _stake(stakeNftId, stakeInfo, targetInfo, targetInfo.lockingPeriod, stakeAmount); } - /// @dev Increases the stake amount and optionally restakes the rewards. - /// IMPORTANT: Function updateRewards must be called before this function in the same transaction. - function increaseStakes( - NftId stakeNftId, - Amount stakeIncreaseAmount, // additional staked amount - Seconds additionalLockingPeriod, // duration to increase locked until - bool restakeRewards + + function stake( + NftId stakeNftId, + bool updateRewards, + bool restakeRewards, + Seconds additionalLockingPeriod, + Amount stakeAmount ) external restricted() - returns ( - Amount stakeBalance, - Amount rewardBalance, - Timestamp lockedUntil - ) { // checks IStaking.StakeInfo storage stakeInfo = _getAndVerifyStake(stakeNftId); IStaking.TargetInfo storage targetInfo = _getAndVerifyTarget(stakeInfo.targetNftId); - Amount restakedRewardAmount = stakeInfo.rewardAmount; - - // calculate new values (with restaking) - if (restakeRewards && restakedRewardAmount.gtz()) { - Amount totalStakeIncreaseAmount = stakeIncreaseAmount + restakedRewardAmount; - _checkMaxStakedAmount(stakeInfo.targetNftId, targetInfo, totalStakeIncreaseAmount); - - // effects - // update target - targetInfo.stakedAmount = targetInfo.stakedAmount + totalStakeIncreaseAmount; - targetInfo.rewardAmount = targetInfo.rewardAmount - stakeInfo.rewardAmount; - - // update stake - stakeInfo.stakedAmount = stakeInfo.stakedAmount + totalStakeIncreaseAmount; - stakeInfo.rewardAmount = AmountLib.zero(); - - // calculate new values (without restaking) - } else { - _checkMaxStakedAmount(stakeInfo.targetNftId, targetInfo, stakeIncreaseAmount); - - // effects - // update target and stake - targetInfo.stakedAmount = targetInfo.stakedAmount + stakeIncreaseAmount; - stakeInfo.stakedAmount = stakeInfo.stakedAmount + stakeIncreaseAmount; - } - // update meta data for target and stake - targetInfo.lastUpdateIn = BlocknumberLib.current(); - stakeInfo.lastUpdateAt = TimestampLib.current(); - stakeInfo.lastUpdateIn = BlocknumberLib.current(); + if (updateRewards) { + _updateRewards(stakeNftId, stakeInfo, targetInfo); + } - // increase locked until if applicable - if (additionalLockingPeriod.gtz()) { - stakeInfo.lockedUntil = stakeInfo.lockedUntil.addSeconds(additionalLockingPeriod); + if (restakeRewards) { + _restakeRewards(stakeNftId, stakeInfo, targetInfo); } - // set return values - stakeBalance = stakeInfo.stakedAmount; - rewardBalance = stakeInfo.rewardAmount; - lockedUntil = stakeInfo.lockedUntil; + _stake(stakeNftId, stakeInfo, targetInfo, additionalLockingPeriod, stakeAmount); } - /// @dev Decreases the staking and reward amounts. - /// The function attepmts to unstake up to the specified max amounts. - /// In case the specified amounts are higher than the current staked and reward amounts, the avaiable amounts are unstaked. - /// The method optionally restakes the rewards. - /// IMPORTANT: Function updateRewards must be called before this function in the same transaction. - function decreaseStakes( - NftId stakeNftId, - Amount maxUnstakedAmount, - Amount maxClaimAmount, - bool restakeRewards + function unstake( + NftId stakeNftId, + bool updateRewards, + bool restakeRewards, + Amount maxUnstakeAmount ) external restricted() - returns ( - Amount restakedRewardAmount, - Amount unstakedAmount, - Amount claimedAmount, - Amount stakeBalance, - Amount rewardBalance, - Timestamp lockedUntil - ) + returns (Amount unstakedAmount) { // checks IStaking.StakeInfo storage stakeInfo = _getAndVerifyStake(stakeNftId); - NftId targetNftId = stakeInfo.targetNftId; - IStaking.TargetInfo storage targetInfo = _getAndVerifyTarget(targetNftId); - - // restake rewards if applicable - if (restakeRewards && stakeInfo.rewardAmount.gtz()) { - restakedRewardAmount = stakeInfo.rewardAmount; - _checkMaxStakedAmount(targetNftId, targetInfo, restakedRewardAmount); - - // reserves used for restaking - _spendRewardReserves(targetNftId, targetInfo, restakedRewardAmount); - - // update target - targetInfo.stakedAmount = targetInfo.stakedAmount + restakedRewardAmount; - targetInfo.rewardAmount = targetInfo.rewardAmount - restakedRewardAmount; - - // update stake - stakeInfo.stakedAmount = stakeInfo.stakedAmount + restakedRewardAmount; - stakeInfo.rewardAmount = AmountLib.zero(); + IStaking.TargetInfo storage targetInfo = _getAndVerifyTarget(stakeInfo.targetNftId); - } else { - restakedRewardAmount = AmountLib.zero(); + if (updateRewards) { + _updateRewards(stakeNftId, stakeInfo, targetInfo); } - - // determine amounts - unstakedAmount = AmountLib.min(maxUnstakedAmount, stakeInfo.stakedAmount); - claimedAmount = AmountLib.min(maxClaimAmount, stakeInfo.rewardAmount); - - // update reserves if rewards are claimed - if (!restakeRewards) { - // check if reserves are sufficient - if (claimedAmount > targetInfo.reserveAmount) { - revert ErrorStakingStoreRewardReservesInsufficient( - stakeInfo.targetNftId, - targetInfo.reserveAmount, - claimedAmount); - } - - targetInfo.reserveAmount = targetInfo.reserveAmount - claimedAmount; + + if (restakeRewards) { + _restakeRewards(stakeNftId, stakeInfo, targetInfo); } - // update target - targetInfo.stakedAmount = targetInfo.stakedAmount - unstakedAmount; - targetInfo.rewardAmount = targetInfo.rewardAmount - claimedAmount; - targetInfo.lastUpdateIn = BlocknumberLib.current(); + return _unstake(stakeNftId, stakeInfo, targetInfo, maxUnstakeAmount); + } - // update stake - stakeInfo.stakedAmount = stakeInfo.stakedAmount - unstakedAmount; - stakeInfo.rewardAmount = stakeInfo.rewardAmount - claimedAmount; - stakeInfo.lastUpdateAt = TimestampLib.current(); - stakeInfo.lastUpdateIn = BlocknumberLib.current(); - // set return values - stakeBalance = stakeInfo.stakedAmount; - rewardBalance = stakeInfo.rewardAmount; - lockedUntil = stakeInfo.lockedUntil; + function updateRewards(NftId stakeNftId) + external + restricted() + { + // checks + IStaking.StakeInfo storage stakeInfo = _getAndVerifyStake(stakeNftId); + IStaking.TargetInfo storage targetInfo = _getAndVerifyTarget(stakeInfo.targetNftId); + _updateRewards(stakeNftId, stakeInfo, targetInfo); } - function updateRewards( - NftId stakeNftId + function restakeRewards( + NftId stakeNftId, + bool updateRewards ) external restricted() - returns ( - Amount rewardIncreaseAmount, - Seconds targetLockingPeriod, - Amount stakeBalance, - Amount rewardBalance, - Timestamp lockedUntil - ) { // checks IStaking.StakeInfo storage stakeInfo = _getAndVerifyStake(stakeNftId); IStaking.TargetInfo storage targetInfo = _getAndVerifyTarget(stakeInfo.targetNftId); - // get seconds since last update on stake - Seconds duration = SecondsLib.toSeconds( - block.timestamp - stakeInfo.lastUpdateAt.toInt()); - - // calculate reward increase since - rewardIncreaseAmount = StakingLib.calculateRewardAmount( - targetInfo.rewardRate, - duration, - stakeInfo.stakedAmount); - - // effects - if (rewardIncreaseAmount.gtz()) { - // update target - targetInfo.rewardAmount = targetInfo.rewardAmount + rewardIncreaseAmount; - targetInfo.lastUpdateIn = BlocknumberLib.current(); - - // update stake - stakeInfo.rewardAmount = stakeInfo.rewardAmount + rewardIncreaseAmount; - stakeInfo.lastUpdateIn = BlocknumberLib.current(); - stakeInfo.lastUpdateAt = TimestampLib.current(); + if (updateRewards) { + _updateRewards(stakeNftId, stakeInfo, targetInfo); } - // set remaining return values - targetLockingPeriod = targetInfo.lockingPeriod; - stakeBalance = stakeInfo.stakedAmount; - rewardBalance = stakeInfo.rewardAmount; - lockedUntil = stakeInfo.lockedUntil; + _restakeRewards(stakeNftId, stakeInfo, targetInfo); } - function restakeRewards( + function claimRewards( NftId stakeNftId, - Amount additionalRewardAmount, - Seconds additionalLockingPeriod // duration to increase locked until + bool updateRewards, + Amount maxClaimAmount ) external restricted() - returns ( - Amount newstakedAmount - ) + returns (Amount claimedAmount) { // checks IStaking.StakeInfo storage stakeInfo = _getAndVerifyStake(stakeNftId); - - Amount oldRewardAmount = stakeInfo.rewardAmount; - Amount updatedRewardAmount = stakeInfo.rewardAmount + additionalRewardAmount; - IStaking.TargetInfo storage targetInfo = _getAndVerifyTarget(stakeInfo.targetNftId); - _checkMaxStakedAmount(stakeInfo.targetNftId, targetInfo, updatedRewardAmount); - - // effects - // update target - targetInfo.stakedAmount = targetInfo.stakedAmount + updatedRewardAmount; - targetInfo.rewardAmount = targetInfo.rewardAmount - oldRewardAmount; - targetInfo.lastUpdateIn = BlocknumberLib.current(); - // update stake - stakeInfo.stakedAmount = stakeInfo.stakedAmount + updatedRewardAmount; - stakeInfo.rewardAmount = AmountLib.zero(); - stakeInfo.lastUpdateAt = TimestampLib.current(); - stakeInfo.lastUpdateIn = BlocknumberLib.current(); - - // increase locked until if applicable - if (additionalLockingPeriod.gtz()) { - stakeInfo.lockedUntil.addSeconds(additionalLockingPeriod); + if (updateRewards) { + _updateRewards(stakeNftId, stakeInfo, targetInfo); } - } + claimedAmount = _claimRewards(stakeNftId, stakeInfo, targetInfo, maxClaimAmount); + } //--- view functions -----------------------------------------------// @@ -750,7 +523,7 @@ contract StakingStore is /// @dev Returns true iff current stake amount is still locked - function isStakeLocked(NftId stakeNftId) external view returns (bool) { + function isStakeLocked(NftId stakeNftId) public view returns (bool) { return _stakeInfo[stakeNftId].lockedUntil > TimestampLib.current(); } @@ -789,6 +562,312 @@ contract StakingStore is return _targetNftIdSet; } + //--- internal functions -----------------------------------------------// + + function _verifyAndUpdateTarget(NftId targetNftId) + private + returns ( + IStaking.TargetInfo storage targetInfo, + Blocknumber lastUpdatedIn + ) + { + // checks + targetInfo = _getAndVerifyTarget(targetNftId); + lastUpdatedIn = targetInfo.lastUpdateIn; + targetInfo.lastUpdateIn = BlocknumberLib.current(); + } + + + function _createTarget( + NftId targetNftId, + ObjectType objectType, + Seconds lockingPeriod, + UFixed rewardRate, + bool checkParameters + ) + private + { + // checks + if (checkParameters) { + TargetManagerLib.checkTargetParameters( + _registry, + _reader, + targetNftId, + objectType, + lockingPeriod, + rewardRate); + } + + // effects + IStaking.TargetInfo storage targetInfo = _targetInfo[targetNftId]; + targetInfo.stakedAmount = AmountLib.zero(); + targetInfo.rewardAmount = AmountLib.zero(); + targetInfo.reserveAmount = AmountLib.zero(); + targetInfo.maxStakedAmount = AmountLib.max(); + + targetInfo.objectType = objectType; + targetInfo.lockingPeriod = lockingPeriod; + targetInfo.rewardRate = rewardRate; + targetInfo.chainId = ChainIdLib.fromNftId(targetNftId); + targetInfo.lastUpdateIn = BlocknumberLib.current(); + + // add new target to target set + _targetNftIdSet.add(targetNftId); + } + + + function _spendRewardReserves( + NftId targetNftId, + IStaking.TargetInfo storage targetInfo, + Amount dipAmount + ) + private + { + Blocknumber lastUpdateIn = _decreaseReserves(targetNftId, targetInfo, dipAmount); + + // logging + emit IStaking.LogStakingRewardReservesSpent( + targetNftId, + dipAmount, + targetInfo.reserveAmount, + lastUpdateIn); + } + + + function _decreaseReserves( + NftId targetNftId, + IStaking.TargetInfo storage targetInfo, + Amount dipAmount + ) + private + returns ( Blocknumber lastUpdateIn) + { + lastUpdateIn = targetInfo.lastUpdateIn; + + // check if reserves are sufficient + if (dipAmount > targetInfo.reserveAmount) { + revert ErrorStakingStoreRewardReservesInsufficient( + targetNftId, + targetInfo.reserveAmount, + dipAmount); + } + + // effects + targetInfo.reserveAmount = targetInfo.reserveAmount - dipAmount; + targetInfo.lastUpdateIn = BlocknumberLib.current(); + } + + + function _updateRewards( + NftId stakeNftId, + IStaking.StakeInfo storage stakeInfo, + IStaking.TargetInfo storage targetInfo + ) + internal + returns (Amount rewardIncreaseAmount) + { + // return if reward rate is zero + if (targetInfo.rewardRate.eqz()) { + return rewardIncreaseAmount; + } + + // get seconds since last update on stake + Seconds duration = SecondsLib.toSeconds( + block.timestamp - stakeInfo.lastUpdateAt.toInt()); + + // return if duration is zero + if (duration.eqz()) { + return AmountLib.zero(); + } + + // calculate reward increase since + rewardIncreaseAmount = StakingLib.calculateRewardAmount( + targetInfo.rewardRate, + duration, + stakeInfo.stakedAmount); + + // update target + stake + targetInfo.rewardAmount = targetInfo.rewardAmount + rewardIncreaseAmount; + stakeInfo.rewardAmount = stakeInfo.rewardAmount + rewardIncreaseAmount; + _setLastUpdatesToCurrent(stakeInfo, targetInfo); + + // logging + emit IStaking.LogStakingStakeRewardsUpdated( + stakeNftId, + rewardIncreaseAmount, + stakeInfo.stakedAmount, + stakeInfo.rewardAmount, + stakeInfo.lockedUntil); + } + + + function _restakeRewards( + NftId stakeNftId, + IStaking.StakeInfo storage stakeInfo, + IStaking.TargetInfo storage targetInfo + ) + internal + returns (Amount restakeAmount) + { + restakeAmount = stakeInfo.rewardAmount; + + // return if reward amount is zero + if (restakeAmount.eqz()) { + return restakeAmount; + } + + // check restaking amount does not exceed target max staked amount + _checkMaxStakedAmount(stakeInfo.targetNftId, targetInfo, restakeAmount); + + // use up reserves for newly staked dips + _spendRewardReserves(stakeInfo.targetNftId, targetInfo, restakeAmount); + + // update target + stake + targetInfo.stakedAmount = targetInfo.stakedAmount + restakeAmount; + targetInfo.rewardAmount = targetInfo.rewardAmount - restakeAmount; + stakeInfo.stakedAmount = stakeInfo.stakedAmount + restakeAmount; + stakeInfo.rewardAmount = AmountLib.zero(); + _setLastUpdatesToCurrent(stakeInfo, targetInfo); + + // logging + emit IStaking.LogStakingRewardsRestaked( + stakeNftId, + restakeAmount, + stakeInfo.stakedAmount, + AmountLib.zero(), + stakeInfo.lockedUntil); + } + + + function _stake( + NftId stakeNftId, + IStaking.StakeInfo storage stakeInfo, + IStaking.TargetInfo storage targetInfo, + Seconds maxAdditionalLockingPeriod, + Amount stakeAmount + ) + internal + { + // return if reward amount is zero + if (stakeAmount.eqz()) { + return; + } + + // check restaking amount does not exceed target max staked amount + _checkMaxStakedAmount(stakeInfo.targetNftId, targetInfo, stakeAmount); + + // update target + stake + targetInfo.stakedAmount = targetInfo.stakedAmount + stakeAmount; + stakeInfo.stakedAmount = stakeInfo.stakedAmount + stakeAmount; + + // increase locked until if applicable + Seconds additionalLockingPeriod = SecondsLib.min(maxAdditionalLockingPeriod, targetInfo.lockingPeriod); + if (stakeAmount.gtz() && additionalLockingPeriod.gtz()) { + stakeInfo.lockedUntil = stakeInfo.lockedUntil.addSeconds(additionalLockingPeriod); + } + + _setLastUpdatesToCurrent(stakeInfo, targetInfo); + + // logging + emit IStaking.LogStakingStaked( + stakeNftId, + stakeAmount, + stakeInfo.stakedAmount, + stakeInfo.rewardAmount, + stakeInfo.lockedUntil); + } + + + function _claimRewards( + NftId stakeNftId, + IStaking.StakeInfo storage stakeInfo, + IStaking.TargetInfo storage targetInfo, + Amount maxClaimAmount + ) + internal + returns (Amount claimAmount) + { + claimAmount = AmountLib.min(maxClaimAmount, stakeInfo.rewardAmount); + + // return if no rewards to claim + if (claimAmount.eqz()) { + return claimAmount; + } + + // effects + // use up reserves for claimed rewards + _spendRewardReserves(stakeInfo.targetNftId, targetInfo, claimAmount); + + // update target + stake + targetInfo.rewardAmount = targetInfo.rewardAmount - claimAmount; + stakeInfo.rewardAmount = stakeInfo.rewardAmount - claimAmount; + _setLastUpdatesToCurrent(stakeInfo, targetInfo); + + // logging + emit IStaking.LogStakingRewardsClaimed( + stakeNftId, + claimAmount, + stakeInfo.stakedAmount, + stakeInfo.rewardAmount, + stakeInfo.lockedUntil); + } + + + function _unstake( + NftId stakeNftId, + IStaking.StakeInfo storage stakeInfo, + IStaking.TargetInfo storage targetInfo, + Amount maxUnstakeAmount + ) + internal + returns (Amount unstakedAmount) + { + unstakedAmount = AmountLib.min(maxUnstakeAmount, stakeInfo.stakedAmount); + + // return if no stakes to claim + if (unstakedAmount.eqz()) { + return unstakedAmount; + } + + // check if stake is still locked + if (isStakeLocked(stakeNftId)) { + revert IStaking.ErrorStakingStakeLocked(stakeNftId, stakeInfo.lockedUntil); + } + + // update target + stake + targetInfo.stakedAmount = targetInfo.stakedAmount - unstakedAmount; + stakeInfo.stakedAmount = stakeInfo.stakedAmount - unstakedAmount; + _setLastUpdatesToCurrent(stakeInfo, targetInfo); + + // logging + emit IStaking.LogStakingUnstaked( + stakeNftId, + unstakedAmount, + stakeInfo.stakedAmount, + stakeInfo.rewardAmount, + stakeInfo.lockedUntil); + } + + + function _setLastUpdatesToCurrent( + IStaking.StakeInfo storage stakeInfo, + IStaking.TargetInfo storage targetInfo + ) + internal + { + targetInfo.lastUpdateIn = BlocknumberLib.current(); + _setStakeLastUpdatesToCurrent(stakeInfo); + } + + + function _setStakeLastUpdatesToCurrent( + IStaking.StakeInfo storage stakeInfo + ) + internal + { + stakeInfo.lastUpdateIn = BlocknumberLib.current(); + stakeInfo.lastUpdateAt = TimestampLib.current(); + } //--- private stake and target functions --------------------------------// @@ -817,7 +896,7 @@ contract StakingStore is private { if (targetInfo.stakedAmount + additionalstakedAmount > targetInfo.maxStakedAmount) { - revert ErrorStakingStoreStakesExceedingTargetMaxAmount( + revert IStaking.ErrorStakingTargetMaxStakedAmountExceeded( targetNftId, targetInfo.maxStakedAmount, targetInfo.stakedAmount + additionalstakedAmount); diff --git a/contracts/type/ChainId.sol b/contracts/type/ChainId.sol index 3c9178bc8..8eaf23426 100644 --- a/contracts/type/ChainId.sol +++ b/contracts/type/ChainId.sol @@ -70,12 +70,14 @@ library ChainIdLib { } - function fromNftId(NftId nftId) public pure returns (ChainId) { - uint256 nftIdInt = nftId.toInt(); - uint256 chainIdDigits = nftIdInt % 100; // Extract the last two digits - uint256 chainIdInt = nftIdInt % 10**(chainIdDigits + 2) / 100; // Extract the chainId + /// @dev returns true iff NFT ID is from the current chain. + function isCurrentChain(NftId nftId) public view returns (bool) { + return _fromNftId(nftId) == block.chainid; + } + - return toChainId(chainIdInt); + function fromNftId(NftId nftId) public pure returns (ChainId) { + return toChainId(_fromNftId(nftId)); } @@ -85,6 +87,13 @@ library ChainIdLib { } + function _fromNftId(NftId nftId) internal pure returns (uint256 chainIdInt) { + uint256 nftIdInt = nftId.toInt(); + uint256 chainIdDigits = nftIdInt % 100; // Extract the last two digits + chainIdInt = nftIdInt % 10**(chainIdDigits + 2) / 100; // Extract the chainId + } + + function _max() internal pure returns (uint96) { // IMPORTANT: type nees to match with actual definition for Amount return type(uint96).max; diff --git a/contracts/type/Seconds.sol b/contracts/type/Seconds.sol index 53f35b6cd..6287ff752 100644 --- a/contracts/type/Seconds.sol +++ b/contracts/type/Seconds.sol @@ -90,6 +90,15 @@ library SecondsLib { return Seconds.unwrap(duration1) < Seconds.unwrap(duration2); } + /// @dev returns the smaller of the duration + function min(Seconds duration1, Seconds duration2) public pure returns (Seconds) { + if (Seconds.unwrap(duration1) < Seconds.unwrap(duration2)) { + return duration1; + } + + return duration2; + } + /// @dev return add duration1 and duration2 function add(Seconds duration1, Seconds duration2) public pure returns (Seconds) { return Seconds.wrap(Seconds.unwrap(duration1) + Seconds.unwrap(duration2)); diff --git a/test/staking/Staking.t.sol b/test/staking/Staking.t.sol index 98b3760d3..9c5afb150 100644 --- a/test/staking/Staking.t.sol +++ b/test/staking/Staking.t.sol @@ -290,6 +290,8 @@ contract StakingTest is GifTest { function test_stakingStakeIncreaseByZeroAfterOneYear() public { + // GIVEN + (, Amount reserveAmount) = _addRewardReserves(instanceNftId, instanceOwner, 500); ( , @@ -346,6 +348,8 @@ contract StakingTest is GifTest { function test_stakingStakeIncreaseByHundredAfterOneYear() public { + // GIVEN + (, Amount reserveAmount) = _addRewardReserves(instanceNftId, instanceOwner, 500); ( , @@ -427,7 +431,7 @@ contract StakingTest is GifTest { // THEN vm.expectRevert( abi.encodeWithSelector( - StakingStore.ErrorStakingStoreStakesExceedingTargetMaxAmount.selector, + IStaking.ErrorStakingTargetMaxStakedAmountExceeded.selector, instanceNftId, dipAmount, dipAmount + stakeIncreaseAmount)); @@ -530,6 +534,10 @@ contract StakingTest is GifTest { function test_stakingStakeClaimRewardsHappyCase() public { // GIVEN + + (, Amount reserveAmount) = _addRewardReserves(instanceNftId, instanceOwner, 500); + assertEq(stakingReader.getReserveBalance(instanceNftId).toInt(), reserveAmount.toInt(), "unexpected reserve balance (initial)"); + ( , Amount stakeAmount, @@ -538,9 +546,6 @@ contract StakingTest is GifTest { uint256 lastUpdateAt = block.timestamp; - (, Amount reserveAmount) = _addRewardReserves(instanceNftId, instanceOwner, 500); - assertEq(stakingReader.getReserveBalance(instanceNftId).toInt(), reserveAmount.toInt(), "unexpected reserve balance (initial)"); - // dip balance of staker after staking assertEq(dip.balanceOf(staker), 0, "unexpected staker balance after staking"); diff --git a/test/staking/StakingProtocolTarget.t.sol b/test/staking/StakingProtocolTarget.t.sol index 9680e3867..920e3ecfe 100644 --- a/test/staking/StakingProtocolTarget.t.sol +++ b/test/staking/StakingProtocolTarget.t.sol @@ -190,7 +190,7 @@ contract StakingProtocolTargetTest is GifTest { // THEN vm.prank(outsider); - (Amount unstakedAmount, Amount rewardsClaimedAmount) = staking.unstake(stakeNftId); + Amount unstakedAmount = staking.unstake(stakeNftId); IStaking.StakeInfo memory stakeInfo = stakingReader.getStakeInfo(stakeNftId); assertEq(stakeInfo.stakedAmount.toInt(), 0, "unexpected staked amount (after unstake)"); @@ -198,7 +198,6 @@ contract StakingProtocolTargetTest is GifTest { Amount expectedRewardAmount = dipAmount.multiplyWith(protocolRewardRate); assertEq(unstakedAmount.toInt(), (dipAmount + expectedRewardAmount).toInt(), "unexpected unstaked amount"); - assertEq(rewardsClaimedAmount.toInt(), 0, "unexpected rewards claimed amount"); assertEq(dip.balanceOf(stakingWallet), (initialProtocolRewardAmount - expectedRewardAmount).toInt(), "unexpected balance for staking wallet (after unstake)"); assertEq(dip.balanceOf(outsider), (dipAmount + expectedRewardAmount).toInt(), "unexpected balance for staker (after unstake)"); }