Summary
Every Time LibFertilizer.pop()
return false the rewardToFertilizer
is revert due to require check.
if (!LibFertilizer.pop()) {
s.bpf = uint128(firstEndBpf);
s.fertilizedIndex = s.fertilizedIndex.add(newFertilized);
require(s.fertilizedIndex == s.unfertilizedIndex, "Paid != owed");
return newFertilized;
}
Vulnerability Details
When fertilizer is added through the addFertilizer
function, it sets specific state variables.
The following state variables are involved:
mapping(uint128 => uint256) fertilizer;
mapping(uint128 => uint128) nextFid;
uint256 activeFertilizer;
uint256 fertilizedIndex;
uint256 unfertilizedIndex;
uint128 fFirst;
uint128 fLast;
uint128 bpf;
uint256 recapitalized;
bool season.fertilizing;
Add Fertilizer:
Calls the addFertilizer
function with specific parameters.
Sets the fertilizer
, activeFertilizer
, unfertilizedIndex
, fFirst
, fLast
, and season.fertilizing
state variables.
I have Explain this bug by Manual analysis in simple paramaters:
step1
: season = 6074 first season ,tokenAmountIn = 5e18,fertilizerAmount = 5, minLP = 2e18
function addFertilizer(
uint128 season,
uint256 tokenAmountIn,
uint256 fertilizerAmount,
uint256 minLP
) internal returns (uint128 id) {
AppStorage storage s = LibAppStorage.diamondStorage();
uint128 fertilizerAmount128 = fertilizerAmount.toUint128();
uint128 bpf = getBpf(season);
s.unfertilizedIndex = s.unfertilizedIndex.add(fertilizerAmount.mul(bpf));
id = s.bpf.add(bpf);
s.fertilizer[id] = s.fertilizer[id].add(fertilizerAmount128);
s.activeFertilizer = s.activeFertilizer.add(fertilizerAmount);
addUnderlying(tokenAmountIn, fertilizerAmount.mul(DECIMALS), minLP);
if (s.fertilizer[id] > fertilizerAmount128) return id;
push(id);
emit SetFertilizer(id, bpf);
}
function push(uint128 id) internal {
AppStorage storage s = LibAppStorage.diamondStorage();
if (s.fFirst == 0) {
s.season.fertilizing = true;
s.fLast = id;
s.fFirst = id;
} else if (id <= s.fFirst) {
}
-
State Update in Step_1
s.fertilizer[1002500] = 5;
s.nextFid// not initialized yet
s.fertilizedIndex;
s.unfertilizedIndex = 5012500;
s.activeFertilizer = 5;
s.fFirst = 1002500;
s.fLast = 1002500;
s.season.fertilizing = true;
Reward Distribution --> sun.sol
deltaB = 50e6
function stepSun(int256 deltaB, uint256 caseId) internal {
if (deltaB > 0) {
uint256 newHarvestable = rewardBeans(uint256(deltaB));
setSoilAbovePeg(newHarvestable, caseId);
s.season.abovePeg = true;
}
else {
setSoilBelowPeg(deltaB);
s.season.abovePeg = false;
}
}
If the deltaB is greater then zero --> call the rewardBeans and pass the value of deltaB
function rewardBeans(uint256 newSupply) internal returns (uint256 newHarvestable) {
uint256 newFertilized;
C.bean().mint(address(this), newSupply);
if (s.season.fertilizing) {
newFertilized = rewardToFertilizer(newSupply);
newSupply = newSupply.sub(newFertilized);
}
We called the rewardToFertilizer
function because s.season.fertilizing
is true
function rewardToFertilizer(uint256 amount) internalreturns (uint256 newFertilized) {
uint256 maxNewFertilized = amount.div(FERTILIZER_DENOMINATOR);
uint256 newBpf = maxNewFertilized.div(s.activeFertilizer);
uint256 oldTotalBpf = s.bpf;
uint256 newTotalBpf = oldTotalBpf.add(newBpf);
uint256 firstEndBpf = s.fFirst;
while(newTotalBpf >= firstEndBpf) {
newBpf = firstEndBpf.sub(oldTotalBpf);
newFertilized = newFertilized.add(newBpf.mul(s.activeFertilizer));
if (!LibFertilizer.pop()) {
s.bpf = uint128(firstEndBpf);
s.fertilizedIndex = s.fertilizedIndex.add(newFertilized);
require(s.fertilizedIndex == s.unfertilizedIndex, "Paid != owed");
return newFertilized;
}
we call the LibFertilizer.pop()
and check if this return true or false
function pop() internal returns (bool) {
AppStorage storage s = LibAppStorage.diamondStorage();
uint128 first = s.fFirst;
s.activeFertilizer = s.activeFertilizer.sub(getAmount(first));
uint128 next = getNext(first);
if (next == 0) {
require(s.activeFertilizer == 0, "Still active fertilizer");
s.fFirst = 0;
s.fLast = 0;
s.season.fertilizing = false;
return false;
}
s.fFirst = getNext(first);
return true;
}
so we return false
and the value of s.fFirst = 0, s.fLast = 0, s.season.fertilizing = false
set back to the default values.
if (!LibFertilizer.pop()) {
s.bpf = uint128(firstEndBpf);
s.fertilizedIndex = s.fertilizedIndex.add(newFertilized);
require(s.fertilizedIndex == s.unfertilizedIndex, "Paid != owed");
return newFertilized;
}
uint256 fertilizedIndex;
uint256 unfertilizedIndex;
the value of the fertlizeIndex and unfertilizedIndex is equal this is revert stepSun
function
function stepSun(int256 deltaB, uint256 caseId) internal {
if (deltaB > 0) {
uint256 newHarvestable = rewardBeans(uint256(deltaB));
setSoilAbovePeg(newHarvestable, caseId);
s.season.abovePeg = true;
}
so we can never s.season.abovePeg
make to true.
Resulting State
After adding fertilizer and attempting to reward it, the following states are reached:
uint256 fertilizedIndex = 5012500;
uint256 unfertilizedIndex = 5012500;
bool season.fertilizing = false;
uint256 activeFertilizer = 0;
Impact
If the require(s.fertilizedIndex == s.unfertilizedIndex, "Paid != owed")
statement fails, the transaction reverts. This revert prevents the season.abovePeg
flag from being set.
Tools Used
Manual Review
Recommendations
Sun::rewardToFertilizer
remove the s.fertilizeIndex == s.unfertilizedIndex
check
if (!LibFertilizer.pop()) {
s.bpf = uint128(firstEndBpf);
s.fertilizedIndex = s.fertilizedIndex.add(newFertilized);
- require(s.fertilizedIndex == s.unfertilizedIndex, "Paid != owed");
return newFertilized;
}