Skip to content

Commit

Permalink
[Feature] [Gas] create a multi-asset ERC-721 vault (#22)
Browse files Browse the repository at this point in the history
This vault takes a similar form to our current ERC-721 vault; however, instead of 
requiring that a unique vault be deployed for each underlying ERC-721 token, this single
vault can support multiple tokenIds each with a distinct entitlement and a beneficial owner.

We believe this will result in substantial gas savings by removing the requirement that a
new vault is deployed for each minted option. Additionally, more care is taken in this
implementation to remove extraneous SSTOREs.

One area where we could get further SSTORE improvements would be by thining the 
entitlement struct that we persist to not include the notion of the vault address, as the
vault adderss is the same for each asset within the vault.
  • Loading branch information
jake-nyquist authored May 19, 2022
1 parent b4424a2 commit 64edc62
Show file tree
Hide file tree
Showing 14 changed files with 1,560 additions and 89 deletions.
6 changes: 4 additions & 2 deletions src/HookCoveredCallFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ contract HookCoveredCallFactory is
getCallInstrument[assetAddress] == address(0),
"makeCallInstrument -- a call instrument already exists"
);
// make sure new instruments created by admins.
// make sure new instruments created by admins or the role
// has been burned
require(
_protocol.hasRole(ALLOWLISTER_ROLE, msg.sender),
_protocol.hasRole(ALLOWLISTER_ROLE, msg.sender) ||
_protocol.hasRole(ALLOWLISTER_ROLE, address(0)),
"makeCallInstrument -- Only admins can make instruments"
);

Expand Down
42 changes: 15 additions & 27 deletions src/HookCoveredCallImplV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ contract HookCoveredCallImplV1 is
address protocol,
address nftContract,
address hookVaultFactory
) public initializer {
) external initializer {
_protocol = IHookProtocol(protocol);
_erc721VaultFactory = IHookERC721VaultFactory(hookVaultFactory);
weth = _protocol.getWETHAddress();
Expand Down Expand Up @@ -179,13 +179,7 @@ contract HookCoveredCallImplV1 is
vault.imposeEntitlement(entitlement, signature);

return
_mintOptionWithVault(
writer,
address(vault),
assetId,
strikePrice,
expirationTime
);
_mintOptionWithVault(writer, vault, assetId, strikePrice, expirationTime);
}

/// @dev See {IHookCoveredCall-mintWithEntitledVault}.
Expand Down Expand Up @@ -223,13 +217,7 @@ contract HookCoveredCallImplV1 is
address writer = vault.getBeneficialOwner(assetId);

return
_mintOptionWithVault(
writer,
vaultAddress,
assetId,
strikePrice,
expirationTime
);
_mintOptionWithVault(writer, vault, assetId, strikePrice, expirationTime);
}

/// @dev See {IHookCoveredCall-mintWithErc721}.
Expand Down Expand Up @@ -259,18 +247,18 @@ contract HookCoveredCallImplV1 is
);

// FIND OR CREATE HOOK VAULT, SET AN ENTITLEMENT
address vault = _erc721VaultFactory.getVault(tokenAddress, tokenId);
if (vault == address(0)) {
vault = _erc721VaultFactory.makeVault(tokenAddress, tokenId);
}
IHookERC721Vault vault = _erc721VaultFactory.findOrCreateVault(
tokenAddress,
tokenId
);

/// IMPORTANT: the entitlement entitles the user to this contract address. That means that even if this
// implementation code were upgraded, the contract at this address (i.e. with the new implementation) would
// retain the entitlement.
Entitlements.Entitlement memory entitlement = Entitlements.Entitlement({
beneficialOwner: tokenOwner,
operator: address(this),
vaultAddress: vault,
vaultAddress: address(vault),
assetId: assetId, /// assume that the asset within the vault has assetId 0
expiry: expirationTime
});
Expand All @@ -279,21 +267,21 @@ contract HookCoveredCallImplV1 is
// here will be accepted by the vault because we are also simultaneously tendering the asset.
IERC721(tokenAddress).safeTransferFrom(
tokenOwner,
vault,
address(vault),
tokenId,
abi.encode(entitlement)
);

// make sure that the vault actually has the asset.
require(
IHookVault(vault).getHoldsAsset(assetId),
vault.getHoldsAsset(assetId),
"mintWithErc712 -- asset must be in vault"
);

return
_mintOptionWithVault(
tokenOwner,
vault,
IHookVault(vault),
assetId,
strikePrice,
expirationTime
Expand All @@ -304,13 +292,13 @@ contract HookCoveredCallImplV1 is
/// @dev the vault is completely unchecked here, so the caller must ensure the vault is created,
/// has a valid entitlement, and has the asset inside it
/// @param writer the writer of the call option, usually the current owner of the underlying asset
/// @param vaultAddress the address of the IHookVault which contains the underlying asset
/// @param vault the address of the IHookVault which contains the underlying asset
/// @param assetId the id of the underlying asset
/// @param strikePrice the strike price for this current option, in ETH
/// @param expirationTime the time after which the option will be considered expired
function _mintOptionWithVault(
address writer,
address vaultAddress,
IHookVault vault,
uint256 assetId,
uint256 strikePrice,
uint256 expirationTime
Expand All @@ -328,7 +316,7 @@ contract HookCoveredCallImplV1 is
// save the option metadata
optionParams[newOptionId] = CallOption({
writer: writer,
vaultAddress: vaultAddress,
vaultAddress: address(vault),
assetId: assetId,
strike: strikePrice,
expiration: expirationTime,
Expand All @@ -348,7 +336,7 @@ contract HookCoveredCallImplV1 is

emit CallCreated(
writer,
vaultAddress,
address(vault),
assetId,
newOptionId,
strikePrice,
Expand Down
24 changes: 24 additions & 0 deletions src/HookERC721MultiVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
pragma solidity ^0.8.10;

import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";

/// @title ERC-721 MultiVault Proxy Contract
/// @author Jake Nyquist -- [email protected]
/// @notice Each instance of this contract is a unique multi-vault which references the
/// shared implementation pointed to by the Beacon
contract HookERC721MultiVault is BeaconProxy {
constructor(
address beacon,
address nftAddress,
address hookProtocolAddress
)
BeaconProxy(
beacon,
abi.encodeWithSignature(
"initialize(address,address)",
nftAddress,
hookProtocolAddress
)
)
{}
}
15 changes: 15 additions & 0 deletions src/HookERC721MultiVaultBeacon.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pragma solidity ^0.8.10;

import "./HookUpgradeableBeacon.sol";

/// @title HookERC721MultiVaultBeacon -- beacon holding pointer to current ERC721MultiVault implementation
/// @author Jake Nyquist -- [email protected]
/// @notice The beacon broadcasts the address which contains the existing implementation of the ERC721MultiVault
/// @dev Permissions for who can upgrade are contained within the protocol contract.
contract HookERC721MultiVaultBeacon is HookUpgradeableBeacon {
constructor(
address implementation,
address hookProtocol,
bytes32 upgraderRole
) HookUpgradeableBeacon(implementation, hookProtocol, upgraderRole) {}
}
Loading

0 comments on commit 64edc62

Please sign in to comment.