diff --git a/src/HookERC721MultiVault.sol b/src/HookERC721MultiVault.sol new file mode 100644 index 0000000..ddec98c --- /dev/null +++ b/src/HookERC721MultiVault.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; + +/// @title ERC-721 MultiVault Proxy Contract +/// @author Jake Nyquist -- j@hook.xyz +/// @notice Each instance of this contract is a unique multi-vault which references the +/// shared implementation pointed to by the Beacon +contract HookERC721MultiVault is BeaconProxy { + constructor( + address beacon, + address nftAddress, + address hookProtocolAddress + ) + BeaconProxy( + beacon, + abi.encodeWithSignature( + "initialize(address,address)", + nftAddress, + hookProtocolAddress + ) + ) + {} +} diff --git a/src/HookERC721MultiVaultBeacon.sol b/src/HookERC721MultiVaultBeacon.sol new file mode 100644 index 0000000..e6b71a5 --- /dev/null +++ b/src/HookERC721MultiVaultBeacon.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.8.10; + +import "./HookUpgradeableBeacon.sol"; + +/// @title HookERC721MultiVaultBeacon -- beacon holding pointer to current ERC721MultiVault implementation +/// @author Jake Nyquist -- j@hook.xyz +/// @notice The beacon broadcasts the address which contains the existing implementation of the ERC721MultiVault +/// @dev Permissions for who can upgrade are contained within the protocol contract. +contract HookERC721MultiVaultBeacon is HookUpgradeableBeacon { + constructor( + address implementation, + address hookProtocol, + bytes32 upgraderRole + ) HookUpgradeableBeacon(implementation, hookProtocol, upgraderRole) {} +} diff --git a/src/HookERC721MultiVaultImplV1.sol b/src/HookERC721MultiVaultImplV1.sol index 7a47845..d5c5802 100644 --- a/src/HookERC721MultiVaultImplV1.sol +++ b/src/HookERC721MultiVaultImplV1.sol @@ -21,7 +21,7 @@ import "./mixin/EIP712.sol"; /// @dev This contract implements ERC721Reciever /// 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 HookERC721VaultImplV1 is +contract HookERC721MultiVaultImplV1 is IHookERC721Vault, EIP712, Initializable, diff --git a/src/HookERC721VaultFactory.sol b/src/HookERC721VaultFactory.sol index eb805b4..b685dd6 100644 --- a/src/HookERC721VaultFactory.sol +++ b/src/HookERC721VaultFactory.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.10; import "./HookERC721Vault.sol"; +import "./HookERC721MultiVault.sol"; import "./interfaces/IHookERC721VaultFactory.sol"; /// @dev The factory itself is non-upgradeable; however, each vault is upgradeable (i.e. all vaults) @@ -12,18 +13,34 @@ contract HookERC721VaultFactory is IHookERC721VaultFactory { /// @dev From this view, we do not know if a vault is empty or full mapping(address => mapping(uint256 => address)) public override getVault; + /// @notice Registry of all of the active multi-vaults within the protocol + mapping(address => address) public override getMultiVault; + address private _hookProtocol; address private _beacon; + address private _multiBeacon; - constructor(address hookProtocolAddress, address beaconAddress) { + constructor( + address hookProtocolAddress, + address beaconAddress, + address multiBeaconAddress + ) { _hookProtocol = hookProtocolAddress; _beacon = beaconAddress; + _multiBeacon = multiBeaconAddress; } + /// @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. function makeVault(address nftAddress, uint256 tokenId) external returns (address vault) { + if (getMultiVault[nftAddress] != address(0)) { + return getMultiVault[nftAddress]; + } require( getVault[nftAddress][tokenId] == address(0), "makeVault -- a vault cannot already exist" @@ -39,7 +56,7 @@ contract HookERC721VaultFactory is IHookERC721VaultFactory { _hookProtocol ) ); - + return getVault[nftAddress][tokenId]; } } diff --git a/src/interfaces/IHookERC721VaultFactory.sol b/src/interfaces/IHookERC721VaultFactory.sol index 01d524b..00e76e8 100644 --- a/src/interfaces/IHookERC721VaultFactory.sol +++ b/src/interfaces/IHookERC721VaultFactory.sol @@ -9,6 +9,11 @@ interface IHookERC721VaultFactory { view returns (address vault); + function getMultiVault(address nftAddress) + external + view + returns (address vault); + function makeVault(address nftAddress, uint256 tokenId) external returns (address vault); diff --git a/src/test/HookMultiVaultTests.sol b/src/test/HookMultiVaultTests.sol new file mode 100644 index 0000000..82c7bff --- /dev/null +++ b/src/test/HookMultiVaultTests.sol @@ -0,0 +1,876 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.10; + +import "ds-test/test.sol"; +import "forge-std/Test.sol"; + +import "./utils/base.sol"; +import "../interfaces/IHookERC721VaultFactory.sol"; + +import "../lib/Entitlements.sol"; +import "../lib/Signatures.sol"; +import "../mixin/EIP712.sol"; + +import "./utils/mocks/FlashLoan.sol"; + +contract HookMultiVaultTests is HookProtocolTest { + IHookERC721VaultFactory vault; + uint256 tokenStartIndex = 300; + + function setUp() public { + setUpAddresses(); + setUpFullProtocol(); + vault = IHookERC721VaultFactory(protocol.vaultContract()); + } + + function createVaultandAsset() + internal + returns (address vaultAddress, uint256 tokenId) + { + vm.startPrank(admin); + tokenStartIndex += 1; + tokenId = tokenStartIndex; + token.mint(address(writer), tokenId); + address vaultAddress = vault.makeVault(address(token), tokenId); + vm.stopPrank(); + return (vaultAddress, tokenId); + } + + function makeEntitlementAndSignature( + uint256 ownerPkey, + address operator, + address vaultAddress, + uint256 _expiry + ) + internal + returns ( + Entitlements.Entitlement memory entitlement, + Signatures.Signature memory signature + ) + { + 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); + } + + function testImposeEntitlmentOnTransferIn() public { + (address vaultAddress, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + + 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(), + "there should be an active entitlement" + ); + } + + function testBasicFlashLoan() public { + (address vaultAddress, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + + 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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + + 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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + + 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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + + 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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + + 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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + + 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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + + 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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + + 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 testImposeEntitlementAfterInitialTransfer() public { + (address vaultAddress, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, sig); + + 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(), + "there should be an active entitlement" + ); + + // verify that beneficial owner cannot withdrawl + // during an active entitlement. + vm.expectRevert( + "withdrawalAsset -- the asset canot be withdrawn with an active entitlement" + ); + vm.prank(writer); + vaultImpl.withdrawalAsset(0); + } + + function testEntitlementGoesAwayAfterExpiration() public { + (address vaultAddress, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + assertTrue( + vaultImpl.hasActiveEntitlement(), + "there should be an active entitlement" + ); + + vm.warp(block.timestamp + 2 days); + + assertTrue( + !vaultImpl.hasActiveEntitlement(), + "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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + vm.prank(mockContract); + vaultImpl.clearEntitlement(0); + + assertTrue( + !vaultImpl.hasActiveEntitlement(), + "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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + assertTrue( + vaultImpl.hasActiveEntitlement(), + "there should be an active entitlement" + ); + + vm.warp(block.timestamp + 2 days); + + assertTrue( + !vaultImpl.hasActiveEntitlement(), + "there should not be any active entitlements" + ); + + // asset is not withdrawn, try to add a new entitlement + uint256 expiration2 = block.timestamp + 10 days; + + ( + Entitlements.Entitlement memory entitlement2, + Signatures.Signature memory sig2 + ) = makeEntitlementAndSignature( + writerpkey, + mockContract, + vaultAddress, + expiration2 + ); + vaultImpl.imposeEntitlement(entitlement2, sig2); + assertTrue( + vaultImpl.hasActiveEntitlement(), + "there should be a new active entitlement" + ); + } + + function testNewEntitlementPossibleAfterClearedEntitlement() public { + (address vaultAddress, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + assertTrue( + vaultImpl.hasActiveEntitlement(), + "there should be an active entitlement" + ); + + vm.prank(mockContract); + vaultImpl.clearEntitlement(0); + + assertTrue( + !vaultImpl.hasActiveEntitlement(), + "there should not be any active entitlements" + ); + + uint256 expiration2 = block.timestamp + 3 days; + + ( + Entitlements.Entitlement memory entitlement2, + Signatures.Signature memory sig2 + ) = makeEntitlementAndSignature( + writerpkey, + mockContract, + vaultAddress, + expiration2 + ); + + vaultImpl.imposeEntitlement(entitlement2, sig2); + assertTrue( + vaultImpl.hasActiveEntitlement(), + "there should be a new active entitlement" + ); + } + + function testOnlyOneEntitlementAllowed() public { + (address vaultAddress, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(3333); + uint256 expiration = block.timestamp + 1 days; + + ( + Entitlements.Entitlement memory entitlement, + Signatures.Signature memory sig + ) = makeEntitlementAndSignature( + writerpkey, + mockContract, + vaultAddress, + expiration + ); + + // transfer in with first entitlement + vm.prank(writer); + token.safeTransferFrom( + writer, + vaultAddress, + tokenId, + abi.encode(entitlement, sig) + ); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + address mockContract2 = address(35553445); + assertTrue( + vaultImpl.hasActiveEntitlement(), + "there should be an active entitlement" + ); + + uint256 expiration2 = block.timestamp + 3 days; + + ( + Entitlements.Entitlement memory entitlement2, + Signatures.Signature memory sig2 + ) = makeEntitlementAndSignature( + writerpkey, + mockContract2, + vaultAddress, + expiration2 + ); + + vm.prank(mockContract2); + vm.expectRevert( + "_verifyAndRegisterEntitlement -- existing entitlement must be cleared before registering a new one" + ); + + vaultImpl.imposeEntitlement(entitlement2, sig2); + } + + function testBeneficialOwnerCannotClearEntitlement() public { + (address vaultAddress, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69420); + uint256 expiration = block.timestamp + 1 days; + + ( + Entitlements.Entitlement memory entitlement, + Signatures.Signature memory sig + ) = makeEntitlementAndSignature( + writerpkey, + mockContract, + vaultAddress, + expiration + ); + + // transfer in with first entitlement + vm.prank(writer); + token.safeTransferFrom( + writer, + vaultAddress, + tokenId, + abi.encode(entitlement, sig) + ); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + assertTrue( + vaultImpl.hasActiveEntitlement(), + "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 testClearAndDistributeReturnsNFT() public { + (address vaultAddress, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + vm.prank(mockContract); + vaultImpl.clearEntitlementAndDistribute(0, writer); + + assertTrue( + !vaultImpl.hasActiveEntitlement(), + "there should not be any active entitlements" + ); + + assertTrue( + token.ownerOf(tokenId) == writer, + "Token should be returned to the owner" + ); + } + + function testAirdropsCanBeDisbled() public { + (address vaultAddress, uint256 tokenId) = 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, uint256 tokenId) = 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, uint256 tokenId) = createVaultandAsset(); + + address mockContract = address(69); + uint256 expiration = 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, + abi.encode(entitlement, sig) + ); + HookERC721VaultImplV1 vaultImpl = HookERC721VaultImplV1(vaultAddress); + + vm.expectRevert( + "clearEntitlementAndDistribute -- Only the beneficial owner can recieve the asset" + ); + vm.prank(mockContract); + vaultImpl.clearEntitlementAndDistribute(0, address(0x033333344545)); + } +}