Skip to content

Latest commit

 

History

History
107 lines (91 loc) · 4.49 KB

File metadata and controls

107 lines (91 loc) · 4.49 KB
title description type network date loss_usd returned_usd tags subcategory vulnerable_contracts tokens_lost attacker_addresses malicious_token attack_block reproduction_command attack_txs sources
Furucombo
Hijacking proxy contexts through malicious delegatecalls
Exploit
ethereum
2021-02-27
15000000
0
business logic
context hijacking
0x17e8Ca1b4798B97602895f63206afCd1Fc90Ca5f
LINK
aWBTC
aWETH
USDC
cETH
0xb624e2b10b84a41687caec94bdd484e48d76b212
0x86765dde9304bEa32f65330d266155c4fA0C4F04
11940500
forge test --match-contract Exploit_Furucombo -vvv
0x8bf64bd802d039d03c63bf3614afc042f345e158ea0814c74be4b5b14436afb9

Step-by-step

  1. Set up a malicious contract
  2. Call AAVE through Furucombo and initialize it from Furucombo's POV
  3. Now your malicious contract is AAVE from Furucombo's POV
  4. Use Furucomob's DELEGATECALL to steal the tokens users had approved to Furucombo

Detailed Description

DELEGATE call is always dangerous, as it requires complete trust in the code that you are running the context of the caller contract. Its most common use is upgradability, and even there it has some nasty footguns one should be aware of.

But Furucombo uses DELEGATECALL in a way that is particularly dangerous: it allows users to DELEGATECALL into several contracts, as long as they are in a whitelist.

    /**
     * @notice The execution of a single cube.
     * @param _to The handler of cube.
     * @param _data The cube execution data.
     */
    function _exec(address _to, bytes memory _data)
        internal
        returns (bytes memory result)
    {
        require(_isValid(_to), "Invalid handler");
        _addCubeCounter();
        assembly {
            let succeeded := delegatecall(
                sub(gas(), 5000),
                _to,
                add(_data, 0x20),
                mload(_data),
                0,
                0
            )
            let size := returndatasize()

            result := mload(0x40)
            mstore(
                0x40,
                add(result, and(add(add(size, 0x20), 0x1f), not(0x1f)))
            )
            mstore(result, size)
            returndatacopy(add(result, 0x20), 0, size)

            switch iszero(succeeded)
                case 1 {
                    revert(add(result, 0x20), size)
                }
        }
    }

Now, one of these whitelisted contracts was AAVE. AAVE, as many other contracts, is upgradable: this means it is only itself a proxy that does DELEGATECALL to an implementation contract.

If the storage slot where the implementation address is not set, anyone can set it. From AAVE's perspective, this was set and all was working. But when Furucombo delegated the call, it is now using its storage to run AAVE's code. From this perspective, AAVE's was not initialized.

So now, the attacker only has to tell Furucombo to DELEGATECALL into AAVE and run its initialize() method, setting their own malicious EVIL AAVE as the implementation. Now, when calling AAVE, users would actually be interacting with the malicious contract, which can run arbitrary code in the context of Furucombo. The attacker used this to steal as many funds as possible.

Possible mitigations

  1. Be extremely careful when using DELEGATECALL
  2. Do not whitelist useless contracts. AAVE has no reason to be in the whitelist, as it actually did not work (it would not be able to find its implementation, balances, or anything else when run through Furucombo's Proxy)
  3. The attack was so profitable because there where many users who had approved Furucombo to use their funds in different tokens.

Related