Paladin - Warden Pledges contest - Ruhum's results

A governance lending protocol transforming users voting power into a new money lego.

General Information

Platform: Code4rena

Start Date: 27/10/2022

Pot Size: $33,500 USDC

Total HM: 8

Participants: 96

Period: 3 days

Judge: kirk-baird

Total Solo HM: 1

Id: 176

League: ETH

Paladin

Findings Distribution

Researcher Performance

Rank: 15/96

Findings: 3

Award: $345.48

QA:
grade-b
Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Jeiwan

Also found by: 0xDjango, Aymen0909, Chom, Lambda, Ruhum, Trust

Labels

bug
2 (Med Risk)
satisfactory
sponsor disputed
duplicate-163

Awards

314.3177 USDC - $314.32

External Links

Lines of code

https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L387 https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L432 https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L327 https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L245

Vulnerability details

Impact

When a pledge owner increases the reward per vote or the duration of the pledge, the contract locks up more of their reward tokens than necessary.

Proof of Concept

The total amount of reward tokens issued to users is specified in createPledge() as:

vars.totalRewardAmount = (rewardPerVote * vars.votesDifference * vars.duration) / UNIT;

When a pledge is updated, the total reward amount of recalculated:

// extendPledge()
uint256 totalRewardAmount = (pledgeParams.rewardPerVote * pledgeParams.votesDifference * addedDuration) / UNIT;

// increasePledgeRewardPerVote()
uint256 totalRewardAmount = (rewardPerVoteDiff * pledgeParams.votesDifference * remainingDuration) / UNIT;

A pledge can be updated at any time between its creation and its endTimestamp as long as it's not closed. When recalculating the new totalRewardAmount, the contract doesn't check whether anybody has already pledged. The votesDifference value can be different between the creation and update of a pledge. And the contract only allows a pledge to gather as many votes as specified in their targetVotes property.

Consider the following situation:

Pledge creation: - initialVotes = 0 - targetVotes = 100e18 - votesDifference = 100e18 - rewardPerVote = 2 - duration = 2 weeks (1209600)

$totalRewardAmount = 2 * 100e18 * 1209600 / 1e18$ $totalRewardAmount = 241920000$

1 week passed and50e18 votes were pledged. The creator decides to update the rewardPerVote parameter to 3 to get the remaining half of them. The contract would determine the totalRewardAmount the creator has to supply as:

uint256 totalRewardAmount = (rewardPerVoteDiff * pledgeParams.votesDifference * remainingDuration) / UNIT;

$totalRewardAmount = 3 * 100e18 * 604800 / 1e18$ $totalRewardAmount = 181440000$

So the contract locks up another 181440000 tokens. But, because 50e18 votes were already pledged, only 50e18 more votes remain. Thus, the actual amount of tokens that are distributed as a reward are:

$totalRewardAmount = 3 * 50e18 * 604800 / 1e18$ $totalRewardAmount = 90720000$

The contract locks up more funds than necessary. Those tokens are not lost. After the pledge ends, the creator can reclaim them using retrievePledgeRewards(). It's only a temporary freezing of funds.

Tools Used

none

When a pledge is updated, recalculate the votesDifference value and determine the totalRewardAmount using that new value.

#0 - Kogaroshi

2022-10-30T23:09:11Z

This is the desired behavior, as delegated boost could end before the endTimestamp of the Pledge, requiring new delegation, but the contract would not have enough reward tokens to reward the delegation. This is why the rewards amounts are always ignoring the delegation, as it could bring issue where the amount of available rewards amount would be insufficient.

#1 - kirk-baird

2022-11-10T21:53:41Z

Nicely written up and explained however as the sponsor mentions it may be required to use the full amount of tokens transferred if existing pledges end before the desired end timestamp. I'll use Nullify so the warden is not penalised for this submission.

#2 - c4-judge

2022-11-10T21:53:45Z

kirk-baird marked the issue as nullified

#3 - kirk-baird

2022-11-10T23:03:00Z

Since cached voteDifference needs to be updated for each call to extendPledge() and increasePledgeRewards() I'm going to mark this as a duplicate of #163 .

#4 - c4-judge

2022-11-10T23:03:05Z

kirk-baird marked the issue as not nullified

#5 - c4-judge

2022-11-10T23:03:10Z

kirk-baird marked the issue as satisfactory

#6 - c4-judge

2022-11-10T23:03:16Z

kirk-baird marked the issue as duplicate of #163

Lines of code

https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L333 https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L394 https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L438 https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L271

Vulnerability details

Impact

Some tokens charge a fee on transfers. The contract can't handle those tokens. It will think that it has more tokens than it actually does. That in turn will result in a pledge not being fulfillable. The most prominent token that might take fees in the future is USDT.

Proof of Concept

When the pledge creator provides the reward tokens, the contract doesn't verify that it received the correct amount

        IERC20(pledgeParams.rewardToken).safeTransferFrom(creator, address(this), totalRewardAmount);

totalRewardAmount is the total amount of tokens that will be distributed to users who delegate their votes. If the contract doesn't actually have a balance of totalRewardAmount, the very last call to pledge() will fail because of the following transfer at the end of _pledge():

        IERC20(pledgeParams.rewardToken).safeTransfer(user, rewardAmount);

Because the contract doesn't have rewardAmount tokens left, the tx will fail. Because the last call to pledge fails, it will never be able to be fulfilled. Nobody will be able to delegate the last votes.

Tools Used

none

Use the actual amount of tokens the contract received instead of the user provided value.

#0 - Kogaroshi

2022-10-30T23:38:43Z

Duplicate of #72

#1 - c4-judge

2022-11-10T07:25:05Z

kirk-baird marked the issue as not a duplicate

#2 - c4-judge

2022-11-10T07:25:14Z

kirk-baird marked the issue as duplicate

#3 - c4-judge

2022-11-10T07:31:46Z

kirk-baird marked the issue as not a duplicate

#4 - c4-judge

2022-11-10T07:32:04Z

kirk-baird changed the severity to QA (Quality Assurance)

#5 - c4-judge

2022-11-12T00:13:37Z

kirk-baird marked the issue as grade-b

Gas Report

G-01: _pledge() shouldn't verify user's balance

In the _pledge() function you check whether the user has enough boost delegation & set the correct allowance. The same checks are already done in BoostV2.boost(). You're wasting gas by repeating the same checks in your contract. If the user doesn't fulfill the necessary requirements, the call will simply fail.

G-02: trigger underflow instead of explicitly checking for it

By adding an if-clause to prevent underflows you're increasing the gas costs of the happy path. Just let the value underflow and trigger a revert that way. Most wallets prevent txs that revert from being executed. Thus, you're wasting gas for no reason.

G-03: retrievePledgeRewards() and closePledge() can be merged to reduce deployment gas

Both functions implement the same thing. One is callable before the pledge ended and the other one after. The effect of both is the same (retrieval of funds). You can merge them into a single one to reduce deployment gas.

#0 - c4-judge

2022-11-11T23:58:17Z

kirk-baird marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter