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: 52/141
Findings: 2
Award: $163.93
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: bbrho
Also found by: 0xNazgul, Saintcode_, codexploder, infosec_us_team, s3cunda, zzzitron
https://github.com/code-423n4/2022-07-fractional/blob/main/src/Vault.sol#L38
Vault owners can install plugins via Vault.install()
, with calls to the installed plugins made through the vault's fallback function. Unlike the vault's external Vault.execute()
function, fallback()
imposes no checks on the permissions of the caller, assuming proper installation of the plugin by the owner at install time.
While this design seems intentional given NFTReceiver.sol
, it can lead to unintended vulnerabilities, like loss of vault NFTs, if the vault owner:
Example successful test similar to those from Vault.t.sol
below.
The original vault owner installs a transfer target plugin, with selector ERC721TransferFrom
on the vault.
Ownership is then transferred to bob, but the original owner uses the installed transfer plugin to steal
the NFT deposited in the vault and send it to alice (without bob's permission):
function testFallbackWithTransferPlugin() public { bytes memory data = setUpExecute(alice); // install transfer from on vault address[] memory plugins = new address[](1); bytes4[] memory selectors = new bytes4[](1); plugins[0] = address(transferTarget); selectors[0] = transferTarget.ERC721TransferFrom.selector; vaultProxy.install(selectors, plugins); // set bob as the owner vaultProxy.transferOwnership(bob.addr); // check vault originally has the nft assertEq(IERC721(erc721).balanceOf(vault), 1); assertEq(IERC721(erc721).balanceOf(alice.addr), 0); // execute the nft transfer via plugin with fallback (bool success,) = address(vaultProxy).call(data); // check this contract transferred nft out of vault to alice // even after bob became owner assertEq(IERC721(erc721).balanceOf(vault), 0); assertEq(IERC721(erc721).balanceOf(alice.addr), 1); }
Editor
Consider tracking which installed plugins might require permissions alongside the methods
mapping in storage. Potentially:
/// @notice Mapping of function selector to plugin address mapping(bytes4 => address) public methods; /// @notice Mapping of plugin address to whether permissions required mapping(address => bool) public auths;
with auths[plugin]
used in fallback()
.
#0 - stevennevins
2022-07-19T16:54:38Z
Duplicate of #266
#1 - HardlyDifficult
2022-08-11T18:29:22Z
A malicious owner is given a lot of flexibility which can be abused to steal funds. I believe this is a Medium risk issue and not a High as it was reported as by many of the dupe submissions because the issues all originate from either a malicious owner or a faulty plugin.
See also https://github.com/code-423n4/2022-07-fractional-findings/issues/266#issuecomment-1189217586
I'm selecting this submission as the primary for including a coded POC, helping to understand one potential path of abuse this could lead to.
🌟 Selected for report: xiaoming90
Also found by: 0x1f8b, 0x29A, 0x52, 0xA5DF, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xsanson, 0xsolstars, 242, 8olidity, Amithuddar, Aymen0909, Bnke0x0, BowTiedWardens, David_, Deivitto, ElKu, Funen, Hawkeye, IllIllI, JC, Kaiziron, Keen_Sheen, Kthere, Kulk0, Kumpa, Lambda, MEP, ReyAdmirado, Rohan16, Ruhum, Sm4rty, TomJ, Tomio, Treasure-Seeker, TrungOre, Tutturu, Viksaa39, Waze, _Adam, __141345__, ak1, apostle0x01, asutorufos, async, ayeslick, aysha, bbrho, benbaessler, berndartmueller, c3phas, cccz, chatch, cloudjunky, codexploder, cryptphi, delfin454000, dipp, durianSausage, dy, exd0tpy, fatherOfBlocks, hake, hansfriese, horsefacts, hubble, joestakey, jonatascm, kebabsec, kenzo, kyteg, mektigboy, neumo, oyc_109, pashov, pedr02b2, peritoflores, rajatbeladiya, rbserver, robee, rokinot, s3cunda, sach1r0, sahar, sashik_eth, scaraven, shenwilly, simon135, sorrynotsorry, sseefried, svskaushik, unforgiven, z3s, zzzitron
61.9486 USDC - $61.95
https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/protoforms/BaseVault.sol#L128
Deployment of BaseVault
through deployVault()
fails when _modules.length * leaves.length > 6
.
BaseVault.generateMerkleTree()
returns the hashes
list of fixed length 6, initialized in L128. But _modules.length * leaves.length
is variable in general (given the allowed deploy vault _modules
input), and could be > 6 resulting in an index error and failed deployment of the BaseVault instance. Migration.sol::generateMerkleTree()
seems to implement the logic around this appropriately.
The following test reverts at vault deployment with an Reason: Index out of bounds
:
function testGenerateMerkleTree() public { address[] memory nmodules = new address[](3); nmodules[0] = address(baseVault); nmodules[1] = address(buyoutModule); nmodules[2] = address(minter); vault = alice.baseVault.deployVault( TOTAL_SUPPLY, nmodules, nftReceiverPlugins, nftReceiverSelectors, mintProof ); (token, tokenId) = registry.vaultToToken(vault); assertEq(getFractionBalance(alice.addr), TOTAL_SUPPLY); }
Editor
Would replicate the logic in Migration.sol::generateMerkleTree()
which seems to implement the check of modules * leaves in initializing the hashes list appropriately.
If a length of 6 for hashes
list should be enforced for BaseVault
as a protoform template, should add a revert in the case _modules.length * leaves.length != 6 and/or simply require the modules given as input to deployVault to be minter and buyout. If not adding this revert, would further check whether empty elements for < 6 causes any issues with merkleRoot generation and module permissions.
#0 - 0x0aa0
2022-07-21T17:30:50Z
Duplicate of #447
#1 - HardlyDifficult
2022-08-22T19:20:50Z
Lowering sev and merging converting this into a QA report for the warden.