You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Jan 12, 2025. It is now read-only.
vote Function Broken Due to Incorrect Ownership Check in BribeRewarder Contract
Summary
The _modify function in the BribeRewarder contract has a logical error. This flaw causes an ownership check error during the voting process, preventing users with voting rights from completing their votes. Consequently, this renders the protocol's core functionality broken and may lead to economic losses.
Vulnerability Detail
Users can stake stakedToken in the MlumStaking contract by calling the createPosition function, creating a position with a specified lockDuration. This allows them to receive staking rewards, which are periodically distributed as rewardToken by the MlumStaking contract, and grants them voting rights for various pools, with the voting logic handled by the vote function in the voter contract.
Additionally, the protocol has a bribery mechanism where stakeholders can create a BribeRewarder for a specific pool through the RewarderFactory contract, setting the bribeToken to incentivize users with voting rights to vote for a particular pool in exchange for bribery tokens.
However, a logical error in the _modify function of the BribeRewarder contract makes the vote function perpetually unavailable (revert), meaning users with a position (i.e., voting rights) cannot call the vote function to complete their vote, rendering the protocol's core functionality inoperative and potentially causing economic losses.
Specifically, when a user with a staked position (i.e., lsNFT) calls the vote function to vote for a particular pool and receive bribery tokens for those pools, the transaction reverts. The vote function is as follows:
function vote(uint256tokenId, address[] calldatapools, uint256[] calldatadeltaAmounts) external {
if (pools.length!= deltaAmounts.length) revertIVoter__InvalidLength();
// check voting startedif (!_votingStarted()) revertIVoter_VotingPeriodNotStarted();
if (_votingEnded()) revertIVoter_VotingPeriodEnded();
// check ownership of tokenIdif (_mlumStaking.ownerOf(tokenId) !=msg.sender) {
revertIVoter__NotOwner();
}
uint256 currentPeriodId = _currentVotingPeriodId;
// check if already votedif (_hasVotedInPeriod[currentPeriodId][tokenId]) {
revertIVoter__AlreadyVoted();
}
// check if _minimumLockTime >= initialLockDuration and it is lockedif (_mlumStaking.getStakingPosition(tokenId).initialLockDuration < _minimumLockTime) {
revertIVoter__InsufficientLockTime();
}
if (_mlumStaking.getStakingPosition(tokenId).lockDuration < _periodDuration) {
revertIVoter__InsufficientLockTime();
}
uint256 votingPower = _mlumStaking.getStakingPosition(tokenId).amountWithMultiplier;
// check if deltaAmounts > votingPoweruint256 totalUserVotes;
for (uint256 i =0; i < pools.length; ++i) {
totalUserVotes += deltaAmounts[i];
}
if (totalUserVotes > votingPower) {
revertIVoter__InsufficientVotingPower();
}
IVoterPoolValidator validator = _poolValidator;
for (uint256 i =0; i < pools.length; ++i) {
address pool = pools[i];
if (address(validator) !=address(0) &&!validator.isValid(pool)) {
revertVoter__PoolNotVotable();
}
uint256 deltaAmount = deltaAmounts[i];
_userVotes[tokenId][pool] += deltaAmount;
_poolVotesPerPeriod[currentPeriodId][pool] += deltaAmount;
if (_votes.contains(pool)) {
_votes.set(pool, _votes.get(pool) + deltaAmount);
} else {
_votes.set(pool, deltaAmount);
}
_notifyBribes(_currentVotingPeriodId, pool, tokenId, deltaAmount); // msg.sender, deltaAmount);
}
_totalVotes += totalUserVotes;
_hasVotedInPeriod[currentPeriodId][tokenId] =true;
emitVoted(tokenId, currentPeriodId, pools, deltaAmounts);
}
The vote function then calls the _notifyBribes function to notify the rewarder which set for the target pool of the user's vote, recording the user's vote for subsequent bribery token distribution:
function _notifyBribes(uint256periodId, addresspool, uint256tokenId, uint256deltaAmount) private {
IBribeRewarder[] storage rewarders = _bribesPerPeriod[periodId][pool];
for (uint256 i =0; i < rewarders.length; ++i) {
if (address(rewarders[i]) !=address(0)) {
rewarders[i].deposit(periodId, tokenId, deltaAmount);
_userBribesPerPeriod[periodId][tokenId].push(rewarders[i]);
}
}
}
The deposit function in the BribeRewarder contract is then called to record the votes:
function deposit(uint256periodId, uint256tokenId, uint256deltaAmount) public onlyVoter {
_modify(periodId, tokenId, deltaAmount.toInt256(), false);
emitDeposited(periodId, tokenId, _pool(), deltaAmount);
}
function _modify(uint256periodId, uint256tokenId, int256deltaAmount, boolisPayOutReward)
privatereturns (uint256rewardAmount)
{
if (!IVoter(_caller).ownerOf(tokenId, msg.sender)) {
revertBribeRewarder__NotOwner();
}
// extra check so we dont calc rewards before starttime
(uint256startTime,) =IVoter(_caller).getPeriodStartEndtime(periodId);
if (block.timestamp<= startTime) {
_lastUpdateTimestamp = startTime;
}
RewardPerPeriod storage reward = _rewards[_indexByPeriodId(periodId)];
Amounts.Parameter storage amounts = reward.userVotes;
Rewarder2.Parameter storage rewarder = reward.rewarder;
(uint256oldBalance, uint256newBalance, uint256oldTotalSupply,) = amounts.update(tokenId, deltaAmount);
uint256 totalRewards =_calculateRewards(periodId);
rewardAmount = rewarder.update(bytes32(tokenId), oldBalance, newBalance, oldTotalSupply, totalRewards);
if (block.timestamp> _lastUpdateTimestamp) {
_lastUpdateTimestamp =block.timestamp;
}
if (isPayOutReward) {
rewardAmount = rewardAmount + unclaimedRewards[periodId][tokenId];
unclaimedRewards[periodId][tokenId] =0;
if (rewardAmount >0) {
IERC20 token =_token();
_safeTransferTo(token, msg.sender, rewardAmount);
}
} else {
unclaimedRewards[periodId][tokenId] += rewardAmount;
}
}
Here, note that the _modify function first checks the ownership of the position:
However, there is a logical error in this check, leading to a series of calls that result in a revert.
Let's review the call chain: A user (Bob) with a staked position calls the vote function of the Voter contract, which in turn calls the deposit function of the BribeRewarder contract, and then checks the ownership in the Voter contract's ownerOf function, as follows:
Bob -> Voter.vote(_notifyBribes) -> BribeRewarder.deposit(_modify) -> Voter.ownerOf
Therefore, when _modify calls Voter.ownerOf, the msg.sender parameter passed is actually the address of the Voter contract, not the user Bob's address. Consequently, the ownership check fails, causing the entire vote function to revert.
A full proof of concept (POC) demonstrating this issue is as follows:
In the setUp function, the project developer (DEV) deploys relevant contracts, including the staking contract MlumStaking, the staking token _stakingToken, the reward token _rewardToken, and the bribery token _bribeToken. It also deploys the MasterChefMock contract, the RewarderFactory contract, and the Voter contract, setting the minimum lock time for voting to two weeks and configuring the BribeRewarder in the factory.
Next, Alice, a stakeholder, wants users to vote for a specific pool. She calls the factory's createBribeRewarder function to create a BribeRewarder with _bribeToken as the bribery token and then calls the fundAndBribe function to bribe for periods 1 and 2 with 10e18 _bribeToken for each period:
Thus, Alice transfers 20e18 _bribeToken to the created BribeRewarder contract for subsequent periods 1 and 2.
The project developer calls the startNewVotingPeriod function, starting a new period (period 1).
Another user, Bob, stakes 2 ether of _stakingToken in the MlumStaking contract for two weeks, meeting the minimum lock time required for voting.
Bob attempts to vote for the pool to receive bribery tokens. However, the vote function call reverts, preventing Bob from exercising his voting rights.
This shows that the voter::ownerOf(1, voter: [0x8e54203dae61b811f3f7059d9bD1e413f65CE677]) check fails because the address passed for checking ownership is the voter contract's address, not Bob's address, leading to the BribeRewarder__NotOwner error.
Clearly, this logical flaw renders the protocol's core voting and bribery mechanisms inoperative, causing economic losses. In this POC scenario, Alice's 20e18 _bribeToken transferred to the BribeRewarder contract for bribery cannot be distributed since no user can successfully vote, resulting in permanent economic loss.
To summarize, the ownership check in the _modify function needs to properly handle the msg.sender to accurately reflect the actual user initiating the vote call. This correction is crucial for enabling the intended functionality of voting and bribery in the protocol.
Impact
The logical error in the _modify function of the BribeRewarder contract disables the protocol's voting mechanism. Users cannot vote, causing the vote function to revert, which prevents the distribution of bribery tokens. This flaw disrupts the protocol's core functionality and leads to economic losses.
In fact, it can be observed that the vote function already includes an ownership check for the position.
function vote(uint256tokenId, address[] calldatapools, uint256[] calldatadeltaAmounts) external {
if (pools.length!= deltaAmounts.length) revertIVoter__InvalidLength();
// check voting startedif (!_votingStarted()) revertIVoter_VotingPeriodNotStarted();
if (_votingEnded()) revertIVoter_VotingPeriodEnded();
// check ownership of tokenIdif (_mlumStaking.ownerOf(tokenId) !=msg.sender) {
revertIVoter__NotOwner();
}
// skip
Therefore, the check in the _modify function is actually intended for verifying ownership when users directly call the claim function of the BribeRewarder contract to withdraw bribery tokens.
function claim(uint256tokenId) externaloverride {
uint256 endPeriod =IVoter(_caller).getLatestFinishedPeriod();
uint256 totalAmount;
// calculate emission per period because every period can have different durationsfor (uint256 i = _startVotingPeriod; i <= endPeriod; ++i) {
totalAmount +=_modify(i, tokenId, 0, true);
}
emitClaimed(tokenId, _pool(), totalAmount);
}
Clearly, the claim function requires an ownership check for the position, and this check is implemented in the subsequent _modify function call.
Thus, a simple and feasible solution is to move the ownership check logic from the _modify function to the claim function, as shown below.
function claim(uint256 tokenId) external override {
uint256 endPeriod = IVoter(_caller).getLatestFinishedPeriod();
uint256 totalAmount;
// calculate emission per period because every period can have different durations
for (uint256 i = _startVotingPeriod; i <= endPeriod; ++i) {
+ if (!IVoter(_caller).ownerOf(tokenId, msg.sender)) {+ revert BribeRewarder__NotOwner();+ }
totalAmount += _modify(i, tokenId, 0, true);
}
emit Claimed(tokenId, _pool(), totalAmount);
}
function _modify(uint256 periodId, uint256 tokenId, int256 deltaAmount, bool isPayOutReward)
private
returns (uint256 rewardAmount)
{
- if (!IVoter(_caller).ownerOf(tokenId, msg.sender)) {- revert BribeRewarder__NotOwner();- }
// skip
}
sherlock-admin4
changed the title
Young Iron Beaver - vote Function Broken Due to Incorrect Ownership Check in BribeRewarder Contract
0uts1der - vote Function Broken Due to Incorrect Ownership Check in BribeRewarder Contract
Jul 29, 2024
0uts1der
High
vote
Function Broken Due to Incorrect Ownership Check in BribeRewarder ContractSummary
The
_modify
function in theBribeRewarder
contract has a logical error. This flaw causes an ownership check error during the voting process, preventing users with voting rights from completing their votes. Consequently, this renders the protocol's core functionality broken and may lead to economic losses.Vulnerability Detail
Users can stake
stakedToken
in theMlumStaking
contract by calling thecreatePosition
function, creating a position with a specifiedlockDuration
. This allows them to receive staking rewards, which are periodically distributed asrewardToken
by theMlumStaking
contract, and grants them voting rights for various pools, with the voting logic handled by thevote
function in thevoter
contract.Additionally, the protocol has a bribery mechanism where stakeholders can create a
BribeRewarder
for a specific pool through theRewarderFactory
contract, setting thebribeToken
to incentivize users with voting rights to vote for a particular pool in exchange for bribery tokens.However, a logical error in the
_modify
function of theBribeRewarder
contract makes thevote
function perpetually unavailable (revert), meaning users with a position (i.e., voting rights) cannot call thevote
function to complete their vote, rendering the protocol's core functionality inoperative and potentially causing economic losses.Specifically, when a user with a staked position (i.e., lsNFT) calls the
vote
function to vote for a particular pool and receive bribery tokens for those pools, the transaction reverts. Thevote
function is as follows:The
vote
function then calls the_notifyBribes
function to notify the rewarder which set for the target pool of the user's vote, recording the user's vote for subsequent bribery token distribution:The
deposit
function in theBribeRewarder
contract is then called to record the votes:Here, note that the
_modify
function first checks the ownership of the position:However, there is a logical error in this check, leading to a series of calls that result in a
revert
.Let's review the call chain: A user (Bob) with a staked position calls the
vote
function of theVoter
contract, which in turn calls thedeposit
function of theBribeRewarder
contract, and then checks the ownership in theVoter
contract'sownerOf
function, as follows:Bob -> Voter.vote(_notifyBribes) -> BribeRewarder.deposit(_modify) -> Voter.ownerOf
Therefore, when
_modify
callsVoter.ownerOf
, themsg.sender
parameter passed is actually the address of theVoter
contract, not the user Bob's address. Consequently, the ownership check fails, causing the entirevote
function to revert.A full proof of concept (POC) demonstrating this issue is as follows:
In the
setUp
function, the project developer (DEV
) deploys relevant contracts, including the staking contractMlumStaking
, the staking token_stakingToken
, the reward token_rewardToken
, and the bribery token_bribeToken
. It also deploys theMasterChefMock
contract, theRewarderFactory
contract, and theVoter
contract, setting the minimum lock time for voting to two weeks and configuring theBribeRewarder
in the factory.Next, Alice, a stakeholder, wants users to vote for a specific
pool
. She calls the factory'screateBribeRewarder
function to create aBribeRewarder
with_bribeToken
as the bribery token and then calls thefundAndBribe
function to bribe for periods 1 and 2 with 10e18_bribeToken
for each period:Thus, Alice transfers 20e18
_bribeToken
to the createdBribeRewarder
contract for subsequent periods 1 and 2.The project developer calls the
startNewVotingPeriod
function, starting a new period (period 1).Another user, Bob, stakes 2 ether of
_stakingToken
in theMlumStaking
contract for two weeks, meeting the minimum lock time required for voting.Bob attempts to vote for the pool to receive bribery tokens. However, the
vote
function call reverts, preventing Bob from exercising his voting rights.The error log shows:
This shows that the
voter::ownerOf(1, voter: [0x8e54203dae61b811f3f7059d9bD1e413f65CE677])
check fails because the address passed for checking ownership is thevoter
contract's address, not Bob's address, leading to theBribeRewarder__NotOwner
error.Clearly, this logical flaw renders the protocol's core voting and bribery mechanisms inoperative, causing economic losses. In this POC scenario, Alice's 20e18
_bribeToken
transferred to theBribeRewarder
contract for bribery cannot be distributed since no user can successfully vote, resulting in permanent economic loss.To summarize, the ownership check in the
_modify
function needs to properly handle themsg.sender
to accurately reflect the actual user initiating thevote
call. This correction is crucial for enabling the intended functionality of voting and bribery in the protocol.Impact
The logical error in the
_modify
function of theBribeRewarder
contract disables the protocol's voting mechanism. Users cannot vote, causing thevote
function to revert, which prevents the distribution of bribery tokens. This flaw disrupts the protocol's core functionality and leads to economic losses.Code Snippet
https://github.com/sherlock-audit/2024-06-magicsea/blob/42e799446595c542eff9519353d3becc50cdba63/magicsea-staking/src/rewarders/BribeRewarder.sol#L260-L266
Tool used
Manual Review
Recommendation
In fact, it can be observed that the
vote
function already includes an ownership check for the position.Therefore, the check in the
_modify
function is actually intended for verifying ownership when users directly call theclaim
function of the BribeRewarder contract to withdraw bribery tokens.Clearly, the
claim
function requires an ownership check for the position, and this check is implemented in the subsequent_modify
function call.Thus, a simple and feasible solution is to move the ownership check logic from the
_modify
function to theclaim
function, as shown below.Duplicate of #39
The text was updated successfully, but these errors were encountered: