Closed sherlock-admin2 closed 11 months ago
Hello,
Thanks a lot for your attention.
After an in-depth review, we have to consider your issue as Confirmed. This scenario is most likely never going to happen anytime. In case we would really need to update the LockingPositionManager, we would properly set the nextId() of the next token to be minted to the correct value.
However and since each StakingPositionService contract integrates the StakingPositionManager this way, we are going to implement the same pattern on the LockingPositionService.
Therefore, we have decided to disagree with the severity and put it as a Low issue.
Regards, Convergence Team
Agree with sponsor, based on step 3 this requires trusted governance to call setLockingPositionManager()
without considering an appropriate nextId()
.
Fix looks good. It is now implemented the same as StakingPositionService
lemonmon
medium
LockingPositionService.mintPosition()
problems with duplicatetokenIds
fromLockingPositionManager
may cause users to lose their fundsSummary
The current implementation and usage of the
LockingPositionManager
contract insideLockingPositionService
may cause users to lose their funds under certain conditions when they are locking a new position by callingLockingPositionService.mintPosition()
. This may happen due to duplicatetokenIds
if theLockingPositionManager
is changed in theCvgControlTower
, causing theLockingPositionService
to overwrite existing locking positions.Vulnerability Detail
When
LockingPositionService.mintPosition()
is called, thetokenId
for the new position is determined by calling_lockingPositionManager.nextId();
(line 257 LockingPositionService.sol). It's important to note that_lockingPositionManager
is not a storage variable, but instead it is dynamically fetched by callingCvgControlTower.lockingPositionManager()
. That means that whenever the state varlockingPositionManager
inside theCvgControlTower
is changed by a call toCvgControlTower.setLockingPositionManager()
, the address for theLockingPositionManager
will as well be different and_lockingPositionManager.nextId();
(line 257 LockingPositionService.sol) will also be different and may return atokenId
that is already in use by another user.Example:
LockingPositionService.mintPosition()
and locking anamount
of 10000 CVG into this new position. The tokenId of her position will be 1 (see line 45, 60 LockingPositionManager.sol)tokenId
with the value 2 from theLockingPositionManager.nextId()
, since thetokenId
is always incremented by 1 (line 60 LockingPositionManager.sol)LockingPositionManager
by callingCvgControlTower.setLockingPositionManager()
LockingPositionService.mintPosition()
withysPercentage
value ofMAX_PERCENTAGE
, and locking anamount
of 10 CVG into this new position. ThetokenId
that is fetched fromLockingPositionManager.nextId()
will have a value of 1 again which is the sametokenId
value from Alice's locking position, sinceLockingPositionManager
was changed. Alice's locking position will be overwritten (line 293 LockingPositionService.sol) with the new lockingPosition from Bob, thus Alice will lose 10000 CVG because her position and her amount (totalCvgLocked
) got overwritten by Bob's position.The issue is that even if Governance would try to help Alice by setting the old
LockingPositionManager
, Alice's 10000 CVG would still be lost, because her amount was overwritten by Bob'slockingPosition
on line 293 in LockingPositionService.sol. So Alice would end up with Bob's amount which is only 10 CVG, so Alice would have lost 9990 CVG even if Governance would have tried to help her in this example.Another issue is that the
lockingExtension
from Bob's locked position would be pushed into the existinglockExtensions[tokenId]
which is not empty since it already contains an entry from Alice's locking extension before. As a consequence, when mgCvg balance is evaluated for Bob, it will use Alices and Bobs locking extension, which means Bob's balance of mgCvg will be higher than it should be.Impact
Users may lose their funds if
LockingPositionManager
is being updated as illustrated in the example above. Also Governance's attempt to help by setting back to the oldLockingPositionManager
won't solve this issue as shown above.Code Snippet
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/Locking/LockingPositionService.sol#L231-L312
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/Locking/LockingPositionManager.sol#L39-L47
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/Locking/LockingPositionManager.sol#L59-L61
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/CvgControlTower.sol#L329-L331
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/Staking/StakeDAO/SdtStakingPositionService.sol#L247-L249
https://github.com/sherlock-audit/2023-11-convergence/blob/main/sherlock-cvg/contracts/Staking/StakeDAO/SdtStakingPositionService.sol#L276-L283
Tool used
Manual Review
Recommendation
Consider adjusting the
LockingPositionService
contract to work similar to theSdtStakingPositionService
contract:SdtStakingPositionService
is fetching thesdtStakingPositionManager
fromCvgControlTower
and assigns it to its storage variablesdtStakingPositionManager
(line 247, 249 SdtStakingPositionService.sol). Then it uses the storedsdtStakingPositionManager
inside itsdeposit
function (line 283 SdtStakingPositionService.sol). Thus avoiding similar issues as were described in this report.In the same way the
LockingPositionService
can be adjusted so that inside it'sinitialze
function theCvgControlTower.lockingPositionManager()
is assigned to a state variable which can then be used in the contract: