• YouTube Channel
  • System Status
  • VS Code Extension
  • Arbitrary Address Spoofing Attack with ERC-2771 and Multicall

    Overview

    What is the Arbitrary Address Spoofing Attack vulnerability?

    Over the years since its initial release, the Ethereum platform has seen the development of numerous standards for smart contract functionality. In late 2023, it was discovered that two of these standards, ERC-2771 and Multicall have a malign interaction that can allow for spoofing the originating address of a transaction. While OpenZeppelin made a public disclosure regarding their implementations of these standards, others may be affected as well.

    In brief, the ERC-2771 allows for "meta-transactions" where an on-chain trusted entity forwards transactions on behalf of other addresses. Data forwarded by the trusted entity is assumed to be trusted data, and the originating address as represented by the entity is assumed to be validated as the address that should be considered the sender of the meta-transaction. The Multicall standard allows for a contract to allow batched calls to its own functionality by delegating calls to itself. If these two standards are implemented in tandem, it is possible that an attacker could forward a call through a trusted relay to the multicall mechanism, and the calldata for the multicall could contain spoofed addresses that were never validated by the forwarder. This allows for the attacker to perform actions as the spoofed account, such as transfer of funds.

    Further reading: OpenZeppelin: Arbitrary Address Spoofing Attack Disclosure

    Technical example of vulnerable code

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      // Minimal implementations of base functionality from OpenZeppelin
    
      abstract contract Context {
          function _msgSender() internal view virtual returns (address) {
              return msg.sender;
          }
    
          function _msgData() internal view virtual returns (bytes calldata) {
              return msg.data;
          }
    
          function _contextSuffixLength() internal view virtual returns (uint256) {
              return 0;
          }
      }
    
      library Address {
          error AddressEmptyCode(address target);
          error FailedCall();
    
          function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
              (bool success, bytes memory returndata) = target.delegatecall(data);
              return verifyCallResultFromTarget(target, success, returndata);
          }
    
          function verifyCallResultFromTarget(
              address target,
              bool success,
              bytes memory returndata
          ) internal view returns (bytes memory) {
              if (!success) {
                  _revert(returndata);
              } else {
                  if (returndata.length == 0 && target.code.length == 0) {
                      revert AddressEmptyCode(target);
                  }
                  return returndata;
              }
          }
    
          function _revert(bytes memory returndata) private pure {
              if (returndata.length > 0) {
                  assembly {
                      let returndata_size := mload(returndata)
                      revert(add(32, returndata), returndata_size)
                  }
              } else {
                  revert FailedCall();
              }
          }
      }
    
      // Vulnerable when used in conjunction: ERC-2771 and Multicall
    
      contract ERC2771Context is Context {
          address private immutable _trustedForwarder;
    
          constructor(address trustedForwarder_) {
              _trustedForwarder = trustedForwarder_;
          }
    
          function trustedForwarder() public view virtual returns (address) {
              return _trustedForwarder;
          }
    
          function isTrustedForwarder(address forwarder) public view virtual returns (bool) {
              return forwarder == trustedForwarder();
          }
    
          function _msgSender() internal view virtual override returns (address) {
              uint256 calldataLength = msg.data.length;
              uint256 contextSuffixLength = _contextSuffixLength();
              if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) {
                  return address(bytes20(msg.data[calldataLength - contextSuffixLength:]));
              } else {
                  return super._msgSender();
              }
          }
    
          function _msgData() internal view virtual override returns (bytes calldata) {
              uint256 calldataLength = msg.data.length;
              uint256 contextSuffixLength = _contextSuffixLength();
              if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) {
                  return msg.data[:calldataLength - contextSuffixLength];
              } else {
                  return super._msgData();
              }
          }
    
          function _contextSuffixLength() internal view virtual override returns (uint256) {
              return 20;
          }
      }
    
      contract Multicall is Context {
          function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
              bytes memory context = msg.sender == _msgSender()
                  ? new bytes(0)
                  : msg.data[msg.data.length - _contextSuffixLength():];
    
              results = new bytes[](data.length);
              for (uint256 i = 0; i < data.length; i++) {
                  results[i] = Address.functionDelegateCall(address(this), bytes.concat(data[i], context));
              }
              return results;
          }
      }
    
      contract SimpleDeFiApplication is ERC2771Context(address(0x01234)), Multicall {
          mapping(address => uint256) public etherBalances;
    
          function depositEther() external payable {
              etherBalances[_msgSender()] += msg.value;
          }
    
          function transferEther(address _to, uint256 _amount) public {
              require(etherBalances[_msgSender()] >= _amount, "Insufficient balance");
              etherBalances[_msgSender()] -= _amount;
              payable(_to).transfer(_amount);
          }
    
          // Override _msgSender, _msgData, and _contextSuffixLength to use ERC2771Context logic
          function _msgSender() internal view override(Context, ERC2771Context) returns (address sender) {
              return ERC2771Context._msgSender();
          }
    
          function _msgData() internal view override(Context, ERC2771Context) returns (bytes calldata) {
              return ERC2771Context._msgData();
          }
    
          function _contextSuffixLength() internal view override(Context, ERC2771Context) returns (uint256){
              return ERC2771Context._contextSuffixLength();
          }
      }
    
    

    In the example above, contract SimpleDeFiApplication which implements some functionality for ether management inherits from both ERC2771Context and Multicall. This could leave it vulnerable to spoofing attacks that call function transferEther() to send ether deposited by accounts other than the transaction originator, potentially resulting in loss or theft.

    Technical example of how to fix the vulnerability

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      // Base contracts and libraries omitted for brevity
    
      contract SimpleDeFiApplicationUpdated is ERC2771Context(address(0x01234)) {
          mapping(address => uint256) public etherBalances;
    
          function depositEther() external payable {
              etherBalances[_msgSender()] += msg.value;
          }
    
          function transferEther(address _to, uint256 _amount) public {
              require(etherBalances[_msgSender()] >= _amount, "Insufficient balance");
              etherBalances[_msgSender()] -= _amount;
              payable(_to).transfer(_amount);
          }
    
          // Override _msgSender, _msgData, and _contextSuffixLength to use ERC2771Context logic
          function _msgSender() internal view override(ERC2771Context) returns (address sender) {
              return ERC2771Context._msgSender();
          }
    
          function _msgData() internal view override(ERC2771Context) returns (bytes calldata) {
              return ERC2771Context._msgData();
          }
    
          function _contextSuffixLength() internal view override(ERC2771Context) returns (uint256){
              return ERC2771Context._contextSuffixLength();
          }
      }
    
    

    In the revised example above, contract SimpleDeFiApplicationUpdated now no longer implements Multicall, having chosen instead to allow for meta-transactions only, and avoiding the potential for malign interaction between these two standards in a contract that handles funds on behalf of users.