The RewardsInitiator
contract implements Chainlink Automation to trigger certain actions on time and custom logic basis. The custom logic trigger's design contains logic flaws that allow the attacker to grief the contract and quickly drain all LINK
funds from the Chainlink Automation Upkeep.
The RewardsInitiatior::checkUpkeep()
method iterates through the pool strategies. If the deposit change is not negative in any of the strategies, the method will return false, indicating that the upkeep is not needed. In other case it will return true, along with the list of IDs of all the strategies which deposit change is negative.
The aforementioned list of IDs is then passed to RewardsInitiator::performUpkeep()
method. To determine if the upkeep should still be performed, the method iterates through the list of provided strategies. If the deposit change of any of the provided strategies is no longer negative, the method reverts. In other scenario, the rewards of the provided strategies are updated, as one can see in the code snippet provided below.
The process of checking the provided strategies again to determine if the deposit change is still negative is flawed. As mentioned previously, this method will revert if any of the provided strategies' balance is not negative, thus not updating the rewards of any of them.
The attacker can easily achieve that scenario by front-running the RewardsInitiator::performUpkeep()
method call with the funding of one of the strategies, making its deposit change not-negative. As such none of the strategies will be updated.
Below is the snippet from the code of one of the strategies, VaultControlStrategy.sol
(out-of-scope for this audit but relevant for the in-scope issue):
As one can see, funding the strategy with enough token
between the RewardsInitiator::checkUpkeep()
and RewardsInitiator::performUpkeep()
calls is enough to make the VaultControlStrategy::getDepositChange()
return a non-negative number, therefore making the VaultControlStrategy::performUpkeep()
revert.
After the RewardsInitiator::performUpkeep()
method reverts because of the strategies' balance was no longer negative, the subsequent RewardsInitiator::checkUpkeep()
call will still return true - as there are still some strategies waiting for an update. Therefore, the Chainlink Automation will try to perform the upkeep again the very next block. The attacker can easily repeat the process described above: withdraw the funds before RewardsInitiator::checkUpkeep()
call and fund them back before RewardsInitiator::performUpkeep()
call.
As such, the attacker will DOS the contract, preventing any update to any of the strategies. Moreover, they will force the Chainlink Automation to retry the RewardsInitiator::performUpkeep()
call, effectively griefing the LINK
funding from the Upkeep.
The POC for the aforementioned scenario is provided below. Please paste this test inside the rewards-initiator.test.ts
file and execute it with yarn test
command.
The RewardsInitiator
contract can be DOSed, preventing the Chainlink Automation from executing the update. The LINK
funds from Chainlink Upkeep can be griefed.
Manual review
Inside the RewardsInitiator::performUpkeep()
method, instead of reverting if any of the strategies' deposit change is not negative, execute the update of those with negative change:
Additionally, please consider implementing update interval into the RewardsInitiator::checkUpkeep()
method, so the subsequent upkeeps can be made only after some specific amount of time has passed between them. A good example can be found in Chanlink Documentation.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.