# Writeup de "Funds Secured" | Blockchain

## **Descripcion del Challenge:**

En Arodor, un programa de crowdfunding de última generación impulsó investigaciones innovadoras. Impulsado por un smart contract, el programa tenía como objetivo recaudar fondos. Un consejo directivo supervisaba esta campaña, siendo responsable de finalizar el programa mediante un esquema de wallet multi-firma. Tu objetivo es explotar el contrato y robar los fondos, representando una amenaza para la noble misión científica de Arodor.

## **Info:**

```plaintext
Private key           :  0x8c5791a5eedf0f28562d0e863116e4a1a19825c32606ca5a1efeabfbe3958cc5
Address               :  0xbD2297b04860d23a168421f5196f7EE0AAcF926a
Crowdfunding contract :  0xaf9955Fe74687503AFDa90fF9a3bd3F57F03F470
Wallet contract       :  0x454B7c647787819D73964E2a42C02480D7771B81
Setup contract        :  0x1364282cC6Eff85cED108ac337D330492891a086
```

Seteo variables para trabajar comodamente:

```plaintext
Private='0x8c5791a5eedf0f28562d0e863116e4a1a19825c32606ca5a1efeabfbe3958cc5'
Address='0xbD2297b04860d23a168421f5196f7EE0AAcF926a'
Crowdfunding='0xaf9955Fe74687503AFDa90fF9a3bd3F57F03F470'
Wallet='0x454B7c647787819D73964E2a42C02480D7771B81'
Setup='0x1364282cC6Eff85cED108ac337D330492891a086'
ETH_RPC_URL='94.237.53.203:47563'
```

Con eso configurado, descargue el challenge y dejé todo listo para empezar. El árbol de archivos en mi carpeta quedó así:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1754022172911/b9b02652-350c-40d1-98a9-6866f540356b.png align="center")

Acá es donde voy a empezar a leer y entender bien la lógica del sistema de votación y la función crítica `closeCampaign()` que es, básicamente, donde va a estar el bug que nos permite vaciar los fondos.

## **Campaign.sol**

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.18;

import {ECDSA} from "./lib/ECDSA.sol";

/// @notice MultiSignature wallet used to end the Crowdfunding and transfer the funds to a desired address
contract CouncilWallet {
    using ECDSA for bytes32;

    address[] public councilMembers;

    /// @notice Register the 11 council members in the wallet
    constructor(address[] memory members) {
        require(members.length == 11);
        councilMembers = members;
    }

    /// @notice Function to close crowdfunding campaign. If at least 6 council members have signed, it ends the campaign and transfers the funds to `to` address
    function closeCampaign(bytes[] memory signatures, address to, address payable crowdfundingContract) public {
        address[] memory voters = new address[](6);
        bytes32 data = keccak256(abi.encode(to));

        for (uint256 i = 0; i < signatures.length; i++) {
            // Get signer address
            address signer = data.toEthSignedMessageHash().recover(signatures[i]);

            // Ensure that signer is part of Council and has not already signed
            require(signer != address(0), "Invalid signature");
            require(_contains(councilMembers, signer), "Not council member");
            require(!_contains(voters, signer), "Duplicate signature");

            // Keep track of addresses that have already signed
            voters[i] = signer;
            // 6 signatures are enough to proceed with `closeCampaign` execution
            if (i > 5) {
                break;
            }
        }

        Crowdfunding(crowdfundingContract).closeCampaign(to);
    }

    /// @notice Returns `true` if the `_address` exists in the address array `_array`, `false` otherwise
    function _contains(address[] memory _array, address _address) private pure returns (bool) {
        for (uint256 i = 0; i < _array.length; i++) {
            if (_array[i] == _address) {
                return true;
            }
        }
        return false;
    }
}

contract Crowdfunding {
    address owner;

    uint256 public constant TARGET_FUNDS = 1000 ether;

    constructor(address _multisigWallet) {
        owner = _multisigWallet;
    }

    receive() external payable {}

    function donate() external payable {}

    /// @notice Delete contract and transfer funds to specified address. Can only be called by owner
    function closeCampaign(address to) public {
        require(msg.sender == owner, "Only owner");
        selfdestruct(payable(to));
    }
}
```

## **Setup.sol**

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.18;

import {Crowdfunding} from "./Campaign.sol";
import {CouncilWallet} from "./Campaign.sol";

contract Setup {
    Crowdfunding public immutable TARGET;
    CouncilWallet public immutable WALLET;

    constructor() payable {
        // Generate the councilMember array
        // which contains the addresses of the council members that control the multi sig wallet.
        address[] memory councilMembers = new address[](11);
        for (uint256 i = 0; i < 11; i++) {
            councilMembers[i] = address(uint160(i));
        }

        WALLET = new CouncilWallet(councilMembers);
        TARGET = new Crowdfunding(address(WALLET));

        // Transfer enough funds to reach the campaing's goal.
        (bool success,) = address(TARGET).call{value: 1100 ether}("");
        require(success, "Transfer failed");
    }

    function isSolved() public view returns (bool) {
        return address(TARGET).balance == 0;
    }
}
```

## **Entendiendo el objetivo y su lógica**

El objetivo basicamente es: **vaciar el contrato Crowdfunding** para que *isSolved()* de **true**.  
Esto se evalua en **SETUP.SOL**:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1754022223845/99162a8a-bb7f-44eb-b8f1-028cc36e2cbb.png align="center")

Ese **TARGET** es simplemente una instancia del contrato de **Crowdfunding** a la que se le transfirieron **1100 ETH** cuando se deployea el **SETUP**. Osea, hay platita y hay que robarla.

## **¿Podemos interactuar directo con** `Crowdfunding`?

Nop. El contrato **Crowdfunding** (que está definido dentro del archivo `Campaign.sol`), tiene una funcion publica llamada `closeCampaign(address to)` pero esta protegida como “Only owner”. Y el único que puede pasar esa verificación es el `CouncilWallet`, que fue seteado como owner durante el deployment.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1754022246691/f424f6bf-c992-4516-a8d7-1a75cc88a719.png align="center")

## **El Bug?**

Cuando miraba la función `closeCampaign` dentro de `CouncilWallet`, en un principio parece segura. Te pide una lista de firmas, chequea que cada firma sea de un miembro del consejo, que no esten duplicadas, etc.

Sin embargo hay un pequeño detalle interesante y es que **en ningun momento se valida que haya al menos 6 firmas en el array!**.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1754022275220/7436bdfc-76c5-4bca-bc76-aa04c6cb0671.png align="center")

Ese `if (i > 5)` corta el loop si ya se tiene 6 firmas, pero **no exige que las haya!**. Esto valida que podriamos enviar una lista totalmente vacia haciendo que no se chequee nada.. luego se terminaria llamando a:

```solidity
Crowdfunding(crowdfundingContract).closeCampaign(to);
```

Y como `CouncilWallet` es el owner del contrato `Crowdfunding`, la llamada pasa el `require(msg.sender == owner)`, y se ejecuta:

```solidity
selfdestruct(payable(to));
```

Por ende, se destruye el contrato y manda los **1100 ETH** a donde le digamos.

## **Explotacion**

Entonces, la solucion es mas simple de lo que parece.. Solo hay que hacer una llamada a `closeCampaign(...)` desde nuestro address con los siguientes requisitos:

* Un array vacío (`[]`) como firma
    
* La dirección `CouncilWallet` como destino (para simplificar, se podria usar nuestro address tambien si queres recuperar los fondos, pero como el reto nos pide que el balance de `Crowdfunding` sea 0, con CouncilWallet alcanza)
    
* Y por ultimo la address del contrato `Crowdfunding`
    

```solidity
cast send $Wallet 'closeCampaign(bytes[] memory, address, address)' '[]' $Wallet $Crowdfunding --private-key $Private
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1754022307894/0d2b2d29-c264-45b9-a38a-05253091e8e5.png align="center")

  
Si verificamos la funcion *isSolved()*:

```solidity
cast call $Setup 'isSolved() (bool)'
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1754022322348/bd10fb6a-46c7-42cd-9949-dc6cd671c3ea.png align="center")

  
Nos da **TRUE**, solo resta ir por nuestra flag :)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1754022330059/cc08d1cc-3589-4deb-88ab-24acae185649.png align="center")
