Platform: Code4rena
Start Date: 28/06/2023
Pot Size: $52,500 USDC
Total HM: 10
Participants: 5
Period: 9 days
Judge: hansfriese
Total Solo HM: 6
Id: 255
League: ETH
Rank: 2/5
Findings: 3
Award: $0.00
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: auditor0517
Data not available
A disputer loses their deposited dispute tokens if someone disputes the tree after them.
The Distributor.disputeTree function is used to dispute a Merkle tree. The function requires the caller to deposit disputeAmount
of disputeToken
; the caller address is stored in the disputer
state variable.
When a dispute is resolved by the governor/guardian via a call to Distributor.resolveDispute, the deposited funds are return to the disputer if the dispute is recognized as valid by the governor/guardian.
Since the Distributor.disputeTree
function is not restricted (a tree can be disputed by anyone), it's likely that when an invalid/malicious tree is submitted, there will be multiple parties willing to dispute it (e.g. the distribution creator and some of the reward claimants). However, any subsequent call to disputeTree
will override the disputer
address, and thus the previous disputer won't be able to get their deposited tokens back after the resolution of the dispute.
Manual review
Consider this change:
diff --git a/contracts/Distributor.sol b/contracts/Distributor.sol index bc4e49f..df56f5f 100644 --- a/contracts/Distributor.sol +++ b/contracts/Distributor.sol @@ -231,7 +231,8 @@ contract Distributor is UUPSHelper { /// @notice Freezes the Merkle tree update until the dispute is resolved /// @dev Requires a deposit of `disputeToken` that'll be slashed if the dispute is not accepted /// @dev It is only possible to create a dispute for `disputePeriod` after each tree update function disputeTree(string memory reason) external { + if (disputer != address(0)) revert UnresolvedDispute(); if (block.timestamp >= endOfDisputePeriod) revert InvalidDispute(); IERC20(disputeToken).safeTransferFrom(msg.sender, address(this), disputeAmount); disputer = msg.sender;
Other
#0 - c4-judge
2023-07-08T11:14:31Z
hansfriese marked the issue as duplicate of #23
#1 - c4-judge
2023-07-10T11:05:33Z
hansfriese marked the issue as satisfactory
🌟 Selected for report: Jeiwan
Also found by: auditor0517
Data not available
Users can claim rewards from a Merkle tree that's being disputed. This can potentially lead to loss of funds since a malicious trusted EOA can claim funds from a malicious tree while it's being disputed.
The Distribution.getMerkleRoot function is used to get the current Merkle root during claiming. The function is aware of the dispute period of the current root and returns the previous root if the current tree is still in the dispute period.
However, the function doesn't take into account the situation when:
block.timestamp >= endOfDisputePeriod
).Such situations can happen realistically when a tree is disputed closer to the end of its dispute period and/or when the governor/guardian takes longer time to resolve the dispute. In such situations, the dispute period checks in the above functions will pass, however the disputer
address will be set, which means that the tree is being disputed and shouldn't be used in claims.
As an example exploit scenario, a malicious trusted EOA can add a Merkle tree root that lets them claim the entire balance of the contract. Even if the tree gets disputed quickly, the success of the attack boils down to how quickly the governor/guardian will resolve the dispute. To increase the chance, the attack can be deliberately executed when the governor/guardian are not active or available immediately.
Manual review
When the disputer
address is set (after a call to disputeTree
), consider treating the current tree as disputed, no matter whether the dispute period has passed or not. E.g. consider these changes:
diff --git a/contracts/Distributor.sol b/contracts/Distributor.sol index bc4e49f..8fb6a4c 100644 --- a/contracts/Distributor.sol +++ b/contracts/Distributor.sol @@ -197,7 +197,7 @@ contract Distributor is UUPSHelper { /// @notice Returns the MerkleRoot that is currently live for the contract function getMerkleRoot() public view returns (bytes32) { - if (block.timestamp >= endOfDisputePeriod) return tree.merkleRoot; + if (block.timestamp >= endOfDisputePeriod && disputer == address(0)) return tree.merkleRoot; else return lastTree.merkleRoot; }
Other
#0 - c4-judge
2023-07-08T11:15:06Z
hansfriese marked the issue as primary issue
#1 - c4-sponsor
2023-07-09T21:13:14Z
Picodes marked the issue as sponsor confirmed
#2 - c4-judge
2023-07-10T10:06:14Z
hansfriese marked the issue as satisfactory
#3 - c4-judge
2023-07-10T15:38:52Z
hansfriese marked the issue as selected for report
🌟 Selected for report: Jeiwan
Data not available
Stablecoin holders can receive wrongly calculated yield in the SavingsVest
contract. Also, wrong vesting profit can be slashed when the protocol is under-collateralized.
The SavingsVest contract lets users deposit their stablecoins and earn vested yield when the stablecoin in the Transmuter protocol is over-collateralized. The interest is accrued via calls to the SavingsVest.accrue function.
There are two parameters that affect the profit of depositors:
The two parameters can be changed via the setParams function. However, before they're changed, the current interest is not accrued. E.g. this may lead to:
protocolSafetyFee
is increased without accruing interest, the next accrual will happen at the increased fees, which will reduce the rewards for the depositors.vestingPeriod
is increased without accruing interest, the yield will be locked for a longer period and the next accrual may slash more vested yield.Thus, users can lose a portion of the yield that was earned at a lower protocol fee after the fee was increased. Likewise, increasing the vesting period may result in slashing yield that was earned before the period was increased.
Manual review
In the SavingsVest.setParams
function, consider accruing interest with the current parameters before setting new protocolSafetyFee
and vestingPeriod
.
Other
#0 - c4-judge
2023-07-08T12:22:42Z
hansfriese marked the issue as primary issue
#1 - c4-sponsor
2023-07-10T10:35:31Z
Picodes marked the issue as sponsor confirmed
#2 - Picodes
2023-07-10T10:36:53Z
Confirmed. The worst scenario is that when modifying the vestingPeriod
it can lead to a jump in lockedProfit
, so to a jump in totalAssets()
. So eventually someone could sandwich attack the update for a profit
#3 - c4-judge
2023-07-10T10:38:29Z
hansfriese marked the issue as satisfactory
#4 - c4-judge
2023-07-10T15:40:07Z
hansfriese marked the issue as selected for report
🌟 Selected for report: Lambda
Also found by: Jeiwan, Udsen, auditor0517
Data not available
Querying and filtering Claimed
events by username or token address will be difficult and in some cases impossible.
The Claimed
event doesn't have indexed fields (Distributor.sol#L111):
event Claimed(address user, address token, uint256 amount);
Manual review
Consider adding indexed fields to the Claimed
event. For example, indexing the user
field will make it possible to find all tokens claimed by an address.
answeredInRound
is deprecatedIn some distant future, answeredInRound
may start returning a stub value (e.g. 0), which will break the check for all cases.
As per the Chainlink documentation, the answeredInRound
variable has been deprecated:
answeredInRound: Deprecated - Previously used when answers could take multiple rounds to be computed
Thus, it's no longer necessary to check that roundId > answeredInRound
. In some distant future, answeredInRound
may start returning a stub value (e.g. 0), which will break the check for all cases.
Manual review
Consider removing the roundId > answeredInRound
check from the LibOracle.readChainlinkFeed
function.
https://github.com/AngleProtocol/angle-transmuter/blob/9707ee4ed3d221e02dcfcd2ebaa4b4d38d280936/contracts/transmuter/facets/SettersGuardian.sol#L16-L18 https://github.com/AngleProtocol/angle-transmuter/blob/9707ee4ed3d221e02dcfcd2ebaa4b4d38d280936/contracts/savings/Savings.sol#L220-L224
The governor and the guardian may fail to pause the Transmuter or the Savings contract when pausing is needed.
SettersGuardian.togglePause and Savings.togglePause are functions used to pause the contracts, they can only be called by the governor or the guardian. These functions are error-prone since they depend on the current state of the blockchain. If the governor and the guardian independently trigger either of the functions at around the same time (e.g. an incident may force them to pause the contract ASAP), the contract won't actually be paused since the second call will unpause it.
Manual review
For the two functions, consider replacing them with functions that explicitly set the paused state to either 0
or 1
.
#0 - c4-judge
2023-07-10T15:57:34Z
hansfriese marked the issue as grade-b