Skip to content

Commit

Permalink
Support batch locking/unlocking
Browse files Browse the repository at this point in the history
  • Loading branch information
matejos committed Nov 7, 2023
1 parent 17ebab1 commit be5d409
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 68 deletions.
85 changes: 51 additions & 34 deletions evm/src/Hololocker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,44 +31,68 @@ contract Hololocker is Ownable, IERC721Receiver {
error TokenNotLocked();
error Unauthorized();
error UnlockAlreadyRequested();

// Permits modifications only by the owner of the specified identity.
modifier authorized(address token, uint256 tokenId) {
_authorized(token, tokenId);
_;
}
error InvalidInputArity();

constructor(uint256 lockTime_) {
lockTime = lockTime_;
}

function lock(address token, uint256 tokenId) external {
nftLockInfo[token][tokenId].owner = msg.sender;
nftLockInfo[token][tokenId].operator = msg.sender;
emit Lock(token, msg.sender, tokenId, msg.sender);
IERC721(token).transferFrom(msg.sender, address(this), tokenId);
/// @dev Can lock multiple NFTs in one batch
function lock(address[] memory tokens, uint256[] memory tokenIds, address owner) external {
if (tokens.length != tokenIds.length) {
revert InvalidInputArity();
}
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
uint256 tokenId = tokenIds[i];
nftLockInfo[token][tokenId].owner = owner;
nftLockInfo[token][tokenId].operator = msg.sender;
emit Lock(token, owner, tokenId, msg.sender);
IERC721(token).transferFrom(owner, address(this), tokenId);
}
}

/// @dev Since only authorized user can use this function, it cannot be used without locking NFT beforehand
function requestUnlock(address token, uint256 tokenId) external authorized(token, tokenId) {
LockInfo storage info = nftLockInfo[token][tokenId];
if (info.unlockTime != 0) {
revert UnlockAlreadyRequested();
/// @dev 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)
function requestUnlock(address[] memory tokens, uint256[] memory tokenIds) external {
if (tokens.length != tokenIds.length) {
revert InvalidInputArity();
}
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
uint256 tokenId = tokenIds[i];
LockInfo storage info = nftLockInfo[token][tokenId];
if (msg.sender != info.owner && msg.sender != info.operator) {
revert Unauthorized();
}
if (info.unlockTime != 0) {
revert UnlockAlreadyRequested();
}
uint256 unlockTime = block.timestamp + lockTime;
info.unlockTime = unlockTime;
emit Unlock(token, info.owner, tokenId, info.operator, unlockTime);
}
uint256 unlockTime = block.timestamp + lockTime;
info.unlockTime = unlockTime;
emit Unlock(token, info.owner, tokenId, info.operator, unlockTime);
}

function withdraw(address token, uint256 tokenId) external authorized(token, tokenId) {
LockInfo storage info = nftLockInfo[token][tokenId];
if (info.unlockTime == 0 || block.timestamp < info.unlockTime) {
revert NotUnlockedYet();
function withdraw(address[] memory tokens, uint256[] memory tokenIds) external {
if (tokens.length != tokenIds.length) {
revert InvalidInputArity();
}
for (uint256 i = 0; i < tokens.length; i++) {
address token = tokens[i];
uint256 tokenId = tokenIds[i];
LockInfo storage info = nftLockInfo[token][tokenId];
if (msg.sender != info.owner && msg.sender != info.operator) {
revert Unauthorized();
}
if (info.unlockTime == 0 || block.timestamp < info.unlockTime) {
revert NotUnlockedYet();
}
address owner = info.owner;
emit Withdraw(token, info.owner, tokenId, info.operator);
delete nftLockInfo[token][tokenId];
IERC721(token).transferFrom(address(this), owner, tokenId);
}
address owner = info.owner;
emit Withdraw(token, info.owner, tokenId, info.operator);
delete nftLockInfo[token][tokenId];
IERC721(token).transferFrom(address(this), owner, tokenId);
}

/// @dev Handles initiating a lock upon direct NFT safeTransferFrom function call
Expand All @@ -90,11 +114,4 @@ contract Hololocker is Ownable, IERC721Receiver {
lockTime = newLockTime;
emit LockTimeUpdate(newLockTime);
}

function _authorized(address token, uint256 tokenId) internal view {
LockInfo storage info = nftLockInfo[token][tokenId];
if (msg.sender != info.owner && msg.sender != info.operator) {
revert Unauthorized();
}
}
}
105 changes: 71 additions & 34 deletions evm/test/Hololocker.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,68 +23,78 @@ contract HololockerTest is Test {

Hololocker hololocker;
MockNFT mockNFT;
address token;
uint256 tokenId;
address[] tokens;
uint256[] tokenIds;
address alice = makeAddr("alice");
uint256 maxLockTime;

function setUp() public {
hololocker = new Hololocker(1 minutes);
mockNFT = new MockNFT("X", "Y");
tokenId = 0;
token = address(mockNFT);
mockNFT.mint(address(this), tokenId);
tokenIds.push(0);
tokens.push(address(mockNFT));
mockNFT.mint(address(this), tokenIds[0]);
mockNFT.setApprovalForAll(address(hololocker), true);
maxLockTime = hololocker.MAXIMUM_LOCK_TIME();
}

function requestUnlockAndWithdrawAndAssert(address owner_, address operator_) public {
(uint256 unlockTime, address owner, address operator) = hololocker.nftLockInfo(token, tokenId);
(uint256 unlockTime, address owner, address operator) = hololocker.nftLockInfo(tokens[0], tokenIds[0]);
assertEq(unlockTime, 0);
assertEq(owner, owner_);
assertEq(operator, operator_);
assertEq(mockNFT.ownerOf(tokenId), address(hololocker));
assertEq(mockNFT.ownerOf(tokenIds[0]), address(hololocker));

vm.roll(block.number + 10);
vm.expectEmit(true, true, true, true);
emit Unlock(token, owner, tokenId, operator, block.timestamp + hololocker.lockTime());
hololocker.requestUnlock(token, tokenId);
(unlockTime, owner, operator) = hololocker.nftLockInfo(token, tokenId);
emit Unlock(tokens[0], owner, tokenIds[0], 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_);

vm.warp(block.timestamp + hololocker.lockTime());
vm.expectEmit(true, true, true, true);
emit Withdraw(token, owner, tokenId, operator);
hololocker.withdraw(token, tokenId);
(unlockTime, owner, operator) = hololocker.nftLockInfo(token, tokenId);
emit Withdraw(tokens[0], owner, tokenIds[0], 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));
assertEq(mockNFT.ownerOf(tokenId), owner_);
assertEq(mockNFT.ownerOf(tokenIds[0]), owner_);
}

function test_LockByLockFunction() public {
vm.expectEmit(true, true, true, true);
emit Lock(token, address(this), tokenId, address(this));
hololocker.lock(token, tokenId);
emit Lock(tokens[0], address(this), tokenIds[0], address(this));
hololocker.lock(tokens, tokenIds, address(this));
requestUnlockAndWithdrawAndAssert(address(this), address(this));
}

function test_LockByOperatorLockFunction() public {
mockNFT.setApprovalForAll(alice, true);
vm.startPrank(alice);

vm.expectEmit(true, true, true, true);
emit Lock(tokens[0], address(this), tokenIds[0], alice);
hololocker.lock(tokens, tokenIds, address(this));
requestUnlockAndWithdrawAndAssert(address(this), alice);
}

function test_LockBySafeTransfer() public {
vm.expectEmit(true, true, true, true);
emit Lock(token, address(this), tokenId, address(this));
ERC721(token).safeTransferFrom(address(this), address(hololocker), tokenId, "");
emit Lock(tokens[0], address(this), tokenIds[0], address(this));
ERC721(tokens[0]).safeTransferFrom(address(this), address(hololocker), tokenIds[0], "");
requestUnlockAndWithdrawAndAssert(address(this), address(this));
}

function test_LockByOperatorSafeTransfer() public {
mockNFT.setApprovalForAll(alice, true);
vm.startPrank(alice);
vm.expectEmit(true, true, true, true);
emit Lock(token, address(this), tokenId, alice);
ERC721(token).safeTransferFrom(address(this), address(hololocker), tokenId, "");
emit Lock(tokens[0], address(this), tokenIds[0], alice);
ERC721(tokens[0]).safeTransferFrom(address(this), address(hololocker), tokenIds[0], "");
requestUnlockAndWithdrawAndAssert(address(this), alice);
}

Expand All @@ -96,46 +106,73 @@ contract HololockerTest is Test {
assertEq(hololocker.lockTime(), newValue);
}

function test_CannotLockInvalidInputArity() public {
address[] memory tokens1 = new address[](1);
tokens[0] = tokens[0];
uint256[] memory tokenIds2 = new uint256[](2);

vm.expectRevert(Hololocker.InvalidInputArity.selector);
hololocker.lock(tokens1, tokenIds2, address(this));
}

function test_CannotRequestUnlockInvalidInputArity() public {
address[] memory tokens1 = new address[](1);
tokens[0] = tokens[0];
uint256[] memory tokenIds2 = new uint256[](2);

vm.expectRevert(Hololocker.InvalidInputArity.selector);
hololocker.requestUnlock(tokens1, tokenIds2);
}

function test_CannotWithdrawInvalidInputArity() public {
address[] memory tokens1 = new address[](1);
tokens[0] = tokens[0];
uint256[] memory tokenIds2 = new uint256[](2);

vm.expectRevert(Hololocker.InvalidInputArity.selector);
hololocker.withdraw(tokens1, tokenIds2);
}

function test_CannotRequestUnlockMultipleTimes() public {
hololocker.lock(token, tokenId);
hololocker.requestUnlock(token, tokenId);
hololocker.lock(tokens, tokenIds, address(this));
hololocker.requestUnlock(tokens, tokenIds);
vm.expectRevert(Hololocker.UnlockAlreadyRequested.selector);
hololocker.requestUnlock(token, tokenId);
hololocker.requestUnlock(tokens, tokenIds);
}

function test_CannotRequestUnlockUnauthorized() public {
hololocker.lock(token, tokenId);
hololocker.lock(tokens, tokenIds, address(this));
vm.prank(alice);
vm.expectRevert(Hololocker.Unauthorized.selector);
hololocker.requestUnlock(token, tokenId);
hololocker.requestUnlock(tokens, tokenIds);
}

function test_CannotRequestUnlockWithoutLocking() public {
vm.expectRevert(Hololocker.Unauthorized.selector);
hololocker.requestUnlock(token, tokenId);
hololocker.requestUnlock(tokens, tokenIds);
}

function test_CannotWithdrawUnauthorized() public {
hololocker.lock(token, tokenId);
hololocker.requestUnlock(token, tokenId);
hololocker.lock(tokens, tokenIds, address(this));
hololocker.requestUnlock(tokens, tokenIds);
vm.warp(block.timestamp + hololocker.lockTime());
vm.prank(alice);
vm.expectRevert(Hololocker.Unauthorized.selector);
hololocker.withdraw(token, tokenId);
hololocker.withdraw(tokens, tokenIds);
}

function test_CannotWithdrawWithoutUnlocking() public {
hololocker.lock(token, tokenId);
hololocker.lock(tokens, tokenIds, address(this));
vm.expectRevert(Hololocker.NotUnlockedYet.selector);
hololocker.withdraw(token, tokenId);
hololocker.withdraw(tokens, tokenIds);
}

function test_CannotWithdrawIfUnlockTimeNotReached() public {
hololocker.lock(token, tokenId);
hololocker.requestUnlock(token, tokenId);
hololocker.lock(tokens, tokenIds, address(this));
hololocker.requestUnlock(tokens, tokenIds);
vm.warp(block.timestamp + hololocker.lockTime() - 1);
vm.expectRevert(Hololocker.NotUnlockedYet.selector);
hololocker.withdraw(token, tokenId);
hololocker.withdraw(tokens, tokenIds);
}

function test_CannotSetLockTimeInvalid() public {
Expand Down

0 comments on commit be5d409

Please sign in to comment.