GoGoPool contest - supernova's results

Liquid staking for Avalanche.

General Information

Platform: Code4rena

Start Date: 15/12/2022

Pot Size: $128,000 USDC

Total HM: 28

Participants: 111

Period: 19 days

Judge: GalloDaSballo

Total Solo HM: 1

Id: 194

League: ETH

GoGoPool

Findings Distribution

Researcher Performance

Rank: 52/111

Findings: 1

Award: $145.30

🌟 Selected for report: 0

šŸš€ Solo Findings: 0

Findings Information

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-648

Awards

145.3017 USDC - $145.30

External Links

Lines of code

https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/RewardsPool.sol#L156

Vulnerability details

Impact

GGP inflation calculation can be impacted by external parties . Ā 

Proof of Concept

The protocol through RewardsPool contract mints GGP tokens by checking if enough time has passed since the last mint. Anyone can call the startRewardsCycle function to run a GGP rewards cycle.

This function then calls the inflate internal function below :-

function inflate() internal {
		ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
		uint256 inflationIntervalElapsedSeconds = (block.timestamp - getInflationIntervalStartTime());
		(uint256 currentTotalSupply, uint256 newTotalSupply) = getInflationAmt();

		TokenGGP ggp = TokenGGP(getContractAddress("TokenGGP"));
		if (newTotalSupply > ggp.totalSupply()) {
			revert MaximumTokensReached();
		}

		uint256 newTokens = newTotalSupply - currentTotalSupply;
 
		emit GGPInflated(newTokens);

		dao.setTotalGGPCirculatingSupply(newTotalSupply);

		addUint(keccak256("RewardsPool.InflationIntervalStartTime"), inflationIntervalElapsedSeconds);
		setUint(keccak256("RewardsPool.RewardsCycleTotalAmt"), newTokens);
	}

Here , the currentTotalSupply and newTotalSupply is derived by calling getInflationAmt below : -

function getInflationAmt() public view returns (uint256 currentTotalSupply, uint256 newTotalSupply) {
		ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
		uint256 inflationRate = dao.getInflationIntervalRate();
		uint256 inflationIntervalsElapsed = getInflationIntervalsElapsed();
		currentTotalSupply = dao.getTotalGGPCirculatingSupply();
		newTotalSupply = currentTotalSupply;

		// Compute inflation for total inflation intervals elapsed
		for (uint256 i = 0; i < inflationIntervalsElapsed; i++) {
			newTotalSupply = newTotalSupply.mulWadDown(inflationRate);
		}

		return (currentTotalSupply, newTotalSupply);
	}

Here it loops through the inflationIntervalsElapsed which is derived by calling getInflationIntervalsElapsed below:-

function getInflationIntervalsElapsed() public view returns (uint256) {
		ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
		uint256 startTime = getInflationIntervalStartTime();
		if (startTime == 0) {
			revert ContractHasNotBeenInitialized();
		}
		return (block.timestamp - startTime) / dao.getInflationIntervalSeconds();
	}

The problem lies in this function above .

It returns the intervals by dividing the time elapsed by the constant(dao.getInflationIntervalSeconds()) which is currently set 1 days.

If time elapsed i.e ( block.timestamp - startTime )= 1 days and 23 hours , it will return 1 (169200/ 84600) . Now , if someone calls startRewardsCycle, the following will happen :-

Demonstrating in reverse

  • getInflationIntervalsElapsed will return 1 .
  • getInflationAmt function will loop only 1 time .
  • inflate function will receive the currentTotalSupply and newTotalSupply values accordingly .

Now see this line in the inflate function , which updates the RewardsPool.InflationIntervalStartTime by inflationIntervalElapsedSeconds(1 days and 23 hours ) (which is nothing but the time elapsed since we called the function ).

https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/RewardsPool.sol#L98

Hence, this is the end state:-

  • Protocol inflated GGP using interval as 1 .
  • Actual time passed was 1 days and 23 hours .
  • But RewardsPool.InflationIntervalStartTime is updated by full time elapsed i.e 1 days and 23 hours.
  • Protocol did not consider the 23 hours as anything and rather discarded it completely .
  • Now , the inflation time counter is reset to current time, thereby making the 23 hours as obsolete.
  • This phenomenon severely impacts the protocols ability to inflate the tokens correctly and thus I view it as high severity.

Tools Used

Manual Review

Rather than updating the RewardsPool.InflationIntervalStartTime by the actual time elapsed , update it using below method

addUint(keccak256("RewardsPool.InflationIntervalStartTime"), getInflationIntervalsElapsed()  * 1 days);

This will update the RewardsPool.InflationIntervalStartTime by 1 days , and thus maintaining the difference of 23 hours , which can be used in future in the next calll.

#0 - c4-judge

2023-01-10T09:52:42Z

GalloDaSballo marked the issue as duplicate of #648

#1 - c4-judge

2023-01-29T18:57:24Z

GalloDaSballo changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-02-08T10:02:36Z

GalloDaSballo marked the issue as satisfactory

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