2025-01-13 12:04:03 +01:00
|
|
|
const { expect } = require("chai")
|
|
|
|
const { ethers } = require("hardhat")
|
|
|
|
const { randomBytes } = ethers.utils
|
2025-01-15 14:51:53 +01:00
|
|
|
const { currentTime, advanceTimeToForNextBlock } = require("./evm")
|
2025-01-13 12:04:03 +01:00
|
|
|
|
|
|
|
describe("Vault", function () {
|
|
|
|
let token
|
|
|
|
let vault
|
2025-01-14 12:09:48 +01:00
|
|
|
let account
|
2025-01-13 12:04:03 +01:00
|
|
|
|
|
|
|
beforeEach(async function () {
|
|
|
|
const TestToken = await ethers.getContractFactory("TestToken")
|
|
|
|
token = await TestToken.deploy()
|
|
|
|
const Vault = await ethers.getContractFactory("Vault")
|
|
|
|
vault = await Vault.deploy(token.address)
|
2025-01-14 12:09:48 +01:00
|
|
|
;[, account] = await ethers.getSigners()
|
|
|
|
await token.mint(account.address, 1_000_000)
|
2025-01-13 12:04:03 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
describe("depositing", function () {
|
2025-01-14 12:09:48 +01:00
|
|
|
const context = randomBytes(32)
|
2025-01-13 12:04:03 +01:00
|
|
|
const amount = 42
|
|
|
|
|
|
|
|
it("accepts deposits of tokens", async function () {
|
2025-01-14 12:09:48 +01:00
|
|
|
await token.connect(account).approve(vault.address, amount)
|
|
|
|
await vault.deposit(context, account.address, amount)
|
|
|
|
expect(await vault.balance(context, account.address)).to.equal(amount)
|
2025-01-13 12:04:03 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
it("keeps custody of tokens that are deposited", async function () {
|
2025-01-14 12:09:48 +01:00
|
|
|
await token.connect(account).approve(vault.address, amount)
|
|
|
|
await vault.deposit(context, account.address, amount)
|
2025-01-13 12:04:03 +01:00
|
|
|
expect(await token.balanceOf(vault.address)).to.equal(amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("deposit fails when tokens cannot be transferred", async function () {
|
2025-01-14 12:09:48 +01:00
|
|
|
await token.connect(account).approve(vault.address, amount - 1)
|
|
|
|
const depositing = vault.deposit(context, account.address, amount)
|
2025-01-13 12:04:03 +01:00
|
|
|
await expect(depositing).to.be.revertedWith("insufficient allowance")
|
|
|
|
})
|
|
|
|
|
2025-01-15 11:43:18 +01:00
|
|
|
it("adds multiple deposits to the balance", async function () {
|
2025-01-14 12:09:48 +01:00
|
|
|
await token.connect(account).approve(vault.address, amount)
|
|
|
|
await vault.deposit(context, account.address, amount / 2)
|
|
|
|
await vault.deposit(context, account.address, amount / 2)
|
|
|
|
expect(await vault.balance(context, account.address)).to.equal(amount)
|
2025-01-13 12:04:03 +01:00
|
|
|
})
|
|
|
|
|
2025-01-14 12:09:48 +01:00
|
|
|
it("separates deposits from different contexts", async function () {
|
|
|
|
const context1 = randomBytes(32)
|
|
|
|
const context2 = randomBytes(32)
|
|
|
|
await token.connect(account).approve(vault.address, 3)
|
|
|
|
await vault.deposit(context1, account.address, 1)
|
|
|
|
await vault.deposit(context2, account.address, 2)
|
|
|
|
expect(await vault.balance(context1, account.address)).to.equal(1)
|
|
|
|
expect(await vault.balance(context2, account.address)).to.equal(2)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("separates deposits from different controllers", async function () {
|
|
|
|
const [, , controller1, controller2] = await ethers.getSigners()
|
|
|
|
const vault1 = vault.connect(controller1)
|
|
|
|
const vault2 = vault.connect(controller2)
|
|
|
|
await token.connect(account).approve(vault.address, 3)
|
|
|
|
await vault1.deposit(context, account.address, 1)
|
|
|
|
await vault2.deposit(context, account.address, 2)
|
|
|
|
expect(await vault1.balance(context, account.address)).to.equal(1)
|
|
|
|
expect(await vault2.balance(context, account.address)).to.equal(2)
|
2025-01-13 12:04:03 +01:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe("withdrawing", function () {
|
2025-01-14 12:09:48 +01:00
|
|
|
const context = randomBytes(32)
|
|
|
|
const amount = 42
|
|
|
|
|
|
|
|
beforeEach(async function () {
|
|
|
|
await token.connect(account).approve(vault.address, amount)
|
|
|
|
await vault.deposit(context, account.address, amount)
|
|
|
|
})
|
2025-01-13 12:04:03 +01:00
|
|
|
|
|
|
|
it("can withdraw a deposit", async function () {
|
2025-01-14 12:09:48 +01:00
|
|
|
const before = await token.balanceOf(account.address)
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
const after = await token.balanceOf(account.address)
|
|
|
|
expect(after - before).to.equal(amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("empties the balance when withdrawing", async function () {
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
expect(await vault.balance(context, account.address)).to.equal(0)
|
2025-01-13 12:04:03 +01:00
|
|
|
})
|
|
|
|
|
2025-01-14 12:09:48 +01:00
|
|
|
it("does not withdraw more than once", async function () {
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
const before = await token.balanceOf(account.address)
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
const after = await token.balanceOf(account.address)
|
|
|
|
expect(after).to.equal(before)
|
2025-01-13 12:04:03 +01:00
|
|
|
})
|
|
|
|
})
|
2025-01-14 14:11:53 +01:00
|
|
|
|
|
|
|
describe("burning", function () {
|
|
|
|
const context = randomBytes(32)
|
|
|
|
const amount = 42
|
|
|
|
|
|
|
|
beforeEach(async function () {
|
|
|
|
await token.connect(account).approve(vault.address, amount)
|
|
|
|
await vault.deposit(context, account.address, amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("can burn a deposit", async function () {
|
|
|
|
await vault.burn(context, account.address)
|
|
|
|
expect(await vault.balance(context, account.address)).to.equal(0)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("no longer allows withdrawal", async function () {
|
|
|
|
await vault.burn(context, account.address)
|
|
|
|
const before = await token.balanceOf(account.address)
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
const after = await token.balanceOf(account.address)
|
|
|
|
expect(after).to.equal(before)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("moves the tokens to address 0xdead", async function () {
|
|
|
|
const dead = "0x000000000000000000000000000000000000dead"
|
|
|
|
const before = await token.balanceOf(dead)
|
|
|
|
await vault.burn(context, account.address)
|
|
|
|
const after = await token.balanceOf(dead)
|
|
|
|
expect(after - before).to.equal(amount)
|
|
|
|
})
|
|
|
|
})
|
2025-01-15 10:08:10 +01:00
|
|
|
|
|
|
|
describe("transfering", function () {
|
|
|
|
const context = randomBytes(32)
|
|
|
|
const amount = 42
|
|
|
|
|
2025-01-15 11:43:18 +01:00
|
|
|
let account2
|
|
|
|
let account3
|
2025-01-15 10:08:10 +01:00
|
|
|
|
|
|
|
beforeEach(async function () {
|
2025-01-15 11:43:18 +01:00
|
|
|
;[, , account2, account3] = await ethers.getSigners()
|
2025-01-15 10:08:10 +01:00
|
|
|
await token.connect(account).approve(vault.address, amount)
|
|
|
|
await vault.deposit(context, account.address, amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("can transfer tokens from one recipient to the other", async function () {
|
2025-01-15 11:43:18 +01:00
|
|
|
await vault.transfer(context, account.address, account2.address, amount)
|
2025-01-15 10:08:10 +01:00
|
|
|
expect(await vault.balance(context, account.address)).to.equal(0)
|
2025-01-15 11:43:18 +01:00
|
|
|
expect(await vault.balance(context, account2.address)).to.equal(amount)
|
2025-01-15 10:08:10 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
it("can transfer part of a balance", async function () {
|
2025-01-15 11:43:18 +01:00
|
|
|
await vault.transfer(context, account.address, account2.address, 10)
|
2025-01-15 10:08:10 +01:00
|
|
|
expect(await vault.balance(context, account.address)).to.equal(
|
|
|
|
amount - 10
|
|
|
|
)
|
2025-01-15 11:43:18 +01:00
|
|
|
expect(await vault.balance(context, account2.address)).to.equal(10)
|
2025-01-15 10:08:10 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
it("does not transfer more than the balance", async function () {
|
|
|
|
await expect(
|
2025-01-15 11:43:18 +01:00
|
|
|
vault.transfer(context, account.address, account2.address, amount + 1)
|
2025-01-15 10:08:10 +01:00
|
|
|
).to.be.revertedWith("InsufficientBalance")
|
|
|
|
})
|
|
|
|
|
|
|
|
it("can withdraw funds that were transfered in", async function () {
|
2025-01-15 11:43:18 +01:00
|
|
|
await vault.transfer(context, account.address, account2.address, amount)
|
|
|
|
const before = await token.balanceOf(account2.address)
|
|
|
|
await vault.withdraw(context, account2.address)
|
|
|
|
const after = await token.balanceOf(account2.address)
|
2025-01-15 10:08:10 +01:00
|
|
|
expect(after - before).to.equal(amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("cannot withdraw funds that were transfered out", async function () {
|
2025-01-15 11:43:18 +01:00
|
|
|
await vault.transfer(context, account.address, account2.address, amount)
|
2025-01-15 10:08:10 +01:00
|
|
|
const before = await token.balanceOf(account.address)
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
const after = await token.balanceOf(account.address)
|
|
|
|
expect(after).to.equal(before)
|
|
|
|
})
|
2025-01-15 11:43:18 +01:00
|
|
|
|
|
|
|
it("can transfer out funds that were transfered in", async function () {
|
|
|
|
await vault.transfer(context, account.address, account2.address, amount)
|
|
|
|
await vault.transfer(context, account2.address, account3.address, amount)
|
|
|
|
expect(await vault.balance(context, account2.address)).to.equal(0)
|
|
|
|
expect(await vault.balance(context, account3.address)).to.equal(amount)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe("designating", async function () {
|
|
|
|
const context = randomBytes(32)
|
|
|
|
const amount = 42
|
|
|
|
|
|
|
|
let account2
|
|
|
|
|
|
|
|
beforeEach(async function () {
|
|
|
|
;[, , account2] = await ethers.getSigners()
|
|
|
|
await token.connect(account).approve(vault.address, amount)
|
|
|
|
await vault.deposit(context, account.address, amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("can designate tokens for a single recipient", async function () {
|
|
|
|
await vault.designate(context, account.address, amount)
|
|
|
|
expect(await vault.designated(context, account.address)).to.equal(amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("can designate part of the balance", async function () {
|
|
|
|
await vault.designate(context, account.address, 10)
|
|
|
|
expect(await vault.designated(context, account.address)).to.equal(10)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("adds up designated tokens", async function () {
|
|
|
|
await vault.designate(context, account.address, 10)
|
|
|
|
await vault.designate(context, account.address, 10)
|
|
|
|
expect(await vault.designated(context, account.address)).to.equal(20)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("cannot designate more than the undesignated balance", async function () {
|
|
|
|
await vault.designate(context, account.address, amount)
|
|
|
|
await expect(
|
|
|
|
vault.designate(context, account.address, 1)
|
|
|
|
).to.be.revertedWith("InsufficientBalance")
|
|
|
|
})
|
|
|
|
|
|
|
|
it("does not change the balance", async function () {
|
|
|
|
await vault.designate(context, account.address, 10)
|
|
|
|
expect(await vault.balance(context, account.address)).to.equal(amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("does not allow designated tokens to be transfered", async function () {
|
|
|
|
await vault.designate(context, account.address, 1)
|
|
|
|
await expect(
|
|
|
|
vault.transfer(context, account.address, account2.address, amount)
|
|
|
|
).to.be.revertedWith("InsufficientBalance")
|
|
|
|
})
|
|
|
|
|
|
|
|
it("allows designated tokens to be withdrawn", async function () {
|
|
|
|
await vault.designate(context, account.address, 10)
|
|
|
|
const before = await token.balanceOf(account.address)
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
const after = await token.balanceOf(account.address)
|
|
|
|
expect(after - before).to.equal(amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("does not withdraw designated tokens more than once", async function () {
|
|
|
|
await vault.designate(context, account.address, 10)
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
const before = await token.balanceOf(account.address)
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
const after = await token.balanceOf(account.address)
|
|
|
|
expect(after).to.equal(before)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("allows designated tokens to be burned", async function () {
|
|
|
|
await vault.designate(context, account.address, 10)
|
|
|
|
await vault.burn(context, account.address)
|
|
|
|
expect(await vault.balance(context, account.address)).to.equal(0)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("moves burned designated tokens to address 0xdead", async function () {
|
|
|
|
const dead = "0x000000000000000000000000000000000000dead"
|
|
|
|
await vault.designate(context, account.address, 10)
|
|
|
|
const before = await token.balanceOf(dead)
|
|
|
|
await vault.burn(context, account.address)
|
|
|
|
const after = await token.balanceOf(dead)
|
|
|
|
expect(after - before).to.equal(amount)
|
|
|
|
})
|
2025-01-15 10:08:10 +01:00
|
|
|
})
|
2025-01-15 14:51:53 +01:00
|
|
|
|
|
|
|
describe("locking", async function () {
|
|
|
|
const context = randomBytes(32)
|
|
|
|
const amount = 42
|
|
|
|
|
|
|
|
beforeEach(async function () {
|
|
|
|
await token.connect(account).approve(vault.address, amount)
|
|
|
|
await vault.deposit(context, account.address, amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("can lock up all tokens in a context", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
let maximum = start + 20
|
|
|
|
await vault.lockup(context, expiry, maximum)
|
|
|
|
expect((await vault.lock(context))[0]).to.equal(expiry)
|
|
|
|
expect((await vault.lock(context))[1]).to.equal(maximum)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("cannot lock up when already locked", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
let maximum = start + 20
|
|
|
|
await vault.lockup(context, expiry, maximum)
|
|
|
|
const locking = vault.lockup(context, expiry, maximum)
|
|
|
|
await expect(locking).to.be.revertedWith("AlreadyLocked")
|
|
|
|
})
|
|
|
|
|
|
|
|
it("cannot lock when expiry is past maximum", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
let maximum = start + 9
|
|
|
|
const locking = vault.lockup(context, expiry, maximum)
|
|
|
|
await expect(locking).to.be.revertedWith("ExpiryPastMaximum")
|
|
|
|
})
|
|
|
|
|
|
|
|
it("does not allow withdrawal before lock expires", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
await vault.lockup(context, expiry, expiry)
|
|
|
|
await advanceTimeToForNextBlock(expiry - 1)
|
|
|
|
const withdrawing = vault.withdraw(context, account.address)
|
|
|
|
await expect(withdrawing).to.be.revertedWith("Locked")
|
|
|
|
})
|
|
|
|
|
|
|
|
it("allows withdrawal after lock expires", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
await vault.lockup(context, expiry, expiry)
|
|
|
|
await advanceTimeToForNextBlock(expiry)
|
|
|
|
const before = await token.balanceOf(account.address)
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
const after = await token.balanceOf(account.address)
|
|
|
|
expect(after - before).to.equal(amount)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("can extend a lock expiry up to its maximum", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
let maximum = start + 20
|
|
|
|
await vault.lockup(context, expiry, maximum)
|
|
|
|
await vault.extend(context, start + 15)
|
|
|
|
expect((await vault.lock(context))[0]).to.equal(start + 15)
|
|
|
|
await vault.extend(context, start + 20)
|
|
|
|
expect((await vault.lock(context))[0]).to.equal(start + 20)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("cannot extend a lock past its maximum", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
let maximum = start + 20
|
|
|
|
await vault.lockup(context, expiry, maximum)
|
|
|
|
const extending = vault.extend(context, start + 21)
|
|
|
|
await expect(extending).to.be.revertedWith("ExpiryPastMaximum")
|
|
|
|
})
|
|
|
|
|
|
|
|
it("cannot move expiry forward", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
let maximum = start + 20
|
|
|
|
await vault.lockup(context, expiry, maximum)
|
|
|
|
const extending = vault.extend(context, start + 9)
|
|
|
|
await expect(extending).to.be.revertedWith("InvalidExpiry")
|
|
|
|
})
|
|
|
|
|
|
|
|
it("cannot extend an expired lock", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
let maximum = start + 20
|
|
|
|
await vault.lockup(context, expiry, maximum)
|
|
|
|
await advanceTimeToForNextBlock(expiry)
|
|
|
|
const extending = vault.extend(context, maximum)
|
|
|
|
await expect(extending).to.be.revertedWith("LockExpired")
|
|
|
|
})
|
2025-01-21 13:37:01 +01:00
|
|
|
|
|
|
|
it("deletes lock when funds are withdrawn", async function () {
|
|
|
|
let start = await currentTime()
|
|
|
|
let expiry = start + 10
|
|
|
|
await vault.lockup(context, expiry, expiry)
|
|
|
|
await advanceTimeToForNextBlock(expiry)
|
|
|
|
await vault.withdraw(context, account.address)
|
|
|
|
expect((await vault.lock(context))[0]).to.equal(0)
|
|
|
|
expect((await vault.lock(context))[1]).to.equal(0)
|
|
|
|
})
|
2025-01-15 14:51:53 +01:00
|
|
|
})
|
2025-01-13 12:04:03 +01:00
|
|
|
})
|