Platform: Code4rena
Start Date: 07/07/2022
Pot Size: $75,000 USDC
Total HM: 32
Participants: 141
Period: 7 days
Judge: HardlyDifficult
Total Solo HM: 4
Id: 144
League: ETH
Rank: 30/141
Findings: 2
Award: $535.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xA5DF
Also found by: 0x, 0xsanson, 242, Critical, sorrynotsorry, unforgiven, zzzitron
The implementation contract of Vault.sol
is deployed in the constructor function if VaultFactory
, but remains uninitialized.
This makes it possible for an attacker to call init()
the implementation contract of Valut.sol
and delegatecall to selfdestruct
and break all the Vaults.
This attack vector is made well-known by the $10m wormhole bug bounty: https://medium.com/immunefi/wormhole-uninitialized-proxy-bugfix-review-90250c41a43a
contract VaultFactory is IVaultFactory { /// @dev Use clones library for address types using Create2ClonesWithImmutableArgs for address; /// @notice Address of Vault proxy contract address public implementation; /// @dev Internal mapping to track the next seed to be used by an EOA mapping(address => bytes32) internal nextSeeds; /// @notice Initializes implementation contract constructor() { implementation = address(new Vault()); }
contract Vault is IVault, NFTReceiver { /// @notice Address of vault owner address public owner; /// @notice Merkle root hash of vault permissions bytes32 public merkleRoot; /// @notice Initializer value uint256 public nonce; /// @dev Minimum reserve of gas units uint256 private constant MIN_GAS_RESERVE = 5_000; /// @notice Mapping of function selector to plugin address mapping(bytes4 => address) public methods; /// @dev Initializes nonce and proxy owner function init() external { if (nonce != 0) revert Initialized(owner, msg.sender, nonce); nonce = 1; owner = msg.sender; emit TransferOwnership(address(0), msg.sender); }
Init the implementation contract right after creation:
constructor() { implementation = address(new Vault()); IVault(implementation).init(); }
#0 - ecmendenhall
2022-07-15T03:13:46Z
🌟 Selected for report: xiaoming90
Also found by: 0x29A, 0xDjango, 0xalpharush, Critical, Treasure-Seeker, ayeslick, infosec_us_team
/// @notice Transfers ERC-20 tokens /// @param _from Source address /// @param _to Target address /// @param _tokens[] Addresses of token contracts /// @param _amounts[] Transfer amounts function batchDepositERC20( address _from, address _to, address[] calldata _tokens, uint256[] calldata _amounts ) external { for (uint256 i = 0; i < _tokens.length; ) { IERC20(_tokens[i]).transferFrom(_from, _to, _amounts[i]); unchecked { ++i; } } } /// @notice Transfers ERC-721 tokens /// @param _from Source address /// @param _to Target address /// @param _tokens[] Addresses of token contracts /// @param _ids[] IDs of the tokens function batchDepositERC721( address _from, address _to, address[] calldata _tokens, uint256[] calldata _ids ) external { for (uint256 i = 0; i < _tokens.length; ) { IERC721(_tokens[i]).safeTransferFrom(_from, _to, _ids[i]); unchecked { ++i; } } } /// @notice Transfers ERC-1155 tokens /// @param _from Source address /// @param _to Target address /// @param _tokens[] Addresses of token contracts /// @param _ids[] Ids of the token types /// @param _amounts[] Transfer amounts /// @param _datas[] Additional transaction data function batchDepositERC1155( address _from, address _to, address[] calldata _tokens, uint256[] calldata _ids, uint256[] calldata _amounts, bytes[] calldata _datas ) external { unchecked { for (uint256 i = 0; i < _tokens.length; ++i) { IERC1155(_tokens[i]).safeTransferFrom( _from, _to, _ids[i], _amounts[i], _datas[i] ); } } }
There are several public methods in BaseVault.sol
with no access control, and it utilizes the approval given by the user to transfer tokens from an arbitrary address (typically the caller's own address) to another arbitrary address (supposed to be the address of a vault?).
But since it's an open method that anyone can call, it puts all the users who approved the BaseVault.sol
for whatever reason at risk for the funds in their wallets, because the attacker will call batchDepositERC20()
with the _from
address specified as the victim's address and transfer funds _to
their own address.
We failed to find any direct reference for these methods in other contracts, so presumably, these methods will be called directly from the frontend.
There should be no _from
parameter, and the "from" address of the transferFrom
should be hardcoded as msg.sender
.
#0 - stevennevins
2022-07-21T17:05:36Z
Duplicate of #468