97 lines
3.6 KiB
Solidity
97 lines
3.6 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.8;
|
|
|
|
/// Implements account locking. The main goal of this design is to allow
|
|
/// unlocking of multiple accounts in O(1). To achieve this we keep a list of
|
|
/// locks per account. Every time an account is locked or unlocked, this list is
|
|
/// checked for inactive locks, which are subsequently removed. To ensure that
|
|
/// this operation does not become too expensive in gas costs, a maximum amount
|
|
/// of active locks per account is enforced.
|
|
contract AccountLocks {
|
|
type LockId is bytes32;
|
|
|
|
uint256 public constant MAX_LOCKS_PER_ACCOUNT = 128;
|
|
|
|
mapping(LockId => Lock) private locks;
|
|
mapping(address => Account) private accounts;
|
|
|
|
/// Creates a lock that can be used to lock accounts. The id needs to be
|
|
/// unique and collision resistant. The expiry time is given in unix time.
|
|
function _createLock(LockId id, uint256 expiry) internal {
|
|
require(locks[id].owner == address(0), "Lock already exists");
|
|
locks[id] = Lock(msg.sender, expiry, false);
|
|
}
|
|
|
|
/// Attaches a lock to an account. Only when the lock is unlocked or expires
|
|
/// can the account be unlocked again.
|
|
/// Calling this function triggers a cleanup of inactive locks, making this
|
|
/// an O(N) operation, where N = MAX_LOCKS_PER_ACCOUNT.
|
|
function _lock(address account, LockId lockId) internal {
|
|
require(locks[lockId].owner != address(0), "Lock does not exist");
|
|
LockId[] storage accountLocks = accounts[account].locks;
|
|
removeInactiveLocks(accountLocks);
|
|
require(accountLocks.length < MAX_LOCKS_PER_ACCOUNT, "Max locks reached");
|
|
accountLocks.push(lockId);
|
|
}
|
|
|
|
/// Unlocks a lock, thereby freeing any accounts that are attached to this
|
|
/// lock. This is an O(1) operation. Only the party that created the lock is
|
|
/// allowed to unlock it.
|
|
function _unlock(LockId lockId) internal {
|
|
Lock storage lock = locks[lockId];
|
|
require(lock.owner != address(0), "Lock does not exist");
|
|
require(lock.owner == msg.sender, "Only lock creator can unlock");
|
|
lock.unlocked = true;
|
|
}
|
|
|
|
/// Extends the locks expiry time. Lock must not have already expired.
|
|
/// NOTE: We do not need to check that msg.sender is the lock.owner because
|
|
/// this function is internal, and is only called after all checks have been
|
|
/// performed in Marketplace.fillSlot.
|
|
function _extendLockExpiryTo(LockId lockId, uint256 expiry) internal {
|
|
Lock storage lock = locks[lockId];
|
|
require(lock.owner != address(0), "Lock does not exist");
|
|
require(lock.expiry >= block.timestamp, "Lock already expired");
|
|
lock.expiry = expiry;
|
|
}
|
|
|
|
/// Unlocks an account. This will fail if there are any active locks attached
|
|
/// to this account.
|
|
/// Calling this function triggers a cleanup of inactive locks, making this
|
|
/// an O(N) operation, where N = MAX_LOCKS_PER_ACCOUNT.
|
|
function _unlockAccount() internal {
|
|
LockId[] storage accountLocks = accounts[msg.sender].locks;
|
|
removeInactiveLocks(accountLocks);
|
|
require(accountLocks.length == 0, "Account locked");
|
|
}
|
|
|
|
function removeInactiveLocks(LockId[] storage lockIds) private {
|
|
uint256 index = 0;
|
|
while (true) {
|
|
if (index >= lockIds.length) {
|
|
return;
|
|
}
|
|
if (isInactive(locks[lockIds[index]])) {
|
|
lockIds[index] = lockIds[lockIds.length - 1];
|
|
lockIds.pop();
|
|
} else {
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
|
|
function isInactive(Lock storage lock) private view returns (bool) {
|
|
return lock.unlocked || lock.expiry <= block.timestamp;
|
|
}
|
|
|
|
struct Lock {
|
|
address owner;
|
|
uint256 expiry;
|
|
bool unlocked;
|
|
}
|
|
|
|
struct Account {
|
|
LockId[] locks;
|
|
}
|
|
}
|