Platform: Code4rena
Start Date: 20/01/2022
Pot Size: $80,000 USDC
Total HM: 5
Participants: 37
Period: 7 days
Judge: Jack the Pug
Total Solo HM: 1
Id: 76
League: ETH
Rank: 4/37
Findings: 2
Award: $7,330.80
π Selected for report: 1
π Solo Findings: 0
π Selected for report: kirk-baird
Also found by: static
7304.7588 USDC - $7,304.76
kirk-baird
This is a reentrancy vulnerability that would allow the attacker to drain the entire SHER balance of the contract.
Note: this attack requires gaining control of execution sher.transfer()
which will depend on the implementation of the SHER token. Control may be gained by the attacker if the contract implements ERC777 or otherwise makes external calls during transfer()
.
See _sendSherRewards
function _sendSherRewardsToOwner(uint256 _id, address _nftOwner) internal { uint256 sherReward = sherRewards_[_id]; if (sherReward == 0) return; // Transfers the SHER tokens associated with this NFT ID to the address of the NFT owner sher.safeTransfer(_nftOwner, sherReward); // Deletes the SHER reward mapping for this NFT ID delete sherRewards_[_id]; }
Here sherRewards
are deleted after the potential external call is made in sher.safeTransfer()
. As a result if an attacker reenters this function sherRewards_
they will still maintain the original balance of rewards and again transfer the SHER tokens.
As _sendSherRewardsToOwner()
is internal
the attack can be initiated through the external
function ownerRestake()
see here.
Steps to produce the attack:
initialStake()
from the attack contract with the smallest period
period
amount of time to passownerRestake()
. The attack contract will gain control of the (See note above about control flow). This will recursively call ownerRestake()
until the balance of Sherlock
is 0 or less than the user's reward amount. Then allow reentrancy loop to unwind and complete.n/a
Reentrancy can be mitigated by one of two solutions.
The first option is to add a reentrancy guard like nonReentrant
the is used in SherlockClaimManager.sol
.
The second option is to use the checks-effects-interactions pattern. This would involve doing all validation checks and state changes before making any potential external calls. For example the above function could be modified as follows.
function _sendSherRewardsToOwner(uint256 _id, address _nftOwner) internal { uint256 sherReward = sherRewards_[_id]; if (sherReward == 0) return; // Deletes the SHER reward mapping for this NFT ID delete sherRewards_[_id]; // Transfers the SHER tokens associated with this NFT ID to the address of the NFT owner sher.safeTransfer(_nftOwner, sherReward); }
Additionally the following functions are not exploitable however should be updated to use the check-effects-interations pattern.
Sherlock._redeemShares()
should do _transferTokensOut()
last.Sherlock.initialStake()
should do token.safeTransferFrom(msg.sender, address(this), _amount);
lastSherClaim.add()
should do sher.safeTransferFrom(msg.sender, address(this), _amount);
after updating userClaims
SherlockProtocolManager.depositToActiveBalance()
should do token.safeTransferFrom(msg.sender, address(this), _amount);
after updating activeBalances
#0 - Evert0x
2022-02-03T21:31:23Z
Good find. I think it's med-risk as it depends on the implementation of SHER token (does it allow callbacks)
#1 - jack-the-pug
2022-03-10T15:44:20Z
Downgrade to Med
as the SHER token is a known token that currently comes with no such hooks like ERC777.
π Selected for report: RamiRond
Also found by: Dravee, Ruhum, kirk-baird, p4st13r4
26.0437 USDC - $26.04
kirk-baird
These updates will save approximately 100 gas per SLOAD that is saves.
The following variables are loaded multiple times from storage in one function.
Sherlock.sol
:
sherDistributionManager
in updateSherDistributionManager(), removeSherDistributionManager() and _stake()
sherlockProtocolManager
in updateSherlockProtocolManager()
sherlockClaimManager
in updateSherlockClaimManager()
yieldStrategy
in updateYeidlStrategy() and yieldStrategyDeposit()
yieldStrategy
, sherDistributionManager
, sherlockProtocolManager
and sherlockClaimManager
in pause() and unpause()
SherlockClaimManager.sol
:
claimCallbacks.length
in [addCallbacks()[https://github.com/code-423n4/2022-01-sherlock/blob/main/contracts/managers/SherlockClaimManager.sol#L225] and once for each iteration of the loop in payoutClaims()n/a
Consider caching each variable listed above in memory to save gas.
#0 - jack-the-pug
2022-03-26T07:08:53Z
Dup #77