Platform: Code4rena
Start Date: 31/01/2023
Pot Size: $90,500 USDC
Total HM: 47
Participants: 169
Period: 7 days
Judge: LSDan
Total Solo HM: 9
Id: 211
League: ETH
Rank: 12/169
Findings: 2
Award: $1,543.97
π Selected for report: 1
π Solo Findings: 1
π Selected for report: 0xWeiss
1508.4941 USDC - $1,508.49
When an erc777 is used as asset for the vault, you can re-enter the _mint function by minting the double you would have minted with a normal erc20 token.
The following 2 functions allow minting in the vault. One is for depositing a certain amount of assets, in this case, erc777 and getting shares in exchange, and the second one is to calculate the number of assets you have to send to mint "x" shares. The problem relies on the following lines:
deposit function:
_mint(receiver, shares); asset.safeTransferFrom(msg.sender, address(this), assets); adapter.deposit(assets, address(this));
The erc777 has a callback to the owner of the tokens before doing transferFrom. In this case, it is a vulnerable function because it is minting the shares before we transfer the tokens. So, on the callback that transferFrom makes to our attacker contract, we directly call the other mint function that is also publicly callable by anyone. The reason why we can't re-enter the deposit is that it has a nonReentrant
modifier, so we have to perform a cross-function re-entrancy.
mint function:
_mint(receiver, shares); asset.safeTransferFrom(msg.sender, address(this), assets); adapter.deposit(assets, address(this));
So eventually, you will be able to get twice as much shares everytime you deposit.
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol?plain=1#L134-L157 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol?plain=1#L170-L198
Eventually you will be able to mint twice as much as you provide assets.
Manual Review
The changes that have to be made are 2:
Either state clearly that erc777 are not supported, or change the flow of the function, transferring first the assets and then getting the shares.
asset.safeTransferFrom(msg.sender, address(this), assets); //changed to the top of the function _mint(receiver, shares); adapter.deposit(assets, address(this));
#0 - c4-sponsor
2023-02-19T11:30:16Z
RedVeil marked the issue as sponsor confirmed
#1 - c4-sponsor
2023-02-19T11:30:20Z
RedVeil marked the issue as disagree with severity
#2 - c4-judge
2023-02-28T23:36:11Z
dmvt changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-02-28T23:36:56Z
dmvt marked the issue as selected for report
π Selected for report: IllIllI
Also found by: 0x3b, 0xAgro, 0xBeirao, 0xMirce, 0xNineDec, 0xRobocop, 0xSmartContract, 0xTraub, 0xWeiss, 2997ms, 41i3xn, Awesome, Aymen0909, Bauer, Bnke0x0, Breeje, Cryptor, DadeKuma, Deathstore, Deekshith99, DevABDee, DevTimSch, Dewaxindo, Diana, Ermaniwe, Guild_3, H0, IceBear, Inspectah, JDeryl, Kaiziron, Kaysoft, Kenshin, Mukund, Praise, RaymondFam, Rickard, Rolezn, Ruhum, Sathish9098, SkyWalkerMan, SleepingBugs, UdarTeam, Udsen, Walter, aashar, adeolu, apvlki, arialblack14, ast3ros, btk, chaduke, chandkommanaboyina, chrisdior4, climber2002, codetilda, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, descharre, dharma09, doublesharp, eccentricexit, ethernomad, fs0c, georgits, halden, hansfriese, hashminer0725, immeas, lukris02, luxartvinsec, matrix_0wl, merlin, mookimgo, mrpathfindr, nadin, olegthegoat, pavankv, rbserver, rebase, savi0ur, sayan, scokaf, seeu, shark, simon135, tnevler, tsvetanovv, ulqiorra, ustas, waldenyan20, y1cunhui, yongskiws, yosuke
35.4779 USDC - $35.48
The following variables, do not have a function to update them and are initialized in the constructor, therefore they should be immutable or constant and hardcode the addresses.
IBeefyVault public beefyVault; IBeefyBooster public beefyBooster; IBeefyBalanceCheck public beefyBalanceCheck;
The variables are here: https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/adapter/beefy/BeefyAdapter.sol#L27-L29
The issue relies on the permit function. Where Popcorn is using the recover opcode instead of the ECDSA library from openzeppelin. That does not bring any issues normally, but it is the best practice. Also, due to not using the ECDSA library,
the function requirement can be bypassed
function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public virtual { if (deadline < block.timestamp) revert PermitDeadlineExpired(deadline); // Unchecked because the only math done is incrementing // the owner's nonce which cannot realistically overflow. unchecked { address recoveredAddress = ecrecover( keccak256( abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR(), keccak256( abi.encode( keccak256( "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ), owner, spender, value, nonces[owner]++, deadline ) ) ) ), v, r, s ); if (recoveredAddress == address(0) || recoveredAddress != owner) revert InvalidSigner(recoveredAddress); _approve(recoveredAddress, spender, value); }
As you can notice, the if statement:
if (recoveredAddress == address(0) || recoveredAddress != owner)
it is not truly well implemented, because in the function it is not checked that owner != address(0). So, if you pass the owner as address(0) and random signature values. The recovered address will be 0. So it will bypass the check
(recoveredAddress == address(0) || recoveredAddress != owner)
because it has and OR || not and &&.
Change the way in how you formulate that if statement:
if (recoveredAddress != address(0) && recoveredAddress == owner)
and replace recover for the ECDSA library by OZ
#0 - c4-judge
2023-02-28T15:04:15Z
dmvt marked the issue as grade-b