mirror of
https://github.com/status-im/dagger-contracts.git
synced 2025-01-31 16:56:34 +00:00
Account locking
This commit is contained in:
parent
61c41e415d
commit
c5fab40535
65
contracts/AccountLocks.sol
Normal file
65
contracts/AccountLocks.sol
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
contract AccountLocks {
|
||||||
|
uint256 public constant MAX_LOCKS_PER_ACCOUNT = 128;
|
||||||
|
|
||||||
|
mapping(bytes32 => Lock) private locks;
|
||||||
|
mapping(address => Account) private accounts;
|
||||||
|
|
||||||
|
function _createLock(bytes32 id, uint256 expiry) internal {
|
||||||
|
require(locks[id].owner == address(0), "Lock already exists");
|
||||||
|
locks[id] = Lock(msg.sender, expiry, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _lock(address account, bytes32 lockId) internal {
|
||||||
|
require(locks[lockId].owner != address(0), "Lock does not exist");
|
||||||
|
bytes32[] storage accountLocks = accounts[account].locks;
|
||||||
|
removeInactiveLocks(accountLocks);
|
||||||
|
require(accountLocks.length < MAX_LOCKS_PER_ACCOUNT, "Max locks reached");
|
||||||
|
accountLocks.push(lockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _unlock(bytes32 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _unlockAccount() internal {
|
||||||
|
bytes32[] storage accountLocks = accounts[msg.sender].locks;
|
||||||
|
removeInactiveLocks(accountLocks);
|
||||||
|
require(accountLocks.length == 0, "Account locked");
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeInactiveLocks(bytes32[] 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) {
|
||||||
|
// solhint-disable-next-line not-rely-on-time
|
||||||
|
return lock.unlocked || lock.expiry <= block.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Lock {
|
||||||
|
address owner;
|
||||||
|
uint256 expiry;
|
||||||
|
bool unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Account {
|
||||||
|
bytes32[] locks;
|
||||||
|
}
|
||||||
|
}
|
23
contracts/TestAccountLocks.sol
Normal file
23
contracts/TestAccountLocks.sol
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "./AccountLocks.sol";
|
||||||
|
|
||||||
|
// exposes internal functions for testing
|
||||||
|
contract TestAccountLocks is AccountLocks {
|
||||||
|
function createLock(bytes32 id, uint256 expiry) public {
|
||||||
|
_createLock(id, expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock(address account, bytes32 id) public {
|
||||||
|
_lock(account, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlock(bytes32 id) public {
|
||||||
|
_unlock(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlockAccount() public {
|
||||||
|
_unlockAccount();
|
||||||
|
}
|
||||||
|
}
|
165
test/AccountLocks.js
Normal file
165
test/AccountLocks.js
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
const { ethers } = require("hardhat")
|
||||||
|
const { expect } = require("chai")
|
||||||
|
const { exampleLock } = require("./examples")
|
||||||
|
const { now } = require("./time")
|
||||||
|
|
||||||
|
describe("Account Locks", function () {
|
||||||
|
let locks
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
let AccountLocks = await ethers.getContractFactory("TestAccountLocks")
|
||||||
|
locks = await AccountLocks.deploy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("creating a lock", function () {
|
||||||
|
it("allows creation of a lock with an expiry time", async function () {
|
||||||
|
let { id, expiry } = exampleLock()
|
||||||
|
await locks.createLock(id, expiry)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to create a lock with an existing id", async function () {
|
||||||
|
let { id, expiry } = exampleLock()
|
||||||
|
await locks.createLock(id, expiry)
|
||||||
|
await expect(locks.createLock(id, expiry + 1)).to.be.revertedWith(
|
||||||
|
"Lock already exists"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("locking an account", function () {
|
||||||
|
let lock
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
lock = exampleLock()
|
||||||
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("locks an account", async function () {
|
||||||
|
let [account] = await ethers.getSigners()
|
||||||
|
await locks.lock(account.address, lock.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to lock account when lock does not exist", async function () {
|
||||||
|
let [account] = await ethers.getSigners()
|
||||||
|
let nonexistent = exampleLock().id
|
||||||
|
await expect(locks.lock(account.address, nonexistent)).to.be.revertedWith(
|
||||||
|
"Lock does not exist"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("unlocking a lock", function () {
|
||||||
|
let lock
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
lock = exampleLock()
|
||||||
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("unlocks a lock", async function () {
|
||||||
|
await locks.unlock(lock.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to unlock a lock that does not exist", async function () {
|
||||||
|
let nonexistent = exampleLock().id
|
||||||
|
await expect(locks.unlock(nonexistent)).to.be.revertedWith(
|
||||||
|
"Lock does not exist"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to unlock by someone other than the creator", async function () {
|
||||||
|
let [_, other] = await ethers.getSigners()
|
||||||
|
await expect(locks.connect(other).unlock(lock.id)).to.be.revertedWith(
|
||||||
|
"Only lock creator can unlock"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("unlocking an account", function () {
|
||||||
|
it("unlocks an account that has not been locked", async function () {
|
||||||
|
await locks.unlockAccount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("unlocks an account whose locks have been unlocked", async function () {
|
||||||
|
let [account] = await ethers.getSigners()
|
||||||
|
let lock = exampleLock()
|
||||||
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
|
await locks.lock(account.address, lock.id)
|
||||||
|
await locks.unlock(lock.id)
|
||||||
|
await locks.unlockAccount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("unlocks an account whose locks have expired", async function () {
|
||||||
|
let [account] = await ethers.getSigners()
|
||||||
|
let lock = { ...exampleLock(), expiry: now() }
|
||||||
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
|
await locks.lock(account.address, lock.id)
|
||||||
|
await locks.unlockAccount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("unlocks multiple accounts tied to the same lock", async function () {
|
||||||
|
let [account0, account1] = await ethers.getSigners()
|
||||||
|
let lock = exampleLock()
|
||||||
|
await locks.createLock(lock.id, lock.expiry)
|
||||||
|
await locks.lock(account0.address, lock.id)
|
||||||
|
await locks.lock(account1.address, lock.id)
|
||||||
|
await locks.unlock(lock.id)
|
||||||
|
await locks.connect(account0).unlockAccount()
|
||||||
|
await locks.connect(account1).unlockAccount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to unlock when some locks are still locked", async function () {
|
||||||
|
let [account] = await ethers.getSigners()
|
||||||
|
let [lock1, lock2] = [exampleLock(), exampleLock()]
|
||||||
|
await locks.createLock(lock1.id, lock1.expiry)
|
||||||
|
await locks.createLock(lock2.id, lock2.expiry)
|
||||||
|
await locks.lock(account.address, lock1.id)
|
||||||
|
await locks.lock(account.address, lock2.id)
|
||||||
|
await locks.unlock(lock1.id)
|
||||||
|
await expect(locks.unlockAccount()).to.be.revertedWith("Account locked")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("limits", function () {
|
||||||
|
let maxlocks
|
||||||
|
let account
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
maxlocks = await locks.MAX_LOCKS_PER_ACCOUNT()
|
||||||
|
;[account] = await ethers.getSigners()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function addLock() {
|
||||||
|
let { id, expiry } = exampleLock()
|
||||||
|
await locks.createLock(id, expiry)
|
||||||
|
await locks.lock(account.address, id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
it("supports a limited amount of locks per account", async function () {
|
||||||
|
for (let i = 0; i < maxlocks; i++) {
|
||||||
|
await addLock()
|
||||||
|
}
|
||||||
|
await expect(addLock()).to.be.revertedWith("Max locks reached")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("doesn't count unlocked locks towards the limit", async function () {
|
||||||
|
for (let i = 0; i < maxlocks; i++) {
|
||||||
|
let id = await addLock()
|
||||||
|
await locks.unlock(id)
|
||||||
|
}
|
||||||
|
await expect(addLock()).not.to.be.reverted
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles maximum amount of locks within gas limit", async function () {
|
||||||
|
let ids = []
|
||||||
|
for (let i = 0; i < maxlocks; i++) {
|
||||||
|
ids.push(await addLock())
|
||||||
|
}
|
||||||
|
for (let id of ids) {
|
||||||
|
await locks.unlock(id)
|
||||||
|
}
|
||||||
|
await locks.unlockAccount()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -1,4 +1,5 @@
|
|||||||
const { ethers } = require("hardhat")
|
const { ethers } = require("hardhat")
|
||||||
|
const { now, hours } = require("./time")
|
||||||
|
|
||||||
const exampleRequest = () => ({
|
const exampleRequest = () => ({
|
||||||
duration: 150, // 150 blocks ≈ half an hour
|
duration: 150, // 150 blocks ≈ half an hour
|
||||||
@ -11,7 +12,12 @@ const exampleRequest = () => ({
|
|||||||
|
|
||||||
const exampleBid = () => ({
|
const exampleBid = () => ({
|
||||||
price: 42,
|
price: 42,
|
||||||
bidExpiry: Math.round(Date.now() / 1000) + 60 * 60, // 1 hour from now
|
bidExpiry: now() + hours(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = { exampleRequest, exampleBid }
|
const exampleLock = () => ({
|
||||||
|
id: ethers.utils.randomBytes(32),
|
||||||
|
expiry: now() + hours(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = { exampleRequest, exampleBid, exampleLock }
|
||||||
|
6
test/time.js
Normal file
6
test/time.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const now = () => Math.round(Date.now() / 1000)
|
||||||
|
const hours = (amount) => amount * minutes(60)
|
||||||
|
const minutes = (amount) => amount * seconds(60)
|
||||||
|
const seconds = (amount) => amount
|
||||||
|
|
||||||
|
module.exports = { now, hours, minutes, seconds }
|
Loading…
x
Reference in New Issue
Block a user