diff --git a/src/HookBeaconProxy.sol b/src/HookBeaconProxy.sol index f4234b6..3060584 100644 --- a/src/HookBeaconProxy.sol +++ b/src/HookBeaconProxy.sol @@ -16,77 +16,64 @@ import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; /// This is an extension of the OpenZeppelin beacon proxy, however differs in that it is initializeable, which means /// it is usable with Create2. contract HookBeaconProxy is Proxy, ERC1967Upgrade { - /// @dev The constructor is empty in this case because the proxy is initializeable - constructor() {} + /// @dev The constructor is empty in this case because the proxy is initializeable + constructor() {} - bytes32 constant _INITIALIZED_SLOT = - bytes32(uint256(keccak256("initializeable.beacon.version")) - 1); - bytes32 constant _INITIALIZING_SLOT = - bytes32(uint256(keccak256("initializeable.beacon.initializing")) - 1); + bytes32 constant _INITIALIZED_SLOT = bytes32(uint256(keccak256("initializeable.beacon.version")) - 1); + bytes32 constant _INITIALIZING_SLOT = bytes32(uint256(keccak256("initializeable.beacon.initializing")) - 1); - /// - /// @dev Triggered when the contract has been initialized or reinitialized. - /// - event Initialized(uint8 version); + /// + /// @dev Triggered when the contract has been initialized or reinitialized. + /// + event Initialized(uint8 version); - /// @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, - /// `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`. - modifier initializer() { - bool isTopLevelCall = _setInitializedVersion(1); - if (isTopLevelCall) { - StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value = true; + /// @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + /// `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`. + modifier initializer() { + bool isTopLevelCall = _setInitializedVersion(1); + if (isTopLevelCall) { + StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value = true; + } + _; + if (isTopLevelCall) { + StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value = false; + emit Initialized(1); + } } - _; - if (isTopLevelCall) { - StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value = false; - emit Initialized(1); - } - } - function _setInitializedVersion(uint8 version) private returns (bool) { - // If the contract is initializing we ignore whether _initialized is set in order to support multiple - // inheritance patterns, but we only do this in the context of a constructor, and for the lowest level - // of initializers, because in other contexts the contract may have been reentered. - if (StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value) { - require( - version == 1 && !Address.isContract(address(this)), - "contract is already initialized" - ); - return false; - } else { - require( - StorageSlot.getUint256Slot(_INITIALIZED_SLOT).value < version, - "contract is already initialized" - ); - StorageSlot.getUint256Slot(_INITIALIZED_SLOT).value = version; - return true; + function _setInitializedVersion(uint8 version) private returns (bool) { + // If the contract is initializing we ignore whether _initialized is set in order to support multiple + // inheritance patterns, but we only do this in the context of a constructor, and for the lowest level + // of initializers, because in other contexts the contract may have been reentered. + if (StorageSlot.getBooleanSlot(_INITIALIZING_SLOT).value) { + require(version == 1 && !Address.isContract(address(this)), "contract is already initialized"); + return false; + } else { + require(StorageSlot.getUint256Slot(_INITIALIZED_SLOT).value < version, "contract is already initialized"); + StorageSlot.getUint256Slot(_INITIALIZED_SLOT).value = version; + return true; + } } - } - /// @dev Initializes the proxy with `beacon`. - /// - /// If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This - /// will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity - /// constructor. - /// - /// Requirements: - /// - ///- `beacon` must be a contract with the interface {IBeacon}. - /// - function initializeBeacon(address beacon, bytes memory data) - public - initializer - { - assert( - _BEACON_SLOT == bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1) - ); - _upgradeBeaconToAndCall(beacon, data, false); - } + /// @dev Initializes the proxy with `beacon`. + /// + /// If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This + /// will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity + /// constructor. + /// + /// Requirements: + /// + ///- `beacon` must be a contract with the interface {IBeacon}. + /// + function initializeBeacon(address beacon, bytes memory data) public initializer { + assert(_BEACON_SLOT == bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1)); + _upgradeBeaconToAndCall(beacon, data, false); + } - /// - /// @dev Returns the current implementation address of the associated beacon. - /// - function _implementation() internal view virtual override returns (address) { - return IBeacon(_getBeacon()).implementation(); - } + /// + /// @dev Returns the current implementation address of the associated beacon. + /// + function _implementation() internal view virtual override returns (address) { + return IBeacon(_getBeacon()).implementation(); + } } diff --git a/src/HookCoveredCallFactory.sol b/src/HookCoveredCallFactory.sol index d07b281..aba90ab 100644 --- a/src/HookCoveredCallFactory.sol +++ b/src/HookCoveredCallFactory.sol @@ -49,100 +49,72 @@ import "@openzeppelin/contracts/utils/Create2.sol"; /// @dev See {IHookCoveredCallFactory}. /// @dev The factory looks up certain roles by calling the {IHookProtocol} to verify // that the caller is allowed to take certain actions -contract HookCoveredCallFactory is - PermissionConstants, - IHookCoveredCallFactory -{ - /// @notice Registry of all of the active markets projects with supported call instruments - mapping(address => address) public override getCallInstrument; - - /// @notice address of the beacon that contains the address of the current {IHookCoveredCall} implementation - address private immutable _beacon; - - /// @notice the address of the protocol, which contains the rule - IHookProtocol private immutable _protocol; - - /// @notice the address of an account that should automatically be approved to transfer the ERC-721 tokens - /// created by the {IHookCoveredCall} to represent instruments. This value is not used by the factory directly, - /// as this functionality is implemented by the {IHookCoveredCall} - address private immutable _preApprovedMarketplace; - - /// @param hookProtocolAddress the address of the deployed {IHookProtocol} contract on this chain - /// @param beaconAddress the address of the deployed beacon pointing to the current covered call implementation - /// @param preApprovedMarketplace the address of an account approved to transfer instrument NFTs without owner approval - constructor( - address hookProtocolAddress, - address beaconAddress, - address preApprovedMarketplace - ) { - require( - Address.isContract(hookProtocolAddress), - "hook protocol must be a contract" - ); - require( - Address.isContract(beaconAddress), - "beacon address must be a contract" - ); - require( - Address.isContract(preApprovedMarketplace), - "pre-approved marketplace must be a contract" - ); - _beacon = beaconAddress; - _protocol = IHookProtocol(hookProtocolAddress); - _preApprovedMarketplace = preApprovedMarketplace; - } - - /// @dev See {IHookCoveredCallFactory-makeCallInstrument}. - /// @dev Only holders of the ALLOWLISTER_ROLE on the {IHookProtocol} can create these addresses. - function makeCallInstrument(address assetAddress) external returns (address) { - require( - getCallInstrument[assetAddress] == address(0), - "makeCallInstrument-a call instrument already exists" - ); - // make sure new instruments created by admins or the role - // has been burned - require( - _protocol.hasRole(ALLOWLISTER_ROLE, msg.sender) || - _protocol.hasRole(ALLOWLISTER_ROLE, address(0)), - "makeCallInstrument-Only admins can make instruments" - ); - - IInitializeableBeacon bp = IInitializeableBeacon( - Create2.deploy( - 0, - _callInstrumentSalt(assetAddress), - type(HookBeaconProxy).creationCode - ) - ); - - bp.initializeBeacon( - _beacon, - /// This is the ABI encoded initializer on the IHookERC721Vault.sol - abi.encodeWithSignature( - "initialize(address,address,address,address)", - _protocol, - assetAddress, - _protocol.vaultContract(), - _preApprovedMarketplace - ) - ); - - // Persist the call instrument onto the hook protocol - getCallInstrument[assetAddress] = address(bp); - - emit CoveredCallInstrumentCreated(assetAddress, address(bp)); - - return address(bp); - } - - /// @dev generate a consistent create2 salt to be used when deploying a - /// call instrument - /// @param underlyingAddress the account for the call option salt - function _callInstrumentSalt(address underlyingAddress) - internal - pure - returns (bytes32) - { - return keccak256(abi.encode(underlyingAddress)); - } +contract HookCoveredCallFactory is PermissionConstants, IHookCoveredCallFactory { + /// @notice Registry of all of the active markets projects with supported call instruments + mapping(address => address) public override getCallInstrument; + + /// @notice address of the beacon that contains the address of the current {IHookCoveredCall} implementation + address private immutable _beacon; + + /// @notice the address of the protocol, which contains the rule + IHookProtocol private immutable _protocol; + + /// @notice the address of an account that should automatically be approved to transfer the ERC-721 tokens + /// created by the {IHookCoveredCall} to represent instruments. This value is not used by the factory directly, + /// as this functionality is implemented by the {IHookCoveredCall} + address private immutable _preApprovedMarketplace; + + /// @param hookProtocolAddress the address of the deployed {IHookProtocol} contract on this chain + /// @param beaconAddress the address of the deployed beacon pointing to the current covered call implementation + /// @param preApprovedMarketplace the address of an account approved to transfer instrument NFTs without owner approval + constructor(address hookProtocolAddress, address beaconAddress, address preApprovedMarketplace) { + require(Address.isContract(hookProtocolAddress), "hook protocol must be a contract"); + require(Address.isContract(beaconAddress), "beacon address must be a contract"); + require(Address.isContract(preApprovedMarketplace), "pre-approved marketplace must be a contract"); + _beacon = beaconAddress; + _protocol = IHookProtocol(hookProtocolAddress); + _preApprovedMarketplace = preApprovedMarketplace; + } + + /// @dev See {IHookCoveredCallFactory-makeCallInstrument}. + /// @dev Only holders of the ALLOWLISTER_ROLE on the {IHookProtocol} can create these addresses. + function makeCallInstrument(address assetAddress) external returns (address) { + require(getCallInstrument[assetAddress] == address(0), "makeCallInstrument-a call instrument already exists"); + // make sure new instruments created by admins or the role + // has been burned + require( + _protocol.hasRole(ALLOWLISTER_ROLE, msg.sender) || _protocol.hasRole(ALLOWLISTER_ROLE, address(0)), + "makeCallInstrument-Only admins can make instruments" + ); + + IInitializeableBeacon bp = IInitializeableBeacon( + Create2.deploy(0, _callInstrumentSalt(assetAddress), type(HookBeaconProxy).creationCode) + ); + + bp.initializeBeacon( + _beacon, + /// This is the ABI encoded initializer on the IHookERC721Vault.sol + abi.encodeWithSignature( + "initialize(address,address,address,address)", + _protocol, + assetAddress, + _protocol.vaultContract(), + _preApprovedMarketplace + ) + ); + + // Persist the call instrument onto the hook protocol + getCallInstrument[assetAddress] = address(bp); + + emit CoveredCallInstrumentCreated(assetAddress, address(bp)); + + return address(bp); + } + + /// @dev generate a consistent create2 salt to be used when deploying a + /// call instrument + /// @param underlyingAddress the account for the call option salt + function _callInstrumentSalt(address underlyingAddress) internal pure returns (bytes32) { + return keccak256(abi.encode(underlyingAddress)); + } } diff --git a/src/HookCoveredCallImplV1.sol b/src/HookCoveredCallImplV1.sol index 3721473..ef3c1cb 100644 --- a/src/HookCoveredCallImplV1.sol +++ b/src/HookCoveredCallImplV1.sol @@ -60,734 +60,610 @@ import "./mixin/HookInstrumentERC721.sol"; /// @dev In the context of a single call option, the role of the writer is non-transferrable. /// @dev This contract is intended to be an implementation referenced by a proxy contract HookCoveredCallImplV1 is - IHookCoveredCall, - HookInstrumentERC721, - ReentrancyGuard, - Initializable, - PermissionConstants + IHookCoveredCall, + HookInstrumentERC721, + ReentrancyGuard, + Initializable, + PermissionConstants { - using Counters for Counters.Counter; - - /// @notice The metadata for each covered call option stored within the protocol - /// @param writer The address of the writer that created the call option - /// @param expiration The expiration time of the call option - /// @param assetId the asset id of the underlying within the vault - /// @param vaultAddress the address of the vault holding the underlying asset - /// @param strike The strike price to exercise the call option - /// @param bid is the current high bid in the settlement auction - /// @param highBidder is the address that made the current winning bid in the settlement auction - /// @param settled a flag that marks when a settlement action has taken place successfully. Once this flag is set, ETH should not - /// be sent from the contract related to this particular option - struct CallOption { - address writer; - uint32 expiration; - uint32 assetId; - address vaultAddress; - uint128 strike; - uint128 bid; - address highBidder; - bool settled; - } - - /// --- Storage - - /// @dev holds the current ID for the last minted option. The optionId also serves as the tokenId for - /// the associated option instrument NFT. - Counters.Counter private _optionIds; - - /// @dev the address of the factory in the Hook protocol that can be used to generate ERC721 vaults - IHookERC721VaultFactory private _erc721VaultFactory; - - /// @dev the address of the deployed hook protocol contract, which has permissions and access controls - IHookProtocol private _protocol; - - /// @dev storage of all existing options contracts. - mapping(uint256 => CallOption) public optionParams; - - /// @dev storage of current call active call option for a specific asset - /// mapping(vaultAddress => mapping(assetId => CallOption)) - // the call option is is referenced via the optionID stored in optionParams - mapping(IHookVault => mapping(uint32 => uint256)) public assetOptions; - - /// @dev mapping to store the amount of eth in wei that may - /// be claimed by the current ownerOf the option nft. - mapping(uint256 => uint256) public optionClaims; - - /// @dev the address of the token contract permitted to serve as underlying assets for this - /// instrument. - address public allowedUnderlyingAddress; - - /// @dev the address of WETH on the chain where this contract is deployed - address public weth; - - /// @dev this is the minimum duration of an option created in this contract instance - uint256 public minimumOptionDuration; - - /// @dev this is the minimum amount of the current bid that the new bid - /// must exceed the current bid by in order to be considered valid. - /// This amount is expressed in basis points (i.e. 1/100th of 1%) - uint256 public minBidIncrementBips; - - /// @dev this is the amount of time before the expiration of the option - /// that the settlement auction will begin. - uint256 public settlementAuctionStartOffset; - - /// @dev this is a flag that can be set to pause this particular - /// instance of the call option contract. - /// NOTE: settlement auctions are still enabled in - /// this case because pausing the market should not change the - /// financial situation for the holder of the options. - bool public marketPaused; - - /// @dev Emitted when the market is paused or unpaused - /// @param paused true if paused false otherwise - event MarketPauseUpdated(bool paused); - - /// @dev Emitted when the bid increment is updated - /// @param bidIncrementBips the new bid increment amount in bips - event MinBidIncrementUpdated(uint256 bidIncrementBips); - - /// @dev emitted when the settlement auction start offset is updated - /// @param startOffset new number of seconds from expiration when the start offset begins - event SettlementAuctionStartOffsetUpdated(uint256 startOffset); - - /// @dev emitted when the minimum duration for an option is changed - /// @param optionDuration new minimum length of an option in seconds. - event MinOptionDurationUpdated(uint256 optionDuration); - - /// --- Constructor - // the constructor cannot have arguments in proxied contracts. - constructor() HookInstrumentERC721("Call") {} - - /// @notice Initializes the specific instance of the instrument contract. - /// @dev Because the deployed contract is proxied, arguments unique to each deployment - /// must be passed in an individual initializer. This function is like a constructor. - /// @param protocol the address of the Hook protocol (which contains configurations) - /// @param nftContract the address for the ERC-721 contract that can serve as underlying instruments - /// @param hookVaultFactory the address of the ERC-721 vault registry - /// @param preApprovedMarketplace the address of the contract which will automatically approved - /// to transfer option ERC721s owned by any account when they're minted - function initialize( - address protocol, - address nftContract, - address hookVaultFactory, - address preApprovedMarketplace - ) public initializer { - _protocol = IHookProtocol(protocol); - _erc721VaultFactory = IHookERC721VaultFactory(hookVaultFactory); - weth = _protocol.getWETHAddress(); - _preApprovedMarketplace = preApprovedMarketplace; - allowedUnderlyingAddress = nftContract; - /// increment the optionId such that id=0 can be treated as the null value - _optionIds.increment(); - - /// Initialize basic configuration. - /// Even though these are defaults, we cannot set them in the constructor because - /// each instance of this contract will need to have the storage initialized - /// to read from these values (this is the implementation contract pointed to by a proxy) - minimumOptionDuration = 1 days; - minBidIncrementBips = 50; - settlementAuctionStartOffset = 1 days; - marketPaused = false; - } - - /// ---- Option Writer Functions ---- // - - /// @dev See {IHookCoveredCall-mintWithVault}. - function mintWithVault( - address vaultAddress, - uint32 assetId, - uint128 strikePrice, - uint32 expirationTime, - Signatures.Signature calldata signature - ) external nonReentrant whenNotPaused returns (uint256) { - IHookVault vault = IHookVault(vaultAddress); - require( - allowedUnderlyingAddress == vault.assetAddress(assetId), - "mWV-token not allowed" - ); - require(vault.getHoldsAsset(assetId), "mWV-asset not in vault"); - require( - _allowedVaultImplementation( - vaultAddress, - allowedUnderlyingAddress, - assetId - ), - "mWV-can only mint with protocol vaults" - ); - // the beneficial owner is the only one able to impose entitlements, so - // we need to require that they've done so here. - address writer = vault.getBeneficialOwner(assetId); - - require( - msg.sender == writer || msg.sender == vault.getApprovedOperator(assetId), - "mWV-called by someone other than the owner or operator" - ); - - vault.imposeEntitlement( - address(this), - expirationTime, - assetId, - signature.v, - signature.r, - signature.s - ); - - return - _mintOptionWithVault(writer, vault, assetId, strikePrice, expirationTime); - } - - /// @dev See {IHookCoveredCall-mintWithEntitledVault}. - function mintWithEntitledVault( - address vaultAddress, - uint32 assetId, - uint128 strikePrice, - uint32 expirationTime - ) external nonReentrant whenNotPaused returns (uint256) { - IHookVault vault = IHookVault(vaultAddress); - - require( - allowedUnderlyingAddress == vault.assetAddress(assetId), - "mWEV-token not allowed" - ); - require(vault.getHoldsAsset(assetId), "mWEV-asset must be in vault"); - (bool active, address operator) = vault.getCurrentEntitlementOperator( - assetId - ); - require( - active && operator == address(this), - "mWEV-call contract not operator" - ); - - require( - expirationTime == vault.entitlementExpiration(assetId), - "mWEV-entitlement expiration different" - ); - require( - _allowedVaultImplementation( - vaultAddress, - allowedUnderlyingAddress, - assetId - ), - "mWEV-only protocol vaults allowed" - ); - - // the beneficial owner owns the asset so - // they should receive the option. - address writer = vault.getBeneficialOwner(assetId); - - require( - writer == msg.sender || vault.getApprovedOperator(assetId) == msg.sender, - "mWEV-only owner or operator may mint" - ); - - return - _mintOptionWithVault(writer, vault, assetId, strikePrice, expirationTime); - } - - /// @dev See {IHookCoveredCall-mintWithErc721}. - function mintWithErc721( - address tokenAddress, - uint256 tokenId, - uint128 strikePrice, - uint32 expirationTime - ) external nonReentrant whenNotPaused returns (uint256) { - address tokenOwner = IERC721(tokenAddress).ownerOf(tokenId); - require( - allowedUnderlyingAddress == tokenAddress, - "mWE7-token not on allowlist" - ); - - require( - msg.sender == tokenOwner || - IERC721(tokenAddress).isApprovedForAll(tokenOwner, msg.sender) || - IERC721(tokenAddress).getApproved(tokenId) == msg.sender, - "mWE7-caller not owner or operator" - ); - - // NOTE: we can mint the option since our contract is approved - // this is to ensure additionally that the msg.sender isn't a unexpected address - require( - IERC721(tokenAddress).isApprovedForAll(tokenOwner, address(this)) || - IERC721(tokenAddress).getApproved(tokenId) == address(this), - "mWE7-not approved operator" - ); - - // FIND OR CREATE HOOK VAULT, SET AN ENTITLEMENT - IHookERC721Vault vault = _erc721VaultFactory.findOrCreateVault( - tokenAddress, - tokenId - ); - - uint32 assetId = 0; - if ( - address(vault) == - Create2.computeAddress( - BeaconSalts.multiVaultSalt(tokenAddress), - BeaconSalts.ByteCodeHash, - address(_erc721VaultFactory) - ) - ) { - // If the vault is a multi-vault, it requires that the assetId matches the - // tokenId, instead of having a standard assetI of 0 - assetId = uint32(tokenId); + using Counters for Counters.Counter; + + /// @notice The metadata for each covered call option stored within the protocol + /// @param writer The address of the writer that created the call option + /// @param expiration The expiration time of the call option + /// @param assetId the asset id of the underlying within the vault + /// @param vaultAddress the address of the vault holding the underlying asset + /// @param strike The strike price to exercise the call option + /// @param bid is the current high bid in the settlement auction + /// @param highBidder is the address that made the current winning bid in the settlement auction + /// @param settled a flag that marks when a settlement action has taken place successfully. Once this flag is set, ETH should not + /// be sent from the contract related to this particular option + struct CallOption { + address writer; + uint32 expiration; + uint32 assetId; + address vaultAddress; + uint128 strike; + uint128 bid; + address highBidder; + bool settled; } - uint256 optionId = _mintOptionWithVault( - tokenOwner, - IHookVault(vault), - assetId, - strikePrice, - expirationTime - ); - - // transfer the underlying asset into our vault, passing along the entitlement. The entitlement specified - // here will be accepted by the vault because we are also simultaneously tendering the asset. - IERC721(tokenAddress).safeTransferFrom( - tokenOwner, - address(vault), - tokenId, - abi.encode(tokenOwner, address(this), expirationTime) - ); - - // make sure that the vault actually has the asset. - require( - IERC721(tokenAddress).ownerOf(tokenId) == address(vault), - "mWE7-asset not in vault" - ); - - return optionId; - } - - /// @notice internal use function to record the option and mint it - /// @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 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, - IHookVault vault, - uint32 assetId, - uint128 strikePrice, - uint32 expirationTime - ) private returns (uint256) { - // NOTE: The settlement auction always occurs one day before expiration - require( - expirationTime > block.timestamp + minimumOptionDuration, - "_mOWV-expires sooner than min duration" - ); - - // verify that, if there is a previous option on this asset, it has already settled. - uint256 prevOptionId = assetOptions[vault][assetId]; - if (prevOptionId != 0) { - require( - optionParams[prevOptionId].settled, - "_mOWV-previous option must be settled" - ); + /// --- Storage + + /// @dev holds the current ID for the last minted option. The optionId also serves as the tokenId for + /// the associated option instrument NFT. + Counters.Counter private _optionIds; + + /// @dev the address of the factory in the Hook protocol that can be used to generate ERC721 vaults + IHookERC721VaultFactory private _erc721VaultFactory; + + /// @dev the address of the deployed hook protocol contract, which has permissions and access controls + IHookProtocol private _protocol; + + /// @dev storage of all existing options contracts. + mapping(uint256 => CallOption) public optionParams; + + /// @dev storage of current call active call option for a specific asset + /// mapping(vaultAddress => mapping(assetId => CallOption)) + // the call option is is referenced via the optionID stored in optionParams + mapping(IHookVault => mapping(uint32 => uint256)) public assetOptions; + + /// @dev mapping to store the amount of eth in wei that may + /// be claimed by the current ownerOf the option nft. + mapping(uint256 => uint256) public optionClaims; + + /// @dev the address of the token contract permitted to serve as underlying assets for this + /// instrument. + address public allowedUnderlyingAddress; + + /// @dev the address of WETH on the chain where this contract is deployed + address public weth; + + /// @dev this is the minimum duration of an option created in this contract instance + uint256 public minimumOptionDuration; + + /// @dev this is the minimum amount of the current bid that the new bid + /// must exceed the current bid by in order to be considered valid. + /// This amount is expressed in basis points (i.e. 1/100th of 1%) + uint256 public minBidIncrementBips; + + /// @dev this is the amount of time before the expiration of the option + /// that the settlement auction will begin. + uint256 public settlementAuctionStartOffset; + + /// @dev this is a flag that can be set to pause this particular + /// instance of the call option contract. + /// NOTE: settlement auctions are still enabled in + /// this case because pausing the market should not change the + /// financial situation for the holder of the options. + bool public marketPaused; + + /// @dev Emitted when the market is paused or unpaused + /// @param paused true if paused false otherwise + event MarketPauseUpdated(bool paused); + + /// @dev Emitted when the bid increment is updated + /// @param bidIncrementBips the new bid increment amount in bips + event MinBidIncrementUpdated(uint256 bidIncrementBips); + + /// @dev emitted when the settlement auction start offset is updated + /// @param startOffset new number of seconds from expiration when the start offset begins + event SettlementAuctionStartOffsetUpdated(uint256 startOffset); + + /// @dev emitted when the minimum duration for an option is changed + /// @param optionDuration new minimum length of an option in seconds. + event MinOptionDurationUpdated(uint256 optionDuration); + + /// --- Constructor + // the constructor cannot have arguments in proxied contracts. + constructor() HookInstrumentERC721("Call") {} + + /// @notice Initializes the specific instance of the instrument contract. + /// @dev Because the deployed contract is proxied, arguments unique to each deployment + /// must be passed in an individual initializer. This function is like a constructor. + /// @param protocol the address of the Hook protocol (which contains configurations) + /// @param nftContract the address for the ERC-721 contract that can serve as underlying instruments + /// @param hookVaultFactory the address of the ERC-721 vault registry + /// @param preApprovedMarketplace the address of the contract which will automatically approved + /// to transfer option ERC721s owned by any account when they're minted + function initialize(address protocol, address nftContract, address hookVaultFactory, address preApprovedMarketplace) + public + initializer + { + _protocol = IHookProtocol(protocol); + _erc721VaultFactory = IHookERC721VaultFactory(hookVaultFactory); + weth = _protocol.getWETHAddress(); + _preApprovedMarketplace = preApprovedMarketplace; + allowedUnderlyingAddress = nftContract; + /// increment the optionId such that id=0 can be treated as the null value + _optionIds.increment(); + + /// Initialize basic configuration. + /// Even though these are defaults, we cannot set them in the constructor because + /// each instance of this contract will need to have the storage initialized + /// to read from these values (this is the implementation contract pointed to by a proxy) + minimumOptionDuration = 1 days; + minBidIncrementBips = 50; + settlementAuctionStartOffset = 1 days; + marketPaused = false; } - // generate the next optionId - _optionIds.increment(); - uint256 newOptionId = _optionIds.current(); - - // save the option metadata - optionParams[newOptionId] = CallOption({ - writer: writer, - vaultAddress: address(vault), - assetId: assetId, - strike: strikePrice, - expiration: expirationTime, - bid: 0, - highBidder: address(0), - settled: false - }); - - // send the option NFT to the underlying token owner. - _safeMint(writer, newOptionId); - - // If msg.sender and tokenOwner are different accounts, approve the msg.sender - // to transfer the option NFT as it already had the right to transfer the underlying NFT. - if (msg.sender != writer) { - _approve(msg.sender, newOptionId); + /// ---- Option Writer Functions ---- // + + /// @dev See {IHookCoveredCall-mintWithVault}. + function mintWithVault( + address vaultAddress, + uint32 assetId, + uint128 strikePrice, + uint32 expirationTime, + Signatures.Signature calldata signature + ) external nonReentrant whenNotPaused returns (uint256) { + IHookVault vault = IHookVault(vaultAddress); + require(allowedUnderlyingAddress == vault.assetAddress(assetId), "mWV-token not allowed"); + require(vault.getHoldsAsset(assetId), "mWV-asset not in vault"); + require( + _allowedVaultImplementation(vaultAddress, allowedUnderlyingAddress, assetId), + "mWV-can only mint with protocol vaults" + ); + // the beneficial owner is the only one able to impose entitlements, so + // we need to require that they've done so here. + address writer = vault.getBeneficialOwner(assetId); + + require( + msg.sender == writer || msg.sender == vault.getApprovedOperator(assetId), + "mWV-called by someone other than the owner or operator" + ); + + vault.imposeEntitlement(address(this), expirationTime, assetId, signature.v, signature.r, signature.s); + + return _mintOptionWithVault(writer, vault, assetId, strikePrice, expirationTime); } - assetOptions[vault][assetId] = newOptionId; - - emit CallCreated( - writer, - address(vault), - assetId, - newOptionId, - strikePrice, - expirationTime - ); - - return newOptionId; - } - - // --- Bidder Functions - - modifier biddingEnabled(uint256 optionId) { - CallOption memory call = optionParams[optionId]; - require(call.expiration > block.timestamp, "bE-expired"); - require( - (call.expiration - settlementAuctionStartOffset) <= block.timestamp, - "bE-bidding starts on last day" - ); - require(!call.settled, "bE-already settled"); - _; - } - - /// @dev method to verify that a particular vault was created by the protocol's vault factory - /// @param vaultAddress location where the vault is deployed - /// @param underlyingAddress address of underlying asset - /// @param assetId id of the asset within the vault - function _allowedVaultImplementation( - address vaultAddress, - address underlyingAddress, - uint32 assetId - ) internal view returns (bool) { - // First check if the multiVault is the one to save a bit of gas - // in the case the user is optimizing for gas savings (by using MultiVault) - if ( - vaultAddress == - Create2.computeAddress( - BeaconSalts.multiVaultSalt(underlyingAddress), - BeaconSalts.ByteCodeHash, - address(_erc721VaultFactory) - ) - ) { - return true; + /// @dev See {IHookCoveredCall-mintWithEntitledVault}. + function mintWithEntitledVault(address vaultAddress, uint32 assetId, uint128 strikePrice, uint32 expirationTime) + external + nonReentrant + whenNotPaused + returns (uint256) + { + IHookVault vault = IHookVault(vaultAddress); + + require(allowedUnderlyingAddress == vault.assetAddress(assetId), "mWEV-token not allowed"); + require(vault.getHoldsAsset(assetId), "mWEV-asset must be in vault"); + (bool active, address operator) = vault.getCurrentEntitlementOperator(assetId); + require(active && operator == address(this), "mWEV-call contract not operator"); + + require(expirationTime == vault.entitlementExpiration(assetId), "mWEV-entitlement expiration different"); + require( + _allowedVaultImplementation(vaultAddress, allowedUnderlyingAddress, assetId), + "mWEV-only protocol vaults allowed" + ); + + // the beneficial owner owns the asset so + // they should receive the option. + address writer = vault.getBeneficialOwner(assetId); + + require( + writer == msg.sender || vault.getApprovedOperator(assetId) == msg.sender, + "mWEV-only owner or operator may mint" + ); + + return _mintOptionWithVault(writer, vault, assetId, strikePrice, expirationTime); } - 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; + /// @dev See {IHookCoveredCall-mintWithErc721}. + function mintWithErc721(address tokenAddress, uint256 tokenId, uint128 strikePrice, uint32 expirationTime) + external + nonReentrant + whenNotPaused + returns (uint256) + { + address tokenOwner = IERC721(tokenAddress).ownerOf(tokenId); + require(allowedUnderlyingAddress == tokenAddress, "mWE7-token not on allowlist"); + + require( + msg.sender == tokenOwner || IERC721(tokenAddress).isApprovedForAll(tokenOwner, msg.sender) + || IERC721(tokenAddress).getApproved(tokenId) == msg.sender, + "mWE7-caller not owner or operator" + ); + + // NOTE: we can mint the option since our contract is approved + // this is to ensure additionally that the msg.sender isn't a unexpected address + require( + IERC721(tokenAddress).isApprovedForAll(tokenOwner, address(this)) + || IERC721(tokenAddress).getApproved(tokenId) == address(this), + "mWE7-not approved operator" + ); + + // FIND OR CREATE HOOK VAULT, SET AN ENTITLEMENT + IHookERC721Vault vault = _erc721VaultFactory.findOrCreateVault(tokenAddress, tokenId); + + uint32 assetId = 0; + if ( + address(vault) + == Create2.computeAddress( + BeaconSalts.multiVaultSalt(tokenAddress), BeaconSalts.ByteCodeHash, address(_erc721VaultFactory) + ) + ) { + // If the vault is a multi-vault, it requires that the assetId matches the + // tokenId, instead of having a standard assetI of 0 + assetId = uint32(tokenId); + } + + uint256 optionId = _mintOptionWithVault(tokenOwner, IHookVault(vault), assetId, strikePrice, expirationTime); + + // transfer the underlying asset into our vault, passing along the entitlement. The entitlement specified + // here will be accepted by the vault because we are also simultaneously tendering the asset. + IERC721(tokenAddress).safeTransferFrom( + tokenOwner, address(vault), tokenId, abi.encode(tokenOwner, address(this), expirationTime) + ); + + // make sure that the vault actually has the asset. + require(IERC721(tokenAddress).ownerOf(tokenId) == address(vault), "mWE7-asset not in vault"); + + return optionId; } - return false; - } - - /// @dev See {IHookCoveredCall-bid}. - function bid(uint256 optionId) - external - payable - nonReentrant - biddingEnabled(optionId) - { - uint128 bidAmt = uint128(msg.value); - CallOption storage call = optionParams[optionId]; - - if (msg.sender == call.writer) { - /// handle the case where an option writer bids on - /// an underlying asset that they owned. In this case, as they would be - /// the recipient of the spread after the auction, they are able to bid - /// paying only the difference between their bid and the strike. - bidAmt += call.strike; + /// @notice internal use function to record the option and mint it + /// @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 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, + IHookVault vault, + uint32 assetId, + uint128 strikePrice, + uint32 expirationTime + ) private returns (uint256) { + // NOTE: The settlement auction always occurs one day before expiration + require(expirationTime > block.timestamp + minimumOptionDuration, "_mOWV-expires sooner than min duration"); + + // verify that, if there is a previous option on this asset, it has already settled. + uint256 prevOptionId = assetOptions[vault][assetId]; + if (prevOptionId != 0) { + require(optionParams[prevOptionId].settled, "_mOWV-previous option must be settled"); + } + + // generate the next optionId + _optionIds.increment(); + uint256 newOptionId = _optionIds.current(); + + // save the option metadata + optionParams[newOptionId] = CallOption({ + writer: writer, + vaultAddress: address(vault), + assetId: assetId, + strike: strikePrice, + expiration: expirationTime, + bid: 0, + highBidder: address(0), + settled: false + }); + + // send the option NFT to the underlying token owner. + _safeMint(writer, newOptionId); + + // If msg.sender and tokenOwner are different accounts, approve the msg.sender + // to transfer the option NFT as it already had the right to transfer the underlying NFT. + if (msg.sender != writer) { + _approve(msg.sender, newOptionId); + } + + assetOptions[vault][assetId] = newOptionId; + + emit CallCreated(writer, address(vault), assetId, newOptionId, strikePrice, expirationTime); + + return newOptionId; } - require( - bidAmt >= call.bid + ((call.bid * minBidIncrementBips) / 10000), - "b-must overbid by minBidIncrementBips" - ); - require(bidAmt > call.strike, "b-bid is lower than the strike price"); + // --- Bidder Functions - _returnBidToPreviousBidder(call); + modifier biddingEnabled(uint256 optionId) { + CallOption memory call = optionParams[optionId]; + require(call.expiration > block.timestamp, "bE-expired"); + require((call.expiration - settlementAuctionStartOffset) <= block.timestamp, "bE-bidding starts on last day"); + require(!call.settled, "bE-already settled"); + _; + } + + /// @dev method to verify that a particular vault was created by the protocol's vault factory + /// @param vaultAddress location where the vault is deployed + /// @param underlyingAddress address of underlying asset + /// @param assetId id of the asset within the vault + function _allowedVaultImplementation(address vaultAddress, address underlyingAddress, uint32 assetId) + internal + view + returns (bool) + { + // First check if the multiVault is the one to save a bit of gas + // in the case the user is optimizing for gas savings (by using MultiVault) + if ( + vaultAddress + == Create2.computeAddress( + BeaconSalts.multiVaultSalt(underlyingAddress), 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; + } - // set the new bidder - call.bid = bidAmt; - call.highBidder = msg.sender; + /// @dev See {IHookCoveredCall-bid}. + function bid(uint256 optionId) external payable nonReentrant biddingEnabled(optionId) { + uint128 bidAmt = uint128(msg.value); + CallOption storage call = optionParams[optionId]; + + if (msg.sender == call.writer) { + /// handle the case where an option writer bids on + /// an underlying asset that they owned. In this case, as they would be + /// the recipient of the spread after the auction, they are able to bid + /// paying only the difference between their bid and the strike. + bidAmt += call.strike; + } + + require( + bidAmt >= call.bid + ((call.bid * minBidIncrementBips) / 10000), "b-must overbid by minBidIncrementBips" + ); + require(bidAmt > call.strike, "b-bid is lower than the strike price"); + + _returnBidToPreviousBidder(call); + + // set the new bidder + call.bid = bidAmt; + call.highBidder = msg.sender; + + // the new high bidder is the beneficial owner of the asset. + // The beneficial owner must be set here instead of with a settlement + // because otherwise the writer will be able to remove the asset from the vault + // between the expiration and the settlement call, effectively stealing the asset. + IHookVault(call.vaultAddress).setBeneficialOwner(call.assetId, msg.sender); + + // emit event + emit Bid(optionId, bidAmt, msg.sender); + } - // the new high bidder is the beneficial owner of the asset. - // The beneficial owner must be set here instead of with a settlement - // because otherwise the writer will be able to remove the asset from the vault - // between the expiration and the settlement call, effectively stealing the asset. - IHookVault(call.vaultAddress).setBeneficialOwner(call.assetId, msg.sender); + function _returnBidToPreviousBidder(CallOption storage call) internal { + uint256 unNormalizedHighBid = call.bid; + if (call.highBidder == call.writer) { + unNormalizedHighBid -= call.strike; + } - // emit event - emit Bid(optionId, bidAmt, msg.sender); - } + // return current bidder's money + if (unNormalizedHighBid > 0) { + _safeTransferETHWithFallback(call.highBidder, unNormalizedHighBid); + } + } - function _returnBidToPreviousBidder(CallOption storage call) internal { - uint256 unNormalizedHighBid = call.bid; - if (call.highBidder == call.writer) { - unNormalizedHighBid -= call.strike; + /// @dev See {IHookCoveredCall-currentBid}. + function currentBid(uint256 optionId) external view returns (uint128) { + return optionParams[optionId].bid; } - // return current bidder's money - if (unNormalizedHighBid > 0) { - _safeTransferETHWithFallback(call.highBidder, unNormalizedHighBid); + /// @dev See {IHookCoveredCall-currentBidder}. + function currentBidder(uint256 optionId) external view returns (address) { + return optionParams[optionId].highBidder; } - } - /// @dev See {IHookCoveredCall-currentBid}. - function currentBid(uint256 optionId) external view returns (uint128) { - return optionParams[optionId].bid; - } + // ----- END OF OPTION FUNCTIONS ---------// + + /// @dev See {IHookCoveredCall-settleOption}. + function settleOption(uint256 optionId) external nonReentrant { + CallOption storage call = optionParams[optionId]; + require(call.highBidder != address(0), "s-bid must be won by someone"); + require(call.expiration < block.timestamp, "s-option must be expired"); + require(!call.settled, "s-the call cannot already be settled"); - /// @dev See {IHookCoveredCall-currentBidder}. - function currentBidder(uint256 optionId) external view returns (address) { - return optionParams[optionId].highBidder; - } + uint256 spread = call.bid - call.strike; - // ----- END OF OPTION FUNCTIONS ---------// + address optionOwner = ownerOf(optionId); - /// @dev See {IHookCoveredCall-settleOption}. - function settleOption(uint256 optionId) external nonReentrant { - CallOption storage call = optionParams[optionId]; - require(call.highBidder != address(0), "s-bid must be won by someone"); - require(call.expiration < block.timestamp, "s-option must be expired"); - require(!call.settled, "s-the call cannot already be settled"); + // set settled to prevent an additional attempt to settle the option + optionParams[optionId].settled = true; - uint256 spread = call.bid - call.strike; + // If the option writer is the high bidder they don't receive the strike because they bid on the spread. + if (call.highBidder != call.writer) { + // send option writer the strike price + _safeTransferETHWithFallback(call.writer, call.strike); + } - address optionOwner = ownerOf(optionId); + bool claimable = false; + if (msg.sender == optionOwner) { + // send option holder their earnings + _safeTransferETHWithFallback(optionOwner, spread); - // set settled to prevent an additional attempt to settle the option - optionParams[optionId].settled = true; + // burn nft + _burn(optionId); + } else { + optionClaims[optionId] = spread; + claimable = true; + } + emit CallSettled(optionId, claimable); + } - // If the option writer is the high bidder they don't receive the strike because they bid on the spread. - if (call.highBidder != call.writer) { - // send option writer the strike price - _safeTransferETHWithFallback(call.writer, call.strike); + /// @dev See {IHookCoveredCall-reclaimAsset}. + function reclaimAsset(uint256 optionId, bool returnNft) external nonReentrant { + CallOption storage call = optionParams[optionId]; + require(msg.sender == call.writer, "rA-only writer"); + require(!call.settled, "rA-option settled"); + require(call.writer == ownerOf(optionId), "rA-writer must own"); + require(call.expiration > block.timestamp, "rA-option expired"); + + // burn the option NFT + _burn(optionId); + + // settle the option + call.settled = true; + + if (call.highBidder != address(0)) { + // return current bidder's money + if (call.highBidder == call.writer) { + // handle the case where the writer is reclaiming as option they were the high bidder of + _safeTransferETHWithFallback(call.highBidder, call.bid - call.strike); + } else { + _safeTransferETHWithFallback(call.highBidder, call.bid); + } + + // if we have a bid, we may have set the bidder, so make sure to revert it here. + IHookVault(call.vaultAddress).setBeneficialOwner(call.assetId, call.writer); + } + + if (returnNft) { + // Because the call is not expired, we should be able to reclaim the asset from the vault + IHookVault(call.vaultAddress).clearEntitlementAndDistribute(call.assetId, call.writer); + } else { + IHookVault(call.vaultAddress).clearEntitlement(call.assetId); + } + + emit CallReclaimed(optionId); } - bool claimable = false; - if (msg.sender == optionOwner) { - // send option holder their earnings - _safeTransferETHWithFallback(optionOwner, spread); + /// @dev See {IHookCoveredCall-burnExpiredOption}. + function burnExpiredOption(uint256 optionId) external nonReentrant whenNotPaused { + CallOption storage call = optionParams[optionId]; + + require(block.timestamp > call.expiration, "bEO-option expired"); - // burn nft - _burn(optionId); - } else { - optionClaims[optionId] = spread; - claimable = true; + require(!call.settled, "bEO-option settled"); + + require(call.highBidder == address(0), "bEO-option has bids"); + + // burn the option NFT + _burn(optionId); + + // settle the option + call.settled = true; + + emit ExpiredCallBurned(optionId); } - emit CallSettled(optionId, claimable); - } - - /// @dev See {IHookCoveredCall-reclaimAsset}. - function reclaimAsset(uint256 optionId, bool returnNft) - external - nonReentrant - { - CallOption storage call = optionParams[optionId]; - require(msg.sender == call.writer, "rA-only writer"); - require(!call.settled, "rA-option settled"); - require(call.writer == ownerOf(optionId), "rA-writer must own"); - require(call.expiration > block.timestamp, "rA-option expired"); - - // burn the option NFT - _burn(optionId); - - // settle the option - call.settled = true; - - if (call.highBidder != address(0)) { - // return current bidder's money - if (call.highBidder == call.writer) { - // handle the case where the writer is reclaiming as option they were the high bidder of - _safeTransferETHWithFallback(call.highBidder, call.bid - call.strike); - } else { - _safeTransferETHWithFallback(call.highBidder, call.bid); - } - - // if we have a bid, we may have set the bidder, so make sure to revert it here. - IHookVault(call.vaultAddress).setBeneficialOwner( - call.assetId, - call.writer - ); + + /// @dev See {IHookCoveredCall-claimOptionProceeds} + function claimOptionProceeds(uint256 optionId) external nonReentrant { + address optionOwner = ownerOf(optionId); + require(msg.sender == optionOwner, "cOP-owner only"); + uint256 claim = optionClaims[optionId]; + delete optionClaims[optionId]; + if (claim != 0) { + _burn(optionId); + emit CallProceedsDistributed(optionId, optionOwner, claim); + _safeTransferETHWithFallback(optionOwner, claim); + } } - if (returnNft) { - // Because the call is not expired, we should be able to reclaim the asset from the vault - IHookVault(call.vaultAddress).clearEntitlementAndDistribute( - call.assetId, - call.writer - ); - } else { - IHookVault(call.vaultAddress).clearEntitlement(call.assetId); + //// ---- Administrative Fns. + + // forward to protocol-level pauseability + modifier whenNotPaused() { + require(!marketPaused, "market paused"); + _protocol.throwWhenPaused(); + _; } - emit CallReclaimed(optionId); - } + modifier onlyMarketController() { + require(_protocol.hasRole(MARKET_CONF, msg.sender), "caller needs MARKET_CONF"); + _; + } - /// @dev See {IHookCoveredCall-burnExpiredOption}. - function burnExpiredOption(uint256 optionId) - external - nonReentrant - whenNotPaused - { - CallOption storage call = optionParams[optionId]; + /// @dev configures the minimum duration for a newly minted option. Options must be at + /// least this far away in the future. + /// @param newMinDuration is the minimum option duration in seconds + function setMinOptionDuration(uint256 newMinDuration) public onlyMarketController { + require(settlementAuctionStartOffset < newMinDuration); + minimumOptionDuration = newMinDuration; + emit MinOptionDurationUpdated(newMinDuration); + } + + /// @dev set the minimum overage, in bips, for a new bid compared to the current bid. + /// @param newBidIncrement the minimum bid increment in basis points (1/100th of 1%) + function setBidIncrement(uint256 newBidIncrement) public onlyMarketController { + require(newBidIncrement < 20 * 100); + minBidIncrementBips = newBidIncrement; + emit MinBidIncrementUpdated(newBidIncrement); + } + + /// @dev set the settlement auction start offset. Settlement auctions begin at this time prior to expiration. + /// @param newSettlementStartOffset in seconds (i.e. block.timestamp increments) + function setSettlementAuctionStartOffset(uint256 newSettlementStartOffset) public onlyMarketController { + require(newSettlementStartOffset < minimumOptionDuration); + settlementAuctionStartOffset = newSettlementStartOffset; + emit SettlementAuctionStartOffsetUpdated(newSettlementStartOffset); + } + + /// @dev sets a paused / unpaused state for the market corresponding to this contract + /// @param paused should the market be set to paused or unpaused + function setMarketPaused(bool paused) public onlyMarketController { + require(marketPaused == !paused, "sMP-must change"); + marketPaused = paused; + emit MarketPauseUpdated(paused); + } - require(block.timestamp > call.expiration, "bEO-option expired"); + //// ------------------------- NFT RELATED FUNCTIONS ------------------------------- //// + //// These functions are overrides needed by the HookInstrumentNFT library in order //// + //// to generate the NFT view for the project. //// - require(!call.settled, "bEO-option settled"); + /// @dev see {IHookCoveredCall-getVaultAddress}. + function getVaultAddress(uint256 optionId) public view override returns (address) { + return optionParams[optionId].vaultAddress; + } - require(call.highBidder == address(0), "bEO-option has bids"); + /// @dev see {IHookCoveredCall-getOptionIdForAsset} + function getOptionIdForAsset(address vault, uint32 assetId) external view returns (uint256) { + return assetOptions[IHookVault(vault)][assetId]; + } - // burn the option NFT - _burn(optionId); + /// @dev see {IHookCoveredCall-getAssetId}. + function getAssetId(uint256 optionId) public view override returns (uint32) { + return optionParams[optionId].assetId; + } - // settle the option - call.settled = true; + /// @dev see {IHookCoveredCall-getStrikePrice}. + function getStrikePrice(uint256 optionId) public view override returns (uint256) { + return optionParams[optionId].strike; + } - emit ExpiredCallBurned(optionId); - } + /// @dev see {IHookCoveredCall-getExpiration}. + function getExpiration(uint256 optionId) public view override returns (uint256) { + return optionParams[optionId].expiration; + } + + //// ----------------------------- ETH TRANSFER UTILITIES --------------------------- //// - /// @dev See {IHookCoveredCall-claimOptionProceeds} - function claimOptionProceeds(uint256 optionId) external nonReentrant { - address optionOwner = ownerOf(optionId); - require(msg.sender == optionOwner, "cOP-owner only"); - uint256 claim = optionClaims[optionId]; - delete optionClaims[optionId]; - if (claim != 0) { - _burn(optionId); - emit CallProceedsDistributed(optionId, optionOwner, claim); - _safeTransferETHWithFallback(optionOwner, claim); + /// @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 impact of these calls. + function _safeTransferETHWithFallback(address to, uint256 amount) internal { + if (!_safeTransferETH(to, amount)) { + IWETH(weth).deposit{value: amount}(); + IWETH(weth).transfer(to, amount); + } } - } - - //// ---- Administrative Fns. - - // forward to protocol-level pauseability - modifier whenNotPaused() { - require(!marketPaused, "market paused"); - _protocol.throwWhenPaused(); - _; - } - - modifier onlyMarketController() { - require( - _protocol.hasRole(MARKET_CONF, msg.sender), - "caller needs MARKET_CONF" - ); - _; - } - - /// @dev configures the minimum duration for a newly minted option. Options must be at - /// least this far away in the future. - /// @param newMinDuration is the minimum option duration in seconds - function setMinOptionDuration(uint256 newMinDuration) - public - onlyMarketController - { - require(settlementAuctionStartOffset < newMinDuration); - minimumOptionDuration = newMinDuration; - emit MinOptionDurationUpdated(newMinDuration); - } - - /// @dev set the minimum overage, in bips, for a new bid compared to the current bid. - /// @param newBidIncrement the minimum bid increment in basis points (1/100th of 1%) - function setBidIncrement(uint256 newBidIncrement) - public - onlyMarketController - { - require(newBidIncrement < 20 * 100); - minBidIncrementBips = newBidIncrement; - emit MinBidIncrementUpdated(newBidIncrement); - } - - /// @dev set the settlement auction start offset. Settlement auctions begin at this time prior to expiration. - /// @param newSettlementStartOffset in seconds (i.e. block.timestamp increments) - function setSettlementAuctionStartOffset(uint256 newSettlementStartOffset) - public - onlyMarketController - { - require(newSettlementStartOffset < minimumOptionDuration); - settlementAuctionStartOffset = newSettlementStartOffset; - emit SettlementAuctionStartOffsetUpdated(newSettlementStartOffset); - } - - /// @dev sets a paused / unpaused state for the market corresponding to this contract - /// @param paused should the market be set to paused or unpaused - function setMarketPaused(bool paused) public onlyMarketController { - require(marketPaused == !paused, "sMP-must change"); - marketPaused = paused; - emit MarketPauseUpdated(paused); - } - - //// ------------------------- NFT RELATED FUNCTIONS ------------------------------- //// - //// These functions are overrides needed by the HookInstrumentNFT library in order //// - //// to generate the NFT view for the project. //// - - /// @dev see {IHookCoveredCall-getVaultAddress}. - function getVaultAddress(uint256 optionId) - public - view - override - returns (address) - { - return optionParams[optionId].vaultAddress; - } - - /// @dev see {IHookCoveredCall-getOptionIdForAsset} - function getOptionIdForAsset(address vault, uint32 assetId) - external - view - returns (uint256) - { - return assetOptions[IHookVault(vault)][assetId]; - } - - /// @dev see {IHookCoveredCall-getAssetId}. - function getAssetId(uint256 optionId) public view override returns (uint32) { - return optionParams[optionId].assetId; - } - - /// @dev see {IHookCoveredCall-getStrikePrice}. - function getStrikePrice(uint256 optionId) - public - view - override - returns (uint256) - { - return optionParams[optionId].strike; - } - - /// @dev see {IHookCoveredCall-getExpiration}. - function getExpiration(uint256 optionId) - public - view - override - returns (uint256) - { - return optionParams[optionId].expiration; - } - - //// ----------------------------- ETH TRANSFER UTILITIES --------------------------- //// - - /// @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 impact 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. + /// this prevents malicious contracts from causing the next bidder to run out of gas, + /// which would prevent them from bidding successfully + function _safeTransferETH(address to, uint256 value) internal returns (bool) { + (bool success,) = to.call{value: value, gas: 30_000}(new bytes(0)); + return success; } - } - - /// @notice Transfer ETH and return the success status. - /// @dev This function only forwards 30,000 gas to the callee. - /// this prevents malicious contracts from causing the next bidder to run out of gas, - /// which would prevent them from bidding successfully - function _safeTransferETH(address to, uint256 value) internal returns (bool) { - (bool success, ) = to.call{value: value, gas: 30_000}(new bytes(0)); - return success; - } } diff --git a/src/HookERC721MultiVaultImplV1.sol b/src/HookERC721MultiVaultImplV1.sol index 9593db4..d55e0a9 100644 --- a/src/HookERC721MultiVaultImplV1.sol +++ b/src/HookERC721MultiVaultImplV1.sol @@ -54,585 +54,424 @@ import "./mixin/EIP712.sol"; /// @dev This contract implements ERC721Receiver /// This contract views the tokenId for the asset on the ERC721 contract as the corresponding assetId for that asset /// when deposited into the vault -contract HookERC721MultiVaultImplV1 is - IHookERC721Vault, - EIP712, - Initializable, - ReentrancyGuard -{ - /// ---------------- STORAGE ---------------- /// - - /// @dev these are the NFT contract address and tokenId the vault is covering - IERC721 internal _nftContract; - - struct Asset { - address beneficialOwner; - address operator; - uint32 expiry; - } - - /// @dev the current entitlement applied to each asset, which includes the beneficialOwner - /// for the asset - /// if the entitled operator field is non-null, it means an unreleased entitlement has been - /// applied; however, that entitlement could still be expired (if block.timestamp > entitlement.expiry) - mapping(uint32 => Asset) internal assets; - - // Mapping from asset ID to approved address - mapping(uint32 => address) private _assetApprovals; - - IHookProtocol internal _hookProtocol; - - /// Upgradeable Implementations cannot have a constructor, so we call the initialize instead; - constructor() {} - - ///-constructor - function initialize(address nftContract, address hookAddress) - public - initializer - { - setAddressForEipDomain(hookAddress); - _nftContract = IERC721(nftContract); - _hookProtocol = IHookProtocol(hookAddress); - } - - /// ---------------- PUBLIC FUNCTIONS ---------------- /// - - /// - /// @dev See {IERC165-supportsInterface}. - /// - function supportsInterface(bytes4 interfaceId) - public - view - virtual - returns (bool) - { - return - interfaceId == type(IHookERC721Vault).interfaceId || - interfaceId == type(IERC165).interfaceId; - } - - /// @dev See {IHookERC721Vault-withdrawalAsset}. - /// @dev withdrawals can only be performed to the beneficial owner if there are no entitlements - function withdrawalAsset(uint32 assetId) public virtual nonReentrant { - require( - !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), - assets[assetId].beneficialOwner, - _assetTokenId(assetId) - ); - - emit AssetWithdrawn(assetId, msg.sender, assets[assetId].beneficialOwner); - } - - /// @dev See {IHookERC721Vault-imposeEntitlement}. - /// @dev The entitlement must be signed by the current beneficial owner of the contract. Anyone can submit the - /// entitlement - function imposeEntitlement( - address operator, - uint32 expiry, - uint32 assetId, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual { - // check that the asset has a current beneficial owner - // before creating a new entitlement - require( - assets[assetId].beneficialOwner != address(0), - "imposeEntitlement-beneficial owner must be set to impose an entitlement" - ); - - // the beneficial owner of an asset is able to set any entitlement on their own asset - // as long as it has not already been committed to someone else. - _verifyAndRegisterEntitlement(operator, expiry, assetId, v, r, s); - } - - /// @dev See {IHookERC721Vault-grantEntitlement}. - /// @dev The entitlement must be sent by the current beneficial owner - function grantEntitlement(Entitlements.Entitlement calldata entitlement) - external - { - require( - assets[entitlement.assetId].beneficialOwner == msg.sender || - _assetApprovals[entitlement.assetId] == msg.sender, - "grantEntitlement-only the beneficial owner or approved operator can grant an entitlement" - ); - - // the beneficial owner of an asset is able to directly set any entitlement on their own asset - // as long as it has not already been committed to someone else. - - _registerEntitlement( - entitlement.assetId, - entitlement.operator, - entitlement.expiry, - msg.sender - ); - } - - /// @dev See {IERC721Receiver-onERC721Received}. - /// - /// Always returns `IERC721Receiver.onERC721Received.selector`. - function onERC721Received( - address operator, // this arg is the address of the operator - address from, - uint256 tokenId, - bytes calldata data - ) external virtual override returns (bytes4) { - require( - tokenId <= type(uint32).max, - "onERC721Received-tokenId is out of range" - ); - /// (1) When receiving a nft from the ERC-721 contract this vault covers, create a new entitlement entry - /// with the sender as the beneficial owner to track the asset within the vault. +contract HookERC721MultiVaultImplV1 is IHookERC721Vault, EIP712, Initializable, ReentrancyGuard { + /// ---------------- STORAGE ---------------- /// + + /// @dev these are the NFT contract address and tokenId the vault is covering + IERC721 internal _nftContract; + + struct Asset { + address beneficialOwner; + address operator; + uint32 expiry; + } + + /// @dev the current entitlement applied to each asset, which includes the beneficialOwner + /// for the asset + /// if the entitled operator field is non-null, it means an unreleased entitlement has been + /// applied; however, that entitlement could still be expired (if block.timestamp > entitlement.expiry) + mapping(uint32 => Asset) internal assets; + + // Mapping from asset ID to approved address + mapping(uint32 => address) private _assetApprovals; + + IHookProtocol internal _hookProtocol; + + /// Upgradeable Implementations cannot have a constructor, so we call the initialize instead; + constructor() {} + + ///-constructor + function initialize(address nftContract, address hookAddress) public initializer { + setAddressForEipDomain(hookAddress); + _nftContract = IERC721(nftContract); + _hookProtocol = IHookProtocol(hookAddress); + } + + /// ---------------- PUBLIC FUNCTIONS ---------------- /// + /// - /// (1a) If the transfer additionally specifies data (i.e. an abi-encoded entitlement), the entitlement will - /// be imposed via that transfer, including a new beneficial owner. - /// NOTE: this is an opinionated approach, however, the authors believe that anyone with the ability to - /// transfer the asset into this contract could also trivially transfer the asset to another address - /// they control and then deposit, so allowing this method of setting the beneficial owner simply - /// saves gas and has no practical impact on the rights a hypothetical sender has regarding the asset. + /// @dev See {IERC165-supportsInterface}. /// - /// (2) If another nft is sent to the contract, we should verify that airdrops are allowed to this vault; - /// if they are disabled, we should not return the selector, otherwise we can allow them. + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IHookERC721Vault).interfaceId || interfaceId == type(IERC165).interfaceId; + } + + /// @dev See {IHookERC721Vault-withdrawalAsset}. + /// @dev withdrawals can only be performed to the beneficial owner if there are no entitlements + function withdrawalAsset(uint32 assetId) public virtual nonReentrant { + require( + !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), assets[assetId].beneficialOwner, _assetTokenId(assetId)); + + emit AssetWithdrawn(assetId, msg.sender, assets[assetId].beneficialOwner); + } + + /// @dev See {IHookERC721Vault-imposeEntitlement}. + /// @dev The entitlement must be signed by the current beneficial owner of the contract. Anyone can submit the + /// entitlement + function imposeEntitlement(address operator, uint32 expiry, uint32 assetId, uint8 v, bytes32 r, bytes32 s) + public + virtual + { + // check that the asset has a current beneficial owner + // before creating a new entitlement + require( + assets[assetId].beneficialOwner != address(0), + "imposeEntitlement-beneficial owner must be set to impose an entitlement" + ); + + // the beneficial owner of an asset is able to set any entitlement on their own asset + // as long as it has not already been committed to someone else. + _verifyAndRegisterEntitlement(operator, expiry, assetId, v, r, s); + } + + /// @dev See {IHookERC721Vault-grantEntitlement}. + /// @dev The entitlement must be sent by the current beneficial owner + function grantEntitlement(Entitlements.Entitlement calldata entitlement) external { + require( + assets[entitlement.assetId].beneficialOwner == msg.sender + || _assetApprovals[entitlement.assetId] == msg.sender, + "grantEntitlement-only the beneficial owner or approved operator can grant an entitlement" + ); + + // the beneficial owner of an asset is able to directly set any entitlement on their own asset + // as long as it has not already been committed to someone else. + + _registerEntitlement(entitlement.assetId, entitlement.operator, entitlement.expiry, msg.sender); + } + + /// @dev See {IERC721Receiver-onERC721Received}. /// - /// IMPORTANT: If an unrelated contract is currently holding the asset on behalf of an owner and then - /// subsequently transfers the asset into the contract, it needs to manually call (setBeneficialOwner) - /// after making this call to ensure that the true owner of the asset is known to the vault. Otherwise, - /// the owner will lose the ability to reclaim their asset. Alternatively, they could pass an entitlement - /// in pre-populated with the correct beneficial owner, which will give that owner the ability to reclaim - /// the asset. - if (msg.sender == address(_nftContract)) { - // There is no need to check if we currently have this token or an entitlement set. - // Even if the contract were able to get into this state, it should still accept the asset - // which will allow it to enforce the entitlement. - - // If additional data is sent with the transfer, we attempt to parse an entitlement from it. - // this allows the entitlement to be registered ahead of time. - if (data.length > 0) { - /// If the abi-encoded parameters are 3 words long, assume no approved operator was provided. - if (data.length == 3 * 32) { - // Decode the order, signature from `data`. If `data` does not encode such parameters, this - // will throw. - ( - address beneficialOwner, - address entitledOperator, - uint32 expirationTime - ) = abi.decode(data, (address, address, uint32)); - - // if someone has the asset, they should be able to set whichever beneficial owner they'd like. - // equally, they could transfer the asset first to themselves and subsequently grant a specific - // entitlement, which is equivalent to this. - _registerEntitlement( - uint32(tokenId), - entitledOperator, - expirationTime, - beneficialOwner - ); + /// Always returns `IERC721Receiver.onERC721Received.selector`. + function onERC721Received( + address operator, // this arg is the address of the operator + address from, + uint256 tokenId, + bytes calldata data + ) external virtual override returns (bytes4) { + require(tokenId <= type(uint32).max, "onERC721Received-tokenId is out of range"); + /// (1) When receiving a nft from the ERC-721 contract this vault covers, create a new entitlement entry + /// with the sender as the beneficial owner to track the asset within the vault. + /// + /// (1a) If the transfer additionally specifies data (i.e. an abi-encoded entitlement), the entitlement will + /// be imposed via that transfer, including a new beneficial owner. + /// NOTE: this is an opinionated approach, however, the authors believe that anyone with the ability to + /// transfer the asset into this contract could also trivially transfer the asset to another address + /// they control and then deposit, so allowing this method of setting the beneficial owner simply + /// saves gas and has no practical impact on the rights a hypothetical sender has regarding the asset. + /// + /// (2) If another nft is sent to the contract, we should verify that airdrops are allowed to this vault; + /// if they are disabled, we should not return the selector, otherwise we can allow them. + /// + /// IMPORTANT: If an unrelated contract is currently holding the asset on behalf of an owner and then + /// subsequently transfers the asset into the contract, it needs to manually call (setBeneficialOwner) + /// after making this call to ensure that the true owner of the asset is known to the vault. Otherwise, + /// the owner will lose the ability to reclaim their asset. Alternatively, they could pass an entitlement + /// in pre-populated with the correct beneficial owner, which will give that owner the ability to reclaim + /// the asset. + if (msg.sender == address(_nftContract)) { + // There is no need to check if we currently have this token or an entitlement set. + // Even if the contract were able to get into this state, it should still accept the asset + // which will allow it to enforce the entitlement. + + // If additional data is sent with the transfer, we attempt to parse an entitlement from it. + // this allows the entitlement to be registered ahead of time. + if (data.length > 0) { + /// If the abi-encoded parameters are 3 words long, assume no approved operator was provided. + if (data.length == 3 * 32) { + // Decode the order, signature from `data`. If `data` does not encode such parameters, this + // will throw. + (address beneficialOwner, address entitledOperator, uint32 expirationTime) = + abi.decode(data, (address, address, uint32)); + + // if someone has the asset, they should be able to set whichever beneficial owner they'd like. + // equally, they could transfer the asset first to themselves and subsequently grant a specific + // entitlement, which is equivalent to this. + _registerEntitlement(uint32(tokenId), entitledOperator, expirationTime, beneficialOwner); + } else { + /// additionally decode the approved operator from the payload. The abi decoder ensures that the + /// there are exactly 4 parameters + (address beneficialOwner, address entitledOperator, uint32 expirationTime, address approvedOperator) + = abi.decode(data, (address, address, uint32, address)); + + _registerEntitlement(uint32(tokenId), entitledOperator, expirationTime, beneficialOwner); + + /// if an approved operator is provided with this contract call, set the approval accepting it for the + /// same reason. + + _approve(approvedOperator, uint32(tokenId)); + } + } else { + _setBeneficialOwner(uint32(tokenId), from); + } + emit AssetReceived(this.getBeneficialOwner(uint32(tokenId)), operator, msg.sender, uint32(tokenId)); + } else { + // If we're receiving an airdrop or other asset uncovered by escrow to this address, we should ensure + // that this is allowed by our current settings. + require( + _hookProtocol.getCollectionConfig(address(_nftContract), keccak256("vault.multiAirdropsAllowed")), + "onERC721Received-non-escrow asset returned when airdrops are disabled" + ); + } + return this.onERC721Received.selector; + } + + /// @dev See {IHookERC721Vault-flashLoan}. + function flashLoan(uint32 assetId, address receiverAddress, bytes calldata params) external override nonReentrant { + IERC721FlashLoanReceiver receiver = IERC721FlashLoanReceiver(receiverAddress); + require(receiverAddress != address(0), "flashLoan-zero address"); + require(_assetOwner(assetId) == address(this), "flashLoan-asset not in vault"); + require(msg.sender == assets[assetId].beneficialOwner, "flashLoan-not called by the asset owner"); + + require( + !_hookProtocol.getCollectionConfig(address(_nftContract), keccak256("vault.flashLoanDisabled")), + "flashLoan-flashLoan feature disabled for this contract" + ); + + // (1) store a hash of our current entitlement state as a snapshot to diff + bytes32 startState = keccak256(abi.encode(assets[assetId])); + + // (2) send the flashloan contract the vaulted NFT + _nftContract.safeTransferFrom(address(this), receiverAddress, _assetTokenId(assetId)); + + // (3) call the flashloan contract, giving it a chance to do whatever it wants + // NOTE: The flashloan contract MUST approve this vault contract as an operator + // for the nft, such that we're able to make sure it has arrived. + require( + receiver.executeOperation(address(_nftContract), _assetTokenId(assetId), msg.sender, address(this), params), + "flashLoan-the flash loan contract must return true" + ); + + // (4) return the nft back into the vault + // Use transferFrom instead of safeTransfer from because transferFrom + // would modify our state ( it calls erc721Receiver ). and because we know + // for sure that this contract can handle ERC-721s. + _nftContract.transferFrom(receiverAddress, address(this), _assetTokenId(assetId)); + + // (5) sanity check to ensure the asset was actually returned to the vault. + // this is a concern because its possible that the safeTransferFrom implemented by + // some contract fails silently + require(_assetOwner(assetId) == address(this)); + + // (6) additional sanity check to ensure that the internal state of + // the entitlement has not somehow been modified during the flash loan, for example + // via some re-entrancy attack or by sending the asset back into the contract + // prematurely + require(startState == keccak256(abi.encode(assets[assetId])), "flashLoan-entitlement state cannot be modified"); + + // (7) emit an event to record the flashloan + emit AssetFlashLoaned(assets[assetId].beneficialOwner, assetId, receiverAddress); + } + + /// @dev See {IHookVault-entitlementExpiration}. + function entitlementExpiration(uint32 assetId) external view returns (uint32) { + if (!hasActiveEntitlement(assetId)) { + return 0; } else { - /// additionally decode the approved operator from the payload. The abi decoder ensures that the - /// there are exactly 4 parameters - ( - address beneficialOwner, - address entitledOperator, - uint32 expirationTime, - address approvedOperator - ) = abi.decode(data, (address, address, uint32, address)); - - _registerEntitlement( - uint32(tokenId), - entitledOperator, - expirationTime, - beneficialOwner - ); - - /// if an approved operator is provided with this contract call, set the approval accepting it for the - /// same reason. - - _approve(approvedOperator, uint32(tokenId)); + return assets[assetId].expiry; } - } else { - _setBeneficialOwner(uint32(tokenId), from); - } - emit AssetReceived( - this.getBeneficialOwner(uint32(tokenId)), - operator, - msg.sender, - uint32(tokenId) - ); - } else { - // If we're receiving an airdrop or other asset uncovered by escrow to this address, we should ensure - // that this is allowed by our current settings. - require( - _hookProtocol.getCollectionConfig( - address(_nftContract), - keccak256("vault.multiAirdropsAllowed") - ), - "onERC721Received-non-escrow asset returned when airdrops are disabled" - ); } - return this.onERC721Received.selector; - } - - /// @dev See {IHookERC721Vault-flashLoan}. - function flashLoan( - uint32 assetId, - address receiverAddress, - bytes calldata params - ) external override nonReentrant { - IERC721FlashLoanReceiver receiver = IERC721FlashLoanReceiver( - receiverAddress - ); - require(receiverAddress != address(0), "flashLoan-zero address"); - require( - _assetOwner(assetId) == address(this), - "flashLoan-asset not in vault" - ); - require( - msg.sender == assets[assetId].beneficialOwner, - "flashLoan-not called by the asset owner" - ); - - require( - !_hookProtocol.getCollectionConfig( - address(_nftContract), - keccak256("vault.flashLoanDisabled") - ), - "flashLoan-flashLoan feature disabled for this contract" - ); - - // (1) store a hash of our current entitlement state as a snapshot to diff - bytes32 startState = keccak256(abi.encode(assets[assetId])); - - // (2) send the flashloan contract the vaulted NFT - _nftContract.safeTransferFrom( - address(this), - receiverAddress, - _assetTokenId(assetId) - ); - - // (3) call the flashloan contract, giving it a chance to do whatever it wants - // NOTE: The flashloan contract MUST approve this vault contract as an operator - // for the nft, such that we're able to make sure it has arrived. - require( - receiver.executeOperation( - address(_nftContract), - _assetTokenId(assetId), - msg.sender, - address(this), - params - ), - "flashLoan-the flash loan contract must return true" - ); - - // (4) return the nft back into the vault - // Use transferFrom instead of safeTransfer from because transferFrom - // would modify our state ( it calls erc721Receiver ). and because we know - // for sure that this contract can handle ERC-721s. - _nftContract.transferFrom( - receiverAddress, - address(this), - _assetTokenId(assetId) - ); - - // (5) sanity check to ensure the asset was actually returned to the vault. - // this is a concern because its possible that the safeTransferFrom implemented by - // some contract fails silently - require(_assetOwner(assetId) == address(this)); - - // (6) additional sanity check to ensure that the internal state of - // the entitlement has not somehow been modified during the flash loan, for example - // via some re-entrancy attack or by sending the asset back into the contract - // prematurely - require( - startState == keccak256(abi.encode(assets[assetId])), - "flashLoan-entitlement state cannot be modified" - ); - - // (7) emit an event to record the flashloan - emit AssetFlashLoaned( - assets[assetId].beneficialOwner, - assetId, - receiverAddress - ); - } - - /// @dev See {IHookVault-entitlementExpiration}. - function entitlementExpiration(uint32 assetId) - external - view - returns (uint32) - { - if (!hasActiveEntitlement(assetId)) { - return 0; - } else { - return assets[assetId].expiry; + + /// @dev See {IHookERC721Vault-getBeneficialOwner}. + function getBeneficialOwner(uint32 assetId) external view returns (address) { + return assets[assetId].beneficialOwner; } - } - - /// @dev See {IHookERC721Vault-getBeneficialOwner}. - function getBeneficialOwner(uint32 assetId) external view returns (address) { - return assets[assetId].beneficialOwner; - } - - /// @dev See {IHookERC721Vault-getHoldsAsset}. - function getHoldsAsset(uint32 assetId) external view returns (bool) { - return _assetOwner(assetId) == address(this); - } - - function assetAddress(uint32) external view returns (address) { - return address(_nftContract); - } - - /// @dev returns the underlying token ID for a given asset. In this case - /// the tokenId == the assetId - function assetTokenId(uint32 assetId) external view returns (uint256) { - return _assetTokenId(assetId); - } - - /// @dev See {IHookERC721Vault-setBeneficialOwner}. - /// setBeneficialOwner can only be called by the entitlementContract if there is an activeEntitlement. - function setBeneficialOwner(uint32 assetId, address newBeneficialOwner) - public - virtual - { - if (hasActiveEntitlement(assetId)) { - require( - msg.sender == assets[assetId].operator, - "setBeneficialOwner-only the contract with the active entitlement can update the beneficial owner" - ); - } else { - require( - msg.sender == assets[assetId].beneficialOwner, - "setBeneficialOwner-only the current owner can update the beneficial owner" - ); + + /// @dev See {IHookERC721Vault-getHoldsAsset}. + function getHoldsAsset(uint32 assetId) external view returns (bool) { + return _assetOwner(assetId) == address(this); + } + + function assetAddress(uint32) external view returns (address) { + return address(_nftContract); + } + + /// @dev returns the underlying token ID for a given asset. In this case + /// the tokenId == the assetId + function assetTokenId(uint32 assetId) external view returns (uint256) { + return _assetTokenId(assetId); + } + + /// @dev See {IHookERC721Vault-setBeneficialOwner}. + /// setBeneficialOwner can only be called by the entitlementContract if there is an activeEntitlement. + function setBeneficialOwner(uint32 assetId, address newBeneficialOwner) public virtual { + if (hasActiveEntitlement(assetId)) { + require( + msg.sender == assets[assetId].operator, + "setBeneficialOwner-only the contract with the active entitlement can update the beneficial owner" + ); + } else { + require( + msg.sender == assets[assetId].beneficialOwner, + "setBeneficialOwner-only the current owner can update the beneficial owner" + ); + } + _setBeneficialOwner(assetId, newBeneficialOwner); + } + + /// @dev See {IHookERC721Vault-clearEntitlement}. + /// @dev This can only be called if an entitlement currently exists, otherwise it would be a no-op + function clearEntitlement(uint32 assetId) public { + require(hasActiveEntitlement(assetId), "clearEntitlement-an active entitlement must exist"); + require( + msg.sender == assets[assetId].operator, + "clearEntitlement-only the entitled address can clear the entitlement" + ); + _clearEntitlement(assetId); + } + + /// @dev See {IHookERC721Vault-clearEntitlementAndDistribute}. + /// @dev The entitlement must be exist, and must be called by the {operator}. The operator can specify a + /// intended receiver, which should match the beneficialOwner. The function will throw if + /// the receiver and owner do not match. + /// @param assetId the id of the specific vaulted asset + /// @param receiver the intended receiver of the asset + function clearEntitlementAndDistribute(uint32 assetId, address receiver) external nonReentrant { + require( + assets[assetId].beneficialOwner == receiver, + "clearEntitlementAndDistribute-Only the beneficial owner can receive the asset" + ); + require(receiver != address(0), "clearEntitlementAndDistribute-assets cannot be sent to null address"); + clearEntitlement(assetId); + IERC721(_nftContract).safeTransferFrom(address(this), receiver, _assetTokenId(assetId)); + emit AssetWithdrawn(assetId, receiver, assets[assetId].beneficialOwner); + } + + /// @dev Validates that a specific signature is actually the entitlement + /// EIP-712 signed by the beneficial owner specified in the entitlement. + function validateEntitlementSignature( + address operator, + uint32 expiry, + uint32 assetId, + uint8 v, + bytes32 r, + bytes32 s + ) public view { + bytes32 entitlementHash = _getEIP712Hash( + Entitlements.getEntitlementStructHash( + Entitlements.Entitlement({ + beneficialOwner: assets[assetId].beneficialOwner, + expiry: expiry, + operator: operator, + assetId: assetId, + vaultAddress: address(this) + }) + ) + ); + address signer = ecrecover(entitlementHash, v, r, s); + + require(signer != address(0), "recovered address is null"); + require( + signer == assets[assetId].beneficialOwner, "validateEntitlementSignature --- not signed by beneficialOwner" + ); + } + + /// + /// @dev See {IHookVault-approveOperator}. + /// + function approveOperator(address to, uint32 assetId) public virtual override { + address beneficialOwner = assets[assetId].beneficialOwner; + + require(to != beneficialOwner, "approve-approval to current beneficialOwner"); + + require(msg.sender == beneficialOwner, "approve-approve caller is not current beneficial owner"); + + _approve(to, assetId); + } + + /// @dev See {IHookVault-getApprovedOperator}. + function getApprovedOperator(uint32 assetId) public view virtual override returns (address) { + return _assetApprovals[assetId]; + } + + /// @dev Approve `to` to operate on `tokenId` + /// + /// Emits an {Approval} event. + /// @param to the address to approve + /// @param assetId the assetId on which the address will be approved + function _approve(address to, uint32 assetId) internal virtual { + _assetApprovals[assetId] = to; + emit Approval(assets[assetId].beneficialOwner, to, assetId); + } + + /// ---------------- INTERNAL/PRIVATE FUNCTIONS ---------------- /// + + /// @notice Verify that an entitlement is properly signed and apply it to the asset if able. + /// @dev The entitlement must be signed by the beneficial owner of the asset in order for it to be considered valid + /// @param operator the operator to entitle + /// @param expiry the duration of the entitlement + /// @param assetId the id of the asset within the vault + /// @param v sig v + /// @param r sig r + /// @param s sig s + function _verifyAndRegisterEntitlement( + address operator, + uint32 expiry, + uint32 assetId, + uint8 v, + bytes32 r, + bytes32 s + ) private { + validateEntitlementSignature(operator, expiry, assetId, v, r, s); + _registerEntitlement(assetId, operator, expiry, assets[assetId].beneficialOwner); + } + + function _registerEntitlement(uint32 assetId, address operator, uint32 expiry, address beneficialOwner) internal { + require( + !hasActiveEntitlement(assetId), + "_registerEntitlement-existing entitlement must be cleared before registering a new one" + ); + + require(expiry > block.timestamp, "_registerEntitlement-entitlement must expire in the future"); + assets[assetId] = Asset({operator: operator, expiry: expiry, beneficialOwner: beneficialOwner}); + emit EntitlementImposed(assetId, operator, expiry, beneficialOwner); + } + + function _clearEntitlement(uint32 assetId) private { + assets[assetId].expiry = 0; + assets[assetId].operator = address(0); + emit EntitlementCleared(assetId, assets[assetId].beneficialOwner); + } + + function hasActiveEntitlement(uint32 assetId) public view returns (bool) { + /// Although we do clear the expiry in _clearEntitlement, making the second half of the AND redundant, + /// we choose to include it here because we rely on this field being null to clear an entitlement. + return block.timestamp < assets[assetId].expiry && assets[assetId].operator != address(0); + } + + function getCurrentEntitlementOperator(uint32 assetId) external view returns (bool, address) { + bool isActive = hasActiveEntitlement(assetId); + address operator = assets[assetId].operator; + + return (isActive, operator); + } + + /// @dev determine the owner of a specific asset according to is contract based + /// on that assets assetId within this vault. + /// + /// this function can be overridden if the assetId -> tokenId mapping is modified. + function _assetOwner(uint32 assetId) internal view returns (address) { + return _nftContract.ownerOf(_assetTokenId(assetId)); + } + + /// @dev get the token id based on an asset's ID + /// + /// this function can be overridden if the assetId -> tokenId mapping is modified. + function _assetTokenId(uint32 assetId) internal view virtual returns (uint256) { + return assetId; + } + + /// @dev sets the new beneficial owner for a particular asset within the vault + function _setBeneficialOwner(uint32 assetId, address newBeneficialOwner) internal { + require(newBeneficialOwner != address(0), "_setBeneficialOwner-new owner is the zero address"); + assets[assetId].beneficialOwner = newBeneficialOwner; + _approve(address(0), assetId); + emit BeneficialOwnerSet(assetId, newBeneficialOwner, msg.sender); } - _setBeneficialOwner(assetId, newBeneficialOwner); - } - - /// @dev See {IHookERC721Vault-clearEntitlement}. - /// @dev This can only be called if an entitlement currently exists, otherwise it would be a no-op - function clearEntitlement(uint32 assetId) public { - require( - hasActiveEntitlement(assetId), - "clearEntitlement-an active entitlement must exist" - ); - require( - msg.sender == assets[assetId].operator, - "clearEntitlement-only the entitled address can clear the entitlement" - ); - _clearEntitlement(assetId); - } - - /// @dev See {IHookERC721Vault-clearEntitlementAndDistribute}. - /// @dev The entitlement must be exist, and must be called by the {operator}. The operator can specify a - /// intended receiver, which should match the beneficialOwner. The function will throw if - /// the receiver and owner do not match. - /// @param assetId the id of the specific vaulted asset - /// @param receiver the intended receiver of the asset - function clearEntitlementAndDistribute(uint32 assetId, address receiver) - external - nonReentrant - { - require( - assets[assetId].beneficialOwner == receiver, - "clearEntitlementAndDistribute-Only the beneficial owner can receive the asset" - ); - require( - receiver != address(0), - "clearEntitlementAndDistribute-assets cannot be sent to null address" - ); - clearEntitlement(assetId); - IERC721(_nftContract).safeTransferFrom( - address(this), - receiver, - _assetTokenId(assetId) - ); - emit AssetWithdrawn(assetId, receiver, assets[assetId].beneficialOwner); - } - - /// @dev Validates that a specific signature is actually the entitlement - /// EIP-712 signed by the beneficial owner specified in the entitlement. - function validateEntitlementSignature( - address operator, - uint32 expiry, - uint32 assetId, - uint8 v, - bytes32 r, - bytes32 s - ) public view { - bytes32 entitlementHash = _getEIP712Hash( - Entitlements.getEntitlementStructHash( - Entitlements.Entitlement({ - beneficialOwner: assets[assetId].beneficialOwner, - expiry: expiry, - operator: operator, - assetId: assetId, - vaultAddress: address(this) - }) - ) - ); - address signer = ecrecover(entitlementHash, v, r, s); - - require(signer != address(0), "recovered address is null"); - require( - signer == assets[assetId].beneficialOwner, - "validateEntitlementSignature --- not signed by beneficialOwner" - ); - } - - /// - /// @dev See {IHookVault-approveOperator}. - /// - function approveOperator(address to, uint32 assetId) public virtual override { - address beneficialOwner = assets[assetId].beneficialOwner; - - require( - to != beneficialOwner, - "approve-approval to current beneficialOwner" - ); - - require( - msg.sender == beneficialOwner, - "approve-approve caller is not current beneficial owner" - ); - - _approve(to, assetId); - } - - /// @dev See {IHookVault-getApprovedOperator}. - function getApprovedOperator(uint32 assetId) - public - view - virtual - override - returns (address) - { - return _assetApprovals[assetId]; - } - - /// @dev Approve `to` to operate on `tokenId` - /// - /// Emits an {Approval} event. - /// @param to the address to approve - /// @param assetId the assetId on which the address will be approved - function _approve(address to, uint32 assetId) internal virtual { - _assetApprovals[assetId] = to; - emit Approval(assets[assetId].beneficialOwner, to, assetId); - } - - /// ---------------- INTERNAL/PRIVATE FUNCTIONS ---------------- /// - - /// @notice Verify that an entitlement is properly signed and apply it to the asset if able. - /// @dev The entitlement must be signed by the beneficial owner of the asset in order for it to be considered valid - /// @param operator the operator to entitle - /// @param expiry the duration of the entitlement - /// @param assetId the id of the asset within the vault - /// @param v sig v - /// @param r sig r - /// @param s sig s - function _verifyAndRegisterEntitlement( - address operator, - uint32 expiry, - uint32 assetId, - uint8 v, - bytes32 r, - bytes32 s - ) private { - validateEntitlementSignature(operator, expiry, assetId, v, r, s); - _registerEntitlement( - assetId, - operator, - expiry, - assets[assetId].beneficialOwner - ); - } - - function _registerEntitlement( - uint32 assetId, - address operator, - uint32 expiry, - address beneficialOwner - ) internal { - require( - !hasActiveEntitlement(assetId), - "_registerEntitlement-existing entitlement must be cleared before registering a new one" - ); - - require( - expiry > block.timestamp, - "_registerEntitlement-entitlement must expire in the future" - ); - assets[assetId] = Asset({ - operator: operator, - expiry: expiry, - beneficialOwner: beneficialOwner - }); - emit EntitlementImposed(assetId, operator, expiry, beneficialOwner); - } - - function _clearEntitlement(uint32 assetId) private { - assets[assetId].expiry = 0; - assets[assetId].operator = address(0); - emit EntitlementCleared(assetId, assets[assetId].beneficialOwner); - } - - function hasActiveEntitlement(uint32 assetId) public view returns (bool) { - /// Although we do clear the expiry in _clearEntitlement, making the second half of the AND redundant, - /// we choose to include it here because we rely on this field being null to clear an entitlement. - return - block.timestamp < assets[assetId].expiry && - assets[assetId].operator != address(0); - } - - function getCurrentEntitlementOperator(uint32 assetId) - external - view - returns (bool, address) - { - bool isActive = hasActiveEntitlement(assetId); - address operator = assets[assetId].operator; - - return (isActive, operator); - } - - /// @dev determine the owner of a specific asset according to is contract based - /// on that assets assetId within this vault. - /// - /// this function can be overridden if the assetId -> tokenId mapping is modified. - function _assetOwner(uint32 assetId) internal view returns (address) { - return _nftContract.ownerOf(_assetTokenId(assetId)); - } - - /// @dev get the token id based on an asset's ID - /// - /// this function can be overridden if the assetId -> tokenId mapping is modified. - function _assetTokenId(uint32 assetId) - internal - view - virtual - returns (uint256) - { - return assetId; - } - - /// @dev sets the new beneficial owner for a particular asset within the vault - function _setBeneficialOwner(uint32 assetId, address newBeneficialOwner) - internal - { - require( - newBeneficialOwner != address(0), - "_setBeneficialOwner-new owner is the zero address" - ); - assets[assetId].beneficialOwner = newBeneficialOwner; - _approve(address(0), assetId); - emit BeneficialOwnerSet(assetId, newBeneficialOwner, msg.sender); - } } diff --git a/src/HookERC721VaultFactory.sol b/src/HookERC721VaultFactory.sol index 3bbbd9c..1f302ed 100644 --- a/src/HookERC721VaultFactory.sol +++ b/src/HookERC721VaultFactory.sol @@ -52,137 +52,86 @@ import "./lib/BeaconSalts.sol"; /// @dev See {IHookERC721VaultFactory}. /// @dev The factory itself is non-upgradeable; however, each vault is upgradeable (i.e. all vaults) /// created by this factory can be upgraded at one time via the beacon pattern. -contract HookERC721VaultFactory is - IHookERC721VaultFactory, - PermissionConstants -{ - /// @notice Registry of all of the active vaults within the protocol, allowing users to find vaults by - /// project address and tokenId; - /// @dev From this view, we do not know if a vault is empty or full - mapping(address => mapping(uint256 => IHookERC721Vault)) - public - override getVault; - - /// @notice Registry of all of the active multi-vaults within the protocol - mapping(address => IHookERC721Vault) public override getMultiVault; - - address private immutable _hookProtocol; - address private immutable _beacon; - address private immutable _multiBeacon; - - constructor( - address hookProtocolAddress, - address beaconAddress, - address multiBeaconAddress - ) { - require( - Address.isContract(hookProtocolAddress), - "hook protocol must be a contract" - ); - require( - Address.isContract(beaconAddress), - "beacon address must be a contract" - ); - require( - Address.isContract(multiBeaconAddress), - "multi beacon address must be a contract" - ); - _hookProtocol = hookProtocolAddress; - _beacon = beaconAddress; - _multiBeacon = multiBeaconAddress; - } - - /// @notice See {IHookERC721VaultFactory-makeMultiVault}. - function makeMultiVault(address nftAddress) - external - returns (IHookERC721Vault) - { - require( - IHookProtocol(_hookProtocol).hasRole(ALLOWLISTER_ROLE, msg.sender) || - IHookProtocol(_hookProtocol).hasRole(ALLOWLISTER_ROLE, address(0)), - "makeMultiVault-Only accounts with the ALLOWLISTER role can make new multiVaults" - ); - - require( - getMultiVault[nftAddress] == IHookERC721Vault(address(0)), - "makeMultiVault-vault cannot already exist" - ); - - IInitializeableBeacon bp = IInitializeableBeacon( - Create2.deploy( - 0, - BeaconSalts.multiVaultSalt(nftAddress), - type(HookBeaconProxy).creationCode - ) - ); - - bp.initializeBeacon( - _multiBeacon, - /// This is the ABI encoded initializer on the IHookERC721Vault.sol - abi.encodeWithSignature( - "initialize(address,address)", - nftAddress, - _hookProtocol - ) - ); - - IHookERC721Vault vault = IHookERC721Vault(address(bp)); - getMultiVault[nftAddress] = vault; - emit ERC721MultiVaultCreated(nftAddress, address(bp)); - - return vault; - } - - /// @notice See {IHookERC721VaultFactory-makeSoloVault}. - function makeSoloVault(address nftAddress, uint256 tokenId) - public - override - returns (IHookERC721Vault) - { - require( - getVault[nftAddress][tokenId] == IHookERC721Vault(address(0)), - "makeVault-a vault cannot already exist" - ); - - IInitializeableBeacon bp = IInitializeableBeacon( - Create2.deploy( - 0, - BeaconSalts.soloVaultSalt(nftAddress, tokenId), - type(HookBeaconProxy).creationCode - ) - ); - - bp.initializeBeacon( - _beacon, - /// This is the ABI encoded initializer on the IHookERC721MultiVault.sol - abi.encodeWithSignature( - "initialize(address,uint256,address)", - nftAddress, - tokenId, - _hookProtocol - ) - ); - IHookERC721Vault vault = IHookERC721Vault(address(bp)); - getVault[nftAddress][tokenId] = vault; - - emit ERC721VaultCreated(nftAddress, tokenId, address(vault)); - - return vault; - } - - /// @notice See {IHookERC721VaultFactory-findOrCreateVault}. - function findOrCreateVault(address nftAddress, uint256 tokenId) - external - returns (IHookERC721Vault) - { - if (getMultiVault[nftAddress] != IHookERC721Vault(address(0))) { - return getMultiVault[nftAddress]; +contract HookERC721VaultFactory is IHookERC721VaultFactory, PermissionConstants { + /// @notice Registry of all of the active vaults within the protocol, allowing users to find vaults by + /// project address and tokenId; + /// @dev From this view, we do not know if a vault is empty or full + mapping(address => mapping(uint256 => IHookERC721Vault)) public override getVault; + + /// @notice Registry of all of the active multi-vaults within the protocol + mapping(address => IHookERC721Vault) public override getMultiVault; + + address private immutable _hookProtocol; + address private immutable _beacon; + address private immutable _multiBeacon; + + constructor(address hookProtocolAddress, address beaconAddress, address multiBeaconAddress) { + require(Address.isContract(hookProtocolAddress), "hook protocol must be a contract"); + require(Address.isContract(beaconAddress), "beacon address must be a contract"); + require(Address.isContract(multiBeaconAddress), "multi beacon address must be a contract"); + _hookProtocol = hookProtocolAddress; + _beacon = beaconAddress; + _multiBeacon = multiBeaconAddress; } - if (getVault[nftAddress][tokenId] != IHookERC721Vault(address(0))) { - return getVault[nftAddress][tokenId]; + /// @notice See {IHookERC721VaultFactory-makeMultiVault}. + function makeMultiVault(address nftAddress) external returns (IHookERC721Vault) { + require( + IHookProtocol(_hookProtocol).hasRole(ALLOWLISTER_ROLE, msg.sender) + || IHookProtocol(_hookProtocol).hasRole(ALLOWLISTER_ROLE, address(0)), + "makeMultiVault-Only accounts with the ALLOWLISTER role can make new multiVaults" + ); + + require(getMultiVault[nftAddress] == IHookERC721Vault(address(0)), "makeMultiVault-vault cannot already exist"); + + IInitializeableBeacon bp = IInitializeableBeacon( + Create2.deploy(0, BeaconSalts.multiVaultSalt(nftAddress), type(HookBeaconProxy).creationCode) + ); + + bp.initializeBeacon( + _multiBeacon, + /// This is the ABI encoded initializer on the IHookERC721Vault.sol + abi.encodeWithSignature("initialize(address,address)", nftAddress, _hookProtocol) + ); + + IHookERC721Vault vault = IHookERC721Vault(address(bp)); + getMultiVault[nftAddress] = vault; + emit ERC721MultiVaultCreated(nftAddress, address(bp)); + + return vault; + } + + /// @notice See {IHookERC721VaultFactory-makeSoloVault}. + function makeSoloVault(address nftAddress, uint256 tokenId) public override returns (IHookERC721Vault) { + require(getVault[nftAddress][tokenId] == IHookERC721Vault(address(0)), "makeVault-a vault cannot already exist"); + + IInitializeableBeacon bp = IInitializeableBeacon( + Create2.deploy(0, BeaconSalts.soloVaultSalt(nftAddress, tokenId), type(HookBeaconProxy).creationCode) + ); + + bp.initializeBeacon( + _beacon, + /// This is the ABI encoded initializer on the IHookERC721MultiVault.sol + abi.encodeWithSignature("initialize(address,uint256,address)", nftAddress, tokenId, _hookProtocol) + ); + IHookERC721Vault vault = IHookERC721Vault(address(bp)); + getVault[nftAddress][tokenId] = vault; + + emit ERC721VaultCreated(nftAddress, tokenId, address(vault)); + + return vault; } - return makeSoloVault(nftAddress, tokenId); - } + /// @notice See {IHookERC721VaultFactory-findOrCreateVault}. + function findOrCreateVault(address nftAddress, uint256 tokenId) external returns (IHookERC721Vault) { + if (getMultiVault[nftAddress] != IHookERC721Vault(address(0))) { + return getMultiVault[nftAddress]; + } + + if (getVault[nftAddress][tokenId] != IHookERC721Vault(address(0))) { + return getVault[nftAddress][tokenId]; + } + + return makeSoloVault(nftAddress, tokenId); + } } diff --git a/src/HookERC721VaultImplV1.sol b/src/HookERC721VaultImplV1.sol index dc19a1e..4118801 100644 --- a/src/HookERC721VaultImplV1.sol +++ b/src/HookERC721VaultImplV1.sol @@ -51,201 +51,147 @@ import "./HookERC721MultiVaultImplV1.sol"; /// NFT while in escrow, which could allow for theft /// (3) At the end of each transaction, the ownerOf the vaulted token must still be the vault contract HookERC721VaultImplV1 is HookERC721MultiVaultImplV1 { - uint32 private constant ASSET_ID = 0; - - /// ---------------- STORAGE ---------------- /// - - /// @dev this is the only tokenID the vault covers. - uint256 internal _tokenId; - - /// Upgradeable Implementations cannot have a constructor, so we call the initialize instead; - constructor() HookERC721MultiVaultImplV1() {} - - ///-constructor - function initialize( - address nftContract, - uint256 tokenId, - address hookAddress - ) public { - _tokenId = tokenId; - // the super function calls "Initialize" - super.initialize(nftContract, hookAddress); - } - - /// ---------------- PUBLIC/EXTERNAL FUNCTIONS ---------------- /// - - /// @dev See {IHookERC721Vault-withdrawalAsset}. - /// @dev withdrawals can only be performed by the beneficial owner if there are no entitlements - function withdrawalAsset(uint32 assetId) - public - override - assetIdIsZero(assetId) - { - super.withdrawalAsset(assetId); - } - - /// @dev See {IHookERC721Vault-imposeEntitlement}. - /// @dev The entitlement must be signed by the current beneficial owner of the contract. Anyone may call this - /// function and successfully impose the entitlement as long as the signature is valid. - function imposeEntitlement( - address operator, - uint32 expiry, - uint32 assetId, - uint8 v, - bytes32 r, - bytes32 s - ) public override assetIdIsZero(assetId) { - super.imposeEntitlement(operator, expiry, assetId, v, r, s); - } - - /// @dev See {IERC721Receiver-onERC721Received}. - /// - /// Always returns `IERC721Receiver.onERC721Received.selector`. - /// - /// This method requires an override implementation because the the arguments must be embedded in the body of the - /// function - function onERC721Received( - address operator, // this arg is the address of the operator - address from, - uint256 tokenId, - bytes calldata data - ) external virtual override returns (bytes4) { - /// (1) If the contract is specified to hold a specific NFT, and that NFT is sent to the contract, - /// set the beneficial owner of this vault to be current owner of the asset getting sent. Alternatively, - /// the sender can specify an entitlement which contains a different beneficial owner. We accept this because - /// that same sender could alternatively first send the token, become the beneficial owner, and then set it - /// the beneficial owner to someone else and finally specify an entitlement. + uint32 private constant ASSET_ID = 0; + + /// ---------------- STORAGE ---------------- /// + + /// @dev this is the only tokenID the vault covers. + uint256 internal _tokenId; + + /// Upgradeable Implementations cannot have a constructor, so we call the initialize instead; + constructor() HookERC721MultiVaultImplV1() {} + + ///-constructor + function initialize(address nftContract, uint256 tokenId, address hookAddress) public { + _tokenId = tokenId; + // the super function calls "Initialize" + super.initialize(nftContract, hookAddress); + } + + /// ---------------- PUBLIC/EXTERNAL FUNCTIONS ---------------- /// + + /// @dev See {IHookERC721Vault-withdrawalAsset}. + /// @dev withdrawals can only be performed by the beneficial owner if there are no entitlements + function withdrawalAsset(uint32 assetId) public override assetIdIsZero(assetId) { + super.withdrawalAsset(assetId); + } + + /// @dev See {IHookERC721Vault-imposeEntitlement}. + /// @dev The entitlement must be signed by the current beneficial owner of the contract. Anyone may call this + /// function and successfully impose the entitlement as long as the signature is valid. + function imposeEntitlement(address operator, uint32 expiry, uint32 assetId, uint8 v, bytes32 r, bytes32 s) + public + override + assetIdIsZero(assetId) + { + super.imposeEntitlement(operator, expiry, assetId, v, r, s); + } + + /// @dev See {IERC721Receiver-onERC721Received}. /// - /// (2) If another nft is sent to the contract, we should verify that airdrops are allowed to this vault; - /// if they are disabled, we should not return the selector, otherwise we can allow them. + /// Always returns `IERC721Receiver.onERC721Received.selector`. + /// + /// This method requires an override implementation because the the arguments must be embedded in the body of the + /// function + function onERC721Received( + address operator, // this arg is the address of the operator + address from, + uint256 tokenId, + bytes calldata data + ) external virtual override returns (bytes4) { + /// (1) If the contract is specified to hold a specific NFT, and that NFT is sent to the contract, + /// set the beneficial owner of this vault to be current owner of the asset getting sent. Alternatively, + /// the sender can specify an entitlement which contains a different beneficial owner. We accept this because + /// that same sender could alternatively first send the token, become the beneficial owner, and then set it + /// the beneficial owner to someone else and finally specify an entitlement. + /// + /// (2) If another nft is sent to the contract, we should verify that airdrops are allowed to this vault; + /// if they are disabled, we should not return the selector, otherwise we can allow them. + /// + /// IMPORTANT: If an unrelated contract is currently holding the asset on behalf of an owner and then + /// subsequently transfers the asset into the contract, it needs to manually call (setBeneficialOwner) + /// after making this call to ensure that the true owner of the asset is known to the vault. Otherwise, + /// the owner will lose the ability to reclaim their asset. Alternatively, they could pass an entitlement + /// in pre-populated with the correct beneficial owner, which will give that owner the ability to reclaim + /// the asset. + if (msg.sender == address(_nftContract) && tokenId == _tokenId) { + // There is no need to check if we currently have this token or an entitlement set. + // Even if the contract were able to get into this state, it should still accept the asset + // which will allow it to enforce the entitlement. + _setBeneficialOwner(ASSET_ID, from); + + // If additional data is sent with the transfer, we attempt to parse an entitlement from it. + // this allows the entitlement to be registered ahead of time. + if (data.length > 0) { + // Decode the order, signature from `data`. If `data` does not encode such parameters, this + // will throw. + (address _beneficialOwner, address entitledOperator, uint32 expirationTime) = + abi.decode(data, (address, address, uint32)); + // if someone has the asset, they should be able to set whichever beneficial owner they'd like. + // equally, they could transfer the asset first to themselves and subsequently grant a specific + // entitlement, which is equivalent to this. + _setBeneficialOwner(ASSET_ID, _beneficialOwner); + _registerEntitlement(ASSET_ID, entitledOperator, expirationTime, assets[ASSET_ID].beneficialOwner); + } + emit AssetReceived(this.getBeneficialOwner(uint32(ASSET_ID)), operator, msg.sender, ASSET_ID); + } else { + // If we're receiving an airdrop or other asset uncovered by escrow to this address, we should ensure + // that this is allowed by our current settings. + require( + !_hookProtocol.getCollectionConfig(address(_nftContract), keccak256("vault.airdropsProhibited")), + "onERC721Received-non-escrow asset returned when airdrops are disabled" + ); + } + return this.onERC721Received.selector; + } + + /// @dev See {IHookERC721Vault-execTransaction}. + /// @dev Allows a beneficial owner to send an arbitrary call from this wallet as long as the underlying NFT + /// is still owned by us after the transaction. The ether value sent is forwarded. Return value is suppressed. /// - /// IMPORTANT: If an unrelated contract is currently holding the asset on behalf of an owner and then - /// subsequently transfers the asset into the contract, it needs to manually call (setBeneficialOwner) - /// after making this call to ensure that the true owner of the asset is known to the vault. Otherwise, - /// the owner will lose the ability to reclaim their asset. Alternatively, they could pass an entitlement - /// in pre-populated with the correct beneficial owner, which will give that owner the ability to reclaim - /// the asset. - if (msg.sender == address(_nftContract) && tokenId == _tokenId) { - // There is no need to check if we currently have this token or an entitlement set. - // Even if the contract were able to get into this state, it should still accept the asset - // which will allow it to enforce the entitlement. - _setBeneficialOwner(ASSET_ID, from); - - // If additional data is sent with the transfer, we attempt to parse an entitlement from it. - // this allows the entitlement to be registered ahead of time. - if (data.length > 0) { - // Decode the order, signature from `data`. If `data` does not encode such parameters, this - // will throw. - ( - address _beneficialOwner, - address entitledOperator, - uint32 expirationTime - ) = abi.decode(data, (address, address, uint32)); - // if someone has the asset, they should be able to set whichever beneficial owner they'd like. - // equally, they could transfer the asset first to themselves and subsequently grant a specific - // entitlement, which is equivalent to this. - _setBeneficialOwner(ASSET_ID, _beneficialOwner); - _registerEntitlement( - ASSET_ID, - entitledOperator, - expirationTime, - assets[ASSET_ID].beneficialOwner + /// Because this contract holds only a single asset owned by a single address, it supports calling exec + /// transaction from this address because such calls are unlikely to impact other owner's assets. + function execTransaction(address to, bytes memory data) external payable virtual returns (bool) { + // Only the beneficial owner can make this call + require( + msg.sender == assets[ASSET_ID].beneficialOwner, + "execTransaction-only the beneficial owner can use the transaction" ); - } - emit AssetReceived( - this.getBeneficialOwner(uint32(ASSET_ID)), - operator, - msg.sender, - ASSET_ID - ); - } else { - // If we're receiving an airdrop or other asset uncovered by escrow to this address, we should ensure - // that this is allowed by our current settings. - require( - !_hookProtocol.getCollectionConfig( - address(_nftContract), - keccak256("vault.airdropsProhibited") - ), - "onERC721Received-non-escrow asset returned when airdrops are disabled" - ); + + // block transactions to the NFT contract to ensure that people cant set approvals as the owner. + require(to != address(_nftContract), "execTransaction-cannot send transactions to the NFT contract itself"); + + // block transactions to the vault to mitigate reentrancy vulnerabilities + require(to != address(this), "execTransaction-cannot call the vault contract"); + + require( + !_hookProtocol.getCollectionConfig(address(_nftContract), keccak256("vault.execTransactionDisabled")), + "execTransaction-feature is disabled for this collection" + ); + + // Execute transaction without further confirmations. + (bool success,) = address(to).call{value: msg.value}(data); + + require(_assetOwner(ASSET_ID) == address(this)); + + return success; + } + + /// @dev See {IHookERC721Vault-setBeneficialOwner}. + function setBeneficialOwner(uint32 assetId, address newBeneficialOwner) public override assetIdIsZero(assetId) { + super.setBeneficialOwner(assetId, newBeneficialOwner); + } + + /// @dev modifier used to ensure that only the valid asset id + /// may be passed into this vault. + modifier assetIdIsZero(uint256 assetId) { + require(assetId == ASSET_ID, "assetIdIsZero-this vault only supports asset id 0"); + _; + } + + /// @dev override the assetOwner method to ensure the allowed + /// token in this vault is checked on the ERC-721 contract + function _assetTokenId(uint32 assetId) internal view override assetIdIsZero(assetId) returns (uint256) { + return _tokenId; } - return this.onERC721Received.selector; - } - - /// @dev See {IHookERC721Vault-execTransaction}. - /// @dev Allows a beneficial owner to send an arbitrary call from this wallet as long as the underlying NFT - /// is still owned by us after the transaction. The ether value sent is forwarded. Return value is suppressed. - /// - /// Because this contract holds only a single asset owned by a single address, it supports calling exec - /// transaction from this address because such calls are unlikely to impact other owner's assets. - function execTransaction(address to, bytes memory data) - external - payable - virtual - returns (bool) - { - // Only the beneficial owner can make this call - require( - msg.sender == assets[ASSET_ID].beneficialOwner, - "execTransaction-only the beneficial owner can use the transaction" - ); - - // block transactions to the NFT contract to ensure that people cant set approvals as the owner. - require( - to != address(_nftContract), - "execTransaction-cannot send transactions to the NFT contract itself" - ); - - // block transactions to the vault to mitigate reentrancy vulnerabilities - require( - to != address(this), - "execTransaction-cannot call the vault contract" - ); - - require( - !_hookProtocol.getCollectionConfig( - address(_nftContract), - keccak256("vault.execTransactionDisabled") - ), - "execTransaction-feature is disabled for this collection" - ); - - // Execute transaction without further confirmations. - (bool success, ) = address(to).call{value: msg.value}(data); - - require(_assetOwner(ASSET_ID) == address(this)); - - return success; - } - - /// @dev See {IHookERC721Vault-setBeneficialOwner}. - function setBeneficialOwner(uint32 assetId, address newBeneficialOwner) - public - override - assetIdIsZero(assetId) - { - super.setBeneficialOwner(assetId, newBeneficialOwner); - } - - /// @dev modifier used to ensure that only the valid asset id - /// may be passed into this vault. - modifier assetIdIsZero(uint256 assetId) { - require( - assetId == ASSET_ID, - "assetIdIsZero-this vault only supports asset id 0" - ); - _; - } - - /// @dev override the assetOwner method to ensure the allowed - /// token in this vault is checked on the ERC-721 contract - function _assetTokenId(uint32 assetId) - internal - view - override - assetIdIsZero(assetId) - returns (uint256) - { - return _tokenId; - } } diff --git a/src/HookProtocol.sol b/src/HookProtocol.sol index ceafd9d..79639d0 100644 --- a/src/HookProtocol.sol +++ b/src/HookProtocol.sol @@ -51,155 +51,111 @@ import "@openzeppelin/contracts/utils/Address.sol"; /// with the principal of least privilege. As the protocol matures, these additional measures can be layered /// by granting these roles to other contracts. In the extreme, the upgrade and other roles can be burned, /// which would effectively make the protocol static and non-upgradeable. -contract HookProtocol is - PermissionConstants, - AccessControl, - IHookProtocol, - Pausable -{ - address public override coveredCallContract; - address public override vaultContract; - address public immutable override getWETHAddress; - mapping(address => mapping(bytes32 => bool)) collectionConfigs; - - constructor( - address allowlister, - address pauser, - address vaultUpgrader, - address callUpgrader, - address marketConf, - address collectionConf, - address weth - ) { - require(Address.isContract(weth), "weth must be a contract"); - require( - allowlister != address(0), - "allowlister address cannot be set to the zero address" - ); - require( - pauser != address(0), - "pauser address cannot be set to the zero address" - ); - require( - vaultUpgrader != address(0), - "admin address cannot be set to the zero address" - ); - require( - callUpgrader != address(0), - "callUpgrader address cannot be set to the zero address" - ); - require( - marketConf != address(0), - "marketConf address cannot be set to the zero address" - ); - require( - collectionConf != address(0), - "collectionConf address cannot be set to the zero address" - ); - _setupRole(ALLOWLISTER_ROLE, allowlister); - _setupRole(PAUSER_ROLE, pauser); - _setupRole(VAULT_UPGRADER, vaultUpgrader); - _setupRole(CALL_UPGRADER, callUpgrader); - _setupRole(MARKET_CONF, marketConf); - _setupRole(COLLECTION_CONF, collectionConf); - - // allow the admin to add and remove other roles - _setRoleAdmin(ALLOWLISTER_ROLE, ALLOWLISTER_ROLE); - _setRoleAdmin(PAUSER_ROLE, PAUSER_ROLE); - _setRoleAdmin(VAULT_UPGRADER, VAULT_UPGRADER); - _setRoleAdmin(CALL_UPGRADER, CALL_UPGRADER); - _setRoleAdmin(MARKET_CONF, MARKET_CONF); - _setRoleAdmin(COLLECTION_CONF, COLLECTION_CONF); - // set weth - getWETHAddress = weth; - } - - /// @notice allows an account with the COLLECTION_CONF role to set a boolean config - /// value for a collection - /// @dev the conf value can be read with getCollectionConfig - /// @param collectionAddress the address for the collection - /// @param config the configuration field to set - /// @param value the value to set for the configuration - function setCollectionConfig( - address collectionAddress, - bytes32 config, - bool value - ) external onlyRole(COLLECTION_CONF) { - collectionConfigs[collectionAddress][config] = value; - } - - /// @dev See {IHookProtocol-getCollectionConfig}. - function getCollectionConfig(address collectionAddress, bytes32 conf) - external - view - returns (bool) - { - return collectionConfigs[collectionAddress][conf]; - } - - modifier callUpgraderOnly() { - require( - hasRole(CALL_UPGRADER, msg.sender), - "Caller is not a call upgrader" - ); - _; - } - - modifier vaultUpgraderOnly() { - require( - hasRole(VAULT_UPGRADER, msg.sender), - "Caller is not a vault upgrader" - ); - _; - } - - /// @notice throws an exception when the protocol is paused - function throwWhenPaused() external view whenNotPaused { - // depend on the modifier to throw. - return; - } - - /// @notice unpauses the protocol if the protocol is already paused - function unpause() external { - require(hasRole(PAUSER_ROLE, msg.sender), "Caller is not an pauser"); - require(paused() == true, "Protocol is already paused"); - _unpause(); - } - - /// @notice pauses the protocol if the protocol is currently unpaused - function pause() external { - require(hasRole(PAUSER_ROLE, msg.sender), "Caller is not an pauser`"); - require(paused() == false, "Protocol is already paused"); - _pause(); - } - - /// @notice Allows an admin to set the address of the deployed covered call factory - /// @dev This address is used by other protocols searching for the registry of - /// protocols. - /// @param coveredCallFactoryContract the address of the deployed covered call contract - function setCoveredCallFactory(address coveredCallFactoryContract) - external - callUpgraderOnly - { - require( - Address.isContract(coveredCallFactoryContract), - "setCoveredCallFactory: implementation is not a contract" - ); - coveredCallContract = coveredCallFactoryContract; - } - - /// @notice Allows an admin to set the address of the deployed vault factory - /// @dev allows all protocol components, including the call factory, to look up the - /// vault factory. - /// @param vaultFactoryContract the deployed vault factory - function setVaultFactory(address vaultFactoryContract) - external - vaultUpgraderOnly - { - require( - Address.isContract(vaultFactoryContract), - "setVaultFactory: implementation is not a contract" - ); - vaultContract = vaultFactoryContract; - } +contract HookProtocol is PermissionConstants, AccessControl, IHookProtocol, Pausable { + address public override coveredCallContract; + address public override vaultContract; + address public immutable override getWETHAddress; + mapping(address => mapping(bytes32 => bool)) collectionConfigs; + + constructor( + address allowlister, + address pauser, + address vaultUpgrader, + address callUpgrader, + address marketConf, + address collectionConf, + address weth + ) { + require(Address.isContract(weth), "weth must be a contract"); + require(allowlister != address(0), "allowlister address cannot be set to the zero address"); + require(pauser != address(0), "pauser address cannot be set to the zero address"); + require(vaultUpgrader != address(0), "admin address cannot be set to the zero address"); + require(callUpgrader != address(0), "callUpgrader address cannot be set to the zero address"); + require(marketConf != address(0), "marketConf address cannot be set to the zero address"); + require(collectionConf != address(0), "collectionConf address cannot be set to the zero address"); + _setupRole(ALLOWLISTER_ROLE, allowlister); + _setupRole(PAUSER_ROLE, pauser); + _setupRole(VAULT_UPGRADER, vaultUpgrader); + _setupRole(CALL_UPGRADER, callUpgrader); + _setupRole(MARKET_CONF, marketConf); + _setupRole(COLLECTION_CONF, collectionConf); + + // allow the admin to add and remove other roles + _setRoleAdmin(ALLOWLISTER_ROLE, ALLOWLISTER_ROLE); + _setRoleAdmin(PAUSER_ROLE, PAUSER_ROLE); + _setRoleAdmin(VAULT_UPGRADER, VAULT_UPGRADER); + _setRoleAdmin(CALL_UPGRADER, CALL_UPGRADER); + _setRoleAdmin(MARKET_CONF, MARKET_CONF); + _setRoleAdmin(COLLECTION_CONF, COLLECTION_CONF); + // set weth + getWETHAddress = weth; + } + + /// @notice allows an account with the COLLECTION_CONF role to set a boolean config + /// value for a collection + /// @dev the conf value can be read with getCollectionConfig + /// @param collectionAddress the address for the collection + /// @param config the configuration field to set + /// @param value the value to set for the configuration + function setCollectionConfig(address collectionAddress, bytes32 config, bool value) + external + onlyRole(COLLECTION_CONF) + { + collectionConfigs[collectionAddress][config] = value; + } + + /// @dev See {IHookProtocol-getCollectionConfig}. + function getCollectionConfig(address collectionAddress, bytes32 conf) external view returns (bool) { + return collectionConfigs[collectionAddress][conf]; + } + + modifier callUpgraderOnly() { + require(hasRole(CALL_UPGRADER, msg.sender), "Caller is not a call upgrader"); + _; + } + + modifier vaultUpgraderOnly() { + require(hasRole(VAULT_UPGRADER, msg.sender), "Caller is not a vault upgrader"); + _; + } + + /// @notice throws an exception when the protocol is paused + function throwWhenPaused() external view whenNotPaused { + // depend on the modifier to throw. + return; + } + + /// @notice unpauses the protocol if the protocol is already paused + function unpause() external { + require(hasRole(PAUSER_ROLE, msg.sender), "Caller is not an pauser"); + require(paused() == true, "Protocol is already paused"); + _unpause(); + } + + /// @notice pauses the protocol if the protocol is currently unpaused + function pause() external { + require(hasRole(PAUSER_ROLE, msg.sender), "Caller is not an pauser`"); + require(paused() == false, "Protocol is already paused"); + _pause(); + } + + /// @notice Allows an admin to set the address of the deployed covered call factory + /// @dev This address is used by other protocols searching for the registry of + /// protocols. + /// @param coveredCallFactoryContract the address of the deployed covered call contract + function setCoveredCallFactory(address coveredCallFactoryContract) external callUpgraderOnly { + require( + Address.isContract(coveredCallFactoryContract), "setCoveredCallFactory: implementation is not a contract" + ); + coveredCallContract = coveredCallFactoryContract; + } + + /// @notice Allows an admin to set the address of the deployed vault factory + /// @dev allows all protocol components, including the call factory, to look up the + /// vault factory. + /// @param vaultFactoryContract the deployed vault factory + function setVaultFactory(address vaultFactoryContract) external vaultUpgraderOnly { + require(Address.isContract(vaultFactoryContract), "setVaultFactory: implementation is not a contract"); + vaultContract = vaultFactoryContract; + } } diff --git a/src/HookUpgradeableBeacon.sol b/src/HookUpgradeableBeacon.sol index 22185d1..477bb23 100644 --- a/src/HookUpgradeableBeacon.sol +++ b/src/HookUpgradeableBeacon.sol @@ -53,72 +53,63 @@ import "./interfaces/IHookProtocol.sol"; /// the owner of this contract. /// This contract is based on the UpgradeableBeaconContract from OZ and DharmaUpgradeBeaconController from Dharma contract HookUpgradeableBeacon is IBeacon, PermissionConstants { - using Address for address; - address private _implementation; - IHookProtocol private _protocol; - bytes32 private _role; + using Address for address; - /// @dev Emitted when the implementation returned by the beacon is changed. - event Upgraded(address indexed implementation); + address private _implementation; + IHookProtocol private _protocol; + bytes32 private _role; - /// @dev Sets the address of the initial implementation, and the deployer account as the owner who can upgrade the - /// beacon. - constructor( - address implementation_, - address hookProtocol, - bytes32 upgraderRole - ) { - require( - Address.isContract(hookProtocol), - "UpgradeableBeacon: hookProtocol is not a contract" - ); + /// @dev Emitted when the implementation returned by the beacon is changed. + event Upgraded(address indexed implementation); - require( - upgraderRole == VAULT_UPGRADER || upgraderRole == CALL_UPGRADER, - "upgrader role must be vault or call upgrader" - ); - _setImplementation(implementation_); - _protocol = IHookProtocol(hookProtocol); - _role = upgraderRole; - } + /// @dev Sets the address of the initial implementation, and the deployer account as the owner who can upgrade the + /// beacon. + constructor(address implementation_, address hookProtocol, bytes32 upgraderRole) { + require(Address.isContract(hookProtocol), "UpgradeableBeacon: hookProtocol is not a contract"); - /// @dev Throws if called by any account other than the owner. - modifier onlyOwner() { - require( - _protocol.hasRole(_role, msg.sender), - "HookUpgradeableBeacon: caller does not have the required upgrade permissions" - ); - _; - } + require( + upgraderRole == VAULT_UPGRADER || upgraderRole == CALL_UPGRADER, + "upgrader role must be vault or call upgrader" + ); + _setImplementation(implementation_); + _protocol = IHookProtocol(hookProtocol); + _role = upgraderRole; + } - /// @dev Returns the current implementation address. - function implementation() external view virtual override returns (address) { - return _implementation; - } + /// @dev Throws if called by any account other than the owner. + modifier onlyOwner() { + require( + _protocol.hasRole(_role, msg.sender), + "HookUpgradeableBeacon: caller does not have the required upgrade permissions" + ); + _; + } - /// @dev Upgrades the beacon to a new implementation. - /// - /// Emits an {Upgraded} event. - /// - /// Requirements: - /// - /// - msg.sender must be the owner of the contract. - /// - `newImplementation` must be a contract. - function upgradeTo(address newImplementation) external virtual onlyOwner { - _setImplementation(newImplementation); - emit Upgraded(newImplementation); - } + /// @dev Returns the current implementation address. + function implementation() external view virtual override returns (address) { + return _implementation; + } - /// @dev Sets the implementation contract address for this beacon - /// - /// Requirements: - /// - /// - `newImplementation` must be a contract. - function _setImplementation(address newImplementation) private { - require( - Address.isContract(newImplementation), - "HookUpgradeableBeacon: implementation is not a contract" - ); - _implementation = newImplementation; - } + /// @dev Upgrades the beacon to a new implementation. + /// + /// Emits an {Upgraded} event. + /// + /// Requirements: + /// + /// - msg.sender must be the owner of the contract. + /// - `newImplementation` must be a contract. + function upgradeTo(address newImplementation) external virtual onlyOwner { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /// @dev Sets the implementation contract address for this beacon + /// + /// Requirements: + /// + /// - `newImplementation` must be a contract. + function _setImplementation(address newImplementation) private { + require(Address.isContract(newImplementation), "HookUpgradeableBeacon: implementation is not a contract"); + _implementation = newImplementation; + } } diff --git a/src/interfaces/IERC721FlashLoanReceiver.sol b/src/interfaces/IERC721FlashLoanReceiver.sol index a572f76..552df71 100644 --- a/src/interfaces/IERC721FlashLoanReceiver.sol +++ b/src/interfaces/IERC721FlashLoanReceiver.sol @@ -50,22 +50,22 @@ import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; /// /// The flashloan receiver is able to abort a flashloan by returning false from the executeOperation method. interface IERC721FlashLoanReceiver is IERC721Receiver { - /// @notice the method that contains the operations to be performed with the loaned asset - /// @dev executeOperation is called immediately after the asset is transferred to this contract. After return, - /// the asset is returned to the vault by the vault contract. The executeOperation implementation MUST - /// approve the {vault} to operate the transferred NFT - /// i.e. `IERC721(nftContract).setApprovalForAll(vault, true);` - /// - /// @param nftContract the address of the underlying erc-721 asset - /// @param tokenId the address of the received erc-721 asset - /// @param beneficialOwner the current beneficialOwner of the vault, who initialized the flashLoan - /// @param vault the address of the vault performing the flashloan (in most cases, equal to msg.sender) - /// @param params additional params passed by the caller into the flashloan - function executeOperation( - address nftContract, - uint256 tokenId, - address beneficialOwner, - address vault, - bytes calldata params - ) external returns (bool); + /// @notice the method that contains the operations to be performed with the loaned asset + /// @dev executeOperation is called immediately after the asset is transferred to this contract. After return, + /// the asset is returned to the vault by the vault contract. The executeOperation implementation MUST + /// approve the {vault} to operate the transferred NFT + /// i.e. `IERC721(nftContract).setApprovalForAll(vault, true);` + /// + /// @param nftContract the address of the underlying erc-721 asset + /// @param tokenId the address of the received erc-721 asset + /// @param beneficialOwner the current beneficialOwner of the vault, who initialized the flashLoan + /// @param vault the address of the vault performing the flashloan (in most cases, equal to msg.sender) + /// @param params additional params passed by the caller into the flashloan + function executeOperation( + address nftContract, + uint256 tokenId, + address beneficialOwner, + address vault, + bytes calldata params + ) external returns (bool); } diff --git a/src/interfaces/IHookCoveredCall.sol b/src/interfaces/IHookCoveredCall.sol index 12f60e1..9d9f7b6 100644 --- a/src/interfaces/IHookCoveredCall.sol +++ b/src/interfaces/IHookCoveredCall.sol @@ -68,133 +68,119 @@ import "../lib/Signatures.sol"; /// of the instrument NFT, the strike price is transferred to the writer. The high bid is transferred to the holder of /// the option. interface IHookCoveredCall is IERC721Metadata { - /// @notice emitted when a new call option is successfully minted with a specific underlying vault - event CallCreated( - address writer, - address vaultAddress, - uint256 assetId, - uint256 optionId, - uint256 strikePrice, - uint256 expiration - ); - - /// @notice emitted when a call option is settled - event CallSettled(uint256 optionId, bool claimable); - - /// @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 - /// @param bidAmount the amount of wei bid - /// @param optionId the option for the underlying that was bid on - event Bid(uint256 optionId, uint256 bidAmount, address bidder); - - /// @notice emitted when an option owner claims their proceeds - /// @param optionId the option the claim is on - /// @param to the option owner making the claim - /// @param amount the amount of the claim distributed - event CallProceedsDistributed(uint256 optionId, address to, uint256 amount); - - /// @notice Mints a new call option for a particular "underlying" ERC-721 NFT with a given strike price and expiration - /// @param tokenAddress the contract address of the ERC-721 token that serves as the underlying asset for the call - /// option - /// @param tokenId the tokenId of the underlying ERC-721 token - /// @param strikePrice the strike price for the call option being written - /// @param expirationTime time the timestamp after which the option will be expired - function mintWithErc721( - address tokenAddress, - uint256 tokenId, - uint128 strikePrice, - uint32 expirationTime - ) external returns (uint256); - - /// @notice Mints a new call option for the assets deposited in a particular vault given strike price and expiration. - /// @param vaultAddress the contract address of the vault currently holding the call option - /// @param assetId the id of the asset within the vault - /// @param strikePrice the strike price for the call option being written - /// @param expirationTime time the timestamp after which the option will be expired - /// @param signature the signature used to place the entitlement onto the vault - function mintWithVault( - address vaultAddress, - uint32 assetId, - uint128 strikePrice, - uint32 expirationTime, - Signatures.Signature calldata signature - ) external returns (uint256); - - /// @notice Mints a new call option for the assets deposited in a particular vault given strike price and expiration. - /// That vault must already have a registered entitlement for this contract with the an expiration equal to {expirationTime} - /// @param vaultAddress the contract address of the vault currently holding the call option - /// @param assetId the id of the asset within the vault - /// @param strikePrice the strike price for the call option being written - /// @param expirationTime time the timestamp after which the option will be expired - function mintWithEntitledVault( - address vaultAddress, - uint32 assetId, - uint128 strikePrice, - uint32 expirationTime - ) external returns (uint256); - - /// @notice Bid in the settlement auction for an option. The paid amount is the bid, - /// and the bidder is required to escrow this amount until either the auction ends or another bidder bids higher - /// - /// The bid must be greater than the strike price - /// @param optionId the optionId corresponding to the settlement to bid on. - function bid(uint256 optionId) external payable; - - /// @notice view function to get the current high settlement bid of an option, or 0 if there is no high bid - /// @param optionId of the option to check - function currentBid(uint256 optionId) external view returns (uint128); - - /// @notice view function to get the current high bidder for an option settlement auction, or the null address if no - /// high bidder exists - /// @param optionId of the option to check - /// @return address of the account for the current high bidder, or the null address if there is none - function currentBidder(uint256 optionId) external view returns (address); - - /// @notice Allows the writer to reclaim an entitled asset. This is only possible when the writer holds the option - /// nft and calls this function. - /// @dev Allows the writer to reclaim a NFT if they also hold the option NFT. - /// @param optionId the option being reclaimed. - /// @param returnNft true if token should be withdrawn from vault, false to leave token in the vault. - function reclaimAsset(uint256 optionId, bool returnNft) external; - - /// @notice Looks up the latest optionId that covers a particular asset, if one exists. This option may be already settled. - /// @dev getOptionIdForAsset - /// @param vault the address of the hook vault that holds the covered asset - /// @param assetId the id of the asset to check - /// @return the optionId, if one exists or 0 otherwise - function getOptionIdForAsset(address vault, uint32 assetId) - external - view - returns (uint256); - - /// @notice Permissionlessly settle an expired option when the option expires in the money, distributing - /// the proceeds to the Writer, Holder, and Bidder as follows: - /// - /// WRITER (who originally called mint() and owned underlying asset) - receives the `strike` - /// HOLDER (ownerOf(optionId)) - receives `b-strike` - /// HIGH BIDDER (call.highBidder) - becomes ownerOf NFT, pays `bid`. - /// - /// @dev the return nft param allows the underlying asset to remain in its vault. This saves gas - /// compared to first distributing it and then re-depositing it. No royalties or other payments - /// are subtracted from the distribution amounts. - /// - /// @param optionId of the option to settle. - function settleOption(uint256 optionId) 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; - - /// @notice allows the option owner to claim proceeds if the option was settled - /// by another account. The option NFT is burned after settlement. - /// @dev this mechanism prevents the proceeds from being sent to an account - /// temporarily custodying the option asset. - /// @param optionId the option to claim and burn. - function claimOptionProceeds(uint256 optionId) external; + /// @notice emitted when a new call option is successfully minted with a specific underlying vault + event CallCreated( + address writer, address vaultAddress, uint256 assetId, uint256 optionId, uint256 strikePrice, uint256 expiration + ); + + /// @notice emitted when a call option is settled + event CallSettled(uint256 optionId, bool claimable); + + /// @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 + /// @param bidAmount the amount of wei bid + /// @param optionId the option for the underlying that was bid on + event Bid(uint256 optionId, uint256 bidAmount, address bidder); + + /// @notice emitted when an option owner claims their proceeds + /// @param optionId the option the claim is on + /// @param to the option owner making the claim + /// @param amount the amount of the claim distributed + event CallProceedsDistributed(uint256 optionId, address to, uint256 amount); + + /// @notice Mints a new call option for a particular "underlying" ERC-721 NFT with a given strike price and expiration + /// @param tokenAddress the contract address of the ERC-721 token that serves as the underlying asset for the call + /// option + /// @param tokenId the tokenId of the underlying ERC-721 token + /// @param strikePrice the strike price for the call option being written + /// @param expirationTime time the timestamp after which the option will be expired + function mintWithErc721(address tokenAddress, uint256 tokenId, uint128 strikePrice, uint32 expirationTime) + external + returns (uint256); + + /// @notice Mints a new call option for the assets deposited in a particular vault given strike price and expiration. + /// @param vaultAddress the contract address of the vault currently holding the call option + /// @param assetId the id of the asset within the vault + /// @param strikePrice the strike price for the call option being written + /// @param expirationTime time the timestamp after which the option will be expired + /// @param signature the signature used to place the entitlement onto the vault + function mintWithVault( + address vaultAddress, + uint32 assetId, + uint128 strikePrice, + uint32 expirationTime, + Signatures.Signature calldata signature + ) external returns (uint256); + + /// @notice Mints a new call option for the assets deposited in a particular vault given strike price and expiration. + /// That vault must already have a registered entitlement for this contract with the an expiration equal to {expirationTime} + /// @param vaultAddress the contract address of the vault currently holding the call option + /// @param assetId the id of the asset within the vault + /// @param strikePrice the strike price for the call option being written + /// @param expirationTime time the timestamp after which the option will be expired + function mintWithEntitledVault(address vaultAddress, uint32 assetId, uint128 strikePrice, uint32 expirationTime) + external + returns (uint256); + + /// @notice Bid in the settlement auction for an option. The paid amount is the bid, + /// and the bidder is required to escrow this amount until either the auction ends or another bidder bids higher + /// + /// The bid must be greater than the strike price + /// @param optionId the optionId corresponding to the settlement to bid on. + function bid(uint256 optionId) external payable; + + /// @notice view function to get the current high settlement bid of an option, or 0 if there is no high bid + /// @param optionId of the option to check + function currentBid(uint256 optionId) external view returns (uint128); + + /// @notice view function to get the current high bidder for an option settlement auction, or the null address if no + /// high bidder exists + /// @param optionId of the option to check + /// @return address of the account for the current high bidder, or the null address if there is none + function currentBidder(uint256 optionId) external view returns (address); + + /// @notice Allows the writer to reclaim an entitled asset. This is only possible when the writer holds the option + /// nft and calls this function. + /// @dev Allows the writer to reclaim a NFT if they also hold the option NFT. + /// @param optionId the option being reclaimed. + /// @param returnNft true if token should be withdrawn from vault, false to leave token in the vault. + function reclaimAsset(uint256 optionId, bool returnNft) external; + + /// @notice Looks up the latest optionId that covers a particular asset, if one exists. This option may be already settled. + /// @dev getOptionIdForAsset + /// @param vault the address of the hook vault that holds the covered asset + /// @param assetId the id of the asset to check + /// @return the optionId, if one exists or 0 otherwise + function getOptionIdForAsset(address vault, uint32 assetId) external view returns (uint256); + + /// @notice Permissionlessly settle an expired option when the option expires in the money, distributing + /// the proceeds to the Writer, Holder, and Bidder as follows: + /// + /// WRITER (who originally called mint() and owned underlying asset) - receives the `strike` + /// HOLDER (ownerOf(optionId)) - receives `b-strike` + /// HIGH BIDDER (call.highBidder) - becomes ownerOf NFT, pays `bid`. + /// + /// @dev the return nft param allows the underlying asset to remain in its vault. This saves gas + /// compared to first distributing it and then re-depositing it. No royalties or other payments + /// are subtracted from the distribution amounts. + /// + /// @param optionId of the option to settle. + function settleOption(uint256 optionId) 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; + + /// @notice allows the option owner to claim proceeds if the option was settled + /// by another account. The option NFT is burned after settlement. + /// @dev this mechanism prevents the proceeds from being sent to an account + /// temporarily custodying the option asset. + /// @param optionId the option to claim and burn. + function claimOptionProceeds(uint256 optionId) external; } diff --git a/src/interfaces/IHookCoveredCallFactory.sol b/src/interfaces/IHookCoveredCallFactory.sol index b9707db..5a4b2aa 100644 --- a/src/interfaces/IHookCoveredCallFactory.sol +++ b/src/interfaces/IHookCoveredCallFactory.sol @@ -41,24 +41,18 @@ pragma solidity ^0.8.10; /// @notice The Factory creates covered call instruments that support specific ERC-721 contracts, and /// also tracks all of the existing active markets. interface IHookCoveredCallFactory { - /// @dev emitted whenever a new call instrument instance is created - /// @param assetAddress the address of the asset underlying the covered call - /// @param instrumentAddress the address of the covered call instrument - event CoveredCallInstrumentCreated( - address assetAddress, - address instrumentAddress - ); + /// @dev emitted whenever a new call instrument instance is created + /// @param assetAddress the address of the asset underlying the covered call + /// @param instrumentAddress the address of the covered call instrument + event CoveredCallInstrumentCreated(address assetAddress, address instrumentAddress); - /// @notice Lookup the call instrument contract based on the asset address - /// @param assetAddress the contract address for the underlying asset - /// @return the address of the instrument contract or the null address if one does not exist - function getCallInstrument(address assetAddress) - external - view - returns (address); + /// @notice Lookup the call instrument contract based on the asset address + /// @param assetAddress the contract address for the underlying asset + /// @return the address of the instrument contract or the null address if one does not exist + function getCallInstrument(address assetAddress) external view returns (address); - /// @notice Create a call option instrument for a specific underlying asset address - /// @param assetAddress the address for the underling asset - /// @return the address of the call option instrument contract (upgradeable) - function makeCallInstrument(address assetAddress) external returns (address); + /// @notice Create a call option instrument for a specific underlying asset address + /// @param assetAddress the address for the underling asset + /// @return the address of the call option instrument contract (upgradeable) + function makeCallInstrument(address assetAddress) external returns (address); } diff --git a/src/interfaces/IHookERC20Vault.sol b/src/interfaces/IHookERC20Vault.sol index 9a3af80..d8a9126 100644 --- a/src/interfaces/IHookERC20Vault.sol +++ b/src/interfaces/IHookERC20Vault.sol @@ -44,6 +44,6 @@ import "./IHookVault.sol"; /// specifically designed to hold and receive ERC20 Tokens. /// interface IHookERC20Vault is IHookVault { - /// @notice returns the balance of the underlying ERC20 token - function assetBalance(uint32 assetId) external view returns (uint256); + /// @notice returns the balance of the underlying ERC20 token + function assetBalance(uint32 assetId) external view returns (uint256); } diff --git a/src/interfaces/IHookERC721Vault.sol b/src/interfaces/IHookERC721Vault.sol index 6bf0c07..16c10c9 100644 --- a/src/interfaces/IHookERC721Vault.sol +++ b/src/interfaces/IHookERC721Vault.sol @@ -50,24 +50,20 @@ import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; /// contract, and then call the flashLoan method. /// (3) At the end of the flashLoan, we ensure the asset is still owned by the vault. interface IHookERC721Vault is IHookVault, IERC721Receiver { - /// @notice emitted after an asset is flash loaned by its beneficial owner. - /// @dev only one asset can be flash loaned at a time, and that asset is - /// denoted by the tokenId emitted. - event AssetFlashLoaned(address owner, uint256 tokenId, address flashLoanImpl); + /// @notice emitted after an asset is flash loaned by its beneficial owner. + /// @dev only one asset can be flash loaned at a time, and that asset is + /// denoted by the tokenId emitted. + event AssetFlashLoaned(address owner, uint256 tokenId, address flashLoanImpl); - /// @notice the tokenID of the underlying ERC721 token; - function assetTokenId(uint32 assetId) external view returns (uint256); + /// @notice the tokenID of the underlying ERC721 token; + function assetTokenId(uint32 assetId) external view returns (uint256); - /// @notice flashLoans the vaulted asset to another contract for use and return to the vault. Only the owner - /// may perform the flashloan - /// @dev the flashloan receiver can perform arbitrary logic, but must approve the vault as an operator - /// before returning. - /// @param receiverAddress the contract which implements the {IERC721FlashLoanReceiver} interface to utilize the - /// asset while it is loaned out - /// @param params calldata params to forward to the receiver - function flashLoan( - uint32 assetId, - address receiverAddress, - bytes calldata params - ) external; + /// @notice flashLoans the vaulted asset to another contract for use and return to the vault. Only the owner + /// may perform the flashloan + /// @dev the flashloan receiver can perform arbitrary logic, but must approve the vault as an operator + /// before returning. + /// @param receiverAddress the contract which implements the {IERC721FlashLoanReceiver} interface to utilize the + /// asset while it is loaned out + /// @param params calldata params to forward to the receiver + function flashLoan(uint32 assetId, address receiverAddress, bytes calldata params) external; } diff --git a/src/interfaces/IHookERC721VaultFactory.sol b/src/interfaces/IHookERC721VaultFactory.sol index 04edd40..5e27646 100644 --- a/src/interfaces/IHookERC721VaultFactory.sol +++ b/src/interfaces/IHookERC721VaultFactory.sol @@ -42,56 +42,40 @@ import "./IHookERC721Vault.sol"; /// /// @notice The Factory creates a specific vault for ERC721s. interface IHookERC721VaultFactory { - event ERC721VaultCreated( - address nftAddress, - uint256 tokenId, - address vaultAddress - ); + event ERC721VaultCreated(address nftAddress, uint256 tokenId, address vaultAddress); - /// @notice emitted when a new MultiVault is deployed by the protocol - /// @param nftAddress the address of the nft contract that may be deposited into the new vault - /// @param vaultAddress address of the newly deployed vault - event ERC721MultiVaultCreated(address nftAddress, address vaultAddress); + /// @notice emitted when a new MultiVault is deployed by the protocol + /// @param nftAddress the address of the nft contract that may be deposited into the new vault + /// @param vaultAddress address of the newly deployed vault + event ERC721MultiVaultCreated(address nftAddress, address vaultAddress); - /// @notice gets the address of a vault for a particular ERC-721 token - /// @param nftAddress the contract address for the ERC-721 - /// @param tokenId the tokenId for the ERC-721 - /// @return the address of a {IERC721Vault} if one exists that supports the particular ERC-721, or the null address otherwise - function getVault(address nftAddress, uint256 tokenId) - external - view - returns (IHookERC721Vault); + /// @notice gets the address of a vault for a particular ERC-721 token + /// @param nftAddress the contract address for the ERC-721 + /// @param tokenId the tokenId for the ERC-721 + /// @return the address of a {IERC721Vault} if one exists that supports the particular ERC-721, or the null address otherwise + function getVault(address nftAddress, uint256 tokenId) external view returns (IHookERC721Vault); - /// @notice gets the address of a multi-asset vault for a particular ERC-721 contract, if one exists - /// @param nftAddress the contract address for the ERC-721 - /// @return the address of the {IERC721Vault} multi asset vault, or the null address if one does not exist - function getMultiVault(address nftAddress) - external - view - returns (IHookERC721Vault); + /// @notice gets the address of a multi-asset vault for a particular ERC-721 contract, if one exists + /// @param nftAddress the contract address for the ERC-721 + /// @return the address of the {IERC721Vault} multi asset vault, or the null address if one does not exist + function getMultiVault(address nftAddress) external view returns (IHookERC721Vault); - /// @notice deploy a multi-asset vault if one has not already been deployed - /// @param nftAddress the contract address for the ERC-721 to be supported by the vault - /// @return the address of the newly deployed {IERC721Vault} multi asset vault - function makeMultiVault(address nftAddress) - external - returns (IHookERC721Vault); + /// @notice deploy a multi-asset vault if one has not already been deployed + /// @param nftAddress the contract address for the ERC-721 to be supported by the vault + /// @return the address of the newly deployed {IERC721Vault} multi asset vault + function makeMultiVault(address nftAddress) external returns (IHookERC721Vault); - /// @notice creates a vault for a specific tokenId. If there - /// is a multi-vault in existence which supports that address - /// the address for that vault is returned as a new one - /// does not need to be made. - /// @param nftAddress the contract address for the ERC-721 - /// @param tokenId the tokenId for the ERC-721 - function findOrCreateVault(address nftAddress, uint256 tokenId) - external - returns (IHookERC721Vault); + /// @notice creates a vault for a specific tokenId. If there + /// is a multi-vault in existence which supports that address + /// the address for that vault is returned as a new one + /// does not need to be made. + /// @param nftAddress the contract address for the ERC-721 + /// @param tokenId the tokenId for the ERC-721 + function findOrCreateVault(address nftAddress, uint256 tokenId) external returns (IHookERC721Vault); - /// @notice make a new vault that can contain a single asset only - /// @dev the only valid asset id in this vault is = 0 - /// @param nftAddress the address of the underlying nft contract - /// @param tokenId the individual token that can be deposited into this vault - function makeSoloVault(address nftAddress, uint256 tokenId) - external - returns (IHookERC721Vault); + /// @notice make a new vault that can contain a single asset only + /// @dev the only valid asset id in this vault is = 0 + /// @param nftAddress the address of the underlying nft contract + /// @param tokenId the individual token that can be deposited into this vault + function makeSoloVault(address nftAddress, uint256 tokenId) external returns (IHookERC721Vault); } diff --git a/src/interfaces/IHookProtocol.sol b/src/interfaces/IHookProtocol.sol index a123991..b5b1899 100644 --- a/src/interfaces/IHookProtocol.sol +++ b/src/interfaces/IHookProtocol.sol @@ -44,30 +44,27 @@ import "@openzeppelin/contracts/access/IAccessControl.sol"; /// is correct as, if it is not, all assets contained within protocol contracts /// can be easily compromised. interface IHookProtocol is IAccessControl { - /// @notice the address of the deployed CoveredCallFactory used by the protocol - function coveredCallContract() external view returns (address); + /// @notice the address of the deployed CoveredCallFactory used by the protocol + function coveredCallContract() external view returns (address); - /// @notice the address of the deployed VaultFactory used by the protocol - function vaultContract() external view returns (address); + /// @notice the address of the deployed VaultFactory used by the protocol + function vaultContract() external view returns (address); - /// @notice callable function that reverts when the protocol is paused - function throwWhenPaused() external; + /// @notice callable function that reverts when the protocol is paused + function throwWhenPaused() external; - /// @notice the standard weth address on this chain - /// @dev these are values for popular chains: - /// mainnet: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - /// kovan: 0xd0a1e359811322d97991e03f863a0c30c2cf029c - /// ropsten: 0xc778417e063141139fce010982780140aa0cd5ab - /// rinkeby: 0xc778417e063141139fce010982780140aa0cd5ab - /// @return the weth address - function getWETHAddress() external view returns (address); + /// @notice the standard weth address on this chain + /// @dev these are values for popular chains: + /// mainnet: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 + /// kovan: 0xd0a1e359811322d97991e03f863a0c30c2cf029c + /// ropsten: 0xc778417e063141139fce010982780140aa0cd5ab + /// rinkeby: 0xc778417e063141139fce010982780140aa0cd5ab + /// @return the weth address + function getWETHAddress() external view returns (address); - /// @notice get a configuration flag with a specific key for a collection - /// @param collectionAddress the collection for which to lookup a configuration flag - /// @param conf the config identifier for the configuration flag - /// @return the true or false value of the config - function getCollectionConfig(address collectionAddress, bytes32 conf) - external - view - returns (bool); + /// @notice get a configuration flag with a specific key for a collection + /// @param collectionAddress the collection for which to lookup a configuration flag + /// @param conf the config identifier for the configuration flag + /// @return the true or false value of the config + function getCollectionConfig(address collectionAddress, bytes32 conf) external view returns (bool); } diff --git a/src/interfaces/IHookVault.sol b/src/interfaces/IHookVault.sol index 1b36051..a41140a 100644 --- a/src/interfaces/IHookVault.sol +++ b/src/interfaces/IHookVault.sol @@ -59,129 +59,99 @@ import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; /// (5) the beneficial owner cannot modify the beneficial owner while an entitlement is in place /// interface IHookVault is IERC165 { - /// @notice emitted when an entitlement is placed on an asset - event EntitlementImposed( - uint32 assetId, - address entitledAccount, - uint32 expiry, - address beneficialOwner - ); - - /// @notice emitted when an entitlement is cleared from an asset - event EntitlementCleared(uint256 assetId, address beneficialOwner); - - /// @notice emitted when the beneficial owner of an asset changes - /// @dev it is not required that this event is emitted when an entitlement is - /// imposed that also modifies the beneficial owner. - event BeneficialOwnerSet( - uint32 assetId, - address beneficialOwner, - address setBy - ); - - /// @notice emitted when an asset is added into the vault - event AssetReceived( - address owner, - address sender, - address contractAddress, - uint32 assetId - ); - - /// @notice Emitted when `beneficialOwner` enables `approved` to manage the `assetId` asset. - event Approval( - address indexed beneficialOwner, - address indexed approved, - uint32 indexed assetId - ); - - /// @notice emitted when an asset is withdrawn from the vault - event AssetWithdrawn(uint32 assetId, address to, address beneficialOwner); - - /// @notice Withdrawal an unencumbered asset from this vault - /// @param assetId the asset to remove from the vault - function withdrawalAsset(uint32 assetId) external; - - /// @notice setBeneficialOwner updates the current address that can claim the asset when it is free of entitlements. - /// @param assetId the id of the subject asset to impose the entitlement - /// @param newBeneficialOwner the account of the person who is able to withdrawal when there are no entitlements. - function setBeneficialOwner(uint32 assetId, address newBeneficialOwner) - external; - - /// @notice Add an entitlement claim to the asset held within the contract - /// @param operator the operator to entitle - /// @param expiry the duration of the entitlement - /// @param assetId the id of the asset within the vault - /// @param v sig v - /// @param r sig r - /// @param s sig s - function imposeEntitlement( - address operator, - uint32 expiry, - uint32 assetId, - uint8 v, - bytes32 r, - bytes32 s - ) external; - - /// @notice Allows the beneficial owner to grant an entitlement to an asset within the contract - /// @dev this function call is signed by the sender per the EVM, so we know the entitlement is authentic - /// @param entitlement The entitlement to impose onto the contract - function grantEntitlement(Entitlements.Entitlement calldata entitlement) - external; - - /// @notice Allows the entitled address to release their claim on the asset - /// @param assetId the id of the asset to clear - function clearEntitlement(uint32 assetId) external; - - /// @notice Removes the active entitlement from a vault and returns the asset to the beneficial owner - /// @param receiver the intended receiver of the asset - /// @param assetId the Id of the asset to clear - function clearEntitlementAndDistribute(uint32 assetId, address receiver) - external; - - /// @notice looks up the current beneficial owner of the asset - /// @param assetId the referenced asset - /// @return the address of the beneficial owner of the asset - function getBeneficialOwner(uint32 assetId) external view returns (address); - - /// @notice checks if the asset is currently stored in the vault - /// @param assetId the referenced asset - /// @return true if the asset is currently within the vault, false otherwise - function getHoldsAsset(uint32 assetId) external view returns (bool); - - /// @notice the contract address of the vaulted asset - /// @param assetId the referenced asset - /// @return the contract address of the vaulted asset - function assetAddress(uint32 assetId) external view returns (address); - - /// @notice looks up the current operator of an entitlement on an asset - /// @param assetId the id of the underlying asset - function getCurrentEntitlementOperator(uint32 assetId) - external - view - returns (bool, address); - - /// @notice Looks up the expiration timestamp of the current entitlement - /// @dev returns the 0 if no entitlement is set - /// @return the block timestamp after which the entitlement expires - function entitlementExpiration(uint32 assetId) external view returns (uint32); - - /// @notice Gives permission to `to` to impose an entitlement upon `assetId` - /// - /// @dev Only a single account can be approved at a time, so approving the zero address clears previous approvals. - /// * Requirements: - /// - /// - The caller must be the beneficial owner - /// - `tokenId` must exist. - /// - /// Emits an {Approval} event. - function approveOperator(address to, uint32 assetId) external; - - /// @dev Returns the account approved for `tokenId` token. - /// - /// Requirements: - /// - /// - `assetId` must exist. - /// - function getApprovedOperator(uint32 assetId) external view returns (address); + /// @notice emitted when an entitlement is placed on an asset + event EntitlementImposed(uint32 assetId, address entitledAccount, uint32 expiry, address beneficialOwner); + + /// @notice emitted when an entitlement is cleared from an asset + event EntitlementCleared(uint256 assetId, address beneficialOwner); + + /// @notice emitted when the beneficial owner of an asset changes + /// @dev it is not required that this event is emitted when an entitlement is + /// imposed that also modifies the beneficial owner. + event BeneficialOwnerSet(uint32 assetId, address beneficialOwner, address setBy); + + /// @notice emitted when an asset is added into the vault + event AssetReceived(address owner, address sender, address contractAddress, uint32 assetId); + + /// @notice Emitted when `beneficialOwner` enables `approved` to manage the `assetId` asset. + event Approval(address indexed beneficialOwner, address indexed approved, uint32 indexed assetId); + + /// @notice emitted when an asset is withdrawn from the vault + event AssetWithdrawn(uint32 assetId, address to, address beneficialOwner); + + /// @notice Withdrawal an unencumbered asset from this vault + /// @param assetId the asset to remove from the vault + function withdrawalAsset(uint32 assetId) external; + + /// @notice setBeneficialOwner updates the current address that can claim the asset when it is free of entitlements. + /// @param assetId the id of the subject asset to impose the entitlement + /// @param newBeneficialOwner the account of the person who is able to withdrawal when there are no entitlements. + function setBeneficialOwner(uint32 assetId, address newBeneficialOwner) external; + + /// @notice Add an entitlement claim to the asset held within the contract + /// @param operator the operator to entitle + /// @param expiry the duration of the entitlement + /// @param assetId the id of the asset within the vault + /// @param v sig v + /// @param r sig r + /// @param s sig s + function imposeEntitlement(address operator, uint32 expiry, uint32 assetId, uint8 v, bytes32 r, bytes32 s) + external; + + /// @notice Allows the beneficial owner to grant an entitlement to an asset within the contract + /// @dev this function call is signed by the sender per the EVM, so we know the entitlement is authentic + /// @param entitlement The entitlement to impose onto the contract + function grantEntitlement(Entitlements.Entitlement calldata entitlement) external; + + /// @notice Allows the entitled address to release their claim on the asset + /// @param assetId the id of the asset to clear + function clearEntitlement(uint32 assetId) external; + + /// @notice Removes the active entitlement from a vault and returns the asset to the beneficial owner + /// @param receiver the intended receiver of the asset + /// @param assetId the Id of the asset to clear + function clearEntitlementAndDistribute(uint32 assetId, address receiver) external; + + /// @notice looks up the current beneficial owner of the asset + /// @param assetId the referenced asset + /// @return the address of the beneficial owner of the asset + function getBeneficialOwner(uint32 assetId) external view returns (address); + + /// @notice checks if the asset is currently stored in the vault + /// @param assetId the referenced asset + /// @return true if the asset is currently within the vault, false otherwise + function getHoldsAsset(uint32 assetId) external view returns (bool); + + /// @notice the contract address of the vaulted asset + /// @param assetId the referenced asset + /// @return the contract address of the vaulted asset + function assetAddress(uint32 assetId) external view returns (address); + + /// @notice looks up the current operator of an entitlement on an asset + /// @param assetId the id of the underlying asset + function getCurrentEntitlementOperator(uint32 assetId) external view returns (bool, address); + + /// @notice Looks up the expiration timestamp of the current entitlement + /// @dev returns the 0 if no entitlement is set + /// @return the block timestamp after which the entitlement expires + function entitlementExpiration(uint32 assetId) external view returns (uint32); + + /// @notice Gives permission to `to` to impose an entitlement upon `assetId` + /// + /// @dev Only a single account can be approved at a time, so approving the zero address clears previous approvals. + /// * Requirements: + /// + /// - The caller must be the beneficial owner + /// - `tokenId` must exist. + /// + /// Emits an {Approval} event. + function approveOperator(address to, uint32 assetId) external; + + /// @dev Returns the account approved for `tokenId` token. + /// + /// Requirements: + /// + /// - `assetId` must exist. + /// + function getApprovedOperator(uint32 assetId) external view returns (address); } diff --git a/src/interfaces/IInitializeableBeacon.sol b/src/interfaces/IInitializeableBeacon.sol index 3298c8f..c9bdd93 100644 --- a/src/interfaces/IInitializeableBeacon.sol +++ b/src/interfaces/IInitializeableBeacon.sol @@ -41,5 +41,5 @@ pragma solidity ^0.8.10; /// @dev the Hook Beacons conform to this interface, and can be called /// with this initializer in order to start a beacon interface IInitializeableBeacon { - function initializeBeacon(address beacon, bytes memory data) external; + function initializeBeacon(address beacon, bytes memory data) external; } diff --git a/src/interfaces/IWETH.sol b/src/interfaces/IWETH.sol index 4ecd732..04d8a9e 100644 --- a/src/interfaces/IWETH.sol +++ b/src/interfaces/IWETH.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.10; interface IWETH { - function deposit() external payable; + function deposit() external payable; - function withdraw(uint256 wad) external; + function withdraw(uint256 wad) external; - function transfer(address to, uint256 value) external returns (bool); + function transfer(address to, uint256 value) external returns (bool); } diff --git a/src/lib/BeaconSalts.sol b/src/lib/BeaconSalts.sol index edc47e8..7a2a0f6 100644 --- a/src/lib/BeaconSalts.sol +++ b/src/lib/BeaconSalts.sol @@ -37,23 +37,18 @@ pragma solidity ^0.8.10; import "../HookBeaconProxy.sol"; library BeaconSalts { - // keep functions internal to prevent the need for library linking - // and to reduce gas costs - // Specify the actually-deployed beacons on mainnet - // bytes32 internal constant ByteCodeHash = - // bytes32(0x9efc74de3a03a3f44d619e7f315880536876e16273d5fdee7b22fd4c1620f1d5); - bytes32 internal constant ByteCodeHash = - keccak256(type(HookBeaconProxy).creationCode); + // keep functions internal to prevent the need for library linking + // and to reduce gas costs + // Specify the actually-deployed beacons on mainnet + // bytes32 internal constant ByteCodeHash = + // bytes32(0x9efc74de3a03a3f44d619e7f315880536876e16273d5fdee7b22fd4c1620f1d5); + bytes32 internal constant ByteCodeHash = keccak256(type(HookBeaconProxy).creationCode); - function soloVaultSalt(address nftAddress, uint256 tokenId) - internal - pure - returns (bytes32) - { - return keccak256(abi.encode(nftAddress, tokenId)); - } + function soloVaultSalt(address nftAddress, uint256 tokenId) internal pure returns (bytes32) { + return keccak256(abi.encode(nftAddress, tokenId)); + } - function multiVaultSalt(address nftAddress) internal pure returns (bytes32) { - return keccak256(abi.encode(nftAddress)); - } + function multiVaultSalt(address nftAddress) internal pure returns (bytes32) { + return keccak256(abi.encode(nftAddress)); + } } diff --git a/src/lib/Entitlements.sol b/src/lib/Entitlements.sol index eedb413..e9b71e9 100644 --- a/src/lib/Entitlements.sol +++ b/src/lib/Entitlements.sol @@ -37,51 +37,45 @@ pragma solidity ^0.8.10; import "./Signatures.sol"; library Entitlements { - uint256 private constant _ENTITLEMENT_TYPEHASH = - uint256( - keccak256( - abi.encodePacked( - "Entitlement(", - "address beneficialOwner,", - "address operator,", - "address vaultAddress,", - "uint32 assetId,", - "uint32 expiry", - ")" + uint256 private constant _ENTITLEMENT_TYPEHASH = uint256( + keccak256( + abi.encodePacked( + "Entitlement(", + "address beneficialOwner,", + "address operator,", + "address vaultAddress,", + "uint32 assetId,", + "uint32 expiry", + ")" + ) ) - ) ); - /// ---- STRUCTS ----- - struct Entitlement { - /// @notice the beneficial owner address this entitlement applies to. This address will also be the signer. - address beneficialOwner; - /// @notice the operating contract that can change ownership during the entitlement period. - address operator; - /// @notice the contract address for the vault that contains the underlying assets - address vaultAddress; - /// @notice the assetId of the asset or assets within the vault - uint32 assetId; - /// @notice the block timestamp after which the asset is free of the entitlement - uint32 expiry; - } + /// ---- STRUCTS ----- + struct Entitlement { + /// @notice the beneficial owner address this entitlement applies to. This address will also be the signer. + address beneficialOwner; + /// @notice the operating contract that can change ownership during the entitlement period. + address operator; + /// @notice the contract address for the vault that contains the underlying assets + address vaultAddress; + /// @notice the assetId of the asset or assets within the vault + uint32 assetId; + /// @notice the block timestamp after which the asset is free of the entitlement + uint32 expiry; + } - function getEntitlementStructHash(Entitlement memory entitlement) - internal - pure - returns (bytes32) - { - // TODO: Hash in place to save gas. - return - keccak256( - abi.encode( - _ENTITLEMENT_TYPEHASH, - entitlement.beneficialOwner, - entitlement.operator, - entitlement.vaultAddress, - entitlement.assetId, - entitlement.expiry - ) - ); - } + function getEntitlementStructHash(Entitlement memory entitlement) internal pure returns (bytes32) { + // TODO: Hash in place to save gas. + return keccak256( + abi.encode( + _ENTITLEMENT_TYPEHASH, + entitlement.beneficialOwner, + entitlement.operator, + entitlement.vaultAddress, + entitlement.assetId, + entitlement.expiry + ) + ); + } } diff --git a/src/lib/HookStrings.sol b/src/lib/HookStrings.sol index dd2fa50..964bb60 100644 --- a/src/lib/HookStrings.sol +++ b/src/lib/HookStrings.sol @@ -35,45 +35,44 @@ pragma solidity ^0.8.10; library HookStrings { - - /// @dev toAsciiString creates a hex encoding of an - /// address as a string to use in the preview NFT. - function toAsciiString(address x) internal pure returns (string memory) { - bytes memory s = new bytes(40); - for (uint256 i = 0; i < 20; i++) { - bytes1 b = bytes1(uint8(uint256(uint160(x)) / (2**(8 * (19 - i))))); - bytes1 hi = bytes1(uint8(b) / 16); - bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); - s[2 * i] = char(hi); - s[2 * i + 1] = char(lo); + /// @dev toAsciiString creates a hex encoding of an + /// address as a string to use in the preview NFT. + function toAsciiString(address x) internal pure returns (string memory) { + bytes memory s = new bytes(40); + for (uint256 i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint256(uint160(x)) / (2 ** (8 * (19 - i))))); + bytes1 hi = bytes1(uint8(b) / 16); + bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); + s[2 * i] = char(hi); + s[2 * i + 1] = char(lo); + } + return string(s); } - return string(s); - } - function char(bytes1 b) internal pure returns (bytes1) { - if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); - else return bytes1(uint8(b) + 0x57); - } + function char(bytes1 b) internal pure returns (bytes1) { + if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); + else return bytes1(uint8(b) + 0x57); + } - function toString(uint256 value) internal pure returns (string memory) { - // Inspired by OraclizeAPI's implementation - MIT license - // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + function toString(uint256 value) internal pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT license + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol - if (value == 0) { - return "0"; - } - uint256 temp = value; - uint256 digits; - while (temp != 0) { - digits++; - temp /= 10; - } - bytes memory buffer = new bytes(digits); - while (value != 0) { - digits -= 1; - buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); - value /= 10; + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); } - return string(buffer); - } } diff --git a/src/lib/Signatures.sol b/src/lib/Signatures.sol index c77562e..4d5bc7a 100644 --- a/src/lib/Signatures.sol +++ b/src/lib/Signatures.sol @@ -36,20 +36,18 @@ pragma solidity ^0.8.10; /// @dev A library for validating signatures from ZeroEx library Signatures { - /// @dev Allowed signature types. - enum SignatureType { - EIP712 - } + /// @dev Allowed signature types. + enum SignatureType {EIP712} - /// @dev Encoded EC signature. - struct Signature { - // How to validate the signature. - SignatureType signatureType; - // EC Signature data. - uint8 v; - // EC Signature data. - bytes32 r; - // EC Signature data. - bytes32 s; - } + /// @dev Encoded EC signature. + struct Signature { + // How to validate the signature. + SignatureType signatureType; + // EC Signature data. + uint8 v; + // EC Signature data. + bytes32 r; + // EC Signature data. + bytes32 s; + } } diff --git a/src/lib/TokenURI.sol b/src/lib/TokenURI.sol index cbe261c..76282db 100644 --- a/src/lib/TokenURI.sol +++ b/src/lib/TokenURI.sol @@ -40,62 +40,57 @@ import "./HookStrings.sol"; /// @dev This contract implements some ERC721 / for hook instruments. library TokenURI { - function _generateMetadataERC721( - address underlyingTokenAddress, - uint256 underlyingTokenId, - uint256 instrumentStrikePrice, - uint256 instrumentExpiration, - uint256 transfers - ) internal pure returns (string memory) { - return - string( - abi.encodePacked( - '"expiration": ', - HookStrings.toString(instrumentExpiration), - ', "underlying_address": "', - HookStrings.toAsciiString(underlyingTokenAddress), - '", "underlying_tokenId": ', - HookStrings.toString(underlyingTokenId), - ', "strike_price": ', - HookStrings.toString(instrumentStrikePrice), - ', "transfer_index": ', - HookStrings.toString(transfers) - ) - ); - } + function _generateMetadataERC721( + address underlyingTokenAddress, + uint256 underlyingTokenId, + uint256 instrumentStrikePrice, + uint256 instrumentExpiration, + uint256 transfers + ) internal pure returns (string memory) { + return string( + abi.encodePacked( + '"expiration": ', + HookStrings.toString(instrumentExpiration), + ', "underlying_address": "', + HookStrings.toAsciiString(underlyingTokenAddress), + '", "underlying_tokenId": ', + HookStrings.toString(underlyingTokenId), + ', "strike_price": ', + HookStrings.toString(instrumentStrikePrice), + ', "transfer_index": ', + HookStrings.toString(transfers) + ) + ); + } - /// @dev this is a basic tokenURI based on the loot contract for an ERC721 - function tokenURIERC721( - uint256 instrumentId, - address underlyingAddress, - uint256 underlyingTokenId, - uint256 instrumentExpiration, - uint256 instrumentStrike, - uint256 transfers - ) public view returns (string memory) { - string memory json = Base64.encode( - bytes( - string( - abi.encodePacked( - '{"name": "Option ID ', - HookStrings.toString(instrumentId), - '",', - _generateMetadataERC721( - underlyingAddress, - underlyingTokenId, - instrumentStrike, - instrumentExpiration, - transfers - ), - ', "description": "Option Instrument NFT on Hook: the NFT-native call options protocol. Learn more at https://hook.xyz", "image": "https://option-images-hook.s3.amazonaws.com/nft/live_0x', - HookStrings.toAsciiString(address(this)), - "_", - HookStrings.toString(instrumentId), - '.png" }' - ) - ) - ) - ); - return string(abi.encodePacked("data:application/json;base64,", json)); - } + /// @dev this is a basic tokenURI based on the loot contract for an ERC721 + function tokenURIERC721( + uint256 instrumentId, + address underlyingAddress, + uint256 underlyingTokenId, + uint256 instrumentExpiration, + uint256 instrumentStrike, + uint256 transfers + ) public view returns (string memory) { + string memory json = Base64.encode( + bytes( + string( + abi.encodePacked( + '{"name": "Option ID ', + HookStrings.toString(instrumentId), + '",', + _generateMetadataERC721( + underlyingAddress, underlyingTokenId, instrumentStrike, instrumentExpiration, transfers + ), + ', "description": "Option Instrument NFT on Hook: the NFT-native call options protocol. Learn more at https://hook.xyz", "image": "https://option-images-hook.s3.amazonaws.com/nft/live_0x', + HookStrings.toAsciiString(address(this)), + "_", + HookStrings.toString(instrumentId), + '.png" }' + ) + ) + ) + ); + return string(abi.encodePacked("data:application/json;base64,", json)); + } } diff --git a/src/mixin/EIP712.sol b/src/mixin/EIP712.sol index 0a7db38..b6f9530 100644 --- a/src/mixin/EIP712.sol +++ b/src/mixin/EIP712.sol @@ -36,43 +36,32 @@ pragma solidity ^0.8.10; /// @dev EIP712 helpers for features. abstract contract EIP712 { - /// @dev The domain hash separator for the entire call option protocol - bytes32 public EIP712_DOMAIN_SEPARATOR; + /// @dev The domain hash separator for the entire call option protocol + bytes32 public EIP712_DOMAIN_SEPARATOR; - function setAddressForEipDomain(address hookAddress) internal { - // Compute `EIP712_DOMAIN_SEPARATOR` - { - uint256 chainId; - assembly { - chainId := chainid() - } - EIP712_DOMAIN_SEPARATOR = keccak256( - abi.encode( - keccak256( - "EIP712Domain(" - "string name," - "string version," - "uint256 chainId," - "address verifyingContract" - ")" - ), - keccak256("Hook"), - keccak256("1.0.0"), - chainId, - hookAddress - ) - ); + function setAddressForEipDomain(address hookAddress) internal { + // Compute `EIP712_DOMAIN_SEPARATOR` + { + uint256 chainId; + assembly { + chainId := chainid() + } + EIP712_DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(" "string name," "string version," "uint256 chainId," "address verifyingContract" + ")" + ), + keccak256("Hook"), + keccak256("1.0.0"), + chainId, + hookAddress + ) + ); + } } - } - function _getEIP712Hash(bytes32 structHash) - internal - view - returns (bytes32) - { - return - keccak256( - abi.encodePacked(hex"1901", EIP712_DOMAIN_SEPARATOR, structHash) - ); - } + function _getEIP712Hash(bytes32 structHash) internal view returns (bytes32) { + return keccak256(abi.encodePacked(hex"1901", EIP712_DOMAIN_SEPARATOR, structHash)); + } } diff --git a/src/mixin/HookInstrumentERC721.sol b/src/mixin/HookInstrumentERC721.sol index 7bb3bcb..5d9cc3c 100644 --- a/src/mixin/HookInstrumentERC721.sol +++ b/src/mixin/HookInstrumentERC721.sol @@ -46,152 +46,106 @@ import "../lib/TokenURI.sol"; /// @dev This contract implements some ERC721 / for hook instruments. abstract contract HookInstrumentERC721 is ERC721Burnable { - using Counters for Counters.Counter; - mapping(uint256 => Counters.Counter) private _transfers; - bytes4 private constant ERC_721 = bytes4(keccak256("ERC721")); - - /// @dev the contact address for a marketplace to pre-approve - address public _preApprovedMarketplace = address(0); - - /// @dev hook called after the ERC721 is transferred, - /// which allows us to increment the counters. - function _afterTokenTransfer( - address, // from - address, // to - uint256 tokenId - ) override internal { - // increment the counter for the token - _transfers[tokenId].increment(); - } - - /// - /// @dev See {IERC721-isApprovedForAll}. - /// this extension ensures that any operator contract located - /// at {_approvedMarketpace} is considered approved internally - /// in the ERC721 contract - /// - function isApprovedForAll(address owner, address operator) - public - view - virtual - override - returns (bool) - { - return - operator == _preApprovedMarketplace || - super.isApprovedForAll(owner, operator); - } - - constructor(string memory instrumentType) - ERC721(makeInstrumentName(instrumentType), "INST") - {} - - function makeInstrumentName(string memory z) - internal - pure - returns (string memory) - { - return string(abi.encodePacked("Hook ", z, " instrument")); - } - - /// @notice the number of times the token has been transferred - /// @dev this count can be used by overbooks to invalidate orders after a - /// token has been transferred, preventing stale order execution by - /// malicious parties - function getTransferCount(uint256 optionId) external view returns (uint256) { - return _transfers[optionId].current(); - } - - /// @notice getter for the address holding the underlying asset - function getVaultAddress(uint256 optionId) - public - view - virtual - returns (address); - - /// @notice getter for the assetId of the underlying asset within a vault - function getAssetId(uint256 optionId) public view virtual returns (uint32); - - /// @notice getter for the option strike price - function getStrikePrice(uint256 optionId) - external - view - virtual - returns (uint256); - - /// @notice getter for the options expiration. After this time the - /// option is invalid - function getExpiration(uint256 optionId) - external - view - virtual - returns (uint256); - - /// @dev this is the OpenSea compatible collection - level metadata URI. - function contractUri(uint256 optionId) external view returns (string memory) { - return - string( - abi.encodePacked( - "token.hook.xyz/option-contract/", - HookStrings.toAsciiString(address(this)), - "/", - HookStrings.toString(optionId) - ) - ); - } - - /// - /// @dev See {IERC721-tokenURI}. - /// - function tokenURI(uint256 tokenId) - public - view - override - returns (string memory) - { - bytes4 class = _underlyingClass(tokenId); - if (class == ERC_721) { - IHookERC721Vault vault = IHookERC721Vault(getVaultAddress(tokenId)); - uint32 assetId = getAssetId(tokenId); - address underlyingAddress = vault.assetAddress(assetId); - uint256 underlyingTokenId = vault.assetTokenId(assetId); - // currently nothing in the contract depends on the actual underlying metadata uri - // IERC721 underlyingContract = IERC721(underlyingAddress); - uint256 instrumentStrikePrice = this.getStrikePrice(tokenId); - uint256 instrumentExpiration = this.getExpiration(tokenId); - uint256 transfers = _transfers[tokenId].current(); - return - TokenURI.tokenURIERC721( - tokenId, - underlyingAddress, - underlyingTokenId, - instrumentExpiration, - instrumentStrikePrice, - transfers + using Counters for Counters.Counter; + + mapping(uint256 => Counters.Counter) private _transfers; + bytes4 private constant ERC_721 = bytes4(keccak256("ERC721")); + + /// @dev the contact address for a marketplace to pre-approve + address public _preApprovedMarketplace = address(0); + + /// @dev hook called after the ERC721 is transferred, + /// which allows us to increment the counters. + function _afterTokenTransfer( + address, // from + address, // to + uint256 tokenId + ) internal override { + // increment the counter for the token + _transfers[tokenId].increment(); + } + + /// + /// @dev See {IERC721-isApprovedForAll}. + /// this extension ensures that any operator contract located + /// at {_approvedMarketpace} is considered approved internally + /// in the ERC721 contract + /// + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return operator == _preApprovedMarketplace || super.isApprovedForAll(owner, operator); + } + + constructor(string memory instrumentType) ERC721(makeInstrumentName(instrumentType), "INST") {} + + function makeInstrumentName(string memory z) internal pure returns (string memory) { + return string(abi.encodePacked("Hook ", z, " instrument")); + } + + /// @notice the number of times the token has been transferred + /// @dev this count can be used by overbooks to invalidate orders after a + /// token has been transferred, preventing stale order execution by + /// malicious parties + function getTransferCount(uint256 optionId) external view returns (uint256) { + return _transfers[optionId].current(); + } + + /// @notice getter for the address holding the underlying asset + function getVaultAddress(uint256 optionId) public view virtual returns (address); + + /// @notice getter for the assetId of the underlying asset within a vault + function getAssetId(uint256 optionId) public view virtual returns (uint32); + + /// @notice getter for the option strike price + function getStrikePrice(uint256 optionId) external view virtual returns (uint256); + + /// @notice getter for the options expiration. After this time the + /// option is invalid + function getExpiration(uint256 optionId) external view virtual returns (uint256); + + /// @dev this is the OpenSea compatible collection - level metadata URI. + function contractUri(uint256 optionId) external view returns (string memory) { + return string( + abi.encodePacked( + "token.hook.xyz/option-contract/", + HookStrings.toAsciiString(address(this)), + "/", + HookStrings.toString(optionId) + ) ); } - return "Invalid underlying asset"; - } - - /// @dev returns an internal identifier for the underlying type contained within - /// the vault to determine what the instrument is on - /// - /// this class evaluation relies on the interfaceId of the underlying asset - /// - function _underlyingClass(uint256 optionId) - internal - view - returns (bytes4) - { - if ( - ERC165Checker.supportsInterface( - getVaultAddress(optionId), - type(IHookERC721Vault).interfaceId - ) - ) { - return ERC_721; - } else { - revert("_underlying-class: Unsupported underlying type"); + + /// + /// @dev See {IERC721-tokenURI}. + /// + function tokenURI(uint256 tokenId) public view override returns (string memory) { + bytes4 class = _underlyingClass(tokenId); + if (class == ERC_721) { + IHookERC721Vault vault = IHookERC721Vault(getVaultAddress(tokenId)); + uint32 assetId = getAssetId(tokenId); + address underlyingAddress = vault.assetAddress(assetId); + uint256 underlyingTokenId = vault.assetTokenId(assetId); + // currently nothing in the contract depends on the actual underlying metadata uri + // IERC721 underlyingContract = IERC721(underlyingAddress); + uint256 instrumentStrikePrice = this.getStrikePrice(tokenId); + uint256 instrumentExpiration = this.getExpiration(tokenId); + uint256 transfers = _transfers[tokenId].current(); + return TokenURI.tokenURIERC721( + tokenId, underlyingAddress, underlyingTokenId, instrumentExpiration, instrumentStrikePrice, transfers + ); + } + return "Invalid underlying asset"; + } + + /// @dev returns an internal identifier for the underlying type contained within + /// the vault to determine what the instrument is on + /// + /// this class evaluation relies on the interfaceId of the underlying asset + /// + function _underlyingClass(uint256 optionId) internal view returns (bytes4) { + if (ERC165Checker.supportsInterface(getVaultAddress(optionId), type(IHookERC721Vault).interfaceId)) { + return ERC_721; + } else { + revert("_underlying-class: Unsupported underlying type"); + } } - } } diff --git a/src/mixin/PermissionConstants.sol b/src/mixin/PermissionConstants.sol index b31b8f5..b78caae 100644 --- a/src/mixin/PermissionConstants.sol +++ b/src/mixin/PermissionConstants.sol @@ -37,24 +37,24 @@ pragma solidity ^0.8.10; /// @notice roles on the hook protocol that can be read by other contract /// @dev new roles here should be initialized in the constructor of the protocol abstract contract PermissionConstants { - /// ----- ROLES -------- + /// ----- ROLES -------- - /// @notice the allowlister is able to enable and disable projects to mint instruments - bytes32 public constant ALLOWLISTER_ROLE = keccak256("ALLOWLISTER_ROLE"); + /// @notice the allowlister is able to enable and disable projects to mint instruments + bytes32 public constant ALLOWLISTER_ROLE = keccak256("ALLOWLISTER_ROLE"); - /// @notice the pauser is able to start and pause various components of the protocol - bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + /// @notice the pauser is able to start and pause various components of the protocol + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); - /// @notice the vault upgrader role is able to upgrade the implementation for all vaults - bytes32 public constant VAULT_UPGRADER = keccak256("VAULT_UPGRADER"); + /// @notice the vault upgrader role is able to upgrade the implementation for all vaults + bytes32 public constant VAULT_UPGRADER = keccak256("VAULT_UPGRADER"); - /// @notice the call upgrader role is able to upgrade the implementation of the covered call options - bytes32 public constant CALL_UPGRADER = keccak256("CALL_UPGRADER"); + /// @notice the call upgrader role is able to upgrade the implementation of the covered call options + bytes32 public constant CALL_UPGRADER = keccak256("CALL_UPGRADER"); - /// @notice the market configuration role allows the actor to make changes to how the market operates - bytes32 public constant MARKET_CONF = keccak256("MARKET_CONF"); + /// @notice the market configuration role allows the actor to make changes to how the market operates + bytes32 public constant MARKET_CONF = keccak256("MARKET_CONF"); - /// @notice the collection configuration role allows the actor to make changes the collection - /// configs on the protocol contract - bytes32 public constant COLLECTION_CONF = keccak256("COLLECTION_CONF"); + /// @notice the collection configuration role allows the actor to make changes the collection + /// configs on the protocol contract + bytes32 public constant COLLECTION_CONF = keccak256("COLLECTION_CONF"); } diff --git a/src/test/HookCoveredCallBiddingRevertTests.t.sol b/src/test/HookCoveredCallBiddingRevertTests.t.sol index 2306f75..25d7dc2 100644 --- a/src/test/HookCoveredCallBiddingRevertTests.t.sol +++ b/src/test/HookCoveredCallBiddingRevertTests.t.sol @@ -7,110 +7,84 @@ import "./utils/mocks/MaliciousBidder.sol"; /// @dev these tests try cases where a bidder maliciously reverts on save. /// @author Jake Nyquist-j@hook.xyz contract HookCoveredCallBiddingRevertTests is HookProtocolTest { - function setUp() public { - setUpAddresses(); - setUpFullProtocol(); - - // add address to the allowlist for minting - vm.prank(address(admin)); - vaultFactory.makeMultiVault(address(token)); - - // 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.startPrank(address(writer)); - uint256 writerStartBalance = writer.balance; - uint256 baseTime = block.timestamp; - uint32 expiration = uint32(baseTime) + 3 days; - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - // assume that the writer somehow sold to the buyer, outside the scope of this test - calls.safeTransferFrom(writer, buyer, optionId); - uint256 buyerStartBalance = buyer.balance; - - vm.stopPrank(); - // 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); - vm.prank(buyer); - calls.settleOption(optionId); - - // verify the balances are correct - uint256 writerEndBalance = writer.balance; - uint256 buyerEndBalance = buyer.balance; - - assertTrue( - writerEndBalance - writerStartBalance == 1000, - "the writer gets the strike price" - ); - assertTrue( - buyerEndBalance - buyerStartBalance == 100, - "the call owner gets the spread" - ); - } + function setUp() public { + setUpAddresses(); + setUpFullProtocol(); + + // add address to the allowlist for minting + vm.prank(address(admin)); + vaultFactory.makeMultiVault(address(token)); + + // 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.startPrank(address(writer)); + uint256 writerStartBalance = writer.balance; + uint256 baseTime = block.timestamp; + uint32 expiration = uint32(baseTime) + 3 days; + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // assume that the writer somehow sold to the buyer, outside the scope of this test + calls.safeTransferFrom(writer, buyer, optionId); + uint256 buyerStartBalance = buyer.balance; + + vm.stopPrank(); + // 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); + vm.prank(buyer); + calls.settleOption(optionId); + + // verify the balances are correct + uint256 writerEndBalance = writer.balance; + uint256 buyerEndBalance = buyer.balance; + + assertTrue(writerEndBalance - writerStartBalance == 1000, "the writer gets the strike price"); + assertTrue(buyerEndBalance - buyerStartBalance == 100, "the call owner gets the spread"); + } } diff --git a/src/test/HookCoveredCallIntegrationTest.t.sol b/src/test/HookCoveredCallIntegrationTest.t.sol index 2b00063..2d11165 100644 --- a/src/test/HookCoveredCallIntegrationTest.t.sol +++ b/src/test/HookCoveredCallIntegrationTest.t.sol @@ -6,304 +6,227 @@ import "./utils/base.t.sol"; /// @notice Integration tests for the Hook Protocol /// @author Regynald Augustin-regy@hook.xyz contract HookCoveredCallIntegrationTest is HookProtocolTest { - function setUp() public { - setUpAddresses(); - setUpFullProtocol(); - - // add address to the allowlist for minting - vm.prank(address(admin)); - vaultFactory.makeMultiVault(address(token)); - - // 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 testMintOption() public { - vm.startPrank(address(writer)); - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - assertTrue( - calls.ownerOf(optionId) == address(writer), - "owner should own the option" - ); - vm.stopPrank(); - } - - function testRevertMintOptionMustBeOwnerOrOperator() public { - vm.expectRevert("mWE7-caller not owner or operator"); - calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - uint32(block.timestamp + 3 days) - ); - } - - function testRevertMintOptionExpirationMustBeMoreThan1DayInTheFuture() - public - { - vm.startPrank(address(writer)); - - vm.expectRevert("_mOWV-expires sooner than min duration"); - calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - uint32(block.timestamp + 30 minutes) - ); - vm.stopPrank(); - } - - function testSuccessfulAuctionAndSettlement() public { - // create the call option - vm.startPrank(address(writer)); - uint256 writerStartBalance = writer.balance; - uint256 baseTime = block.timestamp; - uint32 expiration = uint32(baseTime) + 3 days; - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - // assume that the writer somehow sold to the buyer, outside the scope of this test - calls.safeTransferFrom(writer, buyer, optionId); - uint256 buyerStartBalance = buyer.balance; - vm.stopPrank(); - - // create some bidders - address bidder1 = address(3456); - address bidder2 = address(33456463); - - // bid at an invalid time - vm.warp(baseTime + 0.5 days); - vm.prank(bidder1); - vm.expectRevert("bE-bidding starts on last day"); - calls.bid{value: 0}(optionId); - - // make the first bid, but have it be too low - vm.warp(baseTime + 2.1 days); - vm.deal(bidder1, 300); - vm.prank(bidder1); - vm.expectRevert("b-bid is lower than the strike price"); - calls.bid{value: 300}(optionId); - - // made a bid - vm.deal(bidder1, 1100); - vm.prank(bidder1); - calls.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) == bidder1, - "bidder1 should be in the lead" - ); - assertTrue( - bidder1.balance == 50, - "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( - bidder1.balance == 1100, - "bidder1 should have their money back from escrow" - ); - 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); - vm.prank(buyer); - calls.settleOption(optionId); - - // verify the balances are correct - uint256 writerEndBalance = writer.balance; - uint256 buyerEndBalance = buyer.balance; - - assertTrue( - writerEndBalance - writerStartBalance == 1000, - "the writer gets the strike price" - ); - assertTrue( - buyerEndBalance - buyerStartBalance == 100, - "the call owner gets the spread" - ); - } - - // Test that the option was not transferred, a bid was made, - // but the owner re-obtained the option and therefore can stop - // the auction. - function testNoSettlemetBidAssetEarlyReclaim() public { - // create the call option - vm.startPrank(address(writer)); - uint256 baseTime = block.timestamp; - uint32 expiration = uint32(baseTime) + 3 days; - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - vm.stopPrank(); - - // made a bid - vm.warp(baseTime + 2.1 days); - address bidder1 = address(3456); - vm.deal(bidder1, 1100); - vm.prank(bidder1); - calls.bid{value: 1050}(optionId); - - vm.prank(address(writer)); - calls.reclaimAsset(optionId, false); - } - - function testNoSettlemetBidAssetRecaimFailRandomClaimer() public { - // create the call option - vm.startPrank(address(writer)); - uint256 baseTime = block.timestamp; - uint32 expiration = uint32(baseTime) + 3 days; - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - // assume that the writer somehow sold to the buyer, outside the scope of this test - calls.safeTransferFrom(writer, buyer, optionId); - vm.stopPrank(); - - vm.warp(expiration + 3 seconds); - - vm.prank(address(5555)); - vm.expectRevert("rA-only writer"); - calls.reclaimAsset(optionId, true); - } - - // test: writer must not steal asset by buying back option nft after expiration. - function testWriterCannotStealBackAssetAfterExpiration() public { - // create the call option - vm.startPrank(address(writer)); - uint256 baseTime = block.timestamp; - uint32 expiration = uint32(baseTime) + 3 days; - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - // assume that the writer somehow sold to the buyer, outside the scope of this test - calls.safeTransferFrom(writer, buyer, optionId); - vm.stopPrank(); - - // made a bid - vm.warp(baseTime + 2.1 days); - address bidder1 = address(3456); - vm.deal(bidder1, 1100); - vm.prank(bidder1); - calls.bid{value: 1050}(optionId); - - vm.warp(expiration + 1 days); - - // The writer somehow buys back the option - vm.prank(address(buyer)); - calls.safeTransferFrom(buyer, writer, optionId); - - vm.prank(address(writer)); - vm.expectRevert("rA-option expired"); - calls.reclaimAsset(optionId, true); - } - - function testWriterCanMintOptionAfterBurning() public { - // mint first call option - vm.startPrank(address(writer)); - - uint256 baseTime = block.timestamp; - uint32 expiration = uint32(baseTime) + 3 days; - uint32 afterExpiration = uint32(baseTime) + 3.1 days; - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - // call option #1 expires - vm.warp(afterExpiration); - calls.burnExpiredOption(optionId); - - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(token), underlyingTokenId) - ); - - // grant entitlement on vault for token id 0 - uint32 expiration2 = expiration + 3 days; - IHookVault(vault).grantEntitlement( - Entitlements.Entitlement( - writer, - address(calls), - address(vault), - 0, - expiration2 - ) - ); - - // mint second call option - vm.expectEmit(true, true, true, true); - emit CallCreated(address(writer), address(vault), 0, 3, 1000, expiration2); - calls.mintWithEntitledVault(address(vault), 0, 1000, expiration2); - vm.stopPrank(); - } + function setUp() public { + setUpAddresses(); + setUpFullProtocol(); + + // add address to the allowlist for minting + vm.prank(address(admin)); + vaultFactory.makeMultiVault(address(token)); + + // 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 testMintOption() public { + vm.startPrank(address(writer)); + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + assertTrue(calls.ownerOf(optionId) == address(writer), "owner should own the option"); + vm.stopPrank(); + } + + function testRevertMintOptionMustBeOwnerOrOperator() public { + vm.expectRevert("mWE7-caller not owner or operator"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, uint32(block.timestamp + 3 days)); + } + + function testRevertMintOptionExpirationMustBeMoreThan1DayInTheFuture() public { + vm.startPrank(address(writer)); + + vm.expectRevert("_mOWV-expires sooner than min duration"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, uint32(block.timestamp + 30 minutes)); + vm.stopPrank(); + } + + function testSuccessfulAuctionAndSettlement() public { + // create the call option + vm.startPrank(address(writer)); + uint256 writerStartBalance = writer.balance; + uint256 baseTime = block.timestamp; + uint32 expiration = uint32(baseTime) + 3 days; + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // assume that the writer somehow sold to the buyer, outside the scope of this test + calls.safeTransferFrom(writer, buyer, optionId); + uint256 buyerStartBalance = buyer.balance; + vm.stopPrank(); + + // create some bidders + address bidder1 = address(3456); + address bidder2 = address(33456463); + + // bid at an invalid time + vm.warp(baseTime + 0.5 days); + vm.prank(bidder1); + vm.expectRevert("bE-bidding starts on last day"); + calls.bid{value: 0}(optionId); + + // make the first bid, but have it be too low + vm.warp(baseTime + 2.1 days); + vm.deal(bidder1, 300); + vm.prank(bidder1); + vm.expectRevert("b-bid is lower than the strike price"); + calls.bid{value: 300}(optionId); + + // made a bid + vm.deal(bidder1, 1100); + vm.prank(bidder1); + calls.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) == bidder1, "bidder1 should be in the lead"); + assertTrue(bidder1.balance == 50, "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(bidder1.balance == 1100, "bidder1 should have their money back from escrow"); + 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); + vm.prank(buyer); + calls.settleOption(optionId); + + // verify the balances are correct + uint256 writerEndBalance = writer.balance; + uint256 buyerEndBalance = buyer.balance; + + assertTrue(writerEndBalance - writerStartBalance == 1000, "the writer gets the strike price"); + assertTrue(buyerEndBalance - buyerStartBalance == 100, "the call owner gets the spread"); + } + + // Test that the option was not transferred, a bid was made, + // but the owner re-obtained the option and therefore can stop + // the auction. + function testNoSettlemetBidAssetEarlyReclaim() public { + // create the call option + vm.startPrank(address(writer)); + uint256 baseTime = block.timestamp; + uint32 expiration = uint32(baseTime) + 3 days; + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + vm.stopPrank(); + + // made a bid + vm.warp(baseTime + 2.1 days); + address bidder1 = address(3456); + vm.deal(bidder1, 1100); + vm.prank(bidder1); + calls.bid{value: 1050}(optionId); + + vm.prank(address(writer)); + calls.reclaimAsset(optionId, false); + } + + function testNoSettlemetBidAssetRecaimFailRandomClaimer() public { + // create the call option + vm.startPrank(address(writer)); + uint256 baseTime = block.timestamp; + uint32 expiration = uint32(baseTime) + 3 days; + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // assume that the writer somehow sold to the buyer, outside the scope of this test + calls.safeTransferFrom(writer, buyer, optionId); + vm.stopPrank(); + + vm.warp(expiration + 3 seconds); + + vm.prank(address(5555)); + vm.expectRevert("rA-only writer"); + calls.reclaimAsset(optionId, true); + } + + // test: writer must not steal asset by buying back option nft after expiration. + function testWriterCannotStealBackAssetAfterExpiration() public { + // create the call option + vm.startPrank(address(writer)); + uint256 baseTime = block.timestamp; + uint32 expiration = uint32(baseTime) + 3 days; + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // assume that the writer somehow sold to the buyer, outside the scope of this test + calls.safeTransferFrom(writer, buyer, optionId); + vm.stopPrank(); + + // made a bid + vm.warp(baseTime + 2.1 days); + address bidder1 = address(3456); + vm.deal(bidder1, 1100); + vm.prank(bidder1); + calls.bid{value: 1050}(optionId); + + vm.warp(expiration + 1 days); + + // The writer somehow buys back the option + vm.prank(address(buyer)); + calls.safeTransferFrom(buyer, writer, optionId); + + vm.prank(address(writer)); + vm.expectRevert("rA-option expired"); + calls.reclaimAsset(optionId, true); + } + + function testWriterCanMintOptionAfterBurning() public { + // mint first call option + vm.startPrank(address(writer)); + + uint256 baseTime = block.timestamp; + uint32 expiration = uint32(baseTime) + 3 days; + uint32 afterExpiration = uint32(baseTime) + 3.1 days; + + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // call option #1 expires + vm.warp(afterExpiration); + calls.burnExpiredOption(optionId); + + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(token), underlyingTokenId)); + + // grant entitlement on vault for token id 0 + uint32 expiration2 = expiration + 3 days; + IHookVault(vault).grantEntitlement( + Entitlements.Entitlement(writer, address(calls), address(vault), 0, expiration2) + ); + + // mint second call option + vm.expectEmit(true, true, true, true); + emit CallCreated(address(writer), address(vault), 0, 3, 1000, expiration2); + calls.mintWithEntitledVault(address(vault), 0, 1000, expiration2); + vm.stopPrank(); + } } diff --git a/src/test/HookCoveredCallTests.t.sol b/src/test/HookCoveredCallTests.t.sol index 43c4f2e..b7f898c 100644 --- a/src/test/HookCoveredCallTests.t.sol +++ b/src/test/HookCoveredCallTests.t.sol @@ -8,1661 +8,1327 @@ import "./utils/base.t.sol"; /// @notice Covered call minting test cases /// @author Regynald Augustin-regy@hook.xyz contract HookCoveredCallMintTests is HookProtocolTest { - function setUp() public { - setUpAddresses(); - setUpFullProtocol(); - - // Set buyer balances and give weth - vm.deal(address(buyer), 100 ether); - vm.prank(address(buyer)); - weth.deposit{value: 50 ether}(); - - vm.prank(address(admin)); - vaultFactory.makeMultiVault(address(token)); - - // Mint underlying token for writer - underlyingTokenId = 0; - token.mint(address(writer), underlyingTokenId); - } - - function testMintOption() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - assertTrue( - calls.ownerOf(optionId) == address(writer), - "owner should own the option" - ); - } - - function testMultiVaultMintOption() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - - // limit this call to 350,000 gas - // overall gas usage depends on the underlying NFT contract - uint256 optionId = calls.mintWithErc721{gas: 350_000}( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - assertTrue( - calls.ownerOf(optionId) == address(writer), - "owner should own the option" - ); - } - - function testTransferApproval() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - vm.stopPrank(); - vm.expectRevert("ERC721: transfer caller is not owner nor approved"); - calls.safeTransferFrom(writer, address(55), optionId); - - vm.prank(preApprovedOperator); - calls.safeTransferFrom(writer, address(55), optionId); - assertTrue( - calls.ownerOf(optionId) == address(55), - "the transfer should work" - ); - } - - function test_MintOptionWithVault() public { - vm.startPrank(address(writer)); - - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(token), underlyingTokenId) - ); - - // place token in the vault - token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - Signatures.Signature memory sig = makeSignature( - underlyingTokenId, - expiration, - writer - ); - vm.expectEmit(true, true, true, true); - emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration); - - uint256 optionId = calls.mintWithVault( - address(vault), - 0, - 1000, - expiration, - sig - ); - - assertTrue( - calls.ownerOf(optionId) == address(writer), - "owner should own the option" - ); - - (bool isActive, address operator) = vault.getCurrentEntitlementOperator(0); - assertTrue(isActive, "there should be an active entitlement"); - assertTrue( - operator == address(calls), - "the call options should be the operator" - ); - } - - function test_MintOptionWithVaultRandomAddress() public { - vm.startPrank(address(writer)); - - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(token), underlyingTokenId) - ); - - // place token in the vault - token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - Signatures.Signature memory sig = makeSignature( - underlyingTokenId, - expiration, - writer - ); - vm.expectEmit(true, true, true, true); - emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration); - - vm.stopPrank(); - vm.prank(address(333456)); // simulating a replay attack, random address calling with the signature] - vm.expectRevert("mWV-called by someone other than the owner or operator"); - calls.mintWithVault(address(vault), 0, 1000, expiration, sig); - - // in this replay attack, the writer's call would land second - vm.prank(address(writer)); - uint256 optionId = calls.mintWithVault( - address(vault), - 0, - 1000, - expiration, - sig - ); - - assertTrue( - calls.ownerOf(optionId) == address(writer), - "owner should own the option" - ); - - (bool isActive, address operator) = vault.getCurrentEntitlementOperator(0); - assertTrue(isActive, "there should be an active entitlement"); - assertTrue( - operator == address(calls), - "the call options should be the operator" - ); - } - - function test_MintOptionWithVaultSpecifiedOperator() public { - vm.startPrank(address(writer)); - - address specifiedOperator = address(44556677); - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(token), underlyingTokenId) - ); - - // place token in the vault - token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); - vault.approveOperator(specifiedOperator, uint32(underlyingTokenId)); - uint32 expiration = uint32(block.timestamp) + 3 days; - - Signatures.Signature memory sig = makeSignature( - underlyingTokenId, - expiration, - writer - ); - vm.expectEmit(true, true, true, true); - emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration); - - vm.stopPrank(); - vm.prank(specifiedOperator); // the specified operator may still mint - uint256 optionId = calls.mintWithVault( - address(vault), - 0, - 1000, - expiration, - sig - ); - - assertTrue( - calls.ownerOf(optionId) == address(writer), - "owner should own the option" - ); - - (bool isActive, address operator) = vault.getCurrentEntitlementOperator(0); - assertTrue(isActive, "there should be an active entitlement"); - assertTrue( - operator == address(calls), - "the call options should be the operator" - ); - } - - function test_MintOptionWithAlienVault() public { - vm.startPrank(address(writer)); - - HookERC721VaultImplV1 alienVault = new HookERC721VaultImplV1(); - - alienVault.initialize(address(token), underlyingTokenId, address(protocol)); - - // place token in the vault - token.safeTransferFrom( - address(writer), - address(alienVault), - underlyingTokenId - ); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - Signatures.Signature memory sig = makeSignature( - underlyingTokenId, - expiration, - writer - ); - - vm.expectRevert("mWV-can only mint with protocol vaults"); - calls.mintWithVault(address(alienVault), 0, 1000, expiration, sig); - } - - function test_MintOptionWithVaultFailsExpiration() public { - vm.startPrank(address(writer)); - - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(token), underlyingTokenId) - ); - - // place token in the vault - token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); - - uint32 expiration = uint32(block.timestamp) + 1 days; - - Signatures.Signature memory sig = makeSignature( - underlyingTokenId, - expiration, - writer - ); - - vm.expectRevert("_mOWV-expires sooner than min duration"); - calls.mintWithVault(address(vault), 0, 1000, expiration, sig); - } - - function test_MintOptionWithVaultFailsEmptyVault() public { - vm.startPrank(address(writer)); - - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(token), underlyingTokenId) - ); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - Signatures.Signature memory sig = makeSignature( - underlyingTokenId, - expiration, - writer - ); - - vm.expectRevert("mWV-asset not in vault"); - calls.mintWithVault(address(vault), 0, 1000, expiration, sig); - } - - function test_MintOptionWithVaultFailsUnsupportedCollection() public { - vm.startPrank(address(writer)); - try - vaultFactory.findOrCreateVault(address(calls), underlyingTokenId) - {} catch {} - - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(calls), underlyingTokenId) - ); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - Signatures.Signature memory sig = makeSignature( - underlyingTokenId, - expiration, - writer - ); - - vm.expectRevert("mWV-token not allowed"); - calls.mintWithVault(address(vault), 0, 1000, expiration, sig); - } - - function testMintMultipleOptions() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - assertTrue( - calls.ownerOf(optionId) == address(writer), - "owner should own the option" - ); - - uint256 secondUnderlyingTokenId = 1; - token.mint(address(writer), secondUnderlyingTokenId); - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 2, // This would be the second option id. - 1000, - expiration - ); - uint256 secondOptionId = calls.mintWithErc721( - address(token), - secondUnderlyingTokenId, - 1000, - expiration - ); - - assertTrue( - calls.ownerOf(secondOptionId) == address(writer), - "owner should own the option" - ); - } - - // Test that proxy smart contracts are able to mint options on behalf of the owner - function testMintOptionAsOperator() public { - address operator = address(10); - vm.label(operator, "additional token operator"); - - vm.startPrank(address(writer)); - // Writer approve operator and covered call - token.setApprovalForAll(operator, true); - token.setApprovalForAll(address(calls), true); - vm.stopPrank(); - - vm.startPrank(operator); - - uint32 expiration = uint32(1661141567); - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 7000000000000000000, - expiration - ); - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 7000000000000000000, - expiration - ); - - assertTrue( - calls.ownerOf(optionId) == address(writer), - "owner should own the option" - ); - - assertTrue( - calls.getApproved(optionId) == address(operator), - "operator should be approved for option" - ); - - emit log(calls.tokenURI(optionId)); - assertTrue( - bytes(calls.tokenURI(optionId)).length > 100, - "tokenURI should be long" - ); - } - - function testCannotMintOptionInvalidSignature() public { - vm.startPrank(address(writer)); - - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(token), underlyingTokenId) - ); - - // place token in the vault - token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); - uint32 expiration = uint32(block.timestamp) + 3 days; - - Signatures.Signature memory signature = makeSignature( - underlyingTokenId + 1, - expiration + 1, - writer - ); - vm.expectRevert( - "validateEntitlementSignature --- not signed by beneficialOwner" - ); - calls.mintWithVault(address(vault), 0, 1000, expiration, signature); - } - - function testCannotMintOptionInvalidExpiration() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 1 hours; - vm.expectRevert("_mOWV-expires sooner than min duration"); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - } - - function testCannotMintOptionInvalidExpiration2() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 1 hours; - - vm.expectRevert("_mOWV-expires sooner than min duration"); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - } - - function testCanMintOptionLongerExpiration() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 100 days; - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - } - - function testCannotMintOptionPaused() public { - vm.startPrank(address(admin)); - protocol.pause(); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectRevert("Pausable: paused"); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - } - - function testCannotMintOptionMarketPaused() public { - vm.startPrank(address(admin)); - callInternal.setMarketPaused(true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectRevert("market paused"); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - } - - function testCannotMintOptionHookContractNotApproved() public { - vm.startPrank(address(writer)); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectRevert("mWE7-not approved operator"); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - } - - function testCannotMintOptionNotUnderlyingOwner() public { - vm.startPrank(address(buyer)); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectRevert("mWE7-caller not owner or operator"); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - } - - function testCannotMintMultipleOptionsSameToken() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - assertTrue( - calls.ownerOf(optionId) == address(writer), - "owner should own the option" - ); - - // Vault is now owner of the underlying token so this fails. - vm.expectRevert("mWE7-caller not owner or operator"); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - vm.stopPrank(); - } - - function testCannotMintMultipleOptionsSameTokenAsOperator() public { - address operator = address(10); - vm.label(operator, "additional token operator"); - - vm.startPrank(address(writer)); - // Writer approve operator and covered call - token.setApprovalForAll(operator, true); - token.setApprovalForAll(address(calls), true); - vm.stopPrank(); - - vm.startPrank(operator); - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - - // Vault is now owner of the underlying token so this fails. - vm.expectRevert("mWE7-caller not owner or operator"); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - } - - function testCannotMintMultipleOptionsWithSameAsset() public { - vm.startPrank(address(writer)); - - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(token), underlyingTokenId) - ); - - // place token in the vault - token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - Signatures.Signature memory sig = makeSignature( - underlyingTokenId, - expiration, - writer - ); - vm.expectEmit(true, true, true, true); - emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration); - uint256 optionId = calls.mintWithVault( - address(vault), - 0, - 1000, - expiration, - sig - ); - - vm.expectRevert("_mOWV-previous option must be settled"); - calls.mintWithEntitledVault(address(vault), 0, 1000, expiration); - } - - function canMintAdditionalOptionsWithSameAssetAfterFirstExpires() public { - vm.startPrank(address(writer)); - - IHookERC721Vault vault = IHookERC721Vault( - vaultFactory.findOrCreateVault(address(token), underlyingTokenId) - ); - - // place token in the vault - token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - Signatures.Signature memory sig = makeSignature( - underlyingTokenId, - expiration, - writer - ); - vm.expectEmit(true, true, true, true); - emit CallCreated( - address(writer), - address(vault), - underlyingTokenId, - 1, - 1000, - expiration - ); - - uint256 optionId = calls.mintWithVault( - address(vault), - 0, - 1000, - expiration, - sig - ); - - assertTrue(optionId == 1); - vm.warp(expiration + 1 days); - - calls.burnExpiredOption(1); - - uint32 expiration2 = uint32(block.timestamp) + 3 days; - IHookVault(vault).grantEntitlement( - Entitlements.Entitlement( - writer, - address(calls), - address(vault), - 0, - expiration2 - ) - ); - - vm.expectEmit(true, true, true, true); - emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration2); - calls.mintWithEntitledVault(address(vault), 0, 1000, expiration2); - } - - function testCannotMintMultipleOptionsSameTokenAsOwnerThenOperator() public { - address operator = address(10); - vm.label(operator, "additional token operator"); - - vm.startPrank(address(writer)); - // Writer approve operator and covered call - token.setApprovalForAll(operator, true); - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - - // Perform next mint attempt as operator - vm.stopPrank(); - vm.startPrank(operator); - - // Vault is now owner of the underlying token so this fails. - vm.expectRevert("mWE7-caller not owner or operator"); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - } - - function testCannotMintOptionForUnallowedContract() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); - - // Minting should only work for TestERC721 - vm.expectRevert("mWE7-token not on allowlist"); - calls.mintWithErc721(address(calls), optionId, 1000, expiration); - } - - /// Approvals /// - - // The operator of the underlying asset will be approved for the option NFT - // but approval for the underlying asset will be removed. - function testApprovalsForUnderlyingRemovedAfterOptionMint() public { - address operator = address(10); - vm.label(operator, "additional token operator"); - - vm.startPrank(address(writer)); - // Writer approve operator and covered call - token.setApprovalForAll(operator, true); - token.setApprovalForAll(address(calls), true); - vm.stopPrank(); - - vm.startPrank(operator); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); - - assertTrue( - token.getApproved(underlyingTokenId) != address(operator), - "operator should be approved for option" - ); - } + function setUp() public { + setUpAddresses(); + setUpFullProtocol(); + + // Set buyer balances and give weth + vm.deal(address(buyer), 100 ether); + vm.prank(address(buyer)); + weth.deposit{value: 50 ether}(); + + vm.prank(address(admin)); + vaultFactory.makeMultiVault(address(token)); + + // Mint underlying token for writer + underlyingTokenId = 0; + token.mint(address(writer), underlyingTokenId); + } + + function testMintOption() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + assertTrue(calls.ownerOf(optionId) == address(writer), "owner should own the option"); + } + + function testMultiVaultMintOption() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + + // limit this call to 350,000 gas + // overall gas usage depends on the underlying NFT contract + uint256 optionId = calls.mintWithErc721{gas: 350_000}(address(token), underlyingTokenId, 1000, expiration); + + assertTrue(calls.ownerOf(optionId) == address(writer), "owner should own the option"); + } + + function testTransferApproval() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + vm.stopPrank(); + vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + calls.safeTransferFrom(writer, address(55), optionId); + + vm.prank(preApprovedOperator); + calls.safeTransferFrom(writer, address(55), optionId); + assertTrue(calls.ownerOf(optionId) == address(55), "the transfer should work"); + } + + function test_MintOptionWithVault() public { + vm.startPrank(address(writer)); + + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(token), underlyingTokenId)); + + // place token in the vault + token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + Signatures.Signature memory sig = makeSignature(underlyingTokenId, expiration, writer); + vm.expectEmit(true, true, true, true); + emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration); + + uint256 optionId = calls.mintWithVault(address(vault), 0, 1000, expiration, sig); + + assertTrue(calls.ownerOf(optionId) == address(writer), "owner should own the option"); + + (bool isActive, address operator) = vault.getCurrentEntitlementOperator(0); + assertTrue(isActive, "there should be an active entitlement"); + assertTrue(operator == address(calls), "the call options should be the operator"); + } + + function test_MintOptionWithVaultRandomAddress() public { + vm.startPrank(address(writer)); + + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(token), underlyingTokenId)); + + // place token in the vault + token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + Signatures.Signature memory sig = makeSignature(underlyingTokenId, expiration, writer); + vm.expectEmit(true, true, true, true); + emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration); + + vm.stopPrank(); + vm.prank(address(333456)); // simulating a replay attack, random address calling with the signature] + vm.expectRevert("mWV-called by someone other than the owner or operator"); + calls.mintWithVault(address(vault), 0, 1000, expiration, sig); + + // in this replay attack, the writer's call would land second + vm.prank(address(writer)); + uint256 optionId = calls.mintWithVault(address(vault), 0, 1000, expiration, sig); + + assertTrue(calls.ownerOf(optionId) == address(writer), "owner should own the option"); + + (bool isActive, address operator) = vault.getCurrentEntitlementOperator(0); + assertTrue(isActive, "there should be an active entitlement"); + assertTrue(operator == address(calls), "the call options should be the operator"); + } + + function test_MintOptionWithVaultSpecifiedOperator() public { + vm.startPrank(address(writer)); + + address specifiedOperator = address(44556677); + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(token), underlyingTokenId)); + + // place token in the vault + token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); + vault.approveOperator(specifiedOperator, uint32(underlyingTokenId)); + uint32 expiration = uint32(block.timestamp) + 3 days; + + Signatures.Signature memory sig = makeSignature(underlyingTokenId, expiration, writer); + vm.expectEmit(true, true, true, true); + emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration); + + vm.stopPrank(); + vm.prank(specifiedOperator); // the specified operator may still mint + uint256 optionId = calls.mintWithVault(address(vault), 0, 1000, expiration, sig); + + assertTrue(calls.ownerOf(optionId) == address(writer), "owner should own the option"); + + (bool isActive, address operator) = vault.getCurrentEntitlementOperator(0); + assertTrue(isActive, "there should be an active entitlement"); + assertTrue(operator == address(calls), "the call options should be the operator"); + } + + function test_MintOptionWithAlienVault() public { + vm.startPrank(address(writer)); + + HookERC721VaultImplV1 alienVault = new HookERC721VaultImplV1(); + + alienVault.initialize(address(token), underlyingTokenId, address(protocol)); + + // place token in the vault + token.safeTransferFrom(address(writer), address(alienVault), underlyingTokenId); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + Signatures.Signature memory sig = makeSignature(underlyingTokenId, expiration, writer); + + vm.expectRevert("mWV-can only mint with protocol vaults"); + calls.mintWithVault(address(alienVault), 0, 1000, expiration, sig); + } + + function test_MintOptionWithVaultFailsExpiration() public { + vm.startPrank(address(writer)); + + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(token), underlyingTokenId)); + + // place token in the vault + token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); + + uint32 expiration = uint32(block.timestamp) + 1 days; + + Signatures.Signature memory sig = makeSignature(underlyingTokenId, expiration, writer); + + vm.expectRevert("_mOWV-expires sooner than min duration"); + calls.mintWithVault(address(vault), 0, 1000, expiration, sig); + } + + function test_MintOptionWithVaultFailsEmptyVault() public { + vm.startPrank(address(writer)); + + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(token), underlyingTokenId)); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + Signatures.Signature memory sig = makeSignature(underlyingTokenId, expiration, writer); + + vm.expectRevert("mWV-asset not in vault"); + calls.mintWithVault(address(vault), 0, 1000, expiration, sig); + } + + function test_MintOptionWithVaultFailsUnsupportedCollection() public { + vm.startPrank(address(writer)); + try vaultFactory.findOrCreateVault(address(calls), underlyingTokenId) {} catch {} + + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(calls), underlyingTokenId)); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + Signatures.Signature memory sig = makeSignature(underlyingTokenId, expiration, writer); + + vm.expectRevert("mWV-token not allowed"); + calls.mintWithVault(address(vault), 0, 1000, expiration, sig); + } + + function testMintMultipleOptions() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + assertTrue(calls.ownerOf(optionId) == address(writer), "owner should own the option"); + + uint256 secondUnderlyingTokenId = 1; + token.mint(address(writer), secondUnderlyingTokenId); + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 2, // This would be the second option id. + 1000, + expiration + ); + uint256 secondOptionId = calls.mintWithErc721(address(token), secondUnderlyingTokenId, 1000, expiration); + + assertTrue(calls.ownerOf(secondOptionId) == address(writer), "owner should own the option"); + } + + // Test that proxy smart contracts are able to mint options on behalf of the owner + function testMintOptionAsOperator() public { + address operator = address(10); + vm.label(operator, "additional token operator"); + + vm.startPrank(address(writer)); + // Writer approve operator and covered call + token.setApprovalForAll(operator, true); + token.setApprovalForAll(address(calls), true); + vm.stopPrank(); + + vm.startPrank(operator); + + uint32 expiration = uint32(1661141567); + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 7000000000000000000, + expiration + ); + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 7000000000000000000, expiration); + + assertTrue(calls.ownerOf(optionId) == address(writer), "owner should own the option"); + + assertTrue(calls.getApproved(optionId) == address(operator), "operator should be approved for option"); + + emit log(calls.tokenURI(optionId)); + assertTrue(bytes(calls.tokenURI(optionId)).length > 100, "tokenURI should be long"); + } + + function testCannotMintOptionInvalidSignature() public { + vm.startPrank(address(writer)); + + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(token), underlyingTokenId)); + + // place token in the vault + token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); + uint32 expiration = uint32(block.timestamp) + 3 days; + + Signatures.Signature memory signature = makeSignature(underlyingTokenId + 1, expiration + 1, writer); + vm.expectRevert("validateEntitlementSignature --- not signed by beneficialOwner"); + calls.mintWithVault(address(vault), 0, 1000, expiration, signature); + } + + function testCannotMintOptionInvalidExpiration() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 1 hours; + vm.expectRevert("_mOWV-expires sooner than min duration"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + } + + function testCannotMintOptionInvalidExpiration2() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 1 hours; + + vm.expectRevert("_mOWV-expires sooner than min duration"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + } + + function testCanMintOptionLongerExpiration() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 100 days; + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + } + + function testCannotMintOptionPaused() public { + vm.startPrank(address(admin)); + protocol.pause(); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectRevert("Pausable: paused"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + } + + function testCannotMintOptionMarketPaused() public { + vm.startPrank(address(admin)); + callInternal.setMarketPaused(true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectRevert("market paused"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + } + + function testCannotMintOptionHookContractNotApproved() public { + vm.startPrank(address(writer)); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectRevert("mWE7-not approved operator"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + } + + function testCannotMintOptionNotUnderlyingOwner() public { + vm.startPrank(address(buyer)); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectRevert("mWE7-caller not owner or operator"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + } + + function testCannotMintMultipleOptionsSameToken() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + assertTrue(calls.ownerOf(optionId) == address(writer), "owner should own the option"); + + // Vault is now owner of the underlying token so this fails. + vm.expectRevert("mWE7-caller not owner or operator"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + vm.stopPrank(); + } + + function testCannotMintMultipleOptionsSameTokenAsOperator() public { + address operator = address(10); + vm.label(operator, "additional token operator"); + + vm.startPrank(address(writer)); + // Writer approve operator and covered call + token.setApprovalForAll(operator, true); + token.setApprovalForAll(address(calls), true); + vm.stopPrank(); + + vm.startPrank(operator); + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // Vault is now owner of the underlying token so this fails. + vm.expectRevert("mWE7-caller not owner or operator"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + } + + function testCannotMintMultipleOptionsWithSameAsset() public { + vm.startPrank(address(writer)); + + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(token), underlyingTokenId)); + + // place token in the vault + token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + Signatures.Signature memory sig = makeSignature(underlyingTokenId, expiration, writer); + vm.expectEmit(true, true, true, true); + emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration); + uint256 optionId = calls.mintWithVault(address(vault), 0, 1000, expiration, sig); + + vm.expectRevert("_mOWV-previous option must be settled"); + calls.mintWithEntitledVault(address(vault), 0, 1000, expiration); + } + + function canMintAdditionalOptionsWithSameAssetAfterFirstExpires() public { + vm.startPrank(address(writer)); + + IHookERC721Vault vault = IHookERC721Vault(vaultFactory.findOrCreateVault(address(token), underlyingTokenId)); + + // place token in the vault + token.safeTransferFrom(address(writer), address(vault), underlyingTokenId); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + Signatures.Signature memory sig = makeSignature(underlyingTokenId, expiration, writer); + vm.expectEmit(true, true, true, true); + emit CallCreated(address(writer), address(vault), underlyingTokenId, 1, 1000, expiration); + + uint256 optionId = calls.mintWithVault(address(vault), 0, 1000, expiration, sig); + + assertTrue(optionId == 1); + vm.warp(expiration + 1 days); + + calls.burnExpiredOption(1); + + uint32 expiration2 = uint32(block.timestamp) + 3 days; + IHookVault(vault).grantEntitlement( + Entitlements.Entitlement(writer, address(calls), address(vault), 0, expiration2) + ); + + vm.expectEmit(true, true, true, true); + emit CallCreated(address(writer), address(vault), 0, 2, 1000, expiration2); + calls.mintWithEntitledVault(address(vault), 0, 1000, expiration2); + } + + function testCannotMintMultipleOptionsSameTokenAsOwnerThenOperator() public { + address operator = address(10); + vm.label(operator, "additional token operator"); + + vm.startPrank(address(writer)); + // Writer approve operator and covered call + token.setApprovalForAll(operator, true); + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // Perform next mint attempt as operator + vm.stopPrank(); + vm.startPrank(operator); + + // Vault is now owner of the underlying token so this fails. + vm.expectRevert("mWE7-caller not owner or operator"); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + } + + function testCannotMintOptionForUnallowedContract() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // Minting should only work for TestERC721 + vm.expectRevert("mWE7-token not on allowlist"); + calls.mintWithErc721(address(calls), optionId, 1000, expiration); + } + + /// Approvals /// + + // The operator of the underlying asset will be approved for the option NFT + // but approval for the underlying asset will be removed. + function testApprovalsForUnderlyingRemovedAfterOptionMint() public { + address operator = address(10); + vm.label(operator, "additional token operator"); + + vm.startPrank(address(writer)); + // Writer approve operator and covered call + token.setApprovalForAll(operator, true); + token.setApprovalForAll(address(calls), true); + vm.stopPrank(); + + vm.startPrank(operator); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + assertTrue(token.getApproved(underlyingTokenId) != address(operator), "operator should be approved for option"); + } } /// Bidding /// contract HookCoveredCallBidTests is HookProtocolTest { - function setUp() public { - setUpAddresses(); - setUpFullProtocol(); - - // Set buyer balances and give weth - vm.deal(address(buyer), 100 ether); - vm.prank(address(buyer)); - weth.deposit{value: 50 ether}(); - - // Mint underlying token for writer - underlyingTokenId = 0; - token.mint(address(writer), underlyingTokenId); - - setUpMintOption(); - } - - function testBidAsOwner() public { - address bidder = address(37); - vm.label(bidder, "Option bidder"); - - vm.warp(block.timestamp + 2.1 days); - hoax(bidder); - calls.bid{value: 0.1 ether}(optionTokenId); - - assertTrue( - calls.currentBid(optionTokenId) == 0.1 ether, - "bid should be 0.1 ether" - ); - assertTrue( - calls.currentBidder(optionTokenId) == bidder, - "bid should be 0.1 ether" - ); - } - - function testBidAsOperator() public { - address operator = address(10); - vm.label(operator, "additional token operator"); - - vm.startPrank(address(writer)); - uint256 underlyingTokenId2 = 1; - token.mint(address(writer), underlyingTokenId2); - - // Writer approve operator and covered call - token.setApprovalForAll(operator, true); - token.setApprovalForAll(address(calls), true); - vm.stopPrank(); - - startHoax(operator); - uint32 expiration = uint32(block.timestamp) + 3 days; - - calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); - - vm.warp(block.timestamp + 2.1 days); - calls.bid{value: 0.1 ether}(optionTokenId); - - assertTrue( - calls.currentBid(optionTokenId) == 0.1 ether, - "bid should be 0.1 ether" - ); - assertTrue( - calls.currentBidder(optionTokenId) == operator, - "bid should be 0.1 ether" - ); - } - - function testNewHighBidReturnOldHighBidTokens() public { - address firstBidder = address(37); - vm.label(firstBidder, "First option bidder"); - vm.deal(address(firstBidder), 1 ether); - - address secondBidder = address(38); - vm.label(secondBidder, "Second option bidder"); - vm.deal(address(secondBidder), 1 ether); - - vm.warp(block.timestamp + 2.1 days); - - vm.prank(firstBidder); - uint256 firstBidderStartBalance = firstBidder.balance; - calls.bid{value: 0.1 ether}(optionTokenId); - - vm.prank(secondBidder); - uint256 secondBidderStartBalance = secondBidder.balance; - calls.bid{value: 0.2 ether}(optionTokenId); - - assertTrue( - firstBidder.balance == firstBidderStartBalance, - "first bidder should have lower bid returned" - ); - - assertTrue( - secondBidder.balance + 0.2 ether == secondBidderStartBalance, - "first bidder should have lower bid returned" - ); - } - - function testCannotBidBeforeAuctionStart() public { - address bidder = address(37); - vm.label(bidder, "Option bidder"); - - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 1.9 days); - hoax(bidder); - vm.expectRevert("bE-bidding starts on last day"); - calls.bid{value: 0.1 ether}(optionTokenId); - } - - function testCannotBidAfterOptionExpired() public { - address bidder = address(37); - vm.label(bidder, "Option bidder"); - - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 4 days); - hoax(bidder); - vm.expectRevert("bE-expired"); - calls.bid{value: 0.1 ether}(optionTokenId); - } - - function testCannotBidLessThanStrikePrice() public { - address bidder = address(37); - vm.label(bidder, "Option bidder"); - - vm.warp(block.timestamp + 2.1 days); - hoax(bidder); - - /// Option strike price is 1000 wei. - vm.expectRevert("b-bid is lower than the strike price"); - calls.bid{value: 1 wei}(optionTokenId); - } - - function testCannotBidLessThanCurrentBid() public { - address firstBidder = address(37); - vm.label(firstBidder, "First option bidder"); - vm.deal(address(firstBidder), 1 ether); - - address secondBidder = address(38); - vm.label(secondBidder, "Second option bidder"); - vm.deal(address(secondBidder), 1 ether); - - vm.warp(block.timestamp + 2.1 days); - - vm.prank(firstBidder); - calls.bid{value: 0.1 ether}(optionTokenId); - - vm.prank(secondBidder); - vm.expectRevert("b-must overbid by minBidIncrementBips"); - calls.bid{value: 0.09 ether}(optionTokenId); - } - - function testCannotBidLessThanCurrentIncrement() public { - vm.prank(admin); - callInternal.setBidIncrement(1000); // set it to 10% for this test - address firstBidder = address(37); - vm.label(firstBidder, "First option bidder"); - vm.deal(address(firstBidder), 1 ether); - - address secondBidder = address(38); - vm.label(secondBidder, "Second option bidder"); - vm.deal(address(secondBidder), 1 ether); - - vm.warp(block.timestamp + 2.1 days); - - vm.prank(firstBidder); - calls.bid{value: 0.1 ether}(optionTokenId); - - vm.prank(secondBidder); - vm.expectRevert("b-must overbid by minBidIncrementBips"); - calls.bid{value: 0.105 ether}(optionTokenId); - } - - function testCanBidLessMoreThanCurrentIncrement() public { - vm.prank(admin); - callInternal.setBidIncrement(1000); // set it to 10% for this test - address firstBidder = address(37); - vm.label(firstBidder, "First option bidder"); - vm.deal(address(firstBidder), 1 ether); - - address secondBidder = address(38); - vm.label(secondBidder, "Second option bidder"); - vm.deal(address(secondBidder), 1 ether); - - vm.warp(block.timestamp + 2.1 days); - - vm.prank(firstBidder); - calls.bid{value: 0.1 ether}(optionTokenId); - - vm.prank(secondBidder); - calls.bid{value: 0.112 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; - - 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 + 0.1 ether; - - vm.prank(writer); - calls.bid{value: bidAmount}(optionTokenId); - - assertTrue( - calls.currentBid(optionTokenId) == 0.1 ether + 0.1 ether, - "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" - ); - } - - function testWriterCanOutbidSelfOnSpread() 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; - - 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 + 0.1 ether; - - vm.prank(writer); - calls.bid{value: bidAmount}(optionTokenId); - - uint256 secondBidAmount = 0.1 ether - strike + 0.2 ether; - vm.prank(writer); - calls.bid{value: secondBidAmount}(optionTokenId); - - assertTrue( - calls.currentBid(optionTokenId) == 0.1 ether + 0.2 ether, - "high bid should be 0.1 ether + 0.2 ether" - ); - 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 - secondBidAmount, - "writer should have only used 0.1 ether + 0.2 ether to bid" - ); - } + function setUp() public { + setUpAddresses(); + setUpFullProtocol(); + + // Set buyer balances and give weth + vm.deal(address(buyer), 100 ether); + vm.prank(address(buyer)); + weth.deposit{value: 50 ether}(); + + // Mint underlying token for writer + underlyingTokenId = 0; + token.mint(address(writer), underlyingTokenId); + + setUpMintOption(); + } + + function testBidAsOwner() public { + address bidder = address(37); + vm.label(bidder, "Option bidder"); + + vm.warp(block.timestamp + 2.1 days); + hoax(bidder); + calls.bid{value: 0.1 ether}(optionTokenId); + + assertTrue(calls.currentBid(optionTokenId) == 0.1 ether, "bid should be 0.1 ether"); + assertTrue(calls.currentBidder(optionTokenId) == bidder, "bid should be 0.1 ether"); + } + + function testBidAsOperator() public { + address operator = address(10); + vm.label(operator, "additional token operator"); + + vm.startPrank(address(writer)); + uint256 underlyingTokenId2 = 1; + token.mint(address(writer), underlyingTokenId2); + + // Writer approve operator and covered call + token.setApprovalForAll(operator, true); + token.setApprovalForAll(address(calls), true); + vm.stopPrank(); + + startHoax(operator); + uint32 expiration = uint32(block.timestamp) + 3 days; + + calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); + + vm.warp(block.timestamp + 2.1 days); + calls.bid{value: 0.1 ether}(optionTokenId); + + assertTrue(calls.currentBid(optionTokenId) == 0.1 ether, "bid should be 0.1 ether"); + assertTrue(calls.currentBidder(optionTokenId) == operator, "bid should be 0.1 ether"); + } + + function testNewHighBidReturnOldHighBidTokens() public { + address firstBidder = address(37); + vm.label(firstBidder, "First option bidder"); + vm.deal(address(firstBidder), 1 ether); + + address secondBidder = address(38); + vm.label(secondBidder, "Second option bidder"); + vm.deal(address(secondBidder), 1 ether); + + vm.warp(block.timestamp + 2.1 days); + + vm.prank(firstBidder); + uint256 firstBidderStartBalance = firstBidder.balance; + calls.bid{value: 0.1 ether}(optionTokenId); + + vm.prank(secondBidder); + uint256 secondBidderStartBalance = secondBidder.balance; + calls.bid{value: 0.2 ether}(optionTokenId); + + assertTrue(firstBidder.balance == firstBidderStartBalance, "first bidder should have lower bid returned"); + + assertTrue( + secondBidder.balance + 0.2 ether == secondBidderStartBalance, "first bidder should have lower bid returned" + ); + } + + function testCannotBidBeforeAuctionStart() public { + address bidder = address(37); + vm.label(bidder, "Option bidder"); + + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 1.9 days); + hoax(bidder); + vm.expectRevert("bE-bidding starts on last day"); + calls.bid{value: 0.1 ether}(optionTokenId); + } + + function testCannotBidAfterOptionExpired() public { + address bidder = address(37); + vm.label(bidder, "Option bidder"); + + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 4 days); + hoax(bidder); + vm.expectRevert("bE-expired"); + calls.bid{value: 0.1 ether}(optionTokenId); + } + + function testCannotBidLessThanStrikePrice() public { + address bidder = address(37); + vm.label(bidder, "Option bidder"); + + vm.warp(block.timestamp + 2.1 days); + hoax(bidder); + + /// Option strike price is 1000 wei. + vm.expectRevert("b-bid is lower than the strike price"); + calls.bid{value: 1 wei}(optionTokenId); + } + + function testCannotBidLessThanCurrentBid() public { + address firstBidder = address(37); + vm.label(firstBidder, "First option bidder"); + vm.deal(address(firstBidder), 1 ether); + + address secondBidder = address(38); + vm.label(secondBidder, "Second option bidder"); + vm.deal(address(secondBidder), 1 ether); + + vm.warp(block.timestamp + 2.1 days); + + vm.prank(firstBidder); + calls.bid{value: 0.1 ether}(optionTokenId); + + vm.prank(secondBidder); + vm.expectRevert("b-must overbid by minBidIncrementBips"); + calls.bid{value: 0.09 ether}(optionTokenId); + } + + function testCannotBidLessThanCurrentIncrement() public { + vm.prank(admin); + callInternal.setBidIncrement(1000); // set it to 10% for this test + address firstBidder = address(37); + vm.label(firstBidder, "First option bidder"); + vm.deal(address(firstBidder), 1 ether); + + address secondBidder = address(38); + vm.label(secondBidder, "Second option bidder"); + vm.deal(address(secondBidder), 1 ether); + + vm.warp(block.timestamp + 2.1 days); + + vm.prank(firstBidder); + calls.bid{value: 0.1 ether}(optionTokenId); + + vm.prank(secondBidder); + vm.expectRevert("b-must overbid by minBidIncrementBips"); + calls.bid{value: 0.105 ether}(optionTokenId); + } + + function testCanBidLessMoreThanCurrentIncrement() public { + vm.prank(admin); + callInternal.setBidIncrement(1000); // set it to 10% for this test + address firstBidder = address(37); + vm.label(firstBidder, "First option bidder"); + vm.deal(address(firstBidder), 1 ether); + + address secondBidder = address(38); + vm.label(secondBidder, "Second option bidder"); + vm.deal(address(secondBidder), 1 ether); + + vm.warp(block.timestamp + 2.1 days); + + vm.prank(firstBidder); + calls.bid{value: 0.1 ether}(optionTokenId); + + vm.prank(secondBidder); + calls.bid{value: 0.112 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; + + 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 + 0.1 ether; + + vm.prank(writer); + calls.bid{value: bidAmount}(optionTokenId); + + assertTrue(calls.currentBid(optionTokenId) == 0.1 ether + 0.1 ether, "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"); + } + + function testWriterCanOutbidSelfOnSpread() 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; + + 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 + 0.1 ether; + + vm.prank(writer); + calls.bid{value: bidAmount}(optionTokenId); + + uint256 secondBidAmount = 0.1 ether - strike + 0.2 ether; + vm.prank(writer); + calls.bid{value: secondBidAmount}(optionTokenId); + + assertTrue(calls.currentBid(optionTokenId) == 0.1 ether + 0.2 ether, "high bid should be 0.1 ether + 0.2 ether"); + 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 - secondBidAmount, "writer should have only used 0.1 ether + 0.2 ether to bid" + ); + } } /// Settlement /// contract HookCoveredCallSettleTests is HookProtocolTest { - function setUp() public { - setUpAddresses(); - setUpFullProtocol(); - - // Set buyer balances and give weth - vm.deal(address(buyer), 100 ether); - vm.prank(address(buyer)); - weth.deposit{value: 50 ether}(); - - // Mint underlying token for writer - underlyingTokenId = 0; - token.mint(address(writer), underlyingTokenId); - - setUpMintOption(); - setUpOptionBids(); - } - - function testSettleOption() public { - uint256 buyerStartBalance = buyer.balance; - uint256 writerStartBalance = writer.balance; - - vm.prank(buyer); - calls.settleOption(optionTokenId); - - assertTrue( - buyerStartBalance + (0.2 ether - 1000 wei) == buyer.balance, - "buyer gets the option spread (winning b-strike price" - ); - assertTrue( - writerStartBalance + 1000 wei == writer.balance, - "buyer should have received the option" - ); - } - - function testSettleOption2() public { - uint256 buyerStartBalance = buyer.balance; - uint256 writerStartBalance = writer.balance; - - IHookERC721Vault vault = vaultFactory.getVault( - address(token), - underlyingTokenId - ); - - vm.prank(buyer); - calls.settleOption(optionTokenId); - - assertTrue( - buyerStartBalance + (0.2 ether - 1000 wei) == buyer.balance, - "buyer gets the option spread (winning b-strike price" - ); - assertTrue( - writerStartBalance + 1000 wei == writer.balance, - "buyer should have received the option" - ); - } - - function testCannotSettleOptionNoWinningBid() public { - vm.startPrank(address(writer)); - uint256 underlyingTokenId2 = 1; - token.mint(address(writer), underlyingTokenId2); - - // Writer approve operator and covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); - - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 3.1 days); - vm.expectRevert("s-bid must be won by someone"); - calls.settleOption(optionId); - } - - function testCannotSettleOptionBeforeExpiration() public { - startHoax(address(writer)); - uint256 underlyingTokenId2 = 1; - token.mint(address(writer), underlyingTokenId2); - - // Writer approve operator and covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); - - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 2.1 days); - calls.bid{value: 0.1 ether}(optionId); - - vm.expectRevert("s-option must be expired"); - calls.settleOption(optionId); - } - - function testCannotSettleSettledOption() public { - vm.prank(writer); - calls.settleOption(optionTokenId); - - vm.expectRevert("s-the call cannot already be settled"); - calls.settleOption(optionTokenId); - } - - 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); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); - - // 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); - - vm.stopPrank(); - vm.prank(buyer); - calls.settleOption(optionId); - - 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)" - ); - } - - function testSettleOptionWhenWriterHighBidderAndCallsSettle() 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); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); - - // 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); - vm.stopPrank(); - - vm.prank(buyer); - calls.claimOptionProceeds(optionId); - - 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)" - ); - } - - function testSettleOptionWhenWriterBidFirst() public { - vm.startPrank(writer); - uint256 underlyingTokenId2 = 1; - token.mint(writer, underlyingTokenId2); - vm.deal(writer, 1 ether); - vm.deal(firstBidder, 1 ether); - - uint256 buyerStartBalance = buyer.balance; - uint256 writerStartBalance = writer.balance; - - // Writer approve operator and covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); - - // 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.stopPrank(); - - vm.prank(firstBidder); - calls.bid{value: 2000 wei}(optionId); - - vm.warp(block.timestamp + 1 days); - - vm.prank(buyer); - calls.settleOption(optionId); - - assertTrue( - buyerStartBalance + 1000 wei == buyer.balance, - "buyer gets the spread (2000 wei - 1000 wei strike)" - ); - assertTrue( - writerStartBalance + 1000 wei == writer.balance, - "option writer only gets strike (1000 wei)" - ); - } + function setUp() public { + setUpAddresses(); + setUpFullProtocol(); + + // Set buyer balances and give weth + vm.deal(address(buyer), 100 ether); + vm.prank(address(buyer)); + weth.deposit{value: 50 ether}(); + + // Mint underlying token for writer + underlyingTokenId = 0; + token.mint(address(writer), underlyingTokenId); + + setUpMintOption(); + setUpOptionBids(); + } + + function testSettleOption() public { + uint256 buyerStartBalance = buyer.balance; + uint256 writerStartBalance = writer.balance; + + vm.prank(buyer); + calls.settleOption(optionTokenId); + + assertTrue( + buyerStartBalance + (0.2 ether - 1000 wei) == buyer.balance, + "buyer gets the option spread (winning b-strike price" + ); + assertTrue(writerStartBalance + 1000 wei == writer.balance, "buyer should have received the option"); + } + + function testSettleOption2() public { + uint256 buyerStartBalance = buyer.balance; + uint256 writerStartBalance = writer.balance; + + IHookERC721Vault vault = vaultFactory.getVault(address(token), underlyingTokenId); + + vm.prank(buyer); + calls.settleOption(optionTokenId); + + assertTrue( + buyerStartBalance + (0.2 ether - 1000 wei) == buyer.balance, + "buyer gets the option spread (winning b-strike price" + ); + assertTrue(writerStartBalance + 1000 wei == writer.balance, "buyer should have received the option"); + } + + function testCannotSettleOptionNoWinningBid() public { + vm.startPrank(address(writer)); + uint256 underlyingTokenId2 = 1; + token.mint(address(writer), underlyingTokenId2); + + // Writer approve operator and covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); + + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 3.1 days); + vm.expectRevert("s-bid must be won by someone"); + calls.settleOption(optionId); + } + + function testCannotSettleOptionBeforeExpiration() public { + startHoax(address(writer)); + uint256 underlyingTokenId2 = 1; + token.mint(address(writer), underlyingTokenId2); + + // Writer approve operator and covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); + + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 2.1 days); + calls.bid{value: 0.1 ether}(optionId); + + vm.expectRevert("s-option must be expired"); + calls.settleOption(optionId); + } + + function testCannotSettleSettledOption() public { + vm.prank(writer); + calls.settleOption(optionTokenId); + + vm.expectRevert("s-the call cannot already be settled"); + calls.settleOption(optionTokenId); + } + + 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); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); + + // 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); + + vm.stopPrank(); + vm.prank(buyer); + calls.settleOption(optionId); + + 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)"); + } + + function testSettleOptionWhenWriterHighBidderAndCallsSettle() 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); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); + + // 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); + vm.stopPrank(); + + vm.prank(buyer); + calls.claimOptionProceeds(optionId); + + 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)"); + } + + function testSettleOptionWhenWriterBidFirst() public { + vm.startPrank(writer); + uint256 underlyingTokenId2 = 1; + token.mint(writer, underlyingTokenId2); + vm.deal(writer, 1 ether); + vm.deal(firstBidder, 1 ether); + + uint256 buyerStartBalance = buyer.balance; + uint256 writerStartBalance = writer.balance; + + // Writer approve operator and covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); + + // 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.stopPrank(); + + vm.prank(firstBidder); + calls.bid{value: 2000 wei}(optionId); + + vm.warp(block.timestamp + 1 days); + + vm.prank(buyer); + calls.settleOption(optionId); + + assertTrue(buyerStartBalance + 1000 wei == buyer.balance, "buyer gets the spread (2000 wei - 1000 wei strike)"); + assertTrue(writerStartBalance + 1000 wei == writer.balance, "option writer only gets strike (1000 wei)"); + } + + function testSettleOptionWhenWriterBidLast() public { + vm.startPrank(writer); + uint256 underlyingTokenId2 = 1; + token.mint(writer, underlyingTokenId2); + vm.deal(writer, 1 ether); + vm.deal(firstBidder, 1 ether); + + uint256 buyerStartBalance = buyer.balance; + uint256 writerStartBalance = writer.balance; + + // Writer approve operator and covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); + + // Assume that the writer somehow sold the option NFT to the buyer. + // Outside of the scope of these tests. + calls.safeTransferFrom(writer, buyer, optionId); + + vm.stopPrank(); + + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 2.1 days); + + vm.prank(firstBidder); + calls.bid{value: 1001 wei}(optionId); + + vm.prank(writer); + calls.bid{value: 200 wei}(optionId); + + vm.warp(block.timestamp + 1 days); + + vm.prank(buyer); + calls.settleOption(optionId); + + assertTrue(buyerStartBalance + 200 wei == buyer.balance, "buyer gets the spread (10002 wei - 1000 wei strike)"); + assertTrue(writerStartBalance - 200 wei == writer.balance, "option writer bid on strike"); + } + + function testSettleOptionWhenWriterOutbid() public { + vm.startPrank(writer); + uint256 underlyingTokenId2 = 1; + token.mint(writer, underlyingTokenId2); + vm.deal(writer, 1 ether); + vm.deal(firstBidder, 1 ether); + + uint256 buyerStartBalance = buyer.balance; + uint256 writerStartBalance = writer.balance; + + // Writer approve operator and covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; - function testSettleOptionWhenWriterBidLast() public { - vm.startPrank(writer); - uint256 underlyingTokenId2 = 1; - token.mint(writer, underlyingTokenId2); - vm.deal(writer, 1 ether); - vm.deal(firstBidder, 1 ether); - - uint256 buyerStartBalance = buyer.balance; - uint256 writerStartBalance = writer.balance; - - // Writer approve operator and covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); - - // Assume that the writer somehow sold the option NFT to the buyer. - // Outside of the scope of these tests. - calls.safeTransferFrom(writer, buyer, optionId); - - vm.stopPrank(); + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 2.1 days); - - vm.prank(firstBidder); - calls.bid{value: 1001 wei}(optionId); - - vm.prank(writer); - calls.bid{value: 200 wei}(optionId); - - vm.warp(block.timestamp + 1 days); - - vm.prank(buyer); - calls.settleOption(optionId); - - assertTrue( - buyerStartBalance + 200 wei == buyer.balance, - "buyer gets the spread (10002 wei - 1000 wei strike)" - ); - assertTrue( - writerStartBalance - 200 wei == writer.balance, - "option writer bid on strike" - ); - } + // Assume that the writer somehow sold the option NFT to the buyer. + // Outside of the scope of these tests. + calls.safeTransferFrom(writer, buyer, optionId); - function testSettleOptionWhenWriterOutbid() public { - vm.startPrank(writer); - uint256 underlyingTokenId2 = 1; - token.mint(writer, underlyingTokenId2); - vm.deal(writer, 1 ether); - vm.deal(firstBidder, 1 ether); + vm.stopPrank(); - uint256 buyerStartBalance = buyer.balance; - uint256 writerStartBalance = writer.balance; + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 2.1 days); - // Writer approve operator and covered call - token.setApprovalForAll(address(calls), true); + vm.prank(firstBidder); + calls.bid{value: 1001 wei}(optionId); - uint32 expiration = uint32(block.timestamp) + 3 days; - - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); + vm.prank(writer); + calls.bid{value: 200 wei}(optionId); - // Assume that the writer somehow sold the option NFT to the buyer. - // Outside of the scope of these tests. - calls.safeTransferFrom(writer, buyer, optionId); + vm.prank(firstBidder); + calls.bid{value: 1300 wei}(optionId); - vm.stopPrank(); + vm.warp(block.timestamp + 1 days); - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 2.1 days); - - vm.prank(firstBidder); - calls.bid{value: 1001 wei}(optionId); - - vm.prank(writer); - calls.bid{value: 200 wei}(optionId); - - vm.prank(firstBidder); - calls.bid{value: 1300 wei}(optionId); - - vm.warp(block.timestamp + 1 days); - - vm.prank(buyer); - calls.settleOption(optionId); + vm.prank(buyer); + calls.settleOption(optionId); - assertTrue( - buyerStartBalance + 300 wei == buyer.balance, - "buyer gets the spread (10002 wei - 1000 wei strike)" - ); - assertTrue( - writerStartBalance + 1000 == writer.balance, - "option writer gets strike (1000 wei)" - ); - } + assertTrue(buyerStartBalance + 300 wei == buyer.balance, "buyer gets the spread (10002 wei - 1000 wei strike)"); + assertTrue(writerStartBalance + 1000 == writer.balance, "option writer gets strike (1000 wei)"); + } } /// Reclaiming /// contract HookCoveredCallReclaimTests is HookProtocolTest { - function setUp() public { - setUpAddresses(); - setUpFullProtocol(); + function setUp() public { + setUpAddresses(); + setUpFullProtocol(); - // Set buyer balances and give weth - vm.deal(address(buyer), 100 ether); - vm.prank(address(buyer)); - weth.deposit{value: 50 ether}(); + // Set buyer balances and give weth + vm.deal(address(buyer), 100 ether); + vm.prank(address(buyer)); + weth.deposit{value: 50 ether}(); - // Mint underlying token for writer - underlyingTokenId = 0; - token.mint(address(writer), underlyingTokenId); + // Mint underlying token for writer + underlyingTokenId = 0; + token.mint(address(writer), underlyingTokenId); - setUpMintOption(); + setUpMintOption(); - // Transfer option NFT from buyer back to the writer - // Writer needs to own the option NFT for reclaimAsset - vm.prank(address(buyer)); - calls.safeTransferFrom(buyer, writer, optionTokenId); - } + // Transfer option NFT from buyer back to the writer + // Writer needs to own the option NFT for reclaimAsset + vm.prank(address(buyer)); + calls.safeTransferFrom(buyer, writer, optionTokenId); + } - function testReclaimAsset() public { - // Option expires in 3 days from current block - vm.warp(block.timestamp + 2.1 days); - vm.prank(writer); - calls.reclaimAsset(optionTokenId, false); - } + function testReclaimAsset() public { + // Option expires in 3 days from current block + vm.warp(block.timestamp + 2.1 days); + vm.prank(writer); + calls.reclaimAsset(optionTokenId, false); + } - function testReclaimAssetReturnNft() public { - // Option expires in 3 days from current block - vm.warp(block.timestamp + 2.1 days); + function testReclaimAssetReturnNft() public { + // Option expires in 3 days from current block + vm.warp(block.timestamp + 2.1 days); - vm.startPrank(writer); + vm.startPrank(writer); - vm.expectEmit(true, false, false, false); - emit CallReclaimed(optionTokenId); - calls.reclaimAsset(optionTokenId, true); - } + vm.expectEmit(true, false, false, false); + emit CallReclaimed(optionTokenId); + calls.reclaimAsset(optionTokenId, true); + } - function testCannotReclaimAssetAsNonCallWriter() public { - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 3.1 days); + function testCannotReclaimAssetAsNonCallWriter() public { + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 3.1 days); - vm.startPrank(buyer); + vm.startPrank(buyer); - vm.expectRevert("rA-only writer"); - calls.reclaimAsset(optionTokenId, true); - } + vm.expectRevert("rA-only writer"); + calls.reclaimAsset(optionTokenId, true); + } - function testCannotReclaimFromSettledOption() public { - setUpOptionBids(); + function testCannotReclaimFromSettledOption() public { + setUpOptionBids(); - vm.startPrank(writer); - calls.settleOption(optionTokenId); + vm.startPrank(writer); + calls.settleOption(optionTokenId); - vm.expectRevert("rA-option settled"); - calls.reclaimAsset(optionTokenId, true); - } + vm.expectRevert("rA-option settled"); + calls.reclaimAsset(optionTokenId, true); + } - function testReclaimWithActiveBid() public { - vm.warp(block.timestamp + 2.1 days); - vm.deal(address(firstBidder), 1 ether); + function testReclaimWithActiveBid() public { + vm.warp(block.timestamp + 2.1 days); + vm.deal(address(firstBidder), 1 ether); - vm.prank(firstBidder); + vm.prank(firstBidder); - calls.bid{value: 0.1 ether}(optionTokenId); + calls.bid{value: 0.1 ether}(optionTokenId); - vm.startPrank(writer); + vm.startPrank(writer); - vm.expectEmit(true, false, false, false); - emit CallReclaimed(optionTokenId); - calls.reclaimAsset(optionTokenId, true); + vm.expectEmit(true, false, false, false); + emit CallReclaimed(optionTokenId); + calls.reclaimAsset(optionTokenId, true); - assertTrue( - token.ownerOf(0) == address(writer), - "writer should own the underlying asset" - ); - } + assertTrue(token.ownerOf(0) == address(writer), "writer should own the underlying asset"); + } - function testReclaimWithActiveBidWriterHighBidder() public { - vm.warp(block.timestamp + 2.1 days); - vm.deal(address(firstBidder), 1 ether); - vm.deal(address(writer), 1 ether); + function testReclaimWithActiveBidWriterHighBidder() public { + vm.warp(block.timestamp + 2.1 days); + vm.deal(address(firstBidder), 1 ether); + vm.deal(address(writer), 1 ether); - vm.prank(firstBidder); - calls.bid{value: 0.1 ether}(optionTokenId); + vm.prank(firstBidder); + calls.bid{value: 0.1 ether}(optionTokenId); - vm.prank(writer); - calls.bid{value: 0.2 ether}(optionTokenId); + vm.prank(writer); + calls.bid{value: 0.2 ether}(optionTokenId); - uint256 writerPostBidBalance = writer.balance; + uint256 writerPostBidBalance = writer.balance; - vm.startPrank(writer); - vm.expectEmit(true, false, false, false); - emit CallReclaimed(optionTokenId); - calls.reclaimAsset(optionTokenId, true); + vm.startPrank(writer); + vm.expectEmit(true, false, false, false); + emit CallReclaimed(optionTokenId); + calls.reclaimAsset(optionTokenId, true); - assertTrue( - token.ownerOf(0) == address(writer), - "writer should own the underlying asset" - ); + assertTrue(token.ownerOf(0) == address(writer), "writer should own the underlying asset"); - assertTrue( - (writerPostBidBalance + 0.2 ether) == writer.balance, - "writer should have have bid returned post reclaim" - ); - } + assertTrue( + (writerPostBidBalance + 0.2 ether) == writer.balance, "writer should have have bid returned post reclaim" + ); + } - function testCannotReclaimAfterExpiration() public { - vm.startPrank(writer); - vm.warp(block.timestamp + 3.1 days); + function testCannotReclaimAfterExpiration() public { + vm.startPrank(writer); + vm.warp(block.timestamp + 3.1 days); - vm.expectRevert("rA-option expired"); - calls.reclaimAsset(optionTokenId, true); - } + vm.expectRevert("rA-option expired"); + calls.reclaimAsset(optionTokenId, true); + } - function testReclaimAssetWriterBidFirst() public { - vm.startPrank(writer); - uint256 underlyingTokenId2 = 1; - token.mint(writer, underlyingTokenId2); - vm.deal(writer, 1 ether); - vm.deal(firstBidder, 1 ether); + function testReclaimAssetWriterBidFirst() public { + vm.startPrank(writer); + uint256 underlyingTokenId2 = 1; + token.mint(writer, underlyingTokenId2); + vm.deal(writer, 1 ether); + vm.deal(firstBidder, 1 ether); - // Writer approve operator and covered call - token.setApprovalForAll(address(calls), true); + // Writer approve operator and covered call + token.setApprovalForAll(address(calls), true); - uint32 expiration = uint32(block.timestamp) + 3 days; + uint32 expiration = uint32(block.timestamp) + 3 days; - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 2.1 days); + // 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.stopPrank(); + calls.bid{value: 1 wei}(optionId); + vm.stopPrank(); - vm.prank(firstBidder); - calls.bid{value: 2000 wei}(optionId); + vm.prank(firstBidder); + calls.bid{value: 2000 wei}(optionId); - vm.startPrank(writer); + vm.startPrank(writer); - vm.expectEmit(true, false, false, false); - emit CallReclaimed(optionId); - calls.reclaimAsset(optionId, true); - } + vm.expectEmit(true, false, false, false); + emit CallReclaimed(optionId); + calls.reclaimAsset(optionId, true); + } - function testReclaimAssetWriterBidLast() public { - vm.startPrank(writer); - uint256 underlyingTokenId2 = 1; - token.mint(writer, underlyingTokenId2); - vm.deal(writer, 1 ether); - vm.deal(firstBidder, 1 ether); + function testReclaimAssetWriterBidLast() public { + vm.startPrank(writer); + uint256 underlyingTokenId2 = 1; + token.mint(writer, underlyingTokenId2); + vm.deal(writer, 1 ether); + vm.deal(firstBidder, 1 ether); - // Writer approve operator and covered call - token.setApprovalForAll(address(calls), true); + // Writer approve operator and covered call + token.setApprovalForAll(address(calls), true); - uint32 expiration = uint32(block.timestamp) + 3 days; + uint32 expiration = uint32(block.timestamp) + 3 days; - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); - vm.stopPrank(); + vm.stopPrank(); - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 2.1 days); + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 2.1 days); - vm.prank(firstBidder); - calls.bid{value: 1001 wei}(optionId); + vm.prank(firstBidder); + calls.bid{value: 1001 wei}(optionId); - vm.prank(writer); - calls.bid{value: 200 wei}(optionId); + vm.prank(writer); + calls.bid{value: 200 wei}(optionId); - vm.startPrank(writer); + vm.startPrank(writer); - vm.expectEmit(true, false, false, false); - emit CallReclaimed(optionId); - calls.reclaimAsset(optionId, true); - } + vm.expectEmit(true, false, false, false); + emit CallReclaimed(optionId); + calls.reclaimAsset(optionId, true); + } - function testReclaimAssetWriterBidMultiple() public { - vm.startPrank(writer); - uint256 underlyingTokenId2 = 1; - token.mint(writer, underlyingTokenId2); - vm.deal(writer, 1 ether); - vm.deal(firstBidder, 1 ether); + function testReclaimAssetWriterBidMultiple() public { + vm.startPrank(writer); + uint256 underlyingTokenId2 = 1; + token.mint(writer, underlyingTokenId2); + vm.deal(writer, 1 ether); + vm.deal(firstBidder, 1 ether); - // Writer approve operator and covered call - token.setApprovalForAll(address(calls), true); + // Writer approve operator and covered call + token.setApprovalForAll(address(calls), true); - uint32 expiration = uint32(block.timestamp) + 3 days; + uint32 expiration = uint32(block.timestamp) + 3 days; - uint256 optionId = calls.mintWithErc721( - address(token), - underlyingTokenId2, - 1000, - expiration - ); + uint256 optionId = calls.mintWithErc721(address(token), underlyingTokenId2, 1000, expiration); - vm.stopPrank(); + vm.stopPrank(); - // Option expires in 3 days from current block; bidding starts in 2 days. - vm.warp(block.timestamp + 2.1 days); + // Option expires in 3 days from current block; bidding starts in 2 days. + vm.warp(block.timestamp + 2.1 days); - vm.prank(firstBidder); - calls.bid{value: 1001 wei}(optionId); + vm.prank(firstBidder); + calls.bid{value: 1001 wei}(optionId); - vm.prank(writer); - calls.bid{value: 200 wei}(optionId); + vm.prank(writer); + calls.bid{value: 200 wei}(optionId); - vm.prank(firstBidder); - calls.bid{value: 1300 wei}(optionId); + vm.prank(firstBidder); + calls.bid{value: 1300 wei}(optionId); - vm.startPrank(writer); + vm.startPrank(writer); - vm.expectEmit(true, false, false, false); - emit CallReclaimed(optionId); - calls.reclaimAsset(optionId, true); - } + vm.expectEmit(true, false, false, false); + emit CallReclaimed(optionId); + calls.reclaimAsset(optionId, true); + } } diff --git a/src/test/HookMultiVaultTests.t.sol b/src/test/HookMultiVaultTests.t.sol index bd851d7..93f4cb2 100644 --- a/src/test/HookMultiVaultTests.t.sol +++ b/src/test/HookMultiVaultTests.t.sol @@ -15,713 +15,470 @@ import "./utils/mocks/FlashLoan.sol"; /// @notice Unit tests for the Hook Multi Vault /// @author Regynald Augustin-regy@hook.xyz contract HookMultiVaultTests is HookProtocolTest { - IHookERC721VaultFactory vault; - uint256 tokenStartIndex = 300; - - function setUp() public { - setUpAddresses(); - setUpFullProtocol(); - vault = IHookERC721VaultFactory(protocol.vaultContract()); - } - - function createVaultandAsset() internal returns (address, uint32) { - vm.startPrank(admin); - tokenStartIndex += 1; - uint32 tokenId = uint32(tokenStartIndex); - token.mint(address(writer), tokenId); - vault.makeMultiVault(address(token)); - address vaultAddress = address( - vault.findOrCreateVault(address(token), tokenId) - ); - vm.stopPrank(); - return (vaultAddress, tokenId); - } - - function makeEntitlementAndSignature( - uint256 ownerPkey, - address operator, - address vaultAddress, - uint256 tokenId, - uint32 _expiry - ) - internal - returns (Entitlements.Entitlement memory, Signatures.Signature memory) - { - address ownerAdd = vm.addr(writerpkey); - - Entitlements.Entitlement memory entitlement = Entitlements.Entitlement({ - beneficialOwner: ownerAdd, - operator: operator, - vaultAddress: vaultAddress, - assetId: uint32(tokenId), - expiry: _expiry - }); - - bytes32 structHash = Entitlements.getEntitlementStructHash(entitlement); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - ownerPkey, - _getEIP712Hash(structHash) - ); - - Signatures.Signature memory sig = Signatures.Signature({ - signatureType: Signatures.SignatureType.EIP712, - v: v, - r: r, - s: s - }); - return (entitlement, sig); - } - - function testImposeEntitlmentOnTransferIn() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - assertTrue( - vaultImpl.getHoldsAsset(tokenId), - "the token should be owned by the vault" - ); - assertTrue( - vaultImpl.getBeneficialOwner(tokenId) == writer, - "writer should be the beneficial owner" - ); - (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator( - tokenId - ); - assertTrue(active, "there should be an active entitlement"); - assertTrue( - operator == mockContract, - "active entitlement is to correct person" - ); - } - - function testBasicFlashLoan() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanSuccess(); - - vm.prank(writer); - vaultImpl.flashLoan(tokenId, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testFlashLoanFailsIfDisabled() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanSuccess(); - vm.prank(admin); - protocol.setCollectionConfig( - address(token), - keccak256("vault.flashLoanDisabled"), - true - ); - vm.prank(writer); - vm.expectRevert("flashLoan-flashLoan feature disabled for this contract"); - vaultImpl.flashLoan(tokenId, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testBasicFlashLoanAlternateApprove() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanApproveForAll(); - - vm.prank(writer); - vaultImpl.flashLoan(tokenId, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testBasicFlashCantReturnFalse() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanReturnsFalse(); - - vm.prank(writer); - vm.expectRevert("flashLoan-the flash loan contract must return true"); - vaultImpl.flashLoan(tokenId, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testBasicFlashMustApprove() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanDoesNotApprove(); - - vm.prank(writer); - vm.expectRevert("ERC721: transfer caller is not owner nor approved"); - vaultImpl.flashLoan(tokenId, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testBasicFlashCantBurn() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanBurnsAsset(); - - vm.prank(writer); - vm.expectRevert("ERC721: operator query for nonexistent token"); - vaultImpl.flashLoan(tokenId, address(flashLoan), " "); - // operation reverted, so we can still mess with the asset - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testFlashCallData() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanVerifyCalldata(); - - vm.prank(writer); - vaultImpl.flashLoan(tokenId, address(flashLoan), "hello world"); - // operation reverted, so we can still mess with the asset - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testFlashWillRevert() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanVerifyCalldata(); - - vm.prank(writer); - vm.expectRevert("should check helloworld"); - vaultImpl.flashLoan(tokenId, address(flashLoan), "hello world wrong!"); - // operation reverted, so we can still mess with the asset - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testImposeEntitlementAfterInitialTransfer() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - ( - Entitlements.Entitlement memory entitlement, - Signatures.Signature memory sig - ) = makeEntitlementAndSignature( - writerpkey, - mockContract, - vaultAddress, - tokenId, - expiration - ); - - vm.prank(writer); - - token.safeTransferFrom(writer, vaultAddress, tokenId); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - - // impose the entitlement onto the vault - vm.prank(mockContract); - vaultImpl.imposeEntitlement( - entitlement.operator, - uint32(entitlement.expiry), - uint32(entitlement.assetId), - sig.v, - sig.r, - sig.s - ); - - assertTrue( - vaultImpl.getHoldsAsset(tokenId), - "the token should be owned by the vault" - ); - assertTrue( - vaultImpl.getBeneficialOwner(tokenId) == writer, - "writer should be the beneficial owner" - ); - (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator( - tokenId - ); - assertTrue(active, "there should be an active entitlement"); - assertTrue( - operator == mockContract, - "active entitlement is to correct person" - ); - - // verify that beneficial owner cannot withdrawal - // during an active entitlement. - vm.expectRevert( - "withdrawalAsset-the asset cannot be withdrawn with an active entitlement" - ); - vm.prank(writer); - vaultImpl.withdrawalAsset(tokenId); - } - - function testEntitlementGoesAwayAfterExpiration() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - - (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator( - tokenId - ); - assertTrue(active, "there should be an active entitlement"); - assertTrue( - operator == mockContract, - "active entitlement is to correct person" - ); - vm.warp(block.timestamp + 2 days); - - (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); - assertTrue(!active, "there should not be an active entitlement"); - - vm.prank(writer); - vaultImpl.withdrawalAsset(tokenId); - assertTrue( - !vaultImpl.getHoldsAsset(tokenId), - "the token should not be owned by the vault" - ); - - assertTrue( - token.ownerOf(tokenId) == writer, - "token should be owned by the writer" - ); - } - - function testEntitlementCanBeClearedByOperator() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - - vm.prank(mockContract); - vaultImpl.clearEntitlement(tokenId); - - (bool active, ) = vaultImpl.getCurrentEntitlementOperator(tokenId); - assertTrue(!active, "there should not be an active entitlement"); - - // check that the owner can actually withdrawl - vm.prank(writer); - vaultImpl.withdrawalAsset(tokenId); - assertTrue( - !vaultImpl.getHoldsAsset(tokenId), - "the token should not be owned by the vault" - ); - - assertTrue( - token.ownerOf(tokenId) == writer, - "token should be owned by the writer" - ); - } - - function testNewEntitlementPossibleAferExpiredEntitlement() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - - (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator( - tokenId - ); - assertTrue(active, "there should be an active entitlement"); - - vm.warp(block.timestamp + 2 days); - - (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); - assertTrue(!active, "there should not be an active entitlement"); - - // asset is not withdrawn, try to add a new entitlement - uint32 expiration2 = uint32(block.timestamp + 10 days); - - ( - Entitlements.Entitlement memory entitlement2, - Signatures.Signature memory sig2 - ) = makeEntitlementAndSignature( - writerpkey, - mockContract, - vaultAddress, - tokenId, - expiration2 - ); - vaultImpl.imposeEntitlement( - entitlement2.operator, - uint32(entitlement2.expiry), - uint32(entitlement2.assetId), - sig2.v, - sig2.r, - sig2.s - ); - (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); - assertTrue(active, "there should be an active entitlement"); - } - - function testNewEntitlementPossibleAfterClearedEntitlement() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - - (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator( - tokenId - ); - assertTrue(active, "there should be an active entitlement"); - vm.prank(mockContract); - vaultImpl.clearEntitlement(tokenId); - - (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); - assertTrue(!active, "there should not be an active entitlement"); - - uint32 expiration2 = uint32(block.timestamp + 3 days); - - ( - Entitlements.Entitlement memory entitlement2, - Signatures.Signature memory sig2 - ) = makeEntitlementAndSignature( - writerpkey, - mockContract, - vaultAddress, - tokenId, - expiration2 - ); - - vaultImpl.imposeEntitlement( - entitlement2.operator, - uint32(entitlement2.expiry), - uint32(entitlement2.assetId), - sig2.v, - sig2.r, - sig2.s - ); - (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); - assertTrue(active, "there should be an active entitlement"); - } - - function testOnlyOneEntitlementAllowed() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(3333); - uint32 expiration = uint32(block.timestamp) + 1 days; - - // transfer in with first entitlement - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - - address mockContract2 = address(35553445); - (bool active, ) = vaultImpl.getCurrentEntitlementOperator(tokenId); - assertTrue(active, "there should be an active entitlement"); - - uint32 expiration2 = uint32(block.timestamp + 3 days); - - ( - Entitlements.Entitlement memory entitlement2, - Signatures.Signature memory sig2 - ) = makeEntitlementAndSignature( - writerpkey, - mockContract2, - vaultAddress, - tokenId, - expiration2 - ); - - vm.prank(mockContract2); - vm.expectRevert( - "_registerEntitlement-existing entitlement must be cleared before registering a new one" - ); - - vaultImpl.imposeEntitlement( - entitlement2.operator, - uint32(entitlement2.expiry), - uint32(entitlement2.assetId), - sig2.v, - sig2.r, - sig2.s - ); - } - - function testBeneficialOwnerCannotClearEntitlement() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69420); - uint32 expiration = uint32(block.timestamp) + 1 days; - // transfer in with first entitlement - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - - (bool active, ) = vaultImpl.getCurrentEntitlementOperator(tokenId); - assertTrue(active, "there should be an active entitlement"); - - vm.prank(writer); - vm.expectRevert( - "clearEntitlement-only the entitled address can clear the entitlement" - ); - vaultImpl.clearEntitlement(tokenId); - - vm.prank(address(55566677788899911)); - vm.expectRevert( - "clearEntitlement-only the entitled address can clear the entitlement" - ); - vaultImpl.clearEntitlement(tokenId); - } - - function testClearAndDistributeReturnsNFT2() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - vaultImpl.getBeneficialOwner(tokenId); - vm.prank(mockContract); - vaultImpl.clearEntitlementAndDistribute(tokenId, writer); - - (bool active, ) = vaultImpl.getCurrentEntitlementOperator(tokenId); - assertTrue(!active, "there should not be an active entitlement"); - - assertTrue( - token.ownerOf(tokenId) == writer, - "Token should be returned to the owner" - ); - } - - function testAirdropsCanBeDisbled() public { - (address vaultAddress, ) = createVaultandAsset(); - - vm.prank(admin); - protocol.setCollectionConfig( - address(token), - keccak256("vault.airdropsProhibited"), - true - ); - - TestERC721 token2 = new TestERC721(); - vm.expectRevert( - "onERC721Received-non-escrow asset returned when airdrops are disabled" - ); - token2.mint(vaultAddress, 0); - } - - function testAirdropsAllowedWhenEnabled() public { - (address vaultAddress, ) = createVaultandAsset(); - - vm.prank(admin); - protocol.setCollectionConfig( - address(token), - keccak256("vault.multiAirdropsAllowed"), - true - ); - - TestERC721 token2 = new TestERC721(); - token2.mint(vaultAddress, 0); - assertTrue(token2.ownerOf(0) == vaultAddress, "vault should hold airdrop"); - } - - function testClearAndDistributeDoesNotReturnToWrongPerson() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); - - vm.expectRevert( - "clearEntitlementAndDistribute-Only the beneficial owner can receive the asset" - ); - vm.prank(mockContract); - vaultImpl.clearEntitlementAndDistribute(0, address(0x033333344545)); - } + IHookERC721VaultFactory vault; + uint256 tokenStartIndex = 300; + + function setUp() public { + setUpAddresses(); + setUpFullProtocol(); + vault = IHookERC721VaultFactory(protocol.vaultContract()); + } + + function createVaultandAsset() internal returns (address, uint32) { + vm.startPrank(admin); + tokenStartIndex += 1; + uint32 tokenId = uint32(tokenStartIndex); + token.mint(address(writer), tokenId); + vault.makeMultiVault(address(token)); + address vaultAddress = address(vault.findOrCreateVault(address(token), tokenId)); + vm.stopPrank(); + return (vaultAddress, tokenId); + } + + function makeEntitlementAndSignature( + uint256 ownerPkey, + address operator, + address vaultAddress, + uint256 tokenId, + uint32 _expiry + ) internal returns (Entitlements.Entitlement memory, Signatures.Signature memory) { + address ownerAdd = vm.addr(writerpkey); + + Entitlements.Entitlement memory entitlement = Entitlements.Entitlement({ + beneficialOwner: ownerAdd, + operator: operator, + vaultAddress: vaultAddress, + assetId: uint32(tokenId), + expiry: _expiry + }); + + bytes32 structHash = Entitlements.getEntitlementStructHash(entitlement); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPkey, _getEIP712Hash(structHash)); + + Signatures.Signature memory sig = + Signatures.Signature({signatureType: Signatures.SignatureType.EIP712, v: v, r: r, s: s}); + return (entitlement, sig); + } + + function testImposeEntitlmentOnTransferIn() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + assertTrue(vaultImpl.getHoldsAsset(tokenId), "the token should be owned by the vault"); + assertTrue(vaultImpl.getBeneficialOwner(tokenId) == writer, "writer should be the beneficial owner"); + (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(active, "there should be an active entitlement"); + assertTrue(operator == mockContract, "active entitlement is to correct person"); + } + + function testBasicFlashLoan() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanSuccess(); + + vm.prank(writer); + vaultImpl.flashLoan(tokenId, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testFlashLoanFailsIfDisabled() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanSuccess(); + vm.prank(admin); + protocol.setCollectionConfig(address(token), keccak256("vault.flashLoanDisabled"), true); + vm.prank(writer); + vm.expectRevert("flashLoan-flashLoan feature disabled for this contract"); + vaultImpl.flashLoan(tokenId, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testBasicFlashLoanAlternateApprove() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanApproveForAll(); + + vm.prank(writer); + vaultImpl.flashLoan(tokenId, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testBasicFlashCantReturnFalse() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanReturnsFalse(); + + vm.prank(writer); + vm.expectRevert("flashLoan-the flash loan contract must return true"); + vaultImpl.flashLoan(tokenId, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testBasicFlashMustApprove() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanDoesNotApprove(); + + vm.prank(writer); + vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + vaultImpl.flashLoan(tokenId, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testBasicFlashCantBurn() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanBurnsAsset(); + + vm.prank(writer); + vm.expectRevert("ERC721: operator query for nonexistent token"); + vaultImpl.flashLoan(tokenId, address(flashLoan), " "); + // operation reverted, so we can still mess with the asset + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testFlashCallData() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanVerifyCalldata(); + + vm.prank(writer); + vaultImpl.flashLoan(tokenId, address(flashLoan), "hello world"); + // operation reverted, so we can still mess with the asset + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testFlashWillRevert() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanVerifyCalldata(); + + vm.prank(writer); + vm.expectRevert("should check helloworld"); + vaultImpl.flashLoan(tokenId, address(flashLoan), "hello world wrong!"); + // operation reverted, so we can still mess with the asset + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testImposeEntitlementAfterInitialTransfer() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + (Entitlements.Entitlement memory entitlement, Signatures.Signature memory sig) = + makeEntitlementAndSignature(writerpkey, mockContract, vaultAddress, tokenId, expiration); + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + + // impose the entitlement onto the vault + vm.prank(mockContract); + vaultImpl.imposeEntitlement( + entitlement.operator, uint32(entitlement.expiry), uint32(entitlement.assetId), sig.v, sig.r, sig.s + ); + + assertTrue(vaultImpl.getHoldsAsset(tokenId), "the token should be owned by the vault"); + assertTrue(vaultImpl.getBeneficialOwner(tokenId) == writer, "writer should be the beneficial owner"); + (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(active, "there should be an active entitlement"); + assertTrue(operator == mockContract, "active entitlement is to correct person"); + + // verify that beneficial owner cannot withdrawal + // during an active entitlement. + vm.expectRevert("withdrawalAsset-the asset cannot be withdrawn with an active entitlement"); + vm.prank(writer); + vaultImpl.withdrawalAsset(tokenId); + } + + function testEntitlementGoesAwayAfterExpiration() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + + (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(active, "there should be an active entitlement"); + assertTrue(operator == mockContract, "active entitlement is to correct person"); + vm.warp(block.timestamp + 2 days); + + (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(!active, "there should not be an active entitlement"); + + vm.prank(writer); + vaultImpl.withdrawalAsset(tokenId); + assertTrue(!vaultImpl.getHoldsAsset(tokenId), "the token should not be owned by the vault"); + + assertTrue(token.ownerOf(tokenId) == writer, "token should be owned by the writer"); + } + + function testEntitlementCanBeClearedByOperator() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + + vm.prank(mockContract); + vaultImpl.clearEntitlement(tokenId); + + (bool active,) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(!active, "there should not be an active entitlement"); + + // check that the owner can actually withdrawl + vm.prank(writer); + vaultImpl.withdrawalAsset(tokenId); + assertTrue(!vaultImpl.getHoldsAsset(tokenId), "the token should not be owned by the vault"); + + assertTrue(token.ownerOf(tokenId) == writer, "token should be owned by the writer"); + } + + function testNewEntitlementPossibleAferExpiredEntitlement() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + + (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(active, "there should be an active entitlement"); + + vm.warp(block.timestamp + 2 days); + + (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(!active, "there should not be an active entitlement"); + + // asset is not withdrawn, try to add a new entitlement + uint32 expiration2 = uint32(block.timestamp + 10 days); + + (Entitlements.Entitlement memory entitlement2, Signatures.Signature memory sig2) = + makeEntitlementAndSignature(writerpkey, mockContract, vaultAddress, tokenId, expiration2); + vaultImpl.imposeEntitlement( + entitlement2.operator, uint32(entitlement2.expiry), uint32(entitlement2.assetId), sig2.v, sig2.r, sig2.s + ); + (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(active, "there should be an active entitlement"); + } + + function testNewEntitlementPossibleAfterClearedEntitlement() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + + (bool active, address operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(active, "there should be an active entitlement"); + vm.prank(mockContract); + vaultImpl.clearEntitlement(tokenId); + + (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(!active, "there should not be an active entitlement"); + + uint32 expiration2 = uint32(block.timestamp + 3 days); + + (Entitlements.Entitlement memory entitlement2, Signatures.Signature memory sig2) = + makeEntitlementAndSignature(writerpkey, mockContract, vaultAddress, tokenId, expiration2); + + vaultImpl.imposeEntitlement( + entitlement2.operator, uint32(entitlement2.expiry), uint32(entitlement2.assetId), sig2.v, sig2.r, sig2.s + ); + (active, operator) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(active, "there should be an active entitlement"); + } + + function testOnlyOneEntitlementAllowed() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(3333); + uint32 expiration = uint32(block.timestamp) + 1 days; + + // transfer in with first entitlement + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + + address mockContract2 = address(35553445); + (bool active,) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(active, "there should be an active entitlement"); + + uint32 expiration2 = uint32(block.timestamp + 3 days); + + (Entitlements.Entitlement memory entitlement2, Signatures.Signature memory sig2) = + makeEntitlementAndSignature(writerpkey, mockContract2, vaultAddress, tokenId, expiration2); + + vm.prank(mockContract2); + vm.expectRevert("_registerEntitlement-existing entitlement must be cleared before registering a new one"); + + vaultImpl.imposeEntitlement( + entitlement2.operator, uint32(entitlement2.expiry), uint32(entitlement2.assetId), sig2.v, sig2.r, sig2.s + ); + } + + function testBeneficialOwnerCannotClearEntitlement() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69420); + uint32 expiration = uint32(block.timestamp) + 1 days; + // transfer in with first entitlement + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + + (bool active,) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(active, "there should be an active entitlement"); + + vm.prank(writer); + vm.expectRevert("clearEntitlement-only the entitled address can clear the entitlement"); + vaultImpl.clearEntitlement(tokenId); + + vm.prank(address(55566677788899911)); + vm.expectRevert("clearEntitlement-only the entitled address can clear the entitlement"); + vaultImpl.clearEntitlement(tokenId); + } + + function testClearAndDistributeReturnsNFT2() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + vaultImpl.getBeneficialOwner(tokenId); + vm.prank(mockContract); + vaultImpl.clearEntitlementAndDistribute(tokenId, writer); + + (bool active,) = vaultImpl.getCurrentEntitlementOperator(tokenId); + assertTrue(!active, "there should not be an active entitlement"); + + assertTrue(token.ownerOf(tokenId) == writer, "Token should be returned to the owner"); + } + + function testAirdropsCanBeDisbled() public { + (address vaultAddress,) = createVaultandAsset(); + + vm.prank(admin); + protocol.setCollectionConfig(address(token), keccak256("vault.airdropsProhibited"), true); + + TestERC721 token2 = new TestERC721(); + vm.expectRevert("onERC721Received-non-escrow asset returned when airdrops are disabled"); + token2.mint(vaultAddress, 0); + } + + function testAirdropsAllowedWhenEnabled() public { + (address vaultAddress,) = createVaultandAsset(); + + vm.prank(admin); + protocol.setCollectionConfig(address(token), keccak256("vault.multiAirdropsAllowed"), true); + + TestERC721 token2 = new TestERC721(); + token2.mint(vaultAddress, 0); + assertTrue(token2.ownerOf(0) == vaultAddress, "vault should hold airdrop"); + } + + function testClearAndDistributeDoesNotReturnToWrongPerson() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + IHookERC721Vault vaultImpl = IHookERC721Vault(vaultAddress); + + vm.expectRevert("clearEntitlementAndDistribute-Only the beneficial owner can receive the asset"); + vm.prank(mockContract); + vaultImpl.clearEntitlementAndDistribute(0, address(0x033333344545)); + } } diff --git a/src/test/HookVaultTests.t.sol b/src/test/HookVaultTests.t.sol index 0175200..eb94f5d 100644 --- a/src/test/HookVaultTests.t.sol +++ b/src/test/HookVaultTests.t.sol @@ -15,724 +15,460 @@ import "./utils/mocks/FlashLoan.sol"; /// @notice Integration tests for the Hook Solo Vault /// @author Regynald Augustin-regy@hook.xyz contract HookVaultTestsBase is HookProtocolTest { - IHookERC721VaultFactory vault; - uint32 tokenStartIndex = 300; - - function setUp() public { - setUpAddresses(); - setUpFullProtocol(); - vault = IHookERC721VaultFactory(protocol.vaultContract()); - } - - function createVaultandAsset() internal returns (address, uint32) { - vm.startPrank(admin); - tokenStartIndex += 1; - uint32 tokenId = tokenStartIndex; - token.mint(address(writer), tokenId); - address vaultAddress = address( - vault.findOrCreateVault(address(token), tokenId) - ); - vm.stopPrank(); - return (vaultAddress, tokenId); - } - - function makeEntitlementAndSignature( - uint256 ownerPkey, - address operator, - address vaultAddress, - uint32 _expiry - ) - internal - returns (Entitlements.Entitlement memory, Signatures.Signature memory) - { - address ownerAdd = vm.addr(writerpkey); - - Entitlements.Entitlement memory entitlement = Entitlements.Entitlement({ - beneficialOwner: ownerAdd, - operator: operator, - vaultAddress: vaultAddress, - assetId: 0, - expiry: _expiry - }); - - bytes32 structHash = Entitlements.getEntitlementStructHash(entitlement); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - ownerPkey, - _getEIP712Hash(structHash) - ); - - Signatures.Signature memory sig = Signatures.Signature({ - signatureType: Signatures.SignatureType.EIP712, - v: v, - r: r, - s: s - }); - return (entitlement, sig); - } + IHookERC721VaultFactory vault; + uint32 tokenStartIndex = 300; + + function setUp() public { + setUpAddresses(); + setUpFullProtocol(); + vault = IHookERC721VaultFactory(protocol.vaultContract()); + } + + function createVaultandAsset() internal returns (address, uint32) { + vm.startPrank(admin); + tokenStartIndex += 1; + uint32 tokenId = tokenStartIndex; + token.mint(address(writer), tokenId); + address vaultAddress = address(vault.findOrCreateVault(address(token), tokenId)); + vm.stopPrank(); + return (vaultAddress, tokenId); + } + + function makeEntitlementAndSignature(uint256 ownerPkey, address operator, address vaultAddress, uint32 _expiry) + internal + returns (Entitlements.Entitlement memory, Signatures.Signature memory) + { + address ownerAdd = vm.addr(writerpkey); + + Entitlements.Entitlement memory entitlement = Entitlements.Entitlement({ + beneficialOwner: ownerAdd, + operator: operator, + vaultAddress: vaultAddress, + assetId: 0, + expiry: _expiry + }); + + bytes32 structHash = Entitlements.getEntitlementStructHash(entitlement); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPkey, _getEIP712Hash(structHash)); + + Signatures.Signature memory sig = + Signatures.Signature({signatureType: Signatures.SignatureType.EIP712, v: v, r: r, s: s}); + return (entitlement, sig); + } } contract HookVaultTestFlash is HookVaultTestsBase { - function testBasicFlashLoan() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanSuccess(); - - vm.prank(writer); - vaultImpl.flashLoan(0, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testFlashLoanFailsIfDisabled() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanSuccess(); - vm.prank(admin); - protocol.setCollectionConfig( - address(token), - keccak256("vault.flashLoanDisabled"), - true - ); - vm.prank(writer); - vm.expectRevert("flashLoan-flashLoan feature disabled for this contract"); - vaultImpl.flashLoan(0, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testBasicFlashLoanAlternateApprove() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanApproveForAll(); - - vm.prank(writer); - vaultImpl.flashLoan(0, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testBasicFlashCantReturnFalse() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanReturnsFalse(); - - vm.prank(writer); - vm.expectRevert("flashLoan-the flash loan contract must return true"); - vaultImpl.flashLoan(0, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testBasicFlashMustApprove() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanDoesNotApprove(); - - vm.prank(writer); - vm.expectRevert("ERC721: transfer caller is not owner nor approved"); - vaultImpl.flashLoan(0, address(flashLoan), " "); - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testBasicFlashCantBurn() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanBurnsAsset(); - - vm.prank(writer); - vm.expectRevert("ERC721: operator query for nonexistent token"); - vaultImpl.flashLoan(0, address(flashLoan), " "); - // operation reverted, so we can still mess with the asset - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testFlashCallData() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanVerifyCalldata(); - - vm.prank(writer); - vaultImpl.flashLoan(0, address(flashLoan), "hello world"); - // operation reverted, so we can still mess with the asset - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } - - function testFlashWillRevert() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - IERC721FlashLoanReceiver flashLoan = new FlashLoanVerifyCalldata(); - - vm.prank(writer); - vm.expectRevert("should check helloworld"); - vaultImpl.flashLoan(0, address(flashLoan), "hello world wrong!"); - // operation reverted, so we can still mess with the asset - assertTrue( - token.ownerOf(tokenId) == vaultAddress, - "good flashloan should work" - ); - } + function testBasicFlashLoan() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanSuccess(); + + vm.prank(writer); + vaultImpl.flashLoan(0, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testFlashLoanFailsIfDisabled() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanSuccess(); + vm.prank(admin); + protocol.setCollectionConfig(address(token), keccak256("vault.flashLoanDisabled"), true); + vm.prank(writer); + vm.expectRevert("flashLoan-flashLoan feature disabled for this contract"); + vaultImpl.flashLoan(0, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testBasicFlashLoanAlternateApprove() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanApproveForAll(); + + vm.prank(writer); + vaultImpl.flashLoan(0, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testBasicFlashCantReturnFalse() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanReturnsFalse(); + + vm.prank(writer); + vm.expectRevert("flashLoan-the flash loan contract must return true"); + vaultImpl.flashLoan(0, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testBasicFlashMustApprove() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanDoesNotApprove(); + + vm.prank(writer); + vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + vaultImpl.flashLoan(0, address(flashLoan), " "); + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testBasicFlashCantBurn() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanBurnsAsset(); + + vm.prank(writer); + vm.expectRevert("ERC721: operator query for nonexistent token"); + vaultImpl.flashLoan(0, address(flashLoan), " "); + // operation reverted, so we can still mess with the asset + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testFlashCallData() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanVerifyCalldata(); + + vm.prank(writer); + vaultImpl.flashLoan(0, address(flashLoan), "hello world"); + // operation reverted, so we can still mess with the asset + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } + + function testFlashWillRevert() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + IERC721FlashLoanReceiver flashLoan = new FlashLoanVerifyCalldata(); + + vm.prank(writer); + vm.expectRevert("should check helloworld"); + vaultImpl.flashLoan(0, address(flashLoan), "hello world wrong!"); + // operation reverted, so we can still mess with the asset + assertTrue(token.ownerOf(tokenId) == vaultAddress, "good flashloan should work"); + } } contract HookVaultTestEntitlement is HookVaultTestsBase { - function testImposeEntitlmentOnTransferIn() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - assertTrue( - vaultImpl.getHoldsAsset(0), - "the token should be owned by the vault" - ); - assertTrue( - vaultImpl.getBeneficialOwner(0) == writer, - "writer should be the beneficial owner" - ); - assertTrue( - vaultImpl.hasActiveEntitlement(0), - "there should be an active entitlement" - ); - } - - function testImposeEntitlementAfterInitialTransfer() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - ( - Entitlements.Entitlement memory entitlement, - Signatures.Signature memory sig - ) = makeEntitlementAndSignature( - writerpkey, - mockContract, - vaultAddress, - expiration - ); - - vm.prank(writer); - - token.safeTransferFrom(writer, vaultAddress, tokenId); - - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - - // impose the entitlement onto the vault - vm.prank(mockContract); - vaultImpl.imposeEntitlement( - entitlement.operator, - uint32(entitlement.expiry), - uint32(entitlement.assetId), - sig.v, - sig.r, - sig.s - ); - - assertTrue( - vaultImpl.getHoldsAsset(0), - "the token should be owned by the vault" - ); - assertTrue( - vaultImpl.getBeneficialOwner(0) == writer, - "writer should be the beneficial owner" - ); - assertTrue( - vaultImpl.hasActiveEntitlement(0), - "there should be an active entitlement" - ); - - // verify that beneficial owner cannot withdrawl - // during an active entitlement. - vm.expectRevert( - "withdrawalAsset-the asset cannot be withdrawn with an active entitlement" - ); - vm.prank(writer); - vaultImpl.withdrawalAsset(0); - } - - function testEntitlementGoesAwayAfterExpiration() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - - assertTrue( - vaultImpl.hasActiveEntitlement(0), - "there should be an active entitlement" - ); - - vm.warp(block.timestamp + 2 days); - - assertTrue( - !vaultImpl.hasActiveEntitlement(0), - "there should not be any active entitlements" - ); - - vm.prank(writer); - vaultImpl.withdrawalAsset(0); - assertTrue( - !vaultImpl.getHoldsAsset(0), - "the token should not be owned by the vault" - ); - - assertTrue( - token.ownerOf(tokenId) == writer, - "token should be owned by the writer" - ); - } - - function testEntitlementCanBeClearedByOperator() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - - vm.prank(mockContract); - vaultImpl.clearEntitlement(0); - - assertTrue( - !vaultImpl.hasActiveEntitlement(0), - "there should not be any active entitlements" - ); - - // check that the owner can actually withdrawl - vm.prank(writer); - vaultImpl.withdrawalAsset(0); - assertTrue( - !vaultImpl.getHoldsAsset(0), - "the token should not be owned by the vault" - ); - - assertTrue( - token.ownerOf(tokenId) == writer, - "token should be owned by the writer" - ); - } - - function testNewEntitlementPossibleAferExpiredEntitlement() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - - assertTrue( - vaultImpl.hasActiveEntitlement(0), - "there should be an active entitlement" - ); - - vm.warp(block.timestamp + 2 days); - - assertTrue( - !vaultImpl.hasActiveEntitlement(0), - "there should not be any active entitlements" - ); - - // asset is not withdrawn, try to add a new entitlement - uint32 expiration2 = uint32(block.timestamp + 10 days); - - ( - Entitlements.Entitlement memory entitlement2, - Signatures.Signature memory sig2 - ) = makeEntitlementAndSignature( - writerpkey, - mockContract, - vaultAddress, - expiration2 - ); - vaultImpl.imposeEntitlement( - entitlement2.operator, - uint32(entitlement2.expiry), - uint32(entitlement2.assetId), - sig2.v, - sig2.r, - sig2.s - ); - assertTrue( - vaultImpl.hasActiveEntitlement(0), - "there should be a new active entitlement" - ); - } - - function testNewEntitlementPossibleAfterClearedEntitlement() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - - assertTrue( - vaultImpl.hasActiveEntitlement(0), - "there should be an active entitlement" - ); - - vm.prank(mockContract); - vaultImpl.clearEntitlement(0); - - assertTrue( - !vaultImpl.hasActiveEntitlement(0), - "there should not be any active entitlements" - ); - - uint32 expiration2 = uint32(block.timestamp + 3 days); - - ( - Entitlements.Entitlement memory entitlement2, - Signatures.Signature memory sig2 - ) = makeEntitlementAndSignature( - writerpkey, - mockContract, - vaultAddress, - expiration2 - ); - - vaultImpl.imposeEntitlement( - entitlement2.operator, - uint32(entitlement2.expiry), - uint32(entitlement2.assetId), - sig2.v, - sig2.r, - sig2.s - ); - assertTrue( - vaultImpl.hasActiveEntitlement(0), - "there should be a new active entitlement" - ); - } - - function testOnlyOneEntitlementAllowed() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(3333); - uint32 expiration = uint32(block.timestamp) + 1 days; - - // transfer in with first entitlement - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - - address mockContract2 = address(35553445); - assertTrue( - vaultImpl.hasActiveEntitlement(0), - "there should be an active entitlement" - ); - - uint32 expiration2 = uint32(block.timestamp + 3 days); - - ( - Entitlements.Entitlement memory entitlement2, - Signatures.Signature memory sig2 - ) = makeEntitlementAndSignature( - writerpkey, - mockContract2, - vaultAddress, - expiration2 - ); - - vm.prank(mockContract2); - vm.expectRevert( - "_registerEntitlement-existing entitlement must be cleared before registering a new one" - ); - - vaultImpl.imposeEntitlement( - entitlement2.operator, - uint32(entitlement2.expiry), - uint32(entitlement2.assetId), - sig2.v, - sig2.r, - sig2.s - ); - } - - function testBeneficialOwnerCannotClearEntitlement() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69420); - uint32 expiration = uint32(block.timestamp) + 1 days; - // transfer in with first entitlement - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - - assertTrue( - vaultImpl.hasActiveEntitlement(0), - "there should be an active entitlement" - ); - - vm.prank(writer); - vm.expectRevert( - "clearEntitlement-only the entitled address can clear the entitlement" - ); - vaultImpl.clearEntitlement(0); - - vm.prank(address(55566677788899911)); - vm.expectRevert( - "clearEntitlement-only the entitled address can clear the entitlement" - ); - vaultImpl.clearEntitlement(0); - } + function testImposeEntitlmentOnTransferIn() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + assertTrue(vaultImpl.getHoldsAsset(0), "the token should be owned by the vault"); + assertTrue(vaultImpl.getBeneficialOwner(0) == writer, "writer should be the beneficial owner"); + assertTrue(vaultImpl.hasActiveEntitlement(0), "there should be an active entitlement"); + } + + function testImposeEntitlementAfterInitialTransfer() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + (Entitlements.Entitlement memory entitlement, Signatures.Signature memory sig) = + makeEntitlementAndSignature(writerpkey, mockContract, vaultAddress, expiration); + + vm.prank(writer); + + token.safeTransferFrom(writer, vaultAddress, tokenId); + + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + // impose the entitlement onto the vault + vm.prank(mockContract); + vaultImpl.imposeEntitlement( + entitlement.operator, uint32(entitlement.expiry), uint32(entitlement.assetId), sig.v, sig.r, sig.s + ); + + assertTrue(vaultImpl.getHoldsAsset(0), "the token should be owned by the vault"); + assertTrue(vaultImpl.getBeneficialOwner(0) == writer, "writer should be the beneficial owner"); + assertTrue(vaultImpl.hasActiveEntitlement(0), "there should be an active entitlement"); + + // verify that beneficial owner cannot withdrawl + // during an active entitlement. + vm.expectRevert("withdrawalAsset-the asset cannot be withdrawn with an active entitlement"); + vm.prank(writer); + vaultImpl.withdrawalAsset(0); + } + + function testEntitlementGoesAwayAfterExpiration() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + assertTrue(vaultImpl.hasActiveEntitlement(0), "there should be an active entitlement"); + + vm.warp(block.timestamp + 2 days); + + assertTrue(!vaultImpl.hasActiveEntitlement(0), "there should not be any active entitlements"); + + vm.prank(writer); + vaultImpl.withdrawalAsset(0); + assertTrue(!vaultImpl.getHoldsAsset(0), "the token should not be owned by the vault"); + + assertTrue(token.ownerOf(tokenId) == writer, "token should be owned by the writer"); + } + + function testEntitlementCanBeClearedByOperator() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + vm.prank(mockContract); + vaultImpl.clearEntitlement(0); + + assertTrue(!vaultImpl.hasActiveEntitlement(0), "there should not be any active entitlements"); + + // check that the owner can actually withdrawl + vm.prank(writer); + vaultImpl.withdrawalAsset(0); + assertTrue(!vaultImpl.getHoldsAsset(0), "the token should not be owned by the vault"); + + assertTrue(token.ownerOf(tokenId) == writer, "token should be owned by the writer"); + } + + function testNewEntitlementPossibleAferExpiredEntitlement() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + assertTrue(vaultImpl.hasActiveEntitlement(0), "there should be an active entitlement"); + + vm.warp(block.timestamp + 2 days); + + assertTrue(!vaultImpl.hasActiveEntitlement(0), "there should not be any active entitlements"); + + // asset is not withdrawn, try to add a new entitlement + uint32 expiration2 = uint32(block.timestamp + 10 days); + + (Entitlements.Entitlement memory entitlement2, Signatures.Signature memory sig2) = + makeEntitlementAndSignature(writerpkey, mockContract, vaultAddress, expiration2); + vaultImpl.imposeEntitlement( + entitlement2.operator, uint32(entitlement2.expiry), uint32(entitlement2.assetId), sig2.v, sig2.r, sig2.s + ); + assertTrue(vaultImpl.hasActiveEntitlement(0), "there should be a new active entitlement"); + } + + function testNewEntitlementPossibleAfterClearedEntitlement() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + assertTrue(vaultImpl.hasActiveEntitlement(0), "there should be an active entitlement"); + + vm.prank(mockContract); + vaultImpl.clearEntitlement(0); + + assertTrue(!vaultImpl.hasActiveEntitlement(0), "there should not be any active entitlements"); + + uint32 expiration2 = uint32(block.timestamp + 3 days); + + (Entitlements.Entitlement memory entitlement2, Signatures.Signature memory sig2) = + makeEntitlementAndSignature(writerpkey, mockContract, vaultAddress, expiration2); + + vaultImpl.imposeEntitlement( + entitlement2.operator, uint32(entitlement2.expiry), uint32(entitlement2.assetId), sig2.v, sig2.r, sig2.s + ); + assertTrue(vaultImpl.hasActiveEntitlement(0), "there should be a new active entitlement"); + } + + function testOnlyOneEntitlementAllowed() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(3333); + uint32 expiration = uint32(block.timestamp) + 1 days; + + // transfer in with first entitlement + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + address mockContract2 = address(35553445); + assertTrue(vaultImpl.hasActiveEntitlement(0), "there should be an active entitlement"); + + uint32 expiration2 = uint32(block.timestamp + 3 days); + + (Entitlements.Entitlement memory entitlement2, Signatures.Signature memory sig2) = + makeEntitlementAndSignature(writerpkey, mockContract2, vaultAddress, expiration2); + + vm.prank(mockContract2); + vm.expectRevert("_registerEntitlement-existing entitlement must be cleared before registering a new one"); + + vaultImpl.imposeEntitlement( + entitlement2.operator, uint32(entitlement2.expiry), uint32(entitlement2.assetId), sig2.v, sig2.r, sig2.s + ); + } + + function testBeneficialOwnerCannotClearEntitlement() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69420); + uint32 expiration = uint32(block.timestamp) + 1 days; + // transfer in with first entitlement + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + assertTrue(vaultImpl.hasActiveEntitlement(0), "there should be an active entitlement"); + + vm.prank(writer); + vm.expectRevert("clearEntitlement-only the entitled address can clear the entitlement"); + vaultImpl.clearEntitlement(0); + + vm.prank(address(55566677788899911)); + vm.expectRevert("clearEntitlement-only the entitled address can clear the entitlement"); + vaultImpl.clearEntitlement(0); + } } contract HookVaultTestsDistribution is HookVaultTestsBase { - event AssetWithdrawn(uint32, address, address); - - function testClearAndDistributeReturnsNFT() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - - vm.prank(mockContract); - vm.expectEmit(true, true, true, false); - emit AssetWithdrawn(0, writer, writer); - vaultImpl.clearEntitlementAndDistribute(0, writer); - - assertTrue( - !vaultImpl.hasActiveEntitlement(0), - "there should not be any active entitlements" - ); - - assertTrue( - token.ownerOf(tokenId) == writer, - "Token should be returned to the owner" - ); - } - - function testAirdropsCanBeDisbled() public { - (address vaultAddress, ) = createVaultandAsset(); - - vm.prank(admin); - protocol.setCollectionConfig( - address(token), - keccak256("vault.airdropsProhibited"), - true - ); - - TestERC721 token2 = new TestERC721(); - vm.expectRevert( - "onERC721Received-non-escrow asset returned when airdrops are disabled" - ); - token2.mint(vaultAddress, 0); - } - - function testAirdropsAllowedWhenEnabled() public { - (address vaultAddress, ) = createVaultandAsset(); - - vm.prank(admin); - protocol.setCollectionConfig( - address(token), - keccak256("vault.airdropsProhibited"), - false - ); - - TestERC721 token2 = new TestERC721(); - token2.mint(vaultAddress, 0); - assertTrue(token2.ownerOf(0) == vaultAddress, "vault should hold airdrop"); - } - - function testClearAndDistributeDoesNotReturnToWrongPerson() public { - (address vaultAddress, uint32 tokenId) = createVaultandAsset(); - - address mockContract = address(69); - uint32 expiration = uint32(block.timestamp) + 1 days; - - vm.prank(writer); - token.safeTransferFrom( - writer, - vaultAddress, - tokenId, - abi.encode(writer, mockContract, expiration) - ); - HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); - - vm.expectRevert( - "clearEntitlementAndDistribute-Only the beneficial owner can receive the asset" - ); - vm.prank(mockContract); - vaultImpl.clearEntitlementAndDistribute(0, address(0x033333344545)); - } + event AssetWithdrawn(uint32, address, address); + + function testClearAndDistributeReturnsNFT() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + vm.prank(mockContract); + vm.expectEmit(true, true, true, false); + emit AssetWithdrawn(0, writer, writer); + vaultImpl.clearEntitlementAndDistribute(0, writer); + + assertTrue(!vaultImpl.hasActiveEntitlement(0), "there should not be any active entitlements"); + + assertTrue(token.ownerOf(tokenId) == writer, "Token should be returned to the owner"); + } + + function testAirdropsCanBeDisbled() public { + (address vaultAddress,) = createVaultandAsset(); + + vm.prank(admin); + protocol.setCollectionConfig(address(token), keccak256("vault.airdropsProhibited"), true); + + TestERC721 token2 = new TestERC721(); + vm.expectRevert("onERC721Received-non-escrow asset returned when airdrops are disabled"); + token2.mint(vaultAddress, 0); + } + + function testAirdropsAllowedWhenEnabled() public { + (address vaultAddress,) = createVaultandAsset(); + + vm.prank(admin); + protocol.setCollectionConfig(address(token), keccak256("vault.airdropsProhibited"), false); + + TestERC721 token2 = new TestERC721(); + token2.mint(vaultAddress, 0); + assertTrue(token2.ownerOf(0) == vaultAddress, "vault should hold airdrop"); + } + + function testClearAndDistributeDoesNotReturnToWrongPerson() public { + (address vaultAddress, uint32 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint32 expiration = uint32(block.timestamp) + 1 days; + + vm.prank(writer); + token.safeTransferFrom(writer, vaultAddress, tokenId, abi.encode(writer, mockContract, expiration)); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + vm.expectRevert("clearEntitlementAndDistribute-Only the beneficial owner can receive the asset"); + vm.prank(mockContract); + vaultImpl.clearEntitlementAndDistribute(0, address(0x033333344545)); + } } diff --git a/src/test/pnm/base.t.sol b/src/test/pnm/base.t.sol index f69dcbe..31d9e80 100644 --- a/src/test/pnm/base.t.sol +++ b/src/test/pnm/base.t.sol @@ -26,66 +26,61 @@ import {PTest, console} from "@narya-ai/contracts/PTest.sol"; /// @notice Utils to setup the protocol to build various test cases /// @author Regynald Augustin-regy@hook.xyz contract HookProtocolTest is PTest, EIP712, PermissionConstants { - address internal admin; - address internal buyer; - uint256 internal writerpkey; - address internal writer; - address internal firstBidder; - address internal secondBidder; - IHookCoveredCall calls; - // can use this identifier to call fns not on the interface - HookCoveredCallImplV1 callInternal; - TestERC721 internal token; - WETH internal weth; - uint256 internal underlyingTokenId; - address internal protocolAddress; - HookProtocol protocol; - uint256 internal optionTokenId; - address internal preApprovedOperator; - HookERC721VaultFactory vaultFactory; - - HookERC721VaultImplV1 vaultImpl; - HookERC721MultiVaultImplV1 multiVaultImpl; - - event CallCreated( - address writer, - address vaultAddress, - uint256 assetId, - uint256 optionId, - uint256 strikePrice, - uint256 expiration - ); - - event CallSettled(uint256 optionId); - - event CallReclaimed(uint256 optionId); - - event ExpiredCallBurned(uint256 optionId); - - function setUpAddresses() public { - token = new TestERC721(); - weth = new WETH(); - - buyer = address(4); - vm.label(buyer, "option buyer"); - - writerpkey = uint256(0xBDCE); - writer = vm.addr(writerpkey); - vm.label(writer, "option writer"); - - admin = address(69); - vm.label(admin, "contract admin"); - - firstBidder = address(37); - vm.label(firstBidder, "First option bidder"); - - secondBidder = address(38); - vm.label(secondBidder, "Second option bidder"); - } - - function setUpFullProtocol() public { - weth = new WETH(); - protocol = new HookProtocol( + address internal admin; + address internal buyer; + uint256 internal writerpkey; + address internal writer; + address internal firstBidder; + address internal secondBidder; + IHookCoveredCall calls; + // can use this identifier to call fns not on the interface + HookCoveredCallImplV1 callInternal; + TestERC721 internal token; + WETH internal weth; + uint256 internal underlyingTokenId; + address internal protocolAddress; + HookProtocol protocol; + uint256 internal optionTokenId; + address internal preApprovedOperator; + HookERC721VaultFactory vaultFactory; + + HookERC721VaultImplV1 vaultImpl; + HookERC721MultiVaultImplV1 multiVaultImpl; + + event CallCreated( + address writer, address vaultAddress, uint256 assetId, uint256 optionId, uint256 strikePrice, uint256 expiration + ); + + event CallSettled(uint256 optionId); + + event CallReclaimed(uint256 optionId); + + event ExpiredCallBurned(uint256 optionId); + + function setUpAddresses() public { + token = new TestERC721(); + weth = new WETH(); + + buyer = address(4); + vm.label(buyer, "option buyer"); + + writerpkey = uint256(0xBDCE); + writer = vm.addr(writerpkey); + vm.label(writer, "option writer"); + + admin = address(69); + vm.label(admin, "contract admin"); + + firstBidder = address(37); + vm.label(firstBidder, "First option bidder"); + + secondBidder = address(38); + vm.label(secondBidder, "Second option bidder"); + } + + function setUpFullProtocol() public { + weth = new WETH(); + protocol = new HookProtocol( admin, admin, admin, @@ -94,147 +89,130 @@ contract HookProtocolTest is PTest, EIP712, PermissionConstants { admin, address(weth) ); - protocolAddress = address(protocol); - // set the operator to a new protocol to make it a contract - preApprovedOperator = address(weth); - setAddressForEipDomain(protocolAddress); + protocolAddress = address(protocol); + // set the operator to a new protocol to make it a contract + preApprovedOperator = address(weth); + setAddressForEipDomain(protocolAddress); - // Deploy new vault factory - vaultImpl = new HookERC721VaultImplV1(); + // Deploy new vault factory + vaultImpl = new HookERC721VaultImplV1(); - HookUpgradeableBeacon vaultBeacon = new HookUpgradeableBeacon( + HookUpgradeableBeacon vaultBeacon = new HookUpgradeableBeacon( address(vaultImpl), address(protocol), PermissionConstants.VAULT_UPGRADER ); - multiVaultImpl = new HookERC721MultiVaultImplV1(); + multiVaultImpl = new HookERC721MultiVaultImplV1(); - HookUpgradeableBeacon multiVaultBeacon = new HookUpgradeableBeacon( + HookUpgradeableBeacon multiVaultBeacon = new HookUpgradeableBeacon( address(multiVaultImpl), address(protocol), PermissionConstants.VAULT_UPGRADER ); - vaultFactory = new HookERC721VaultFactory( + vaultFactory = new HookERC721VaultFactory( protocolAddress, address(vaultBeacon), address(multiVaultBeacon) ); - vm.prank(address(admin)); - protocol.setVaultFactory(address(vaultFactory)); + vm.prank(address(admin)); + protocol.setVaultFactory(address(vaultFactory)); - // Deploy a new Covered Call Factory - HookCoveredCallImplV1 callImpl = new HookCoveredCallImplV1(); - HookUpgradeableBeacon callBeacon = new HookUpgradeableBeacon( + // Deploy a new Covered Call Factory + HookCoveredCallImplV1 callImpl = new HookCoveredCallImplV1(); + HookUpgradeableBeacon callBeacon = new HookUpgradeableBeacon( address(callImpl), address(protocol), PermissionConstants.CALL_UPGRADER ); - HookCoveredCallFactory callFactory = new HookCoveredCallFactory( + HookCoveredCallFactory callFactory = new HookCoveredCallFactory( protocolAddress, address(callBeacon), preApprovedOperator ); - vm.prank(address(admin)); - protocol.setCoveredCallFactory(address(callFactory)); - vm.prank(address(admin)); - - // make a call insturment for our token - calls = IHookCoveredCall(callFactory.makeCallInstrument(address(token))); - callInternal = HookCoveredCallImplV1(address(calls)); - } - - function setUpMintOption() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - optionTokenId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); + vm.prank(address(admin)); + protocol.setCoveredCallFactory(address(callFactory)); + vm.prank(address(admin)); - // Assume that the writer somehow sold the option NFT to the buyer. - // Outside of the scope of these tests. - calls.safeTransferFrom(writer, buyer, optionTokenId); - vm.stopPrank(); - } + // make a call insturment for our token + calls = IHookCoveredCall(callFactory.makeCallInstrument(address(token))); + callInternal = HookCoveredCallImplV1(address(calls)); + } - function setUpOptionBids() public { - vm.deal(address(firstBidder), 1 ether); + function setUpMintOption() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + optionTokenId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // Assume that the writer somehow sold the option NFT to the buyer. + // Outside of the scope of these tests. + calls.safeTransferFrom(writer, buyer, optionTokenId); + vm.stopPrank(); + } - vm.deal(address(secondBidder), 1 ether); + function setUpOptionBids() public { + vm.deal(address(firstBidder), 1 ether); - vm.warp(block.timestamp + 2.1 days); + vm.deal(address(secondBidder), 1 ether); - vm.prank(firstBidder); - calls.bid{value: 0.1 ether}(optionTokenId); + vm.warp(block.timestamp + 2.1 days); - vm.prank(secondBidder); - calls.bid{value: 0.2 ether}(optionTokenId); + vm.prank(firstBidder); + calls.bid{value: 0.1 ether}(optionTokenId); - // Fast forward to beyond the expiration date. - vm.warp(block.timestamp + 3.1 days); - } + vm.prank(secondBidder); + calls.bid{value: 0.2 ether}(optionTokenId); - function makeSignature( - uint256 tokenId, - uint32 expiry, - address _writer - ) internal returns (Signatures.Signature memory) { - address va = address( - vaultFactory.findOrCreateVault(address(token), tokenId) - ); - - uint32 assetId = 0; - if ( - va == - Create2.computeAddress( - BeaconSalts.multiVaultSalt(address(token)), - BeaconSalts.ByteCodeHash, - address(vaultFactory) - ) - ) { - // If the vault is a multi-vault, it requires that the assetId matches the - // tokenId, instead of having a standard assetI of 0 - assetId = uint32(tokenId); + // Fast forward to beyond the expiration date. + vm.warp(block.timestamp + 3.1 days); } - bytes32 structHash = Entitlements.getEntitlementStructHash( - Entitlements.Entitlement({ - beneficialOwner: address(_writer), - operator: address(calls), - vaultAddress: va, - assetId: assetId, - expiry: expiry - }) - ); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - writerpkey, - _getEIP712Hash(structHash) - ); - Signatures.Signature memory sig = Signatures.Signature({ - signatureType: Signatures.SignatureType.EIP712, - v: v, - r: r, - s: s - }); - return sig; - } + function makeSignature(uint256 tokenId, uint32 expiry, address _writer) + internal + returns (Signatures.Signature memory) + { + address va = address(vaultFactory.findOrCreateVault(address(token), tokenId)); + + uint32 assetId = 0; + if ( + va + == Create2.computeAddress( + BeaconSalts.multiVaultSalt(address(token)), BeaconSalts.ByteCodeHash, address(vaultFactory) + ) + ) { + // If the vault is a multi-vault, it requires that the assetId matches the + // tokenId, instead of having a standard assetI of 0 + assetId = uint32(tokenId); + } + + bytes32 structHash = Entitlements.getEntitlementStructHash( + Entitlements.Entitlement({ + beneficialOwner: address(_writer), + operator: address(calls), + vaultAddress: va, + assetId: assetId, + expiry: expiry + }) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(writerpkey, _getEIP712Hash(structHash)); + Signatures.Signature memory sig = + Signatures.Signature({signatureType: Signatures.SignatureType.EIP712, v: v, r: r, s: s}); + return sig; + } } diff --git a/src/test/utils/base.t.sol b/src/test/utils/base.t.sol index e38af4d..bd11e2c 100644 --- a/src/test/utils/base.t.sol +++ b/src/test/utils/base.t.sol @@ -26,63 +26,58 @@ import "../../interfaces/IHookCoveredCall.sol"; /// @notice Utils to setup the protocol to build various test cases /// @author Regynald Augustin-regy@hook.xyz contract HookProtocolTest is Test, EIP712, PermissionConstants { - address internal admin; - address internal buyer; - uint256 internal writerpkey; - address internal writer; - address internal firstBidder; - address internal secondBidder; - IHookCoveredCall calls; - // can use this identifier to call fns not on the interface - HookCoveredCallImplV1 callInternal; - TestERC721 internal token; - WETH internal weth; - uint256 internal underlyingTokenId; - address internal protocolAddress; - HookProtocol protocol; - uint256 internal optionTokenId; - address internal preApprovedOperator; - HookERC721VaultFactory vaultFactory; - - event CallCreated( - address writer, - address vaultAddress, - uint256 assetId, - uint256 optionId, - uint256 strikePrice, - uint256 expiration - ); - - event CallSettled(uint256 optionId); - - event CallReclaimed(uint256 optionId); - - event ExpiredCallBurned(uint256 optionId); - - function setUpAddresses() public { - token = new TestERC721(); - weth = new WETH(); - - buyer = address(4); - vm.label(buyer, "option buyer"); - - writerpkey = uint256(0xBDCE); - writer = vm.addr(writerpkey); - vm.label(writer, "option writer"); - - admin = address(69); - vm.label(admin, "contract admin"); - - firstBidder = address(37); - vm.label(firstBidder, "First option bidder"); - - secondBidder = address(38); - vm.label(secondBidder, "Second option bidder"); - } - - function setUpFullProtocol() public { - weth = new WETH(); - protocol = new HookProtocol( + address internal admin; + address internal buyer; + uint256 internal writerpkey; + address internal writer; + address internal firstBidder; + address internal secondBidder; + IHookCoveredCall calls; + // can use this identifier to call fns not on the interface + HookCoveredCallImplV1 callInternal; + TestERC721 internal token; + WETH internal weth; + uint256 internal underlyingTokenId; + address internal protocolAddress; + HookProtocol protocol; + uint256 internal optionTokenId; + address internal preApprovedOperator; + HookERC721VaultFactory vaultFactory; + + event CallCreated( + address writer, address vaultAddress, uint256 assetId, uint256 optionId, uint256 strikePrice, uint256 expiration + ); + + event CallSettled(uint256 optionId); + + event CallReclaimed(uint256 optionId); + + event ExpiredCallBurned(uint256 optionId); + + function setUpAddresses() public { + token = new TestERC721(); + weth = new WETH(); + + buyer = address(4); + vm.label(buyer, "option buyer"); + + writerpkey = uint256(0xBDCE); + writer = vm.addr(writerpkey); + vm.label(writer, "option writer"); + + admin = address(69); + vm.label(admin, "contract admin"); + + firstBidder = address(37); + vm.label(firstBidder, "First option bidder"); + + secondBidder = address(38); + vm.label(secondBidder, "Second option bidder"); + } + + function setUpFullProtocol() public { + weth = new WETH(); + protocol = new HookProtocol( admin, admin, admin, @@ -91,147 +86,130 @@ contract HookProtocolTest is Test, EIP712, PermissionConstants { admin, address(weth) ); - protocolAddress = address(protocol); - // set the operator to a new protocol to make it a contract - preApprovedOperator = address(weth); - setAddressForEipDomain(protocolAddress); + protocolAddress = address(protocol); + // set the operator to a new protocol to make it a contract + preApprovedOperator = address(weth); + setAddressForEipDomain(protocolAddress); - // Deploy new vault factory - HookERC721VaultImplV1 vaultImpl = new HookERC721VaultImplV1(); + // Deploy new vault factory + HookERC721VaultImplV1 vaultImpl = new HookERC721VaultImplV1(); - HookUpgradeableBeacon vaultBeacon = new HookUpgradeableBeacon( + HookUpgradeableBeacon vaultBeacon = new HookUpgradeableBeacon( address(vaultImpl), address(protocol), PermissionConstants.VAULT_UPGRADER ); - HookERC721MultiVaultImplV1 multiVaultImpl = new HookERC721MultiVaultImplV1(); + HookERC721MultiVaultImplV1 multiVaultImpl = new HookERC721MultiVaultImplV1(); - HookUpgradeableBeacon multiVaultBeacon = new HookUpgradeableBeacon( + HookUpgradeableBeacon multiVaultBeacon = new HookUpgradeableBeacon( address(multiVaultImpl), address(protocol), PermissionConstants.VAULT_UPGRADER ); - vaultFactory = new HookERC721VaultFactory( + vaultFactory = new HookERC721VaultFactory( protocolAddress, address(vaultBeacon), address(multiVaultBeacon) ); - vm.prank(address(admin)); - protocol.setVaultFactory(address(vaultFactory)); + vm.prank(address(admin)); + protocol.setVaultFactory(address(vaultFactory)); - // Deploy a new Covered Call Factory - HookCoveredCallImplV1 callImpl = new HookCoveredCallImplV1(); - HookUpgradeableBeacon callBeacon = new HookUpgradeableBeacon( + // Deploy a new Covered Call Factory + HookCoveredCallImplV1 callImpl = new HookCoveredCallImplV1(); + HookUpgradeableBeacon callBeacon = new HookUpgradeableBeacon( address(callImpl), address(protocol), PermissionConstants.CALL_UPGRADER ); - HookCoveredCallFactory callFactory = new HookCoveredCallFactory( + HookCoveredCallFactory callFactory = new HookCoveredCallFactory( protocolAddress, address(callBeacon), preApprovedOperator ); - vm.prank(address(admin)); - protocol.setCoveredCallFactory(address(callFactory)); - vm.prank(address(admin)); - - // make a call insturment for our token - calls = IHookCoveredCall(callFactory.makeCallInstrument(address(token))); - callInternal = HookCoveredCallImplV1(address(calls)); - } - - function setUpMintOption() public { - vm.startPrank(address(writer)); - - // Writer approve covered call - token.setApprovalForAll(address(calls), true); - - uint32 expiration = uint32(block.timestamp) + 3 days; - - vm.expectEmit(true, true, true, false); - emit CallCreated( - address(writer), - address(token), - 0, - 1, // This would be the first option id. - 1000, - expiration - ); - optionTokenId = calls.mintWithErc721( - address(token), - underlyingTokenId, - 1000, - expiration - ); + vm.prank(address(admin)); + protocol.setCoveredCallFactory(address(callFactory)); + vm.prank(address(admin)); - // Assume that the writer somehow sold the option NFT to the buyer. - // Outside of the scope of these tests. - calls.safeTransferFrom(writer, buyer, optionTokenId); - vm.stopPrank(); - } + // make a call insturment for our token + calls = IHookCoveredCall(callFactory.makeCallInstrument(address(token))); + callInternal = HookCoveredCallImplV1(address(calls)); + } - function setUpOptionBids() public { - vm.deal(address(firstBidder), 1 ether); + function setUpMintOption() public { + vm.startPrank(address(writer)); + + // Writer approve covered call + token.setApprovalForAll(address(calls), true); + + uint32 expiration = uint32(block.timestamp) + 3 days; + + vm.expectEmit(true, true, true, false); + emit CallCreated( + address(writer), + address(token), + 0, + 1, // This would be the first option id. + 1000, + expiration + ); + optionTokenId = calls.mintWithErc721(address(token), underlyingTokenId, 1000, expiration); + + // Assume that the writer somehow sold the option NFT to the buyer. + // Outside of the scope of these tests. + calls.safeTransferFrom(writer, buyer, optionTokenId); + vm.stopPrank(); + } - vm.deal(address(secondBidder), 1 ether); + function setUpOptionBids() public { + vm.deal(address(firstBidder), 1 ether); - vm.warp(block.timestamp + 2.1 days); + vm.deal(address(secondBidder), 1 ether); - vm.prank(firstBidder); - calls.bid{value: 0.1 ether}(optionTokenId); + vm.warp(block.timestamp + 2.1 days); - vm.prank(secondBidder); - calls.bid{value: 0.2 ether}(optionTokenId); + vm.prank(firstBidder); + calls.bid{value: 0.1 ether}(optionTokenId); - // Fast forward to beyond the expiration date. - vm.warp(block.timestamp + 3.1 days); - } + vm.prank(secondBidder); + calls.bid{value: 0.2 ether}(optionTokenId); - function makeSignature( - uint256 tokenId, - uint32 expiry, - address _writer - ) internal returns (Signatures.Signature memory) { - address va = address( - vaultFactory.findOrCreateVault(address(token), tokenId) - ); - - uint32 assetId = 0; - if ( - va == - Create2.computeAddress( - BeaconSalts.multiVaultSalt(address(token)), - BeaconSalts.ByteCodeHash, - address(vaultFactory) - ) - ) { - // If the vault is a multi-vault, it requires that the assetId matches the - // tokenId, instead of having a standard assetI of 0 - assetId = uint32(tokenId); + // Fast forward to beyond the expiration date. + vm.warp(block.timestamp + 3.1 days); } - bytes32 structHash = Entitlements.getEntitlementStructHash( - Entitlements.Entitlement({ - beneficialOwner: address(_writer), - operator: address(calls), - vaultAddress: va, - assetId: assetId, - expiry: expiry - }) - ); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - writerpkey, - _getEIP712Hash(structHash) - ); - Signatures.Signature memory sig = Signatures.Signature({ - signatureType: Signatures.SignatureType.EIP712, - v: v, - r: r, - s: s - }); - return sig; - } + function makeSignature(uint256 tokenId, uint32 expiry, address _writer) + internal + returns (Signatures.Signature memory) + { + address va = address(vaultFactory.findOrCreateVault(address(token), tokenId)); + + uint32 assetId = 0; + if ( + va + == Create2.computeAddress( + BeaconSalts.multiVaultSalt(address(token)), BeaconSalts.ByteCodeHash, address(vaultFactory) + ) + ) { + // If the vault is a multi-vault, it requires that the assetId matches the + // tokenId, instead of having a standard assetI of 0 + assetId = uint32(tokenId); + } + + bytes32 structHash = Entitlements.getEntitlementStructHash( + Entitlements.Entitlement({ + beneficialOwner: address(_writer), + operator: address(calls), + vaultAddress: va, + assetId: assetId, + expiry: expiry + }) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(writerpkey, _getEIP712Hash(structHash)); + Signatures.Signature memory sig = + Signatures.Signature({signatureType: Signatures.SignatureType.EIP712, v: v, r: r, s: s}); + return sig; + } } diff --git a/src/test/utils/mocks/FlashLoan.sol b/src/test/utils/mocks/FlashLoan.sol index 6d2f0a1..a4b30e8 100644 --- a/src/test/utils/mocks/FlashLoan.sol +++ b/src/test/utils/mocks/FlashLoan.sol @@ -5,151 +5,101 @@ import "../tokens/TestERC721.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; contract FlashLoanSuccess is IERC721FlashLoanReceiver { - constructor() {} - - function executeOperation( - address nftContract, - uint256 tokenId, - address, - address vault, - bytes calldata - ) external returns (bool) { - IERC721(nftContract).approve(vault, tokenId); - return IERC721(nftContract).ownerOf(tokenId) == address(this); - } - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) public pure override returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } + constructor() {} + + function executeOperation(address nftContract, uint256 tokenId, address, address vault, bytes calldata) + external + returns (bool) + { + IERC721(nftContract).approve(vault, tokenId); + return IERC721(nftContract).ownerOf(tokenId) == address(this); + } + + function onERC721Received(address, address, uint256, bytes calldata) public pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } } contract FlashLoanDoesNotApprove is IERC721FlashLoanReceiver { - constructor() {} - - function executeOperation( - address nftContract, - uint256 tokenId, - address, - address, - bytes calldata - ) external view returns (bool) { - // skip this: - // IERC721(nftContract).approve(vault, tokenId); - return IERC721(nftContract).ownerOf(tokenId) == address(this); - } - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) public pure override returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } + constructor() {} + + function executeOperation(address nftContract, uint256 tokenId, address, address, bytes calldata) + external + view + returns (bool) + { + // skip this: + // IERC721(nftContract).approve(vault, tokenId); + return IERC721(nftContract).ownerOf(tokenId) == address(this); + } + + function onERC721Received(address, address, uint256, bytes calldata) public pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } } contract FlashLoanReturnsFalse is IERC721FlashLoanReceiver { - constructor() {} - - function executeOperation( - address nftContract, - uint256 tokenId, - address, - address vault, - bytes calldata - ) external returns (bool) { - IERC721(nftContract).approve(vault, tokenId); - return false; - } - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) public pure override returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } + constructor() {} + + function executeOperation(address nftContract, uint256 tokenId, address, address vault, bytes calldata) + external + returns (bool) + { + IERC721(nftContract).approve(vault, tokenId); + return false; + } + + function onERC721Received(address, address, uint256, bytes calldata) public pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } } contract FlashLoanApproveForAll is IERC721FlashLoanReceiver { - constructor() {} - - function executeOperation( - address nftContract, - uint256 tokenId, - address, - address vault, - bytes calldata - ) external returns (bool) { - IERC721(nftContract).setApprovalForAll(vault, true); - return IERC721(nftContract).ownerOf(tokenId) == address(this); - } - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) public pure override returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } + constructor() {} + + function executeOperation(address nftContract, uint256 tokenId, address, address vault, bytes calldata) + external + returns (bool) + { + IERC721(nftContract).setApprovalForAll(vault, true); + return IERC721(nftContract).ownerOf(tokenId) == address(this); + } + + function onERC721Received(address, address, uint256, bytes calldata) public pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } } contract FlashLoanBurnsAsset is IERC721FlashLoanReceiver { - constructor() {} - - function executeOperation( - address nftContract, - uint256 tokenId, - address, - address vault, - bytes calldata - ) external returns (bool) { - IERC721(nftContract).setApprovalForAll(vault, true); - TestERC721(nftContract).burn(tokenId); - return true; - } - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) public pure override returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } + constructor() {} + + function executeOperation(address nftContract, uint256 tokenId, address, address vault, bytes calldata) + external + returns (bool) + { + IERC721(nftContract).setApprovalForAll(vault, true); + TestERC721(nftContract).burn(tokenId); + return true; + } + + function onERC721Received(address, address, uint256, bytes calldata) public pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } } contract FlashLoanVerifyCalldata is IERC721FlashLoanReceiver { - constructor() {} - - function executeOperation( - address nftContract, - uint256, - address, - address vault, - bytes calldata params - ) external returns (bool) { - require( - keccak256(params) == keccak256("hello world"), - "should check helloworld" - ); - IERC721(nftContract).setApprovalForAll(vault, true); - return true; - } - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) public pure override returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } + constructor() {} + + function executeOperation(address nftContract, uint256, address, address vault, bytes calldata params) + external + returns (bool) + { + require(keccak256(params) == keccak256("hello world"), "should check helloworld"); + IERC721(nftContract).setApprovalForAll(vault, true); + return true; + } + + function onERC721Received(address, address, uint256, bytes calldata) public pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } } diff --git a/src/test/utils/mocks/MaliciousBidder.sol b/src/test/utils/mocks/MaliciousBidder.sol index 2e4ae05..231e438 100644 --- a/src/test/utils/mocks/MaliciousBidder.sol +++ b/src/test/utils/mocks/MaliciousBidder.sol @@ -7,19 +7,19 @@ import "../../../interfaces/IHookCoveredCall.sol"; // this can be used to write tests that fail if a contract reverting // prevents new bids. contract MaliciousBidder { - IHookCoveredCall private callOption; - bool private throwOnReceive; + IHookCoveredCall private callOption; + bool private throwOnReceive; - constructor(address _callOption) { - callOption = IHookCoveredCall(_callOption); - throwOnReceive = true; - } + constructor(address _callOption) { + callOption = IHookCoveredCall(_callOption); + throwOnReceive = true; + } - function bid(uint256 optionId) public payable { - callOption.bid{value: msg.value}(optionId); - } + function bid(uint256 optionId) public payable { + callOption.bid{value: msg.value}(optionId); + } - receive() external payable { - require(!throwOnReceive, "ha ha ha gotcha"); - } + receive() external payable { + require(!throwOnReceive, "ha ha ha gotcha"); + } } diff --git a/src/test/utils/tokens/TestERC721.sol b/src/test/utils/tokens/TestERC721.sol index f99e601..ed77bbc 100644 --- a/src/test/utils/tokens/TestERC721.sol +++ b/src/test/utils/tokens/TestERC721.sol @@ -7,13 +7,13 @@ import "@openzeppelin/contracts/access/Ownable.sol"; /// @title TestERC721 /// @notice FOR TEST PURPOSES ONLY. contract TestERC721 is ERC721, Ownable { - constructor() ERC721("TestERC721", "TEST") {} + constructor() ERC721("TestERC721", "TEST") {} - function mint(address to, uint256 tokenId) public { - _safeMint(to, tokenId); - } + function mint(address to, uint256 tokenId) public { + _safeMint(to, tokenId); + } - function burn(uint256 tokenId) public { - _burn(tokenId); - } + function burn(uint256 tokenId) public { + _burn(tokenId); + } } diff --git a/src/test/utils/tokens/WETH.sol b/src/test/utils/tokens/WETH.sol index 872c250..566b7a5 100644 --- a/src/test/utils/tokens/WETH.sol +++ b/src/test/utils/tokens/WETH.sol @@ -5,69 +5,65 @@ pragma solidity ^0.8.10; /// @notice FOR TEST PURPOSES ONLY. /// Source: https://github.com/gnosis/canonical-weth/blob/0dd1ea3e295eef916d0c6223ec63141137d22d67/contracts/WETH9.sol contract WETH { - string public name = "Wrapped Ether"; - string public symbol = "WETH"; - uint8 public decimals = 18; + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; - event Approval(address indexed src, address indexed guy, uint256 wad); - event Transfer(address indexed src, address indexed dst, uint256 wad); - event Deposit(address indexed dst, uint256 wad); - event Withdrawal(address indexed src, uint256 wad); + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); - mapping(address => uint256) public balanceOf; - mapping(address => mapping(address => uint256)) public allowance; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; - fallback() external payable { - deposit(); - } + fallback() external payable { + deposit(); + } - receive() external payable { - deposit(); - } + receive() external payable { + deposit(); + } - function deposit() public payable { - balanceOf[msg.sender] += msg.value; - emit Deposit(msg.sender, msg.value); - } + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } - function withdraw(uint256 wad) public { - require(balanceOf[msg.sender] >= wad); - balanceOf[msg.sender] -= wad; - payable(msg.sender).transfer(wad); - emit Withdrawal(msg.sender, wad); - } + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } - function totalSupply() public view returns (uint256) { - return address(this).balance; - } + function totalSupply() public view returns (uint256) { + return address(this).balance; + } - function approve(address guy, uint256 wad) public returns (bool) { - allowance[msg.sender][guy] = wad; - emit Approval(msg.sender, guy, wad); - return true; - } + function approve(address guy, uint256 wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } - function transfer(address dst, uint256 wad) public returns (bool) { - return transferFrom(msg.sender, dst, wad); - } + function transfer(address dst, uint256 wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } - function transferFrom( - address src, - address dst, - uint256 wad - ) public returns (bool) { - require(balanceOf[src] >= wad); + function transferFrom(address src, address dst, uint256 wad) public returns (bool) { + require(balanceOf[src] >= wad); - if (src != msg.sender && allowance[src][msg.sender] != type(uint128).max) { - require(allowance[src][msg.sender] >= wad); - allowance[src][msg.sender] -= wad; - } + if (src != msg.sender && allowance[src][msg.sender] != type(uint128).max) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } - balanceOf[src] -= wad; - balanceOf[dst] += wad; + balanceOf[src] -= wad; + balanceOf[dst] += wad; - emit Transfer(src, dst, wad); + emit Transfer(src, dst, wad); - return true; - } + return true; + } }