Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 130 additions & 8 deletions integration/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 +2225,30 @@ describe("Call Instrument Tests", function () {
expect(callCreatedEvent.args.strikePrice).to.equal(1000);
expect(callCreatedEvent.args.expiration).to.equal(expiration);
});

it("should mint covered call with unvaulted erc721 with existing multivault", async function () {
await vaultFactory.makeMultiVault(token.address);

const expiration = String(
Math.floor(Date.now() / 1000 + SECS_IN_A_DAY * 1.5)
);

// Mint call option
const createCall = await calls
.connect(writer)
.mintWithErc721(token.address, 0, 1000, expiration);
const cc = await createCall.wait();

const callCreatedEvent = cc.events.find(
(event: any) => event?.event === "CallCreated"
);

expect(createCall).to.emit(calls, "CallCreated");
expect(callCreatedEvent.args.writer).to.equal(writer.address);
expect(callCreatedEvent.args.optionId).to.equal(1);
expect(callCreatedEvent.args.strikePrice).to.equal(1000);
expect(callCreatedEvent.args.expiration).to.equal(expiration);
});
});

/*
Expand Down Expand Up @@ -2549,6 +2573,41 @@ describe("Call Instrument Tests", function () {

expect(createCall).to.emit(calls, "CallCreated");
});

it("should mint covered call with entitled solo vault", async function () {
// Create solovault for token 1
await vaultFactory.makeSoloVault(token.address, 1);
const soloValutAddress = await vaultFactory.getVault(token.address, 1);
const soloVault = await ethers.getContractAt(
"HookERC721VaultImplV1",
soloValutAddress
);

await token
.connect(writer)
["safeTransferFrom(address,address,uint256)"](
writer.address,
soloVault.address,
1
);

const expiration = Math.floor(Date.now() / 1000 + SECS_IN_A_DAY * 1.5);

await soloVault.connect(writer).grantEntitlement({
beneficialOwner: writer.address,
operator: calls.address,
vaultAddress: soloVault.address,
assetId: 0,
expiry: expiration,
});

// Mint call option
const createCall = await calls
.connect(writer)
.mintWithEntitledVault(soloVault.address, 0, 1000, expiration);

expect(createCall).to.emit(calls, "CallCreated");
});
});

/*
Expand Down Expand Up @@ -2852,7 +2911,8 @@ describe("Call Instrument Tests", function () {
const settleCall = calls
.connect(writer)
.settleOption(optionTokenId, false);
await expect(settleCall).to.emit(calls, "CallDestroyed");
await expect(settleCall)
.to.emit(calls, "CallSettled");

const vaultAddress = await calls.getVaultAddress(optionTokenId);
const vault = await ethers.getContractAt(
Expand All @@ -2872,7 +2932,7 @@ describe("Call Instrument Tests", function () {
const settleCall = calls
.connect(writer)
.settleOption(optionTokenId, true);
await expect(settleCall).to.emit(calls, "CallDestroyed");
await expect(settleCall).to.emit(calls, "CallSettled");

expect(await token.ownerOf(0)).to.eq(writer.address);
});
Expand All @@ -2884,7 +2944,8 @@ describe("Call Instrument Tests", function () {
const settleCall = calls
.connect(secondBidder)
.settleOption(secondOptionTokenId, false);
await expect(settleCall).to.emit(calls, "CallDestroyed");
await expect(settleCall)
.to.emit(calls, "CallSettled");

const vaultAddress = await calls.getVaultAddress(secondOptionTokenId);
const vault = await ethers.getContractAt(
Expand Down Expand Up @@ -2996,7 +3057,7 @@ describe("Call Instrument Tests", function () {
.connect(writer)
.reclaimAsset(optionTokenId, false);

await expect(reclaimAsset).to.emit(calls, "CallDestroyed");
await expect(reclaimAsset).to.emit(calls, "CallReclaimed");

// Check that there's no entitlment on the vault
const vaultAddress = await calls.getVaultAddress(optionTokenId);
Expand All @@ -3014,7 +3075,7 @@ describe("Call Instrument Tests", function () {
.connect(writer)
.reclaimAsset(optionTokenId, false);

await expect(reclaimAsset).to.emit(calls, "CallDestroyed");
await expect(reclaimAsset).to.emit(calls, "CallReclaimed");

// Check that there's no entitlment on the vault
const vaultAddress = await calls.getVaultAddress(optionTokenId);
Expand Down Expand Up @@ -3129,9 +3190,6 @@ describe("Call Instrument Tests", function () {
expiry: expiration,
});

console.log(await multiVault.getCurrentEntitlementOperator(0));
console.log(calls.address);

// Mint call option
const createCall = await calls
.connect(writer)
Expand Down Expand Up @@ -3162,4 +3220,68 @@ describe("Call Instrument Tests", function () {
expect(await calls.getExpiration(optionTokenId)).to.eq(expiration);
});
});

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~ burnExpiredOption ~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
describe("burnExpiredOption", function () {
let optionTokenId: BigNumber;

this.beforeEach(async function () {
// Create solovault for token 0
await vaultFactory.makeSoloVault(token.address, 0);
const soloValutAddress = await vaultFactory.getVault(token.address, 0);
const soloVault = await ethers.getContractAt(
"HookERC721VaultImplV1",
soloValutAddress
);

const blockNumber = await ethers.provider.getBlockNumber();
const block = await ethers.provider.getBlock(blockNumber);
const blockTimestamp = block.timestamp;
const expiration = BigNumber.from(Math.floor(blockTimestamp + SECS_IN_A_DAY * 1.5));

// Mint call option
const createCall = await calls
.connect(writer)
.mintWithErc721(token.address, 0, 1000, expiration);

const cc = await createCall.wait();

const callCreatedEvent = cc.events.find(
(event: any) => event?.event === "CallCreated"
);

optionTokenId = callCreatedEvent.args.optionId;
});

it("should not burn expired option before expiration", async function () {
await expect(calls.burnExpiredOption(optionTokenId)).to.be.revertedWith("burnExpiredOption -- the option must be expired")
});

it("should burn expired option", async function () {
// Move forward past expiration
await ethers.provider.send("evm_increaseTime", [2 * SECS_IN_A_DAY]);

await expect(calls.burnExpiredOption(optionTokenId))
.to.emit(calls, 'ExpiredCallBurned')
});

it("should not burn expired option with bids", async function () {
// Move forward to settlement auction
await ethers.provider.send("evm_increaseTime", [0.5 * SECS_IN_A_DAY]);

// First bidder bid
await calls.connect(firstBidder).bid(optionTokenId, { value: 1001 });

// Move forward past option expiration
await ethers.provider.send("evm_increaseTime", [2 * SECS_IN_A_DAY]);

// Burn expired option
await expect(calls.burnExpiredOption(optionTokenId)).to.be.revertedWith("burnExpiredOption -- the option must not have bids");
});

});
});
54 changes: 43 additions & 11 deletions src/HookCoveredCallImplV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -413,15 +413,19 @@ contract HookCoveredCallImplV1 is
return true;
}

if (
vaultAddress ==
Create2.computeAddress(
BeaconSalts.soloVaultSalt(underlyingAddress, assetId),
BeaconSalts.ByteCodeHash,
address(_erc721VaultFactory)
)
) {
return true;
try IHookERC721Vault(vaultAddress).assetTokenId(assetId) returns (uint256 _tokenId) {
if (
vaultAddress ==
Create2.computeAddress(
BeaconSalts.soloVaultSalt(underlyingAddress, _tokenId),
BeaconSalts.ByteCodeHash,
address(_erc721VaultFactory)
)
) {
return true;
}
} catch(bytes memory) {
return false;
}

return false;
Expand Down Expand Up @@ -525,7 +529,7 @@ contract HookCoveredCallImplV1 is
// set settled to prevent an additional attempt to settle the option
optionParams[optionId].settled = true;

emit CallDestroyed(optionId);
emit CallSettled(optionId);
}

/// @dev See {IHookCoveredCall-reclaimAsset}.
Expand Down Expand Up @@ -577,7 +581,7 @@ contract HookCoveredCallImplV1 is

// settle the option
call.settled = true;
emit CallDestroyed(optionId);
emit CallReclaimed(optionId);

/// WARNING:
/// Currently, if the owner writes an option, and never sells that option, a settlement auction will exist on
Expand All @@ -586,6 +590,34 @@ contract HookCoveredCallImplV1 is
/// the current bidder to reclaim their money.
}

/// @dev See {IHookCoveredCall-burnExpiredOption}.
function burnExpiredOption(uint256 optionId) external {
CallOption storage call = optionParams[optionId];

require(
block.timestamp > call.expiration,
"burnExpiredOption -- the option must be expired"
);

require(
!call.settled,
"burnExpiredOption -- the option has already been settled"
);

require(
call.highBidder == address(0),
"burnExpiredOption -- the option must not have bids"
);

// burn the option NFT
_burn(optionId);

// settle the option
call.settled = true;

emit ExpiredCallBurned(optionId);
}

//// ---- Administrative Fns.

// forward to protocol pauseability
Expand Down
15 changes: 13 additions & 2 deletions src/interfaces/IHookCoveredCall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ interface IHookCoveredCall is IERC721Metadata {
uint256 expiration
);

/// @notice emitted when a call option is destroyed
event CallDestroyed(uint256 optionId);
/// @notice emitted when a call option is settled
event CallSettled(uint256 optionId);

/// @notice emitted when a call option is reclaimed
event CallReclaimed(uint256 optionId);

/// @notice emitted when a expired call option is burned
event ExpiredCallBurned(uint256 optionId);

/// @notice emitted when a call option settlement auction gets and accepts a new bid
/// @param bidder the account placing the bid that is now the high bidder
Expand Down Expand Up @@ -126,4 +132,9 @@ interface IHookCoveredCall is IERC721Metadata {
/// @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;


/// @notice Allows anyone to burn the instrument NFT for an expired option.
/// @param optionId of the option to burn.
function burnExpiredOption(uint256 optionId) external;
}
Loading