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.