hats-finance / Safe-0x2909fdefd24a1ced675cb1444918fa766d76bdac

A collection of modules that can be used with the Safe contract
GNU Lesser General Public License v3.0
0 stars 1 forks source link

Attacker can brick the creation of signers rendering thr factory useless. #28

Open hats-bug-reporter[bot] opened 4 months ago

hats-bug-reporter[bot] commented 4 months ago

Github username: @shealtielanz Twitter username: shealtielanz Submission hash (on-chain): 0x206c952bef7f13b55cfc44a6bf5945ffd021bfaa60a8030113e1516614386332 Severity: high

Description: Description

On the call to createSigner() the function uses the opcode extecodesize() to check if the contract has been created, however, an attacker can trick the factory into thinking that the signer has already been created when actually it hasn't been created, thereby making the calls to createSigner() useless as the signer cannot be created anymore.

this iszero(extcodesize(account)) check is insufficient to determine if an address has an existing code. According to EIP-1052, addresses without code only return a 0x0 codehash when they are empty:

In case the account does not exist or is empty (as defined by EIP-161) 0 is pushed to the stack.

In case the account does not have code the keccak256 hash of empty data (i.e. c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) is pushed to the stack.

As seen from above, addresses without code can also return keccak256("") as its codehash if it is non-empty. EIP-161 states that an address must have a zero ETH balance for it to be empty:

An account is considered empty when it has no code and zero nonce and zero balance.

As such, if anyone transfers 1 wei to an address, .codehash will return keccak256("") instead of bytes32(0), making the checks shown above skip incorrectly.

Attack Scenario

Mitigation

Fix for this is easy - just change from result := iszero(extcodesize(account)) to x.code.length != 0 (ie. Check the code length of the address instead.

0xEricTee commented 4 months ago

I've tested with foundry test, the mentioned issue does not exist.

EXTCODESIZE.t.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.24;

import {Test, console2} from "forge-std/Test.sol";

contract Testing {

}

contract EXTCODESIZETest is Test {

    address public user1;
    address public user2;
    Testing public testing_contract;

    function _hasNoCode(address account) public view returns (bool result) {
        // solhint-disable-next-line no-inline-assembly
        assembly ("memory-safe") {
            result := iszero(extcodesize(account))
        }
    }

    function setUp() public {
        user1 = makeAddr("USER1");
        user2 = makeAddr("USER2");

        deal(user2, 1); //fund user2 with 1 wei.
        testing_contract = new Testing();

    }

    function test_AccountHasNoCode() public {
        console2.log(_hasNoCode(user1)); //true

        console2.log("ETH balance of user 2: ", user2.balance);

        console2.log(_hasNoCode(user2));

        console2.log(_hasNoCode(address(testing_contract)));

    }

}

Result:

Running 1 test for test/EXTCODESIZE.t.sol:EXTCODESIZETest
[PASS] test_AccountHasNoCode() (gas: 18585)
Logs:
  true
  ETH balance of user 2:  1
  true
  false

Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.82ms

Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Noticed that the function _hasNoCode returned true for user2 with balance of 1 wei, therefore the DoS situation is not valid.

nlordell commented 4 months ago

extcodesize is for getting the code length of an external account, and thus equivalent to account.code.length.

I will mark this as invalid.