Platform: Code4rena
Start Date: 29/06/2022
Pot Size: $50,000 USDC
Total HM: 20
Participants: 133
Period: 5 days
Judge: hickuphh3
Total Solo HM: 1
Id: 142
League: ETH
Rank: 28/133
Findings: 3
Award: $509.54
π Selected for report: 0
π Solo Findings: 0
π Selected for report: kirk-baird
Also found by: 0xA5DF, Kenshin, cccz, chatch, csanuragjain, hansfriese, hyh, itsmeSTYJ, pedroais, sashik_eth, unforgiven, xiaoming90
41.8933 USDC - $41.89
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L636 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L440
An attacker can make a scam with an option that uses a real NFT but can't be exercised.
In putty, users can make calls and puts on NFTs. The code also allows to include an array of ERC20s or/and ERC721's as part of a call or put. https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L80
An attacker can make an order for a call with a rea NFT (let's sey a BAYC) but include a fake attacker token as part of the offer. The user seeing this would think he's buying a call on a BAYC and some useless token and decide to buy the call based on the BAYC. In the worst case, it would be reasonable to think he should at least get the BAYC when exercising.
This fake token could be crafted to return true or false at the attacker's will.
Example attacker token transfer function :
Function transferFrom(address from, address to) public { if (setTransferOn) return true Else return false }
function setTransfer(bool transfer){ serTransferOn = transfer }
In this way, the attacker can choose the return value for transferFrom.
Step-by-step attack:
The attacker makes a call order with a BAYC and this fake token as one of the ERC20's. Transfer is first set to true so the BAYC is transfered into the contract when the order is filled by a user. Users see the call order with a very attractive strike price and decides to pay the premium (let's say 1 eth) to take it.
A few days later the user decides to exercise his call. The fake token is now set to return false so the option exercising transaction reverts and the user can't get the bayc. https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L440 After order expiration the attacker sets the token to return true and withdraws his bayc.
The attacker got the premium (1 eth) without incurring in any risk since the taker couldn't exercise his option.
An argument could be made this issue shouldn't be high severity since it requires the user to accept a malicious offer. I think the attack is high severity since the user would be reasonable to think if he is accepting an offer for a BAYC and some token that in the worse case he should at least get the BAYC. This attack doesn't require the user to be dumb or act recklessly but just normal functioning of the protocol. The fake token shouldn't prevent the user from exercising the real BAYC.
If this isn't fixed, the door is left open for a full gamma of scams in which a single malicious token can compromise an offer that includes other real and valuable NFT's or ERC20's.
One token failing shouldn't prevent the user from getting the real NFT from the option. When sensing assets out a try-catch mechanism should be implemented. In that way, if one of the malicious tokens reverts the real token can still be exercised.
#0 - outdoteth
2022-07-07T13:35:36Z
Duplicate: Setting malicious or invalid erc721Assets, erc20Assets or floorTokens prevents the option from being exercised: https://github.com/code-423n4/2022-06-putty-findings/issues/50
π Selected for report: 0xc0ffEE
Also found by: horsefacts, pedroais, unforgiven
420.5209 USDC - $420.52
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L303 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L324 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L442 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L646
An attacker can force a flash loan and get temporary ownership of any NFT held inside putty and perform arbitrary logic with it.
A reentrancy gate can be exploited to provoke an unintended flash loan of any NFT held inside the contract.
Initial state : The putty contract is holding an NFT (from now on called "real NFT") from a legitimate option opened by regular users. An attacker can use reentrancy to get a flash loan of "real NFT" and perform arbitrary transactions with ownership of that NFT before giving it back (in the same transaction).
Attack :
An attacker makes and fulfills a fake order (he is the maker and the taker) with a base asset (the asset in which the premium and strike are paid) equal to an attacker contract. The base asset isn't a token but a contract that can perform arbitrary logic.
The order is a long call with a strike and premium of 0 for the NFT that's inside the contract. The erc721Assets[] array in the order includes the real NFT to be flashloaned and then a fake NFT allowing to do another external call. The attacker doesn't have the real NFT (it's inside the contract). The goal of the attack is to get a flash loan on that NFT and perform arbitrary logic with it with the goal of getting to claim any rewards or funds claimable by the real NFT. The erc721Assets[] array is composed of [realNFTAdddress, fakeNFTaddress]
Attack flow : -The attacker calls fillOrder() with the order made by himself. Fill order first mints the option NFT and then makes the base asset transfer. https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L303 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L324
-This base asset transfer is the first reentrancy gate. The base asset is an attacker contract that will immediately call the exercise() function (the attacker is the maker and taker so he can do this with contracts). Note that the real NFT still didn't have to get transfered in since the base asset transfer happens before that.
-The exercise function sets exercised to true, burns the position and transfers the real NFT out. https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L442
-The attacker now has ownership of the real NFT -Then the fake NFT is transferred out in the function loop which handles execution back to the attacker. At this point, he can perform arbitrary logic with the real NFT including cashing out on anything that requires being the owner of the NFT. This attack is only useful with NFTs that provide access to funds (this is why the issue is medium and not high severity).
After the arbitrary logic is performed by the attacker. The execution comes back to fillOrder() and assets have to be transferred inside the contract again, ending the forced flash loan. https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L376
To recap the flow : The attacker calls fillOrder() and reenters the contract in the baseAssetTransfer by calling exercise() which means the NFT is transferred out before being transferred in. The real NFT is transferred out. The fake NFT transfer external call is used to perform arbitrary logic. Then the execution comes back to fillOrder() and the NFT is transferred into putty again.
The attack has no cost besides gas since both premium and strike are paid in the fake attacker contract.
Add reentrancy protection
#0 - GalloDaSballo
2022-07-05T01:45:10Z
"-The exercise function sets exercised to true, burns the position and transfers the real NFT out."
If fillOrder
is being exited during the premium-transfer, the NFT is still not in the contract, meaning the "flashborrow" would revert at the last part of exercise
because PuttyV2 still has to receive the NFT tokens.
Also, this would require a user that is wiling to be the counter-party to a fake token
#1 - Pedroais
2022-07-05T04:12:10Z
"-The exercise function sets exercised to true, burns the position and transfers the real NFT out."
If
fillOrder
is being exited during the premium-transfer, the NFT is still not in the contract, meaning the "flashborrow" would revert at the last part ofexercise
because PuttyV2 still has to receive the NFT tokens.Also, this would require a user that is wiling to be the counter-party to a fake token
Hey Alex ! I think your points are not true 1 - the NFT was already in the contract, that's the idea of the attack, to make a flashborrow on an NFT that's already inside the contract (from an unrelated legitimate position). Transfer out is made before transfer in and won't revert since the contract was already holding the nft. Then the attacker uses the nft and transfers it back in.
2 - The attacker can make an order and take it himself so a counterparty is not needed
I actually sent a help request to edit the submission but it wasn't responded since the team is on holiday. The fake token isn't actually needed since the external call of ERC721 safe transfer can be used to allow the attacker to perform arbitrary logic on the NFT. The issue is still correct as described but could be a bit simpler.
So to recap lets use BAYC as an example:
BAYC 1 is inside the contract and an attacker wants to get a flash loan on it
-Attacker makes order with himself and reenters in base transfer to exercise -BAYC 1 is transfered out before in and it doesn't revert since it's already inside putty -The attacker performs arbitrary logic on the BAYC using the ERC721 safe transfer -Execution comes back to fillOrder() and the attacker returns the BAYC
#2 - GalloDaSballo
2022-07-05T14:28:30Z
I was wrong, finding is valid:
-> fillOrder
-> premium transfer execution hijack -> exercise
Β -> Receive any NFT inside the contract -> Do w/e -> Execution returns to exercise
Because fee
token can be the malicious token, this is a free flashloan
#3 - outdoteth
2022-07-06T19:32:33Z
Duplicate: Itβs possible to flashloan all assets in the contract without paying a protocol fee: https://github.com/code-423n4/2022-06-putty-findings/issues/377
π Selected for report: xiaoming90
Also found by: 0x1f8b, 0x29A, 0x52, 0xDjango, 0xNazgul, 0xNineDec, 0xSolus, 0xf15ers, 0xsanson, AmitN, Bnke0x0, BowTiedWardens, Chom, David_, ElKu, Funen, GalloDaSballo, GimelSec, Hawkeye, IllIllI, JC, JohnSmith, Kaiziron, Kenshin, Lambda, Limbooo, MadWookie, Metatron, MiloTruck, Nethermind, Picodes, ReyAdmirado, Sneakyninja0129, StErMi, TomJ, Treasure-Seeker, TrungOre, Waze, Yiko, _Adam, __141345__, antonttc, async, aysha, catchup, cccz, cryptphi, csanuragjain, danb, datapunk, defsec, delfin454000, dirk_y, doddle0x, durianSausage, exd0tpy, fatherOfBlocks, gogo, hake, hansfriese, horsefacts, hubble, itsmeSTYJ, joestakey, oyc_109, pedroais, peritoflores, rajatbeladiya, reassor, robee, rokinot, samruna, saneryee, sashik_eth, shenwilly, shung, simon135, sseefried, unforgiven, zer0dot, zzzitron
47.1302 USDC - $47.13
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L240 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L228
Any eth sent to these functions will be permanently lost
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L240 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L228
These functions shouldn't be payables
Remove payable from the functions
#0 - GalloDaSballo
2022-07-05T01:35:33Z
Admin functions are set as payable to save gas
#1 - outdoteth
2022-07-05T13:23:03Z
Confirming what @GalloDaSballo said.
#2 - rotcivegaf
2022-07-06T03:26:02Z
Duplicate of #259
#3 - HickupHH3
2022-07-13T06:59:43Z
User has no QA