diff --git a/integration/integration.test.ts b/integration/integration.test.ts index 342febb..0141c66 100644 --- a/integration/integration.test.ts +++ b/integration/integration.test.ts @@ -829,6 +829,49 @@ describe("Vault", function () { await vaultInstance.connect(beneficialOwner).withdrawalAsset(0); }); + it("allows asset to be withdrawn by beneficialOwner", async function () { + const nowEpoch = Date.now() / 1000; + await testNFT + .connect(beneficialOwner) + ["safeTransferFrom(address,address,uint256)"]( + beneficialOwner.address, + vaultInstance.address, + 1 + ); + + expect(await vaultInstance.getBeneficialOwner(0)).eq( + beneficialOwner.address + ); + + expect(await vaultInstance.connect(beneficialOwner).withdrawalAsset(0)) + .not.to.throw; + + expect(await vaultInstance.getHoldsAsset(0)).to.be.false; + }); + + it("allows asset to be blocks withdrawals by others", async function () { + const nowEpoch = Date.now() / 1000; + await testNFT + .connect(beneficialOwner) + ["safeTransferFrom(address,address,uint256)"]( + beneficialOwner.address, + vaultInstance.address, + 1 + ); + + expect(await vaultInstance.getBeneficialOwner(0)).eq( + beneficialOwner.address + ); + + await expect( + vaultInstance.connect(runner).withdrawalAsset(0) + ).to.be.revertedWith( + "withdrawalAsset -- only the beneficial owner can withdrawal an asset" + ); + + expect(await vaultInstance.getHoldsAsset(0)).to.be.true; + }); + it("allows operator to clear entitlement and distribute", async function () { const nowEpoch = Date.now() / 1000; await testNFT @@ -1087,7 +1130,9 @@ describe("Vault", function () { runner.address, "0x0000000000000000000000000000000000000000" ) - ).to.be.revertedWith("assetIdIsZero -- this vault only supports asset id 0"); + ).to.be.revertedWith( + "assetIdIsZero -- this vault only supports asset id 0" + ); }); it("allows basic flashloans", async function () { @@ -2005,12 +2050,11 @@ describe("Call Instrument Tests", function () { const callFactoryFactory = await ethers.getContractFactory( "HookCoveredCallFactory" ); - const tokenURILib = await ethers.getContractFactory( - "TokenURI" - ) + const tokenURILib = await ethers.getContractFactory("TokenURI"); const tokenURI = await tokenURILib.deploy(); const callImplFactory = await ethers.getContractFactory( - "HookCoveredCallImplV1", {libraries: {TokenURI: tokenURI.address}} + "HookCoveredCallImplV1", + { libraries: { TokenURI: tokenURI.address } } ); const callBeaconFactory = await ethers.getContractFactory( "HookUpgradeableBeacon" @@ -2828,16 +2872,14 @@ describe("Call Instrument Tests", function () { // Move forward to after auction period ends await ethers.provider.send("evm_increaseTime", [4 * SECS_IN_A_DAY]); - const settleCall = calls.connect(writer).settleOption(tokenId, false); + const settleCall = calls.connect(writer).settleOption(tokenId); await expect(settleCall).to.be.revertedWith( "settle -- bid must be won by someone" ); }); it("should not settle auction before expiration", async function () { - const settleCall = calls - .connect(writer) - .settleOption(optionTokenId, false); + const settleCall = calls.connect(writer).settleOption(optionTokenId); await expect(settleCall).to.be.revertedWith( "settle -- option must be expired" ); @@ -2847,10 +2889,8 @@ describe("Call Instrument Tests", function () { // Move forward to after auction period ends await ethers.provider.send("evm_increaseTime", [1 * SECS_IN_A_DAY]); - await calls.connect(writer).settleOption(optionTokenId, false); - const settleCallAgain = calls - .connect(writer) - .settleOption(optionTokenId, false); + await calls.connect(writer).settleOption(optionTokenId); + const settleCallAgain = calls.connect(writer).settleOption(optionTokenId); await expect(settleCallAgain).to.be.revertedWith( "settle -- the call cannot already be settled" ); @@ -2860,9 +2900,7 @@ describe("Call Instrument Tests", function () { // Move forward to after auction period ends await ethers.provider.send("evm_increaseTime", [1 * SECS_IN_A_DAY]); - const settleCall = calls - .connect(writer) - .settleOption(optionTokenId, false); + const settleCall = calls.connect(writer).settleOption(optionTokenId); await expect(settleCall).to.emit(calls, "CallSettled"); const vaultAddress = await calls.getVaultAddress(optionTokenId); @@ -2871,21 +2909,7 @@ describe("Call Instrument Tests", function () { vaultAddress ); - expect(await vault.getBeneficialOwner(0)).to.eq( - writer.address - ); - }); - - it("should settle auction and return nft", async function () { - // Move forward to after auction period ends - await ethers.provider.send("evm_increaseTime", [1 * SECS_IN_A_DAY]); - - const settleCall = calls - .connect(writer) - .settleOption(optionTokenId, true); - await expect(settleCall).to.emit(calls, "CallSettled"); - - expect(await token.ownerOf(0)).to.eq(writer.address); + expect(await vault.getBeneficialOwner(0)).to.eq(writer.address); }); it("should settle auction when option writer is high bidder", async function () { @@ -2894,7 +2918,7 @@ describe("Call Instrument Tests", function () { const settleCall = calls .connect(secondBidder) - .settleOption(secondOptionTokenId, false); + .settleOption(secondOptionTokenId); await expect(settleCall).to.emit(calls, "CallSettled"); const vaultAddress = await calls.getVaultAddress(secondOptionTokenId); @@ -2903,9 +2927,7 @@ describe("Call Instrument Tests", function () { vaultAddress ); - expect(await vault.getBeneficialOwner(0)).to.eq( - secondBidder.address - ); + expect(await vault.getBeneficialOwner(0)).to.eq(secondBidder.address); }); }); @@ -2956,7 +2978,7 @@ describe("Call Instrument Tests", function () { await ethers.provider.send("evm_increaseTime", [1 * SECS_IN_A_DAY]); // Settle option - await calls.connect(writer).settleOption(optionTokenId, false); + await calls.connect(writer).settleOption(optionTokenId); const reclaimAsset = calls .connect(writer) @@ -3194,6 +3216,10 @@ describe("Call Instrument Tests", function () { it("should get expiration", async function () { expect(await calls.getExpiration(optionTokenId)).to.eq(expiration); }); + + it("should have a tokenURI", async function () { + expect(await calls.tokenURI(optionTokenId)).to.not.be.null; + }); }); /* diff --git a/src/HookCoveredCallImplV1.sol b/src/HookCoveredCallImplV1.sol index 15a9d12..9bdcdde 100644 --- a/src/HookCoveredCallImplV1.sol +++ b/src/HookCoveredCallImplV1.sol @@ -520,10 +520,7 @@ contract HookCoveredCallImplV1 is // ----- END OF OPTION FUNCTIONS ---------// /// @dev See {IHookCoveredCall-settleOption}. - function settleOption(uint256 optionId, bool returnNft) - external - nonReentrant - { + function settleOption(uint256 optionId) external nonReentrant { CallOption storage call = optionParams[optionId]; require( call.highBidder != address(0), @@ -551,13 +548,9 @@ contract HookCoveredCallImplV1 is _safeTransferETHWithFallback(call.writer, call.strike); } - // return send option holder their earnings + // send option holder their earnings _safeTransferETHWithFallback(optionOwner, spread); - if (returnNft) { - IHookVault(call.vaultAddress).withdrawalAsset(call.assetId); - } - emit CallSettled(optionId); } diff --git a/src/HookERC721MultiVaultImplV1.sol b/src/HookERC721MultiVaultImplV1.sol index cf90028..07d1f16 100644 --- a/src/HookERC721MultiVaultImplV1.sol +++ b/src/HookERC721MultiVaultImplV1.sol @@ -114,6 +114,10 @@ contract HookERC721MultiVaultImplV1 is !hasActiveEntitlement(assetId), "withdrawalAsset -- the asset cannot be withdrawn with an active entitlement" ); + require( + assets[assetId].beneficialOwner == msg.sender, + "withdrawalAsset -- only the beneficial owner can withdrawal an asset" + ); _nftContract.safeTransferFrom( address(this), diff --git a/src/interfaces/IHookCoveredCall.sol b/src/interfaces/IHookCoveredCall.sol index f5f1999..feb5366 100644 --- a/src/interfaces/IHookCoveredCall.sol +++ b/src/interfaces/IHookCoveredCall.sol @@ -164,8 +164,7 @@ interface IHookCoveredCall is IERC721Metadata { /// are subtracted from the distribution amounts. /// /// @param optionId of the option to settle. - /// @param returnNft true if token should be withdrawn from vault, false to leave token in the vault. - function settleOption(uint256 optionId, bool returnNft) external; + function settleOption(uint256 optionId) external; /// @notice Allows anyone to burn the instrument NFT for an expired option. /// @param optionId of the option to burn. diff --git a/src/test/HookCoveredCallBiddingRevertTests.t.sol b/src/test/HookCoveredCallBiddingRevertTests.t.sol index 9475b01..9cd521d 100644 --- a/src/test/HookCoveredCallBiddingRevertTests.t.sol +++ b/src/test/HookCoveredCallBiddingRevertTests.t.sol @@ -96,16 +96,12 @@ contract HookCoveredCallBiddingRevertTests is HookProtocolTest { // settle the auction // assertTrue(token.ownerOf(underlyingTokenId) == address(calls), "call contract should own the token"); vm.warp(expiration + 3 seconds); - calls.settleOption(optionId, true); + calls.settleOption(optionId); // 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" diff --git a/src/test/HookCoveredCallIntegrationTest.t.sol b/src/test/HookCoveredCallIntegrationTest.t.sol index 2814891..d665269 100644 --- a/src/test/HookCoveredCallIntegrationTest.t.sol +++ b/src/test/HookCoveredCallIntegrationTest.t.sol @@ -163,16 +163,12 @@ contract HookCoveredCallIntegrationTest is HookProtocolTest { // settle the auction // assertTrue(token.ownerOf(underlyingTokenId) == address(calls), "call contract should own the token"); vm.warp(expiration + 3 seconds); - calls.settleOption(optionId, true); + calls.settleOption(optionId); // 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" diff --git a/src/test/HookCoveredCallTests.t.sol b/src/test/HookCoveredCallTests.t.sol index 8443d29..eebc327 100644 --- a/src/test/HookCoveredCallTests.t.sol +++ b/src/test/HookCoveredCallTests.t.sol @@ -930,7 +930,7 @@ contract HookCoveredCallSettleTests is HookProtocolTest { uint256 writerStartBalance = writer.balance; vm.prank(writer); - calls.settleOption(optionTokenId, false); + calls.settleOption(optionTokenId); assertTrue( buyerStartBalance + (0.2 ether - 1000 wei) == buyer.balance, @@ -942,7 +942,7 @@ contract HookCoveredCallSettleTests is HookProtocolTest { ); } - function testSettleOptionReturnNft() public { + function testSettleOption2() public { uint256 buyerStartBalance = buyer.balance; uint256 writerStartBalance = writer.balance; @@ -950,13 +950,9 @@ contract HookCoveredCallSettleTests is HookProtocolTest { address(token), underlyingTokenId ); - vm.expectCall( - address(vault), - abi.encodeWithSignature("withdrawalAsset(uint32)", 0) - ); vm.prank(writer); - calls.settleOption(optionTokenId, true); + calls.settleOption(optionTokenId); assertTrue( buyerStartBalance + (0.2 ether - 1000 wei) == buyer.balance, @@ -966,10 +962,6 @@ contract HookCoveredCallSettleTests is HookProtocolTest { writerStartBalance + 1000 wei == writer.balance, "buyer should have received the option" ); - assertTrue( - token.ownerOf(underlyingTokenId) == address(secondBidder), - "secondBidder (winner) should get the underlying asset" - ); } function testCannotSettleOptionNoWinningBid() public { @@ -992,7 +984,7 @@ contract HookCoveredCallSettleTests is HookProtocolTest { // Option expires in 3 days from current block; bidding starts in 2 days. vm.warp(block.timestamp + 3.1 days); vm.expectRevert("settle -- bid must be won by someone"); - calls.settleOption(optionId, true); + calls.settleOption(optionId); } function testCannotSettleOptionBeforeExpiration() public { @@ -1017,15 +1009,15 @@ contract HookCoveredCallSettleTests is HookProtocolTest { calls.bid{value: 0.1 ether}(optionId); vm.expectRevert("settle -- option must be expired"); - calls.settleOption(optionId, true); + calls.settleOption(optionId); } function testCannotSettleSettledOption() public { vm.prank(writer); - calls.settleOption(optionTokenId, false); + calls.settleOption(optionTokenId); vm.expectRevert("settle -- the call cannot already be settled"); - calls.settleOption(optionTokenId, true); + calls.settleOption(optionTokenId); } function testSettleOptionWhenWriterHighBidder() public { @@ -1058,7 +1050,7 @@ contract HookCoveredCallSettleTests is HookProtocolTest { calls.bid{value: 1 wei}(optionId); vm.warp(block.timestamp + 1 days); - calls.settleOption(optionId, false); + calls.settleOption(optionId); assertTrue( buyerStartBalance + 1 wei == buyer.balance, @@ -1109,7 +1101,7 @@ contract HookCoveredCallSettleTests is HookProtocolTest { vm.warp(block.timestamp + 1 days); vm.prank(writer); - calls.settleOption(optionId, false); + calls.settleOption(optionId); assertTrue( buyerStartBalance + 1000 wei == buyer.balance, @@ -1161,7 +1153,7 @@ contract HookCoveredCallSettleTests is HookProtocolTest { vm.warp(block.timestamp + 1 days); vm.prank(writer); - calls.settleOption(optionId, false); + calls.settleOption(optionId); assertTrue( buyerStartBalance + 2 wei == buyer.balance, @@ -1216,7 +1208,7 @@ contract HookCoveredCallSettleTests is HookProtocolTest { vm.warp(block.timestamp + 1 days); vm.prank(writer); - calls.settleOption(optionId, false); + calls.settleOption(optionId); assertTrue( buyerStartBalance + 3 wei == buyer.balance, @@ -1286,7 +1278,7 @@ contract HookCoveredCallReclaimTests is HookProtocolTest { setUpOptionBids(); vm.startPrank(writer); - calls.settleOption(optionTokenId, false); + calls.settleOption(optionTokenId); vm.expectRevert("reclaimAsset -- the option has already been settled"); calls.reclaimAsset(optionTokenId, true);