-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix vulnerability that allows bidders to block people who will outbid #4
Merged
regynald
merged 3 commits into
main
from
nyquist/hook-826-fix-vulnerability-related-to-bidders-who
May 2, 2022
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ import "./interfaces/IHookERC721VaultFactory.sol"; | |
import "./interfaces/IHookERC721Vault.sol"; | ||
import "./interfaces/IHookCoveredCall.sol"; | ||
import "./interfaces/IHookProtocol.sol"; | ||
import "./interfaces/IWETH.sol"; | ||
|
||
/// @title HookCoveredCallImplV1 an implementation of covered calls on Hook | ||
/// @author Jake Nyquist -- [email protected] | ||
|
@@ -74,6 +75,9 @@ contract HookCoveredCallImplV1 is | |
/// insturment. | ||
address public allowedNftContract; | ||
|
||
/// @dev the address of WETH on the chain where this contract is deployed | ||
address public weth; | ||
|
||
/// --- Constructor | ||
// the constructor cannot have arugments in proxied contracts. | ||
constructor() ERC721("CallOption", "CALL") {} | ||
|
@@ -91,6 +95,7 @@ contract HookCoveredCallImplV1 is | |
) public initializer { | ||
_protocol = IHookProtocol(protocol); | ||
_erc721VaultFactory = IHookERC721VaultFactory(hookVaultFactory); | ||
weth = _protocol.getWETHAddress(); | ||
allowedNftContract = _allowedNftContract; | ||
} | ||
|
||
|
@@ -264,8 +269,7 @@ contract HookCoveredCallImplV1 is | |
require(bidAmt > call.strike, "bid - bid is lower than the strike price"); | ||
|
||
// return current bidder's money | ||
(bool sent, ) = call.highBidder.call{value: call.bid}(""); | ||
require(sent, "Failed to send Ether"); | ||
_safeTransferETHWithFallback(call.highBidder, call.bid); | ||
|
||
// set the new bidder | ||
call.bid = bidAmt; | ||
|
@@ -335,15 +339,10 @@ contract HookCoveredCallImplV1 is | |
uint256 spread = call.bid - call.strike; | ||
|
||
// return send option holder their earnings | ||
(bool sent, ) = ownerOf(optionId).call{value: spread}(""); | ||
require(sent, "Failed to send Ether to option holder"); | ||
|
||
// return send option holder their earnings | ||
( | ||
bool writerSent, /* bytes memory writerData */ | ||
_safeTransferETHWithFallback(ownerOf(optionId), spread); | ||
|
||
) = call.writer.call{value: call.strike}(""); | ||
require(writerSent, "Failed to send Ether to option writer"); | ||
// send option writer the strike price | ||
_safeTransferETHWithFallback(call.writer, call.strike); | ||
|
||
if (returnNft) { | ||
IHookERC721Vault(call.vaultAddress).withdrawalAsset(); | ||
|
@@ -401,8 +400,8 @@ contract HookCoveredCallImplV1 is | |
|
||
if (call.highBidder != address(0)) { | ||
// return current bidder's money | ||
(bool sent, ) = call.highBidder.call{value: call.bid}(""); | ||
require(sent, "Failed to send Ether"); | ||
_safeTransferETHWithFallback(call.highBidder, call.bid); | ||
|
||
// if we have a bid, we may have set the bidder, so make sure to revert it here. | ||
IHookERC721Vault(call.vaultAddress).setBeneficialOwner(call.writer); | ||
} | ||
|
@@ -495,4 +494,25 @@ contract HookCoveredCallImplV1 is | |
|
||
return output; | ||
} | ||
|
||
/** | ||
* @notice Transfer ETH. If the ETH transfer fails, wrap the ETH and try send it as WETH. | ||
* @dev this transfer failure could occur if the transferee is a malicious contract | ||
* so limiting the gas and persisting on fail helps prevent the impace of these calls. | ||
*/ | ||
function _safeTransferETHWithFallback(address to, uint256 amount) internal { | ||
if (!_safeTransferETH(to, amount)) { | ||
IWETH(weth).deposit{value: amount}(); | ||
IWETH(weth).transfer(to, amount); | ||
} | ||
} | ||
|
||
/** | ||
* @notice Transfer ETH and return the success status. | ||
* @dev This function only forwards 30,000 gas to the callee. | ||
*/ | ||
function _safeTransferETH(address to, uint256 value) internal returns (bool) { | ||
(bool success, ) = to.call{value: value, gas: 30_000}(new bytes(0)); | ||
return success; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
|
||
pragma solidity ^0.8.10; | ||
|
||
interface IWETH { | ||
function deposit() external payable; | ||
|
||
function withdraw(uint256 wad) external; | ||
|
||
function transfer(address to, uint256 value) external returns (bool); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.10; | ||
|
||
import "./utils/base.sol"; | ||
import "./utils/mocks/MaliciousBidder.sol"; | ||
|
||
// @dev these tests try cases where a bidder maliciously reverts on save. | ||
contract HookCoveredCallBiddingReverTests is HookProtocolTest { | ||
function setUp() public { | ||
setUpAddresses(); | ||
setUpFullProtocol(); | ||
|
||
// add address to the allowlist for minting | ||
|
||
// Set user balances | ||
vm.deal(address(buyer), 100 ether); | ||
|
||
// Mint underlying token | ||
underlyingTokenId = 0; | ||
token.mint(address(writer), underlyingTokenId); | ||
|
||
// Buyer swap 50 ETH <> 50 WETH | ||
vm.prank(address(buyer)); | ||
weth.deposit{value: 50 ether}(); | ||
|
||
// Seller approve ERC721TransferHelper | ||
vm.prank(address(writer)); | ||
token.setApprovalForAll(address(calls), true); | ||
|
||
// Buyer approve covered call | ||
vm.prank(address(buyer)); | ||
weth.approve(address(calls), 50 ether); | ||
} | ||
|
||
function test_SuccessfulAuctionAndSettlement() public { | ||
// create the call option | ||
vm.prank(address(writer)); | ||
uint256 writerStartBalance = writer.balance; | ||
uint256 baseTime = block.timestamp; | ||
uint256 expiration = baseTime + 3 days; | ||
uint256 optionId = calls.mint( | ||
address(token), | ||
underlyingTokenId, | ||
1000, | ||
expiration, | ||
makeSignature(underlyingTokenId, expiration, writer) | ||
); | ||
vm.prank(address(writer)); | ||
// assume that the writer somehow sold to the buyer, outside the scope of this test | ||
calls.safeTransferFrom(writer, buyer, optionId); | ||
uint256 buyerStartBalance = buyer.balance; | ||
|
||
// create some bidders | ||
MaliciousBidder bidder1 = new MaliciousBidder(address(calls)); | ||
address mbcaller = address(6969420); | ||
address bidder2 = address(33456463); | ||
|
||
// made a bid | ||
vm.warp(baseTime + 2.1 days); | ||
vm.deal(mbcaller, 1100); | ||
vm.prank(mbcaller); | ||
bidder1.bid{value: 1050}(optionId); | ||
|
||
// validate that bid is updated | ||
assertTrue( | ||
calls.currentBid(optionId) == 1050, | ||
"contract should update the current high bid for the option" | ||
); | ||
assertTrue( | ||
calls.currentBidder(optionId) == address(bidder1), | ||
"bidder1 should be in the lead" | ||
); | ||
assertTrue( | ||
address(calls).balance == 1050, | ||
"bidder1 should have deposited money into escrow" | ||
); | ||
|
||
// make a competing bid | ||
vm.deal(bidder2, 1100); | ||
vm.prank(bidder2); | ||
calls.bid{value: 1100}(optionId); | ||
|
||
// validate that bid is updated | ||
assertTrue( | ||
calls.currentBid(optionId) == 1100, | ||
"contract should update the current high bid for the option" | ||
); | ||
assertTrue( | ||
calls.currentBidder(optionId) == bidder2, | ||
"bidder2 should be in the lead" | ||
); | ||
assertTrue(bidder2.balance == 0, "bidder2 should have funds in escrow"); | ||
|
||
// settle the auction | ||
// assertTrue(token.ownerOf(underlyingTokenId) == address(calls), "call contract should own the token"); | ||
vm.warp(expiration + 3 seconds); | ||
calls.settleOption(optionId, true); | ||
|
||
// verify the balances are correct | ||
uint256 writerEndBalance = writer.balance; | ||
uint256 buyerEndBalance = buyer.balance; | ||
|
||
assertTrue( | ||
token.ownerOf(underlyingTokenId) == bidder2, | ||
"the high bidder should own the nft" | ||
); | ||
assertTrue( | ||
writerEndBalance - writerStartBalance == 1000, | ||
"the writer gets the strike price" | ||
); | ||
assertTrue( | ||
buyerEndBalance - buyerStartBalance == 100, | ||
"the call owner gets the spread" | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
pragma solidity ^0.8.10; | ||
|
||
import "../../../interfaces/IHookCoveredCall.sol"; | ||
|
||
// @dev a smart contract that reverts upon recieveing funds | ||
// and allows a bid to be mocked to a specific covered call option. | ||
// this can be used to write tests that fail if a contract reverting | ||
// prevents new bids. | ||
contract MaliciousBidder { | ||
IHookCoveredCall private callOption; | ||
bool private throwOnReceive; | ||
|
||
constructor(address _callOption) { | ||
callOption = IHookCoveredCall(_callOption); | ||
throwOnReceive = true; | ||
} | ||
|
||
function bid(uint256 optionId) public payable { | ||
callOption.bid{value: msg.value}(optionId); | ||
} | ||
|
||
receive() external payable { | ||
require(!throwOnReceive, "ha ha ha gotcha"); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we start doing direct imports instead of long relative import paths like this?
Maybe if it's a
../
relative is fine. Otherwise direct