TimelockController.sol will fail to execute any proposal where the execution call originates from the Governance.sol contract if the calldata requires an ETH value
TimelockController.sol manages time-delayed execution of governance proposals with role-based access control. In the Governance.sol users with enough voting power can create a new proposal where they provide following parameters:
Users with enough voting power have the ability to vote either for or against the proposal through function castVote(). At some point in time a user can call execute() function to execute the proposal. There are different checks before the proposal can be executed (like quorum check, status, etc.) and the proposal is first queued in the TimelockController with the following function:
After the delay has passed, user can call execute() function again, this time not to queue the proposal execution (since it has already been queued) but to actually execute it through the TimelockController. The function _executeProposal() in Governance.sol is defined as following:
We can see that all the parameters from the proposal (targets, values, calldatas) are passed to the TimelockController without sending any ETH. The problem occurs in the executeBatch() function of TimelockController.sol, which iterates over the targets array, executes each call, and sends the specified ETH value. If any of the proposal actions requires ETH to be sent, these calls will fail if the balance of the TimelockController is smaller then what is requested. Since the TimelockController cannot be funded (i.e., no receive() or fallback() function defined), the only option to be able to execute these target calls is to send ETH when calling executeBatch() function which is payable. As it can be seen from the above defined functions, Governance.sol does not check if the values array contains non-null values and does not send any ETH to TimelockController when executing the proposal, causing these calls to revert.
The only workaround is that some other user with EXECUTOR_ROLE executes queued proposals and provides needed ETH. Otherwise, all proposals that are directly executed from the Governance.sol that require ETH will fail.
Make setValue() function in TimelockTestTarget.sol payable, then add this test to the TimelockController.test.js:
The impact is high since the likelihood of this occurring is significant (it will happen sooner or later). The actual impact is also high, as the only way to address these proposals is for another executor to execute the call and provide ETH. However, the feasibility of this workaround is highly questionable.
Manual Review
The solution to this problem requires several implementation and reconstruction points, because it is nowhere defined who should provide ETH if the proposal action requires it. Is it the creator of the proposal? Should users who voted for the proposal split the costs?
This needs to be tought through but here are some points, assuming that the user who created a proposal needs to pay the costs for all the actions
Make propose() function payable
When creating a proposal in Governance.sol check if the values array contains non-null values. If yes, make sure that msg.value() is equal to the sum of all values
If the proposal get's rejected, make sure to refund the user who provided necessary ETH for the proposal
When executing the proposal thorugh TimelockController make sure to use low-level call and forward needed ETH to the TimelockController::executeBatch() function so that the function can execute these calls
Make sure to carefully handle reverts and errors to be able to refund the ETH to the original caller and not have them stuck in the contract.
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.