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: 6/141
Findings: 10
Award: $3,913.01
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: 0xA5DF
Also found by: 0x, 0xsanson, 242, Critical, sorrynotsorry, unforgiven, zzzitron
267.7106 USDC - $267.71
HIGH - Assets can be lost directly
Anybody can initialize the Vault's implementation contract. The worst case would be to selfdestruct
and make all the (already deployed and to be deployed) Vault's proxies useless and assets in the deployed proxies will be lost.
setUp
, due to limitation of foundry. The code size will be checked in the testInitAndDestroy_poc
.In the proof of concept, alice calls init
on the Vault implementation and takes the ownership (line 38). Alice can now install a malicious plugin, the SelfdestructPlugin
located in the above of the test contract (line 6-10). Then simply calling the destroy
function on the Vault implementation (line 54). Alice could simply renounce her ownership to zero address to bypass the check on ownership change.
The main cause is the Vault implementation was not initialized. The deploy scripts also do not call init
on deployed Vault implementation. It is safer and easier to init
right after the contract is deployed.
// VaultFactory::constructor // Vault implementation is deployed but not initialized 20 constructor() { 21 implementation = address(new Vault()); 22 }
The problem could be exploited even further because any functionality could be installed as plugins. Also lack of zero address check for transferring ownership made it simple to bypass the check on ownership change.
foundry
Add init
after deploy the vault implementation.
// VaultFactory::constructor // to mitigate, add init after deploy 20 constructor() { 21 implementation = address(new Vault()); // implementation.init(); 22 }
#0 - ecmendenhall
2022-07-15T03:13:55Z
440.6759 USDC - $440.68
https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L211 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L343 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L367 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L393 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L420
HIGH - Assets can be stolen directly When there are multiple proposals for a vault, a failed proposal can withdraw the assets.
The proof of concept shows a scenario alice is taking assets.
end
on the buyoutmoduleAlice can install any plugins and modules she wants in her migration proposal. So as soon as the assets are in her vault, she can easily transferred out of it.
The cause of it is
proposal.isCommited
is never unset even after the proposal fails. It enabled to reuse failed old proposal to withdrawfoundry
Functions which withdraws from old vault to new vault should explicitly check the status of proposal, i.e. it is the proposal with successful buyout.
#0 - stevennevins
2022-07-20T17:57:39Z
Duplicate of #619
#1 - HardlyDifficult
2022-08-02T21:24:27Z
🌟 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
HIGH - Assets can be stolen directly. An attacker can steal eth from buyout module
The proof of concept1 shows that the same amount of fractions will result in different amount of eth upon cash out. The first cash out for 1000 FERC1155 token will result in 0.2 ETH. Bob cashes out 1000 FERC1155 again, and he will get 0.2666.. ETH. The increased payout is coming from other buyout processes, so essentially bob is stealing eth by dividing the payout.
The proof of concept2 demonstrates a scenario, where one can drain eth from buyout module using a vault with supply target as plugin. The plugin enables to mint more FERC1155 tokens on the way.
For both cases the main issue is that the ethBalance
is not properly updated:
// modules/Buyout.sol::cash // the balance should be updated // Mitigation idea: update the ethBalance // ethBalance -= buyoutShare; 266 // Transfers buyout share amount to caller based on total supply 267 uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault); 268 uint256 buyoutShare = (tokenBalance * ethBalance) / 269 (totalSupply + tokenBalance); 270 _sendEthOrWeth(msg.sender, buyoutShare);
However, the existence of plugins made the issue a lot easier to exploit.
foundry
Update the ethBalance
before sending ether.
#0 - ecmendenhall
2022-07-15T03:02:12Z
🌟 Selected for report: xiaoming90
Also found by: 0x52, Lambda, cccz, codexploder, hyh, kenzo, oyc_109, zzzitron
214.1685 USDC - $214.17
https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L334-L350 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Buyout.sol#L343-L367
HIGH - Assets can be lost directly After proposal and proposed buyout is successful, anyone can transfer ERC20 asset in the vault to the zero address and the asset will be lost.
The proof of concept demonstrates a scenario, where alice is sending ERC20 asset from bob's vault to zero address, so the asset is lost.
end
on buyoutModulemigrateVaultERC20
with proposalId 2 and send ERC20 to the zero address// modules/Migration.sol // in the line 341, zero address for newVault was not checked 334 function migrateVaultERC20( 335 address _vault, 336 uint256 _proposalId, 337 address _token, 338 uint256 _amount, 339 bytes32[] calldata _erc20TransferProof 340 ) external { 341 address newVault = migrationInfo[_vault][_proposalId].newVault; 342 // Withdraws an ERC-20 token from the old vault and transfers to the new vault 343 IBuyout(buyout).withdrawERC20( 344 _vault, 345 _token, 346 newVault, 347 _amount, 348 _erc20TransferProof 349 ); 350 }
The migrateVaultERC20
does not check whether the newVault
is zero address or not. It just sends ERC20 to the newVault. The buyout module does not check whether to
address is zero either: (code). The target Transfer does not check it, either.
Some ERC20 tokens will refuse to send to zero address, but some will just send it. For example, the MockERC20 based on solmate's ERC20 does not check for zero address. And The standard does not require to throw upon sending to zero address.
Migrate functions for ERC721 and ERC1155 are safe for this issue because they must throw upon sending to zero address. Also The Target Transfer for ERC1155 uses safeTransferFrom.
foundry
Add zero address check in the migrateVaultERC20 for the address newVault
.
Also add to check whether the propose was successful. The lack of check can be exploited in a different way combined with other issues. (see the issue Migration Module: The assets can be taken by a failed proposal
)
#0 - stevennevins
2022-07-20T15:25:30Z
Duplicate of #649
#1 - HardlyDifficult
2022-08-02T23:53:50Z
🌟 Selected for report: infosec_us_team
Also found by: 0x29A, 0xsanson, BowTiedWardens, Lambda, MEP, PwnedNoMore, Treasure-Seeker, TrungOre, berndartmueller, panprog, shenwilly, smiling_heretic, xiaoming90, zzzitron
68.2907 USDC - $68.29
HIGH - Assets can be compromised directly. One can use eth from the module for buyout process. (Basically sending somebody else's eth from migration module to buyout module)
The proof of concepts shows a scenario where one can use the eth from module for buyout. 0. setup: some people are using migration module, so some eth are depositted in the module.
join
and send some ethleave
function to leave, but instead she uses withdrawContribution
commit
, somebody else's eth will be used in buyout.Alice will not be able to get her fractions. But it can be combined with other issues of buyout, where one can drain eth from buyout module. Also, from the point of view of the other users using the migration modules, their money is stolen.
// modules/Migration.sol::withdrawContribution // The check below allows to use withdrawContribution even before buyout kickoff // Mitigation idea: add a check `proposal.isCommited == true` 302 if ( 303 current != State.INACTIVE || 304 migrationInfo[_vault][_proposalId].newVault != address(0) 305 ) revert NoContributionToWithdraw(); // modules/Migration.sol::withdrawContribution // `proposal.totalEth` is not updated // Mitigation idea: update `proposal.totalEth -= userEth;` 320 // Temporarily store user's eth for the transfer 321 uint256 userEth = userProposalEth[_proposalId][msg.sender]; 322 // Udpates ether balance of caller 323 userProposalEth[_proposalId][msg.sender] = 0;
foundry
proposal.isCommited == true
proposal.totalEth
#0 - mehtaculous
2022-07-20T18:10:31Z
Duplicate of #27
2014.9788 USDC - $2,014.98
HIGH - Assets can be compromised directly. One can drain eth out from migration module to buyout module using custom made FERC1155 token.
The proof of concept shows a scenario where alice is draining migration module using custom made FERC1155 token.
evil_token
)evil_token
commit
, the evil_token
will reenter commit
and send money to buyout moduleNote: For a simplicity, the evil_token
reenters for a fixed number of times. But one can adjust to drain all the eth in the migration module.
Note2: For now the eth is in the buyout module, but given the current implementation of buyout
module, the same actor can drain eth from buyout.
The commit
function is not written in Checks, Effects, Interactions (CEI) patterns.
// modules/Migration.sol::commit // proposal.isCommited and started are set after the out going calls (i.e. start, setApprovalFor) // Mitigation idea: set the values before the out going calls 206 if (currentPrice > proposal.targetPrice) { 207 // Sets token approval to the buyout contract 208 IFERC1155(token).setApprovalFor(address(buyout), id, true); 209 // Starts the buyout process 210 IBuyout(buyout).start{value: proposal.totalEth}(_vault); 211 proposal.isCommited = true; 212 started = true; 213 }
foundry
Follow Checks, Effects, Interactions patterns. One can also consider adding reentrancy guard.
#0 - HardlyDifficult
2022-08-11T16:12:54Z
The 1155 callback could be used to reentrancy and steal funds. Agree this is high risk.
🌟 Selected for report: shenwilly
Also found by: 0x52, Lambda, MEP, Treasure-Seeker, TrungOre, codexploder, dipp, kenzo, panprog, smiling_heretic, xiaoming90, zzzitron
97.2803 USDC - $97.28
https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L116-L118 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L148-L150 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L189-L191
MED - the function of the protocol could be impacted.
Anyone can call Buyout::start
to disable to join
, leave
, commit
functions for migration proposal
The Buyout module is unaware of migration module. So, even when some migration is proposed, anybody can start a buyout process. On the other hand, one cannot use propose
, leave
, join
, commit
, withdrawContribution
from Migration module when a buyout for the relevant vault is active.
Using this, a malicious actor can start buyout process just to prevent people from using these functions. For example, somebody proposed a migration, and an attacker starts a buyout so that none can join
.
For another example, somebody proposed a migration and sent some eth with join
function. Then an attacker starts a buyout. As the result, the eth is locked during the buyout period. The attacker can call end
and start
to keep the asset locked.
// modules/Migration.sol::join // The buyout state is checked 115 // Reverts if buyout state is not inactive 116 (, , State current, , , ) = IBuyout(buyout).buyoutInfo(_vault); 117 State required = State.INACTIVE; 118 if (current != required) revert IBuyout.InvalidState(required, current);
none
One idea for mitigation would be to prevent to start buyout when there is a proposal. But this weakens the modularity and it can be used to disable the buyout functionality.
Another idea would be not checking the buyout state for functions like join
, leave
. However, it may open up some attack vectors.
#0 - stevennevins
2022-07-20T16:45:39Z
#1 - HardlyDifficult
2022-08-11T16:31:33Z
🌟 Selected for report: zzzitron
Also found by: unforgiven
604.4936 USDC - $604.49
MED - a hypothetical attack path with stated assumptions, but external requirements.
Attacker can create a vault with successful buyout status and non zero supply. The attacker can sell the fractions and then simply withdraw the assets.
evil_redeemer
: it will deployVault and calls redeem when the minted FERC1155 token is receivedstart
will start the process : baseVault.deployVault
evil_redeemer
evil_redeemer
calls redeem
and set the state to SUCCESS
Now, the attacker can send in some assets to the vault and sell the fractions. Then, the attacker can withdraw any asset at any time from the vault using the buyout module, because the state is already SUCCESS
.
Note: An attacker can achieve the similar result with plugins. However, users might just refuse to buy tokens associated with such vaults and plugins. With the current issue, the users who is only looking at the vault will not notice this possibility unless they also check the status in the buyout module for the vault.
// FERC1155.sol::mint // totalSupply is updated after _mint // _mint contains out going call if the `_to` address's codesize is non zero // Mitigation idea: update totalSupply before _mint 79 function mint( 80 address _to, 81 uint256 _id, 82 uint256 _amount, 83 bytes memory _data 84 ) external onlyRegistry { 85 _mint(_to, _id, _amount, _data); 86 totalSupply[_id] += _amount; 87 }
foundry
update totalSupply
before _mint
.
#0 - stevennevins
2022-07-18T19:02:23Z
Duplicate of #573
#1 - HardlyDifficult
2022-08-08T19:28:58Z
Since totalSupply is updated after an external call, a vault can be created with an incorrect buyout status. Agree that this is medium risk.
🌟 Selected for report: bbrho
Also found by: 0xNazgul, Saintcode_, codexploder, infosec_us_team, s3cunda, zzzitron
101.985 USDC - $101.98
https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/Vault.sol#L73-L82 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/VaultRegistry.sol#L102-L112
HIGH - Assets can be stolen/compromised/lost directly. The creator of vault can add any functionality they want by plugins. Also they can bring any tokens for the vault. It can be used against users, or it will make exploits easier to execute.
Plugins are very powerful and can be used maliciously.
As a concrete example, a plugin with minting functionality can be combined with the existing bug to make it much more effective to drain eth from buyout module (for more detailed report, see "Buyout Module: ethBalance
is not properly updated").
Here is the summary:
Users would have to check which plugins are used in a vault to know whether it is secure, which will hurt the confidence of the users.
Similarly a vault creator can bring their own token by using createInCollection
. This enables many attack vectors possible. For example, this proof of concept uses a custom made token to make the attack possible. (More detailed explanation is in the report "Migration Module: Re-enter commit
using custom token").
none
Limit the usage of plugins. idea 1. only certain white listed plugins can be used idea 2. only white listed creators can use plugins idea 3. get rid of the plugin functionalities
#0 - stevennevins
2022-07-19T16:36:26Z
Duplicate of #266
#1 - HardlyDifficult
2022-08-11T18:30:04Z
🌟 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.9433 USDC - $61.94
Risk | Title |
---|---|
L00 | Not following Check-Effect-Interaction patterns |
L01 | Usage of transfer to send eth |
L02 | Lack of zero address check |
L03 | Unresolved TODO |
There are multiple occasions where Check-Effect-Interaction pattern was not followed. The pattern is a way to avoid reentrancy attack.
transfer
to send ethUsing transfer
to send eth is not anymore recommended as it has a fixed gas stipend of 2300. Use call
instead.
file | function | note | link |
---|---|---|---|
modules/Migration.sol | leave | https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L172 | |
modules/Migration.sol | withdrawContribution | https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L325 |
There are missing zero address check.
file | function | note | link |
---|---|---|---|
Vault.sol | transferOwnership | https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/Vault.sol#L95 | |
modules/Buyout.sol | constructor | https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Buyout.sol#L47-L49 | |
modules/Buyout.sol | withdrawERC20 | https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Buyout.sol#L329-L334 | |
modules/Buyout.sol | withdrawERC721 | https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Buyout.sol#L361-L366 | |
modules/Migration.sol | constructor | https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L58-L59 | |
modules/Migration.sol | migrateVaultERC20 | https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L341 |
./utils/MerkleBase.sol-22- ./utils/MerkleBase.sol-23- assembly { ./utils/MerkleBase.sol:24: // TODO: This can be aesthetically simplified with a switch. Not sure it will ./utils/MerkleBase.sol-25- // save much gas but there are other optimizations to be had in here. ./utils/MerkleBase.sol-26- if or(lt(_left, _right), eq(_left, _right)) {
#0 - HardlyDifficult
2022-08-22T19:32:14Z