Due to setCurves function lacks access controls, the curves state variable can be set by anyone at any time (e.g. front-running any tx that reads from curves state variable).
function setCurves(Curves curves_) public {
curves = curves_;
}
This means that a malicious actor can swap the Curves contract by one that alters the balance and/or supply for a given Curves token, which has the following consequences:
Steal of FeeSplitter funds (ETH).
Impair the distribution of fees amount the given Curves token.
Halt Curves traiding.
function balanceOf(address token, address account) public view returns (uint256) {
return curves.curvesTokenBalance(token, account) * PRECISION;
}
function totalSupply(address token) public view returns (uint256) {
//@dev: this is the amount of tokens that are not locked in the contract. The locked tokens are in the ERC20 contract
return (curves.curvesTokenSupply(token) - curves.curvesTokenBalance(token, address(curves))) * PRECISION;
}
The following external/public functions are compromised:
In FeeSplitter.sol (via balanceOf and totalSupply functions):
claimFees.
addFees.
onBalanceChange.
batchClaiming.
In Curves.sol (via _transferFees function, as long as there is a holders fee percentage set and feeRedistributor has been set):
buyCurvesToken.
buyCurvesTokenWhitelisted.
buyCurvesTokenForPresale.
buyCurvesTokenWithName.
sellCurvesToken.
sellExternalCurvesToken.
Proof of Concept
Let's imagine a few scenarios. Both have in common that the genuine Curves contract has set a a holders fee percentage (in feeEconomics.holdersFeePercent state variable) and a FeeRedistributor (in feeRedistributor state variable).
Scenario 1 - Increased claimable fees (steal of funds)
Mallory replaces curves with a malicious Curves contract that makes balanceOf function return a number that, after being processed by updateFeeCredit and getClaimableFees functions, will allow her to claim fees up to the FeeSplitter ETH balance.
Mallory calls claimFees and receives up to all FeeSplitter ETH balance as claimable fees.
Mallory replaces curves with a malicious Curves contract that makes balanceOf function return 1.
Alice, that has been trading Curves tokens, calls claimFees. However, the amount of ETH that receives is lower than the expected one due to owed variable (in getClaimableFees) being 0, which lowers the amout of claimable fees.
This case also produces inconsistencies on the amount of unclaimed fees (by claimFees function resetting the value to 0):
function claimFees(address token) external {
updateFeeCredit(token, msg.sender);
uint256 claimable = getClaimableFees(token, msg.sender);
if (claimable == 0) revert NoFeesToClaim();
tokensData[token].unclaimedFees[msg.sender] = 0; // HERE
payable(msg.sender).transfer(claimable);
emit FeesClaimed(token, msg.sender, claimable);
}
Scenario 3 - DDOS claim fees
Mallory replaces curves with a malicious Curves contract that makes balanceOf function return 0.
Alice, that has been trading Curves tokens, calls claimFees for the first time. However, her transaction reverts wtih a NoFeesToClaim (reverted by FeeRedistributor.claimFees function that couldn't make updateFeeCredit update the unclaimed fees logic and results in 0 claimable fees).
function claimFees(address token) external {
updateFeeCredit(token, msg.sender);
uint256 claimable = getClaimableFees(token, msg.sender);
if (claimable == 0) revert NoFeesToClaim(); // HERE
tokensData[token].unclaimedFees[msg.sender] = 0;
payable(msg.sender).transfer(claimable);
emit FeesClaimed(token, msg.sender, claimable);
}
Scenario 4 - DDOS subject's Curve tokens trading
Mallory replaces curves with a malicious Curves contract that makes totalSupply function return 0.
Alice, either buys or sells Curves, but her transaction reverts with a NoTokenHolders (reverted by FeeRedistributor.addFees function attempting to manage the token cummulative fees).
uint256 totalSupply_ = totalSupply(token);
if (totalSupply_ == 0) revert NoTokenHolders();
Scenario 5 - Inconsistent userTokens data
Mallory replaces curves with a malicious Curves contract that makes balanceOf function return 0.
Alice buys Curves tokens, but FeeSplitter.userTokens state variable is not properly updated to reflect that (as the condition below evaluates falsy).
function onBalanceChange(address token, address account) public onlyManager {
TokenData storage data = tokensData[token];
data.userFeeOffset[account] = data.cumulativeFeePerToken;
if (balanceOf(token, account) > 0) userTokens[account].push(token); // HERE
}
Then getUserTokens function returns an inconsistent array of token addresses.
Tools Used
Manually reviewed.
Recommended Mitigation Steps
Add an access control modifier (e.g. onlyOwner) in the setCurves function if the current contracts architecture is kept.
function setCurves(Curves curves_) public onlyOwner {
curves = curves_;
}
Lines of code
https://github.com/code-423n4/2024-01-curves/blob/main/contracts/FeeSplitter.sol#L35
Vulnerability details
Impact
Due to
setCurves
function lacks access controls, thecurves
state variable can be set by anyone at any time (e.g. front-running any tx that reads fromcurves
state variable).This means that a malicious actor can swap the
Curves
contract by one that alters the balance and/or supply for a given Curves token, which has the following consequences:FeeSplitter
funds (ETH).The following external/public functions are compromised:
In
FeeSplitter.sol
(viabalanceOf
andtotalSupply
functions):claimFees
.addFees
.onBalanceChange
.batchClaiming
.In
Curves.sol
(via_transferFees
function, as long as there is a holders fee percentage set andfeeRedistributor
has been set):buyCurvesToken
.buyCurvesTokenWhitelisted
.buyCurvesTokenForPresale
.buyCurvesTokenWithName
.sellCurvesToken
.sellExternalCurvesToken
.Proof of Concept
Let's imagine a few scenarios. Both have in common that the genuine
Curves
contract has set a a holders fee percentage (infeeEconomics.holdersFeePercent
state variable) and a FeeRedistributor (infeeRedistributor
state variable).Scenario 1 - Increased claimable fees (steal of funds)
curves
with a maliciousCurves
contract that makesbalanceOf
function return a number that, after being processed byupdateFeeCredit
andgetClaimableFees
functions, will allow her to claim fees up to theFeeSplitter
ETH balance.claimFees
and receives up to allFeeSplitter
ETH balance as claimable fees.Scenario 2 - Diminished claimable fees
curves
with a maliciousCurves
contract that makesbalanceOf
function return 1.claimFees
. However, the amount of ETH that receives is lower than the expected one due toowed
variable (ingetClaimableFees
) being 0, which lowers the amout of claimable fees.This case also produces inconsistencies on the amount of unclaimed fees (by
claimFees
function resetting the value to 0):Scenario 3 - DDOS claim fees
curves
with a maliciousCurves
contract that makesbalanceOf
function return 0.claimFees
for the first time. However, her transaction reverts wtih aNoFeesToClaim
(reverted byFeeRedistributor.claimFees
function that couldn't makeupdateFeeCredit
update the unclaimed fees logic and results in 0 claimable fees).Scenario 4 - DDOS subject's Curve tokens trading
curves
with a maliciousCurves
contract that makestotalSupply
function return 0.NoTokenHolders
(reverted byFeeRedistributor.addFees
function attempting to manage the token cummulative fees).Scenario 5 - Inconsistent userTokens data
curves
with a maliciousCurves
contract that makesbalanceOf
function return 0.FeeSplitter.userTokens
state variable is not properly updated to reflect that (as the condition below evaluates falsy).Then
getUserTokens
function returns an inconsistent array of token addresses.Tools Used
Manually reviewed.
Recommended Mitigation Steps
Add an access control modifier (e.g.
onlyOwner
) in thesetCurves
function if the current contracts architecture is kept.Assessed type
Access Control