From b29eeb99cd5f1a03b5b5cfc6e707b4cdb83639d7 Mon Sep 17 00:00:00 2001 From: Regynald Augustin Date: Sun, 24 Apr 2022 22:02:58 -0400 Subject: [PATCH] Better functionality and test for spread bidding --- src/HookCoveredCallImplV1.sol | 17 ++--- src/test/HookCoveredCallTests.sol | 103 ++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 8 deletions(-) diff --git a/src/HookCoveredCallImplV1.sol b/src/HookCoveredCallImplV1.sol index 3fed6bf..0b0003f 100644 --- a/src/HookCoveredCallImplV1.sol +++ b/src/HookCoveredCallImplV1.sol @@ -257,9 +257,6 @@ contract HookCoveredCallImplV1 is uint256 bidAmt = msg.value; CallOption storage call = optionParams[optionId]; - // TODO(HOOK-805) allow option writer to make a bid with an deposited amount equal - // to the spread between the their bid and the option's strike price. - uint256 normalizedBid = bidAmt; if (call.writer == msg.sender) { @@ -352,12 +349,16 @@ contract HookCoveredCallImplV1 is (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 */ - ) = call.writer.call{value: call.strike}(""); - require(writerSent, "Failed to send Ether to option writer"); + // If the option writer is the high bidder then they don't recieve the strike and only the underlying asset + if (call.highBidder != call.writer) { + // return send option holder their earnings + ( + bool writerSent, /* bytes memory writerData */ + + ) = call.writer.call{value: call.strike}(""); + require(writerSent, "Failed to send Ether to option writer"); + } if (returnNft) { IHookERC721Vault(call.vaultAddress).withdrawalAsset(); diff --git a/src/test/HookCoveredCallTests.sol b/src/test/HookCoveredCallTests.sol index 7ea9878..40ae459 100644 --- a/src/test/HookCoveredCallTests.sol +++ b/src/test/HookCoveredCallTests.sol @@ -596,6 +596,65 @@ contract HookCoveredCallBidTests is HookProtocolTest { vm.expectRevert("bid - bid is lower than the current bid"); calls.bid{value: 0.09 ether}(optionTokenId); } + + function testWriterCanBidOnSpread() public { + vm.deal(writer, 1 ether); + vm.warp(block.timestamp + 2.1 days); + + vm.prank(writer); + calls.bid{value: 1}(optionTokenId); + + assertTrue( + calls.currentBid(optionTokenId) == 1001, + "bid 1 wei over strike price" + ); + assertTrue( + calls.currentBidder(optionTokenId) == writer, + "writer should be highest bidder" + ); + assertTrue( + writer.balance == 1 ether - 1, + "writer should have only used 1 wei to bid" + ); + } + + function testWriterCanOutbidOnSpread() public { + address firstBidder = address(37); + vm.label(firstBidder, "First option bidder"); + vm.deal(firstBidder, 1 ether); + vm.deal(writer, 1 ether); + + uint256 firstBidderStartBalance = firstBidder.balance; + uint256 writerStartBalance = writer.balance; + + vm.warp(block.timestamp + 2.1 days); + + vm.prank(firstBidder); + calls.bid{value: 0.1 ether}(optionTokenId); + + uint256 strike = 1000; + uint256 bidAmount = 0.1 ether - strike + 1; + + vm.prank(writer); + calls.bid{value: bidAmount}(optionTokenId); + + assertTrue( + calls.currentBid(optionTokenId) == 0.1 ether + 1, + "high bid should be 0.1 ether + 1 wei" + ); + assertTrue( + calls.currentBidder(optionTokenId) == writer, + "writer should be highest bidder" + ); + assertTrue( + firstBidderStartBalance == firstBidder.balance, + "first bidder should have been refunded their bid" + ); + assertTrue( + writer.balance == 1 ether - bidAmount, + "writer should have only used 0.1 ether + 1 wei to bid" + ); + } } @@ -720,6 +779,50 @@ contract HookCoveredCallSettleTests is HookProtocolTest { vm.expectRevert("settle -- the call cannot already be settled"); calls.settleOption(optionTokenId, true); } + + function testSettleOptionWhenWriterHighBidder() public { + vm.startPrank(writer); + uint256 underlyingTokenId2 = 1; + token.mint(writer, underlyingTokenId2); + vm.deal(writer, 1 ether); + + uint256 buyerStartBalance = buyer.balance; + uint256 writerStartBalance = writer.balance; + + // Writer approve operator and covered call + token.setApprovalForAll(address(calls), true); + + uint256 expiration = block.timestamp + 3 days; + + uint256 optionId = calls.mint( + address(token), + underlyingTokenId2, + 1000, + expiration, + makeSignature(underlyingTokenId2, expiration, writer) + ); + + // Assume that the writer somehow sold the option NFT to the buyer. + // Outside of the scope of these tests. + calls.safeTransferFrom(writer, buyer, optionId); + + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 2.1 days); + calls.bid{value: 1 wei}(optionId); + vm.warp(block.timestamp + 1 days); + + calls.settleOption(optionId, false); + + assertTrue( + buyerStartBalance + 1 wei == buyer.balance, + "buyer gets the option spread (winning bid of 1001 wei - strike price of 1000)" + ); + + assertTrue( + writerStartBalance - 1 == writer.balance, + "option writer only loses spread (1 wei)" + ); + } }