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: 91/141
Findings: 2
Award: $76.58
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xA5DF
Also found by: 0x52, 0xDjango, 0xsanson, Lambda, PwnedNoMore, Ruhum, Treasure-Seeker, async, berndartmueller, cccz, hubble, kenzo, scaraven, shenwilly, sseefried, xiaoming90
14.6423 USDC - $14.64
The creation of a legitimate auction may be denied by a malicious front-runner indefinitely. This is because a vault may only have one active buyout at a given time.
Alice calls start
in the Buyout module to initiate an auction for a vault. Along with her fractional tokens, she includes 10 Eth of value for the pool.
Bob, an attacker observing the mempool, can front-run Alice's transaction to start an auction with a price that is far below the vault's market rate.
Bob includes .0001 Eth of value for the pool and submits his transaction with a higher gas fee than Alice to ensure it is included ahead of her transaction in the next block.
Bob has effectively blocked other users from starting good faith auctions for the duration of his auction which is 6 days (2 days of proposal period + 4 days of rejection period).
Bob can repeat this attack indefinitely as long as he is willing to pay a slightly higher gas fee to have his transaction included ahead of a legitimate auction proposers.
Rationale for Medium-severity impact: While the likelihood of this may be low, the impact is high because valid auctions can be prevented from being successfully created indefinitely and will lead to a DoS against the protocol’s functioning. So, with low likelihood and high impact, the severity (according to OWASP) is medium.
Manual analysis
Mitigate this DoS vector by allowing multiple buyouts at the same time since users will naturally gravitate towards the auction that nets them the highest return.
#0 - 0x0aa0
2022-07-18T16:42:28Z
Duplicate of #87
#1 - HardlyDifficult
2022-08-02T21:54:38Z
🌟 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.9383 USDC - $61.94
It is possible to move the state to success
by having only 50.1% of the total circulating tokens in the pool.
The documentation for end()
states, "if the current pool has at least 51% of the total circulating supply, then the buyout is considered a success. The remaining fractions in the pool are then burned, allowing the proposer to safely withdraw the underlying assets from the vault."
However, it is possible to call end()
and move the state to SUCCESS
by having only 50.1% of the total circulating tokens in the pool.
For example, assume tokenBalance == 50_100
and IVaultRegistry(registry).totalSupply(_vault) == 100_000
. Using these values in the formula below, the result is 501 which equates to 50.1% of the total circulating supply.
// L184 function end(address _vault, bytes32[] calldata _burnProof) external { ... // L210 uint256 tokenBalance = IERC1155(token).balanceOf(address(this), id); // Checks totalSupply of auction pool to determine if buyout is successful or not if ( (tokenBalance * 1000) / IVaultRegistry(registry).totalSupply(_vault) > 500 ){...} }
Below is a simple fuzz test to validate the percentage formula.
pragma solidity 0.8.9; import "lib/forge-std/src/Test.sol"; contract Fractional{ function checkMath (uint tokenBalance, uint totalSupply)public pure returns(uint percent){ require (tokenBalance > 0); require (totalSupply > tokenBalance); percent = (tokenBalance * 1000 / totalSupply); } } contract FractionalTest is Test { Fractional frac; function setUp() public { frac = new Fractional(); } function testMath(uint tokenBalance) public { vm.assume(tokenBalance > 0); vm.assume(tokenBalance <= 100_000); uint percent = frac.checkMath(tokenBalance,100_000); assert(percent < 501 || percent > 509); } }
VSCode, Foundry
Consider changing the code in the above if-block to be > 509
or alternatively >= 510
for improved readability and a small increase in gas used.
===========================================================================
In Vault the install
function does not validate the passed array lengths which may lead to unintended storage outcomes.
The same issue is present in the BaseVault batch deposit functions: batchDepositERC20
, batchDepositERC721
and batchDepositERC1155
.
In Vault, if a caller passes one bytes4 _selectors
and two _plugins
addresses to install()
then the second _plugins
address will not be registered to a methods
mapping. This is because the for loop iterates based on the length of the _selectors
array.
function install(bytes4[] memory _selectors, address[] memory _plugins) external { if (owner != msg.sender) revert NotOwner(owner, msg.sender); uint256 length = _selectors.length; for (uint256 i = 0; i < length; i++) { methods[_selectors[i]] = _plugins[i]; } emit InstallPlugin(_selectors, _plugins); }
Manual analysis
Consider adding require
statements in functions to validate user-controlled data input and to ensure that array lengths are equal.
The documentation for the Buyout module states that "A fractional owner starts an auction for a vault by depositing any amount of ether and fractional tokens into a pool."
However, in start()
the amount of fractional tokens a user must deposit is the entire balance of msg.sender
.
uint256 depositAmount = IERC1155(token).balanceOf(msg.sender, id);
N/A
Manual Analysis
Update the documentation to make it clear that a user is required to deposit all of their fractional tokens into a pool.
#0 - HardlyDifficult
2022-08-05T12:14:28Z