28971 - [SC - Low] Double spending or double execution of transact...
Submitted on Mar 3rd 2024 at 20:33:14 UTC by @codesentry for Boost | Puffer Finance
Report ID: #28971
Report type: Smart Contract
Report severity: Low
Target: https://etherscan.io/address/0x3C28B7c7Ba1A1f55c9Ce66b263B33B204f2126eA#code
Impacts:
Protocol insolvency
Permanent freezing of unclaimed yield
Temporary freezing of funds for at least 1 hour
Description
Brief/Intro
COMMUNITY_MULTISIG wallet can execute the transaction queued by OPERATIONS_MULTISIG . Transaction is not removed from queue when COMMUNITY_MULTISIG execute it. In this case OPERATIONS_MULTISIG can execute same transaction again. This is a sever bug and can cause double spending, double execution of transaction. Even OPERATIONS_MULTISIG can execute tx without waiting for delay period to pass. All scenario explained in ## Impact Details section.
Vulnerability Details
This bug is in executeTransaction()
method of TimeLock contract. OPERATIONS_MULTISIG queue a transaction and COMMUNITY_MULTISIG execute it by calling below method
executeTransaction(target, callData,operationId)
Whenever COMMUNITY_MULTISIG execute it, it does not remove it from queue. This tx exist in queue even it has been executed by COMMUNITY_MULTISIG hence OPERATIONS_MULTISIG can execute it again.
Steps
OPERATIONS_MULTISIG calls
timelock.queueTransaction(target, callData, 1);
Time delay passed but actually transaction executed by COMMUNITY_MULTISIG. COMMUNITY_MULTISIG calls timelock.executeTransaction(target, callData, 1);
OPERATIONS_MULTISIG is able to call same tx again even thought it has already been executed by COMMUNITY_MULTISIG. OPERATIONS_MULTISIG calls below method successfully. timelock.executeTransaction(target, callData, 1);
This result in calling timelock.executeTransaction(target, callData, 1) two times.
Impact Details
Below are some scenario that explains the impact. There are can be multiple scenarios exists but I writing two examples below
Scenario1:
OPERATIONS_MULTISIG queue a transaction to upgrade implementation contract of PufferDepositor to implV2. This upgrade is being done to fix a bug.
After tx was queued, community found that bug is sever hence upgrade the proxy contract immediately by calling timelock.executeTransaction(target, callData, 1) by COMMUNITY_MULTISIG. Now proxy's implementation contract is implV2.
After few days community found that implV2 also has another sever bug hence it upgrade to implV3 by executing timelock.executeTransaction(target, callData, 2) immediately.
Previous queued transaction was not removed from queue. OPERATIONS_MULTISIG calls timelock.executeTransaction(target, callData, 1) and upgrade proxy to old version which has some bug. Implication of this scenario is that proxy is upgraded to old version by OPERATIONS_MULTISIG without waiting for delay period.
Scenario 2: Timelock contract is owner of some treasury contract. Treasury contract has some ERC20 balance.
OPERATIONS_MULTISIG queue a transaction to transfer 10 token to Alice for operationId =1 .
COMMUNITY_MULTISIG execute this transaction and transfers 10 token to Alice. 3) OPERATIONS_MULTISIG execute same tx and transfer 10 more token because tx is not removed from queue. Net result is loss of asset or double spending of treasury asset.
Upgrade from implV3 to implV2 is done with no waiting or delay period. This can have serious implication for users as proxy upgraded to older version that too without wait.
Overall timelock contract has sever bug in executeTransaction(). Ideally this should remove the queued tx if already executed.
References
Add any relevant links to documentation or code
Proof of Concept
Last updated