Skip to content

Commit

Permalink
Add HololockerInterface
Browse files Browse the repository at this point in the history
  • Loading branch information
matejos committed Nov 8, 2023
1 parent 01e6bef commit 54b0a9b
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 43 deletions.
87 changes: 58 additions & 29 deletions evm/src/Hololocker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,21 @@
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./HololockerInterface.sol";

contract Hololocker is Ownable, IERC721Receiver {
struct LockInfo {
// Timestamp when NFT will be withdrawable, 0 if withdrawal hasn't been requested
uint256 unlockTime;
// Rightful owner of the NFT
address owner;
// Account that initiated the lock
address operator;
}

contract Hololocker is HololockerInterface, Ownable {
/// Upper limit for lockTime
uint256 public constant MAXIMUM_LOCK_TIME = 1 days;

/// Delay that has to pass from unlocking until NFT can be withdrawn
uint256 public lockTime;

// token address => token ID => LockInfo
// NFT address => NFT ID => LockInfo
mapping(address => mapping(uint256 => LockInfo)) public nftLockInfo;

event Lock(address indexed token, address indexed owner, uint256 tokenId, address operator);
event Unlock(address indexed token, address indexed owner, uint256 tokenId, address operator, uint256 unlockTime);
event Withdraw(address indexed token, address indexed owner, uint256 tokenId, address operator);
event LockTimeUpdate(uint256 newValue);

error InvalidLockTime();
error NotUnlockedYet();
error TokenNotLocked();
error Unauthorized();
error UnlockAlreadyRequested();
error InvalidInputArity();
Expand All @@ -37,7 +25,22 @@ contract Hololocker is Ownable, IERC721Receiver {
lockTime = lockTime_;
}

/// @dev Can lock multiple NFTs in one batch
/// @notice Returns `LockInfo` for specified `token => tokenId`
/// @param token NFT tokens contract address
/// @param tokenId NFT tokens identifier
/// @return The `LockInfo` struct information
function getLockInfo(address token, uint256 tokenId) external view returns (LockInfo memory) {
return nftLockInfo[token][tokenId];
}

/// @notice Initiates a lock for one or more NFTs
/// @dev Reverts if `tokens` length is not equal to `tokenIds` length.
/// Stores a `LockInfo` struct `{owner: owner, operator: msg.sender, unlockTime: 0}` for each `token => tokenId`
/// Emits `Lock` event.
/// Transfers each token:tokenId to this contract.
/// @param tokens NFT tokens contract addresses
/// @param tokenIds NFT tokens identifiers
/// @param owner NFT tokens owner
function lock(address[] memory tokens, uint256[] memory tokenIds, address owner) external {
if (tokens.length != tokenIds.length) {
revert InvalidInputArity();
Expand All @@ -52,8 +55,17 @@ contract Hololocker is Ownable, IERC721Receiver {
}
}

/// @dev Since only authorized user can use this function, it cannot be used without locking NFT beforehand,
/// @notice Requests unlock for one or more NFTs
/// @dev Reverts if `tokens` length is not equal to `tokenIds` length.
/// Reverts if msg.sender is neither `owner` nor `operator` of LockInfo struct for
/// any of the input tokens.
/// Reverts if `unlockTime` of LockInfo struct for any of the input tokens is not 0.
/// Modifies a `LockInfo` struct `{unlockTime: block.timestamp + lockTime}` for each `token => tokenId`
/// Emits `Unlock` event.
/// Since only authorized user can use this function, it cannot be used without locking NFT beforehand,
/// because both info.owner and info.operator would be address(0)
/// @param tokens NFT tokens contract addresses
/// @param tokenIds NFT tokens identifiers
function requestUnlock(address[] memory tokens, uint256[] memory tokenIds) external {
if (tokens.length != tokenIds.length) {
revert InvalidInputArity();
Expand All @@ -74,6 +86,16 @@ contract Hololocker is Ownable, IERC721Receiver {
}
}

/// @notice Withdraws one or more NFTs to their rightful owner
/// @dev Reverts if `tokens` length is not equal to `tokenIds` length.
/// Reverts if msg.sender is neither `owner` nor `operator` of LockInfo struct for
/// any of the input tokens.
/// Reverts if `unlockTime` of LockInfo struct for any of the input tokens is
/// either 0 or greater than block.timestamp.
/// Modifies a `LockInfo` struct `{unlockTime: block.timestamp + lockTime}` for each `token => tokenId`
/// Emits `Unlock` event.
/// @param tokens NFT tokens contract addresses
/// @param tokenIds NFT tokens identifiers
function withdraw(address[] memory tokens, uint256[] memory tokenIds) external {
if (tokens.length != tokenIds.length) {
revert InvalidInputArity();
Expand All @@ -95,7 +117,22 @@ contract Hololocker is Ownable, IERC721Receiver {
}
}

/// @dev Handles initiating a lock upon direct NFT safeTransferFrom function call
/// @notice Changes `lockTime` variable that is used in `requestUnlock`.
/// @dev Reverts if new value is greater than `MAXIMUM_LOCK_TIME`.
/// Emits `LockTimeUpdate` event.
/// @param newLockTime New lockTime value
function setLockTime(uint256 newLockTime) external onlyOwner {
if (newLockTime > MAXIMUM_LOCK_TIME) {
revert InvalidLockTime();
}
lockTime = newLockTime;
emit LockTimeUpdate(newLockTime);
}

/// @dev From IERC721Receiver, handles initiating a lock upon direct NFT safeTransferFrom function call
/// @param operator The address which called `safeTransferFrom` function
/// @param from The address which previously owned the token
/// @param tokenId The NFT identifier which is being transferred
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata)
external
returns (bytes4)
Expand All @@ -106,12 +143,4 @@ contract Hololocker is Ownable, IERC721Receiver {
emit Lock(token, from, tokenId, operator);
return IERC721Receiver.onERC721Received.selector;
}

function setLockTime(uint256 newLockTime) external onlyOwner {
if (newLockTime > MAXIMUM_LOCK_TIME) {
revert InvalidLockTime();
}
lockTime = newLockTime;
emit LockTimeUpdate(newLockTime);
}
}
89 changes: 89 additions & 0 deletions evm/src/HololockerInterface.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

interface HololockerInterface is IERC721Receiver {
/// @dev Data structure that must exist for each locked NFT
struct LockInfo {
/// Timestamp when NFT will be withdrawable, 0 if unlock hasn't been requested
uint256 unlockTime;
/// Rightful owner of the NFT
address owner;
/// Account that initiated the lock
address operator;
}

/// @dev This emits when NFT is locked, either via lock function or via NFT being sent to this contract
/// @param token NFT address
/// @param owner Rightful owner of the NFT
/// @param tokenId NFT token identifier
/// @param operator Address initiating the lock
event Lock(address indexed token, address indexed owner, uint256 tokenId, address operator);

/// @dev This emits when NFT is requested to unlock.
/// @param token NFT address
/// @param owner Rightful owner of the NFT
/// @param tokenId NFT token identifier
/// @param operator Address initiating the unlock request
/// @param unlockTime Timestamp when NFT will be withdrawable.
event Unlock(address indexed token, address indexed owner, uint256 tokenId, address operator, uint256 unlockTime);

/// @dev This emits when NFT is withdrawn.
/// @param token NFT address
/// @param owner Rightful owner of the NFT
/// @param tokenId NFT token identifier
/// @param operator Address initiating the withdraw
event Withdraw(address indexed token, address indexed owner, uint256 tokenId, address operator);

/// @dev This emits when lockTime value changes.
/// @param newValue New lockTime value
event LockTimeUpdate(uint256 newValue);

/// @notice Returns `LockInfo` for specified `token => tokenId`
/// @param token NFT tokens contract address
/// @param tokenId NFT tokens identifier
/// @return The `LockInfo` struct information
function getLockInfo(address token, uint256 tokenId) external view returns (LockInfo memory);

/// @notice Initiates a lock for one or more NFTs
/// @dev Reverts if `tokens` length is not equal to `tokenIds` length.
/// Stores a `LockInfo` struct `{owner: owner, operator: msg.sender, unlockTime: 0}` for each `token => tokenId`
/// Emits `Lock` event.
/// Transfers each token:tokenId to this contract.
/// @param tokens NFT tokens contract addresses
/// @param tokenIds NFT tokens identifiers
/// @param owner NFT tokens owner
function lock(address[] memory tokens, uint256[] memory tokenIds, address owner) external;

/// @notice Requests unlock for one or more NFTs
/// @dev Reverts if `tokens` length is not equal to `tokenIds` length.
/// Reverts if msg.sender is neither `owner` nor `operator` of LockInfo struct for
/// any of the input tokens.
/// Reverts if `unlockTime` of LockInfo struct for any of the input tokens is not 0.
/// Modifies a `LockInfo` struct `{unlockTime: block.timestamp + lockTime}` for each `token => tokenId`
/// Emits `Unlock` event.
/// @param tokens NFT tokens contract addresses
/// @param tokenIds NFT tokens identifiers
function requestUnlock(address[] memory tokens, uint256[] memory tokenIds) external;

/// @notice Withdraws one or more NFTs to their rightful owner
/// @dev Reverts if `tokens` length is not equal to `tokenIds` length.
/// Reverts if msg.sender is neither `owner` nor `operator` of LockInfo struct for
/// any of the input tokens.
/// Reverts if `unlockTime` of LockInfo struct for any of the input tokens is
/// either 0 or greater than block.timestamp.
/// Modifies a `LockInfo` struct `{unlockTime: block.timestamp + lockTime}` for each `token => tokenId`
/// Emits `Unlock` event.
/// @param tokens NFT tokens contract addresses
/// @param tokenIds NFT tokens identifiers
function withdraw(address[] memory tokens, uint256[] memory tokenIds) external;

/// @notice Changes `lockTime` variable that is used in `requestUnlock`.
/// @dev This function should be protected with appropriate access control mechanisms.
/// The new value should be checked against a sane upper limit constant, which if exceeded,
/// should cause a revert.
/// Emits `LockTimeUpdate` event.
/// @param newLockTime New lockTime value
function setLockTime(uint256 newLockTime) external;
}
29 changes: 15 additions & 14 deletions evm/test/Hololocker.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,30 @@ contract HololockerTest is Test {
}

function requestUnlockAndWithdrawAndAssert(address owner_, address operator_) public {
(uint256 unlockTime, address owner, address operator) = hololocker.nftLockInfo(tokens[0], tokenIds[0]);
assertEq(unlockTime, 0);
assertEq(owner, owner_);
assertEq(operator, operator_);
Hololocker.LockInfo memory info;
info = hololocker.getLockInfo(tokens[0], tokenIds[0]);
assertEq(info.unlockTime, 0);
assertEq(info.owner, owner_);
assertEq(info.operator, operator_);
assertEq(mockNFT.ownerOf(tokenIds[0]), address(hololocker));

vm.roll(block.number + 10);
vm.expectEmit(true, true, true, true);
emit Unlock(tokens[0], owner, tokenIds[0], operator, block.timestamp + hololocker.lockTime());
emit Unlock(tokens[0], info.owner, tokenIds[0], info.operator, block.timestamp + hololocker.lockTime());
hololocker.requestUnlock(tokens, tokenIds);
(unlockTime, owner, operator) = hololocker.nftLockInfo(tokens[0], tokenIds[0]);
assertEq(unlockTime, block.timestamp + hololocker.lockTime());
assertEq(owner, owner_);
assertEq(operator, operator_);
info = hololocker.getLockInfo(tokens[0], tokenIds[0]);
assertEq(info.unlockTime, block.timestamp + hololocker.lockTime());
assertEq(info.owner, owner_);
assertEq(info.operator, operator_);

vm.warp(block.timestamp + hololocker.lockTime());
vm.expectEmit(true, true, true, true);
emit Withdraw(tokens[0], owner, tokenIds[0], operator);
emit Withdraw(tokens[0], info.owner, tokenIds[0], info.operator);
hololocker.withdraw(tokens, tokenIds);
(unlockTime, owner, operator) = hololocker.nftLockInfo(tokens[0], tokenIds[0]);
assertEq(unlockTime, 0);
assertEq(owner, address(0));
assertEq(operator, address(0));
info = hololocker.getLockInfo(tokens[0], tokenIds[0]);
assertEq(info.unlockTime, 0);
assertEq(info.owner, address(0));
assertEq(info.operator, address(0));
assertEq(mockNFT.ownerOf(tokenIds[0]), owner_);
}

Expand Down

0 comments on commit 54b0a9b

Please sign in to comment.