Ethena engaged Guardian to review the security of their TimelockController with whitelist functionality. From the 15th of May to the 17th of May, a team of 5 auditors reviewed the source code in scope.
- Published
- Review window
- May 15 to 17, 2025
- Language
- Solidity
- Chains
- Ethereum
- Sector
- Governance
- 0 Critical
- 0 High
- 1 Medium
- 2 Low
- 19 Informational
Scope
Overview
Ethena engaged Guardian to review the security of their TimelockController with whitelist functionality. From the 15th of May to the 17th of May, a team of 5 auditors reviewed the source code in scope.
Findings 22
-
M-01 Medium Native Asset Transfers Always Revert Logical Error Resolved
Description
When
executeorexecuteWhitelistedBatchattempts to parse the function selector viabytes4(data[:4]), any call wheredata.length < 4(e.g. a pure Ether transfer that passes an empty data payload) will revert because slicingdata[:4]triggers an out-of-bounds error.This deviates from the standard
TimelockControllerbehavior, which allows zero-length data for native asset transfers.Notice the following check in the
_executefunction:function _execute(address target, uint256 value, bytes calldata data) internal virtual override { if (data.length > 0 target.code.length = 0) revert InvalidTarget(target); super._execute(target, value, data); }The current test
testExecuteWithValuepasses because: 1.targetis a smart contract. 2.abi.encodeWithSignature("")actually returns non-empty calldata:0xc5d24601However, if the
targetwas an EOA it would revert withInvalidTarget(EOA_ADDR)error, becausedata.lengthcheck would pass in the_executefunction buttarget.code.length = 0would not.And, if we tried to, set
data.lengthto 0, it would revert in theEthenaTimelockController.executefunction, when trimming the call data as the call data would be empty as we initially stated.Recommendation
Consider checking that
data.lengthis>than 4 before setting:bytes4 selector = bytes4(data[:4]);.Resolution
Ethena Team: The issue was resolved in PR#4. We have added
_extractSelectorto mitigate this and followed up with the testtestCanWhitelistAndExecuteEmptyBytes4DataToSmartContractto verify the functionality. -
L-01 Low Ether Refunds Break Balance Invariant Trapped Funds Resolved
Description
In the
execute,executeBatch, andexecuteWhitelistedBatchfunctions themsg.valueis validated to be exactly equal to the total of the value parameters provided. This validation is performed in an effort to ensure that no ETH is retained in the contract as mentioned in the README.However, in the event that Ether is sent to a function and any portion is refunded by the arbitrary function call then this amount of ETH will be left in the
EthenaTimelockControllerfunction and will not be rescuable due to themsg.valuevalidations.Refunds from arbitrary function calls are made possible by the receive function in the
OpenZeppelinbaseTimelockControllercontract. Furthermore, if any calls with nonzero value are made with theEthenaTimelockControllercontract as the target, this Ether will be left within the contract.Recommendation
Consider performing a balance check at the end of the
execute,executeBatch, andexecuteWhitelistedBatchfunctions and refunding any ether in the contract to the caller, under the assumption that the caller can accept ether, or a holding address.Resolution
Ethena Team: The issue was resolved in PR#4. We have removed this check to allow funds to end up in the
TimelockControllercontract and followed up with testtestEthCanBeRescuedFromTimelockandtestErc20TokensCanBeRescuedFromTimelockto validate that funds can be recovered if they do end up in the controller. Under normal circumstances we do not expect funds to end up in the controller unless there is a difference betweenmsg.valueand thevalueparameters or a refunds from an arbitrary function call is made or someone sends Ether directly into theTimelockControllercontract. -
L-02 Low Stuck Actions Due To Missing Whitelist Check Validation Resolved
Description
In the
schedulefunction there is no validation to ensure that a whitelisted function is not scheduled through the normal timelock flow. As a result, when the scheduled whitelisted function becomes executable, it cannot be executed because the overridden execute function does not invokesuper.executeand clear the action from the timelock.If any actions were based on the whitelisted function call that was scheduled as a predecessor, these subsequent actions are stuck. Even in the case where the scheduled whitelisted action is cancelled, as it never reaches a
OperationState.Donestate.Consider the following series of actions as an example:
- Action A is created with a whitelisted function selector in it’s payload
- Action B is created for a non-whitelisted function with Action A as its predecessor
- Action A cannot be completed from the scheduled queue since the
executefunction does not allow
the base
TimelockControllerlogic to be executed.- Action B cannot be executed since Action A is listed as its predecessor, even if Action A is
cancelled.
- All actions will have to be cancelled and re-queued, forcing an additional wait time of the
minDelay.
Furthermore, this scenario can also arise if the function selector of a pending action is whitelisted after that action was already scheduled.
Recommendation
Consider overriding the schedule function to prevent whitelisted actions from being queued. Be aware that if this solution is adopted, whitelisting a function in the queue will also yield this behavior.
Alternatively, consider separating the whitelisted execution logic and non-whitelisted execution logic into separate functions. Notice that this is similar to the approach for batch executions, which do not exhibit this deficiency.
Resolution
Ethena Team: The issue was resolved in PR#4. We have opted to separate the whitelisted execution logic and non-whitelisted execution logic into
executeandexecutedWhitelisted. -
I-01 Informational Ether Sent Is Trapped Trapped Funds Resolved
Description
The base
TimelockControllercontract fromOpenZeppelinimplements areceivefunction and therefore ether can be sent directly to theEthenaTimelockControllercontract.Any ether sent to the
EthenaTimelockControllercontract this way will be unrescuable as a result of themsg.valuechecks in theexecute,executeBatch, andexecuteWhitelistedBatchfunctions.Recommendation
Consider either adding a rescue function, implementing refunds in the execution functions as mentioned in L-01, or implementing a receive function that reverts to disable direct Ether transfers.
Resolution
Ethena Team: The issue was resolved in PR#4.
-
I-02 Informational Misleading Event Emission Events Resolved
Description
In the
removeFromWhitelistfunction theFunctionRemovedFromWhitelistevent is emitted regardless of whether the specific target address and function selector were in the whitelist to begin with.This could hypothetically be misleading to consumers of the
FunctionRemovedFromWhitelistin the event that the target and selector combination value was not previously set astruein the_functionWhitelistmapping.Similarly the
addToWhitelistfunction performs no validation that the function was not already added to the whitelist.Recommendation
Consider validating that the
_functionWhitelistentry for the specified target and selector istruein theremoveFromWhitelistfunction andfalsein theaddToWhitelistfunction.Resolution
Ethena Team: The issue was resolved in PR#4.
-
I-03 Informational Lacking minDelay Validation Validation Acknowledged
Description
There is no validation on the
minDelayvalue in the constructor of theEthenaTimelockControllercontract. As a result theminDelaymay technically be assigned to an insufficient delay, even though the deployment script currently sets the minDelay as 1 day.Recommendation
Consider implementing validation for the
minDelayvalue, otherwise be aware of this lack of validation during deployment.Resolution
Ethena Team: Acknowledged.
-
I-04 Informational Upgradeable Target Whitelisting Risk Warning Acknowledged
Description
Arbitrary target contracts may be whitelisted with the
addToWhitelistfunction. The target address in question may be a proxy contract which can have the implementation of it’s whitelisted selector upgraded.For this reason, care should be taken to examine the centralized risk of whitelisted target contracts.
Recommendation
Be aware of this risk and consider the centralized upgrade risks for any upgradeable contracts which are whitelisted.
Resolution
Ethena Team: Acknowledged. We do not intend to use
addToWhiteliston functions outside of contracts that are currently controlled by the Ethenamultisig. Assuming that is true and that the upgrade function on the target contract is not whitelisted, any upgrades happening to target whitelist contracts would have to go through theminDelay. -
I-05 Informational Missing Documentation Documentation Acknowledged
Description
In the README it is mentioned that there are strict
msg.valuechecks for theexecuteandexecuteWhitelistedBatchfunctions, however there is also the same strictmsg.valuecheck in theexecuteBatchfunction which is not mentioned.Recommendation
Amend the README file to include that the
executeBatchfunction also implements themsg.valueinvariant check.Resolution
Ethena Team: Acknowledged.
-
I-06 Informational Missing Event Data Events Resolved
Description
The
WhitelistedFunctionExecutedevent does not include relevant information such as the value and payload of the execution. These data fields are in the baseTimelockController CallExecutedevent and if anything would provide parity with this event.Recommendation
Consider including value and payload fields in the
WhitelistedFunctionExecutedevent.Resolution
Ethena Team: Resolved.
-
I-07 Informational Potential Censoring With Open Execution Warning Acknowledged
Description
The
EXECUTOR_ROLEis planned to be left as an open role, however this introduces a potential censoring vector whereby a malicious actor may be able to control the execution flow of certain external calls.For a particular external call where the execution of the target function uses a try/catch or allows the failure of a nested external call, a malicious actor may be able to perform the execution while providing insufficient gas to the
executeorexecuteBatchfunctions such that the nested external call runs out of gas and fails but allows the top level transaction to succeed with the remaining 1/64 gas.Recommendation
None of the existing permissioned functions which may be used by the
EthenaTimelockControllerwere observed to use a try/catch or allow the failure of an external call, therefore this is not an immediate concern. However be aware of this risk when adapting future use-cases for the timelock contract.Resolution
Ethena Team: Acknowledged.
-
I-08 Informational Lacking Admin Rules Best Practices Acknowledged
Description
The base
TimelockControllercontract does not implement theDefaultAdminRulescontract nor any base default admin role protections.Similarly, other Ethena related contracts use a
SingleAdminAccessControlcontract which provides a simpler implementation of default admin role protections.Recommendation
Consider implementing the protections from the
SingleAdminAccessControlcontract for the default admin role as seen in other Ethena contracts.Resolution
Ethena Team: Acknowledged. Worst case here is that someone could propose the Admin to renounce or revoke their role which would mean that other roles cannot be managed. In this scenario it is still possible for the timelock contract the propose the underlying contract to change its admin.
-
I-09 Informational renounceRole Called Without Timelock Warning Resolved
Description
In the
AccessControlcontract therenounceRolefunction is exposed to be called by any address without going through the timelock period. This does not pose any notable risk and this finding serves only to document that this action is not kept behind a timelock.Recommendation
Be aware of this behavior, and if desired consider overriding the
renounceRolefunction and requiring that it goes through the queued delay with theonlyTimelockmodifier.Resolution
Ethena Team: The issue was resolved in commit 573bbdb. We’ve removed the ability for addresses to renounce roles directly. They must be revoked through the timelock
https://github.com/ethena-labs/timelock-contract/pull/4/commits/573bbdb8aa5e213df79f219693
-
I-10 Informational Missing Zero Address Checks Validation Resolved
Description
In the constructor of the
EthenaTimelockControllercontract the proposers list is validated to have all nonzero entries, however no such validation is performed for thewhitelistedExecutorslist which should also not contain the zero address.Recommendation
Consider implementing a zero address validation for the
whitelistedExecutorslist.Resolution
Ethena Team: The issue was resolved in PR#4.
-
I-11 Informational Deployment Script Missing Validations Validation Resolved
Description
In the deployment script for the
EthenaTimelockControllercontract there is validation performed on the first entry of theproposersandexecutorslists.Firstly, the entire proposers and executors lists could be validated to ensure that all proposers and executors received the expected roles, in the event that these lists were extended before deployment.
Secondly, the
whitelistedExecutorslist is not validated to ensure that the intended whitelisted executors received the expectedWHITELISTED_EXECUTOR_ROLErole.Finally, the proposer addresses will each also receive the
CANCELLER_ROLEwhich can also be validated for.Recommendation
Be aware of these potential deployment script improvements and consider implementing them if you wish.
Resolution
Ethena Team: Resolved.
-
I-12 Informational Magic Selector Length Number Best Practices Resolved
Description
In the
EthenaTimelockControllercontract the value 4 is repeatedly used to denote the length of a function selector and create a slice of the selector. As a best practice magic numbers can be referenced as named constants to improve code readability.Recommendation
Consider creating a constant
SELECTOR_LENGTHvalue to use when slicing selectors.Resolution
Ethena Team: Resolved.
-
I-13 Informational Timelock Cannot Interact With Precompiles Warning Acknowledged
Description
Within the
_executeoverride inEthenaTimelockController, there is a conditional that reverts ifdata.length > 0andtarget.code.length = 0:if (data.length > 0 && target.code.length = 0) {revert InvalidTarget(target);}This prevents the contract from calling addresses where
code.lengthis zero. While this is intended to block whitelisting for addresses with empty code, it also blocks calls to Ethereum precompiles (e.g., 0x1 through 0x9 on mainnet), which often have no bytecode in the traditional sense.Consequently, any attempt to schedule or execute interactions with precompiles will revert. Keep in mind that since the Pectra update EOAs can also have code in their accounts since they via delegatation. This will bypass the check in
_executeand allow execution of an EOA’s code.Recommendation
- If interacting with precompiles is a requirement, remove or relax the
target.code.length = 0check. - Alternatively, explicitly allow known precompile addresses in the code or via a separate allow-list so
that legitimate precompiles can be called without inadvertently allowing empty addresses.
Resolution
Ethena Team: The issue was resolved in PR#4. Precompiled are not intended to be called directly by the
TimelockControllercontract. Fixed through the splitting of execute andexecuteWhitelistedwhich allows us to remove this override. - If interacting with precompiles is a requirement, remove or relax the
-
I-14 Informational Unnecessary Unchecked Blocks Gas Optimization Resolved
Description
Since Solidity 0.8.22, the compiler performs advanced range analysis to determine when overflow checks can safely be omitted.
In a typical loop increment scenario such as for
(uint256 i = 0; i < length; i++), the compiler can prove that i will not overflow auint256and therefore it automatically removes the checks.Manually placing increments in an unchecked block is now redundant, making the code less clear without providing a meaningful gas optimization.
Recommendation
Replace
unchecked { ++i; }with a simple++i, allowing the compiler’s range analysis to handle overflow optimizations automatically. This cleaner style increases code clarity while maintaining code efficiency in Solidity 0.8.22 and above.Resolution
Ethena Team: The issue was resolved in PR#4.
-
I-15 Informational Predecessor Is Not Used For Whitelisted Actions Warning Resolved
Description
When a whitelisted operation is being executed by calling
execute(), thepredecessorparameter is ignored, even though theNatSpecsays* @param predecessor The operation that must be executedbefore this one.Because of this, whitelisted executors may expect that their operations will be executed only if the predecessor is already done, but in reality their operations will be executed regardless of the predecessor.
Recommendation
Either clarify that the predecessor is not used for whitelisted operations in the
NatSpecor modify the code to actually use it.Resolution
Ethena Team: The issue was resolved in PR#4. Fixed through splitting out of
executeWhitelistedfunction. -
I-16 Informational Executor Role Can Execute Whitelisted Functions Documentation Acknowledged
Description
The protocol documentation states that “only whitelisted executors can execute whitelisted functions” as a invariant. It also claims that batches cannot be scheduled if they contain whitelisted functions.
However, the current implementation allows scheduling such batches. This breaks the documented feature and effectively bypasses the invariant, as any address with
EXECUTOR_ROLEcan then execute whitelisted functions via the executeBatch function.Recommendation
If this behaviour is intended, clarify it in the documentation. If not, add a check to prevent scheduling batches that include whitelisted functions.
Resolution
Ethena Team: Acknowledged. One caveat here is that a non-whitelisted function can be scheduled and then subsequently added to the whitelist before the scheduled function is executed.
-
I-17 Informational Malicious Canceller Can DOS The Timelock DoS Acknowledged
Description
When TimelockController is created, all of the proposers receive the
PROPOSER_ROLEandCANCELLER_ROLE, which allow them to both propose and cancel transactions.This gives them a lot of power, since they can cancel any transaction they want to before the timelock delay has passed.
Normally, if any of the proposers becomes malicious, their role should be revoked by calling AccessControl.revokeRole() which can be only executed by the admin of the given role.
In the case of the timelock, the admin is the address with the
DEFAULT_ADMIN_ROLE -this address is the timelock itself.Because of this, the revoke transaction itself is a subject to the timelock delay, which makes it possible for the malicious proposer to just cancel it, keeping their
CANCELLER_ROLE.Once a proposer is compromised, the result is DOS of the timelock and permanent freezing of the funds it holds (since any transaction can be cancelled).
Recommendation
Be careful with who you give the
CANCELLER_ROLE, ideally only to one account.Resolution
Ethena Team: Acknowledged. Proposer and canceller roles are intended to be assigned only to the multisig that is the current controller of the ethena protocol contracts.
-
I-18 Informational Timelock Is Not Compatible With Some Chains Warning Resolved
Description
The timelock inherits from
ReentrancyGuardTransientwhich makes use of thetloadandtstoreopcodes introduced inEIP-1153. If the contract were to be deployed to a chain with no support for these opcodes, the functionality would be broken.Recommendation
Keep in mind you need to change the reentrancy guard if you are going to deploy to such chains.
Resolution
Ethena Team: Resolved. We’ve opted to replace with
ReentrancyGuardinstead. The gas saving is not a concern. -
I-19 Informational Timelock Unable To Blacklist Logical Error Acknowledged
Description
The
NatSpecfor theaddToBlacklistfunction in theStakedUSDecontract states that itAllows theowner (DEFAULT_ADMIN_ROLE) and blacklist managers to blacklist addresses.However, the
addToBlacklistandremoveFromBlacklistfunctions use theonlyRole(BLACKLIST_MANAGER_ROLE)modifier which only allows the blacklist manager role to execute these functions.As a result if the
EthenaTimelockControlleris only granted theDEFAULT_ADMIN_ROLEover the contracts it will be unable to invoke theaddToBlacklistorremoveFromBlacklistfunctions.Recommendation
Be aware of this discrepancy and be sure to assign the
BLACKLIST_MANAGER_ROLEto theEthenaTimelockControllerin addition to theDEFAULT_ADMIN_ROLEfor theStakedUSDecontract.Resolution
Ethena Team: Acknowledged. It should be noted that the
DEFAULT_ADMIN_ROLEcan manage addresses with theBLACKLIST_MANAGER_ROLE. The blacklisting role inStakedUSDeis intended to be given to addresses other than the admin.
No findings match.
Invariants 7
The review's fuzzing suite asserted 7 invariants. 7 held.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
INV-01 | The setValue function in MockTarget correctly updates the value state variable to the provided | Held |
INV-02 | input. The toggleFlag function in MockTarget correctly toggles the flag state variable. | Held |
INV-03 | The complexOperation function in MockTarget correctly updates value to the sum of inputs | Held |
INV-04 | and toggles the flag. MockTarget functions (setValue, toggleFlag, complexOperation) do not cause unintended | Held |
INV-05 | state changes. Scheduled transactions in the timelock controller are marked as pending until | Held |
INV-06 | executed. Transactions cannot be executed before their scheduled delay period has elapsed. | Held |
INV-07 | Executed transactions in the timelock controller are marked as done and not pending. | Held |
More from Ethena
All 7 reportsPut your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.