Formal Peach Starling
Medium
If the call intended to be executed is a delegatecall
, the contract's checkTransaction
stores the current list of owners. Then in the checkAfterExecution
it verifies that the list has not changed. The checkAfterExecution
function also checks that no modules have been added.
if (operation == Enum.Operation.DelegateCall) {
// case: DELEGATECALL
// We disallow delegatecalls to unapproved targets
if (!enabledDelegatecallTargets[to]) revert DelegatecallTargetNotEnabled();
// Otherwise record the existing owners and threshold for post-flight checks to ensure that Safe state has not
// been altered
_existingOwnersHash = keccak256(abi.encode(owners));
_existingThreshold = threshold;
_existingFallbackHandler = safe.getSafeFallbackHandler();
However, both of these assume that the owner or module to be added is added to the regular functions, which uses an array as a sorted list. The delegatecall
however allows to simply bypass the intended functions and change the mapping values in such way that an address is given owner rights without actually being in that ordered list.
This renders the following checks useless.
- Signers do a
delegatecall
- Said delegatecall adds a module by simply setting the storage value
modules[moduleAddr] = randomAddr
- This will bypass the
checkAfterExecution
checks and renders them useless.
Also breaks the following invariant which should never be broken
There should never be more than 1 module enabled on the safe
Users can bypass intended restrictions
Fix is non-trivial.