diff --git a/contracts/Vault.sol b/contracts/Vault.sol index c6ca226..aed1db7 100644 --- a/contracts/Vault.sol +++ b/contracts/Vault.sol @@ -11,8 +11,7 @@ import "./vault/VaultBase.sol"; /// /// A vault keeps track of funds for a smart contract. This smart contract is /// called the controller of the funds. Each controller has its own independent -/// set of funds. Each fund has a number of recipients, and every recipient has -/// its own account within the fund. +/// set of funds. Each fund has a number of accounts. /// /// Vault -> Controller -> Fund -> Account /// @@ -21,6 +20,8 @@ import "./vault/VaultBase.sol"; /// /// An account has a balance, of which a part can be designated. Designated /// tokens can no longer be transfered to another account. +/// Accounts are identified by the address of the account holder, and an id that +/// can be used to create different accounts for the same holder. /// /// A typical flow in which a controller uses the vault to handle funds: /// 1. the controller chooses a unique id for the fund @@ -28,8 +29,8 @@ import "./vault/VaultBase.sol"; /// 3. the controller deposits ERC20 tokens into the fund /// 4. the controller transfers tokens between accounts in the fund /// 5. the fund unlocks after a while, freezing the account balances -/// 6. the controller withdraws ERC20 tokens from the fund for a recipient, or -/// the recipient initiates the withdrawal itself +/// 6. the controller withdraws ERC20 tokens from the fund for an account holder, +/// or the account holder initiates the withdrawal itself /// /// The vault makes it harder for an attacker to extract funds, through several /// mechanisms: @@ -40,34 +41,49 @@ import "./vault/VaultBase.sol"; /// that they can no longer be reassigned to an attacker /// - when storing collateral, it can be designated for the collateral provider, /// ensuring that it cannot be reassigned to an attacker -/// - malicious upgrades to a fund controller cannot prevent recipients from -/// withdrawing their tokens +/// - malicious upgrades to a fund controller cannot prevent account holders +/// from withdrawing their tokens /// - burning tokens in a fund ensures that these tokens can no longer be /// extracted by an attacker /// contract Vault is VaultBase, Pausable, Ownable { constructor(IERC20 token) VaultBase(token) Ownable(msg.sender) {} - /// The amount of tokens that are currently assigned to a recipient in a fund. + /// Creates an account id that encodes the address of the account holder, and + /// a discriminator. The discriminator can be used to create different + /// accounts within a fund that all belong to the same account holder. + function encodeAccountId( + address holder, + bytes12 discriminator + ) public pure returns (AccountId) { + return Accounts.encodeId(holder, discriminator); + } + + /// Extracts the address of the account holder and the discriminator from the + /// account id. + function decodeAccountId( + AccountId account + ) public pure returns (address holder, bytes12 discriminator) { + return Accounts.decodeId(account); + } + + /// The amount of tokens that are currently in an account. /// This includes available and designated tokens. Available tokens can be /// transfered to other accounts, but designated tokens cannot. - function getBalance( - Fund fund, - Recipient recipient - ) public view returns (uint128) { + function getBalance(Fund fund, AccountId id) public view returns (uint128) { Controller controller = Controller.wrap(msg.sender); - Balance memory balance = _getBalance(controller, fund, recipient); + Balance memory balance = _getBalance(controller, fund, id); return balance.available + balance.designated; } - /// The amount of tokens that are currently designated to a recipient in a - /// fund. These tokens can no longer be transfered to other accounts. + /// The amount of tokens that are currently designated in an account + /// These tokens can no longer be transfered to other accounts. function getDesignatedBalance( Fund fund, - Recipient recipient + AccountId id ) public view returns (uint128) { Controller controller = Controller.wrap(msg.sender); - Balance memory balance = _getBalance(controller, fund, recipient); + Balance memory balance = _getBalance(controller, fund, id); return balance.designated; } @@ -107,45 +123,44 @@ contract Vault is VaultBase, Pausable, Ownable { } /// Deposits an amount of tokens into the vault, and adds them to the balance - /// of the recipient. ERC20 tokens are transfered from the caller to the vault + /// of the account. ERC20 tokens are transfered from the caller to the vault /// contract. /// Only allowed when the fund is locked. function deposit( Fund fund, - Recipient recipient, + AccountId id, uint128 amount ) public whenNotPaused { Controller controller = Controller.wrap(msg.sender); - _deposit(controller, fund, recipient, amount); + _deposit(controller, fund, id, amount); } - /// Takes an amount of tokens from the recipient's balance and designates them - /// for the recipient. These tokens are no longer available to be transfered - /// to other accounts. + /// Takes an amount of tokens from the account balance and designates them + /// for the account holder. These tokens are no longer available to be + /// transfered to other accounts. /// Only allowed when the fund is locked. function designate( Fund fund, - Recipient recipient, + AccountId id, uint128 amount ) public whenNotPaused { Controller controller = Controller.wrap(msg.sender); - _designate(controller, fund, recipient, amount); + _designate(controller, fund, id, amount); } - /// Transfers an amount of tokens from the acount of one recipient to the - /// other. + /// Transfers an amount of tokens from one account to the other. /// Only allowed when the fund is locked. function transfer( Fund fund, - Recipient from, - Recipient to, + AccountId from, + AccountId to, uint128 amount ) public whenNotPaused { Controller controller = Controller.wrap(msg.sender); _transfer(controller, fund, from, to, amount); } - /// Transfers tokens from the account of one recipient to the other over time. + /// Transfers tokens from one account the other over time. /// Every second a number of tokens are transfered, until the fund is /// unlocked. After flowing into an account, these tokens become designated /// tokens, so they cannot be transfered again. @@ -154,31 +169,31 @@ contract Vault is VaultBase, Pausable, Ownable { /// fund unlocks, even if the lock expiry time is extended to its maximum. function flow( Fund fund, - Recipient from, - Recipient to, + AccountId from, + AccountId to, TokensPerSecond rate ) public whenNotPaused { Controller controller = Controller.wrap(msg.sender); _flow(controller, fund, from, to, rate); } - /// Burns an amount of designated tokens from the account of the recipient. + /// Burns an amount of designated tokens from the account. /// Only allowed when the fund is locked. function burnDesignated( Fund fund, - Recipient recipient, + AccountId account, uint128 amount ) public whenNotPaused { Controller controller = Controller.wrap(msg.sender); - _burnDesignated(controller, fund, recipient, amount); + _burnDesignated(controller, fund, account, amount); } - /// Burns all tokens from the account of the recipient. + /// Burns all tokens from the account. /// Only allowed when the fund is locked. /// Only allowed when no funds are flowing into or out of the account. - function burnAccount(Fund fund, Recipient recipient) public whenNotPaused { + function burnAccount(Fund fund, AccountId account) public whenNotPaused { Controller controller = Controller.wrap(msg.sender); - _burnAccount(controller, fund, recipient); + _burnAccount(controller, fund, account); } /// Burns all tokens from all accounts in a fund. @@ -188,23 +203,28 @@ contract Vault is VaultBase, Pausable, Ownable { _burnFund(controller, fund); } - /// Transfers all ERC20 tokens in the recipient's account out of the vault to - /// the recipient address. + /// Transfers all ERC20 tokens in the account out of the vault to the account + /// owner. /// Only allowed when the fund is unlocked. - /// ⚠️ The recipient can also withdraw itself, so when designing a smart + /// ⚠️ The account holder can also withdraw itself, so when designing a smart /// contract that controls funds in the vault, don't assume that only this /// smart contract can initiate a withdrawal ⚠️ - function withdraw(Fund fund, Recipient recipient) public whenNotPaused { + function withdraw(Fund fund, AccountId account) public whenNotPaused { Controller controller = Controller.wrap(msg.sender); - _withdraw(controller, fund, recipient); + _withdraw(controller, fund, account); } - /// Allows a recipient to withdraw its tokens from a fund directly, bypassing - /// the need to ask the controller of the fund to initiate the withdrawal. + /// Allows an account holder to withdraw its tokens from a fund directly, + /// bypassing the need to ask the controller of the fund to initiate the + /// withdrawal. /// Only allowed when the fund is unlocked. - function withdrawByRecipient(Controller controller, Fund fund) public { - Recipient recipient = Recipient.wrap(msg.sender); - _withdraw(controller, fund, recipient); + function withdrawByRecipient( + Controller controller, + Fund fund, + AccountId account + ) public { + (address holder, ) = Accounts.decodeId(account); + _withdraw(controller, fund, account); } function pause() public onlyOwner { diff --git a/contracts/vault/Accounts.sol b/contracts/vault/Accounts.sol index e822e21..7ca9129 100644 --- a/contracts/vault/Accounts.sol +++ b/contracts/vault/Accounts.sol @@ -4,6 +4,10 @@ pragma solidity 0.8.28; import "./TokenFlows.sol"; import "./Timestamps.sol"; +/// Used to identify an account. The first 20 bytes consist of the address of +/// the account holder, and the last 12 bytes consist of a discriminator value. +type AccountId is bytes32; + /// Records the token balance and the incoming and outgoing token flows struct Account { Balance balance; @@ -37,6 +41,26 @@ library Accounts { using TokenFlows for TokensPerSecond; using Timestamps for Timestamp; + /// Creates an account id from the account holder address and a discriminator. + /// The discriminiator can be used to create different accounts that belong to + /// the same account holder. + function encodeId( + address holder, + bytes12 discriminator + ) internal pure returns (AccountId) { + bytes32 left = bytes32(bytes20(holder)); + bytes32 right = bytes32(uint256(uint96(discriminator))); + return AccountId.wrap(left | right); + } + + /// Extracts the account holder and the discriminator from the the account id + function decodeId(AccountId id) internal pure returns (address, bytes12) { + bytes32 unwrapped = AccountId.unwrap(id); + address holder = address(bytes20(unwrapped)); + bytes12 discriminator = bytes12(uint96(uint256(unwrapped))); + return (holder, discriminator); + } + /// Calculates whether the available balance is sufficient to sustain the /// outgoing flow of tokens until the specified timestamp function isSolventAt( diff --git a/contracts/vault/VaultBase.sol b/contracts/vault/VaultBase.sol index 269dd47..54f08ea 100644 --- a/contracts/vault/VaultBase.sol +++ b/contracts/vault/VaultBase.sol @@ -24,17 +24,17 @@ import "./Locks.sol"; /// The account invariant ensures that the outgoing token flow can be sustained /// for the maximum time that a fund can be locked: /// -/// (∀ controller ∈ Controller, fund ∈ Fund, recipient ∈ Recipient: +/// (∀ controller ∈ Controller, fund ∈ Fund, account ∈ AccountId: /// flow.outgoing * (lock.maximum - flow.updated) <= balance.available /// where lock = _locks[controller][fund]) -/// and flow = _accounts[controller][fund][recipient].flow -/// and balance = _accounts[controller][fund][recipient].balance +/// and flow = _accounts[controller][fund][account].flow +/// and balance = _accounts[controller][fund][account].balance /// /// The flow invariant ensures that incoming and outgoing flow rates match: /// /// (∀ controller ∈ Controller, fund ∈ Fund: -/// (∑ recipient ∈ Recipient: accounts[recipient].flow.incoming) = -/// (∑ recipient ∈ Recipient: accounts[recipient].flow.outgoing) +/// (∑ account ∈ AccountId: accounts[account].flow.incoming) = +/// (∑ account ∈ AccountId: accounts[account].flow.outgoing) /// where accounts = _accounts[controller][fund]) /// abstract contract VaultBase { @@ -48,13 +48,11 @@ abstract contract VaultBase { type Controller is address; /// Unique identifier for a fund, chosen by the controller type Fund is bytes32; - /// Receives the balance of an account when withdrawing - type Recipient is address; /// Each fund has its own time lock mapping(Controller => mapping(Fund => Lock)) private _locks; - /// Each recipient has its own account in a fund - mapping(Controller => mapping(Fund => mapping(Recipient => Account))) + /// Each account holder has its own set of accounts in a fund + mapping(Controller => mapping(Fund => mapping(AccountId => Account))) private _accounts; constructor(IERC20 token) { @@ -78,17 +76,17 @@ abstract contract VaultBase { function _getBalance( Controller controller, Fund fund, - Recipient recipient + AccountId id ) internal view returns (Balance memory) { Lock memory lock = _locks[controller][fund]; LockStatus lockStatus = lock.status(); if (lockStatus == LockStatus.Locked) { - Account memory account = _accounts[controller][fund][recipient]; + Account memory account = _accounts[controller][fund][id]; account.update(Timestamps.currentTime()); return account.balance; } if (lockStatus == LockStatus.Unlocked) { - Account memory account = _accounts[controller][fund][recipient]; + Account memory account = _accounts[controller][fund][id]; account.update(lock.expiry); return account.balance; } @@ -125,13 +123,13 @@ abstract contract VaultBase { function _deposit( Controller controller, Fund fund, - Recipient recipient, + AccountId id, uint128 amount ) internal { Lock storage lock = _locks[controller][fund]; require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); - Account storage account = _accounts[controller][fund][recipient]; + Account storage account = _accounts[controller][fund][id]; account.balance.available += amount; lock.value += amount; @@ -146,27 +144,27 @@ abstract contract VaultBase { function _designate( Controller controller, Fund fund, - Recipient recipient, + AccountId id, uint128 amount ) internal { Lock memory lock = _locks[controller][fund]; require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); - Account memory account = _accounts[controller][fund][recipient]; + Account memory account = _accounts[controller][fund][id]; require(amount <= account.balance.available, VaultInsufficientBalance()); account.balance.available -= amount; account.balance.designated += amount; _checkAccountInvariant(account, lock); - _accounts[controller][fund][recipient] = account; + _accounts[controller][fund][id] = account; } function _transfer( Controller controller, Fund fund, - Recipient from, - Recipient to, + AccountId from, + AccountId to, uint128 amount ) internal { Lock memory lock = _locks[controller][fund]; @@ -186,8 +184,8 @@ abstract contract VaultBase { function _flow( Controller controller, Fund fund, - Recipient from, - Recipient to, + AccountId from, + AccountId to, TokensPerSecond rate ) internal { Lock memory lock = _locks[controller][fund]; @@ -206,13 +204,13 @@ abstract contract VaultBase { function _burnDesignated( Controller controller, Fund fund, - Recipient recipient, + AccountId id, uint128 amount ) internal { Lock storage lock = _locks[controller][fund]; require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); - Account storage account = _accounts[controller][fund][recipient]; + Account storage account = _accounts[controller][fund][id]; require(account.balance.designated >= amount, VaultInsufficientBalance()); account.balance.designated -= amount; @@ -225,18 +223,18 @@ abstract contract VaultBase { function _burnAccount( Controller controller, Fund fund, - Recipient recipient + AccountId id ) internal { Lock storage lock = _locks[controller][fund]; require(lock.status() == LockStatus.Locked, VaultFundNotLocked()); - Account memory account = _accounts[controller][fund][recipient]; + Account memory account = _accounts[controller][fund][id]; require(account.flow.incoming == account.flow.outgoing, VaultFlowNotZero()); uint128 amount = account.balance.available + account.balance.designated; lock.value -= amount; - delete _accounts[controller][fund][recipient]; + delete _accounts[controller][fund][id]; _token.safeTransfer(address(0xdead), amount); } @@ -250,15 +248,11 @@ abstract contract VaultBase { _token.safeTransfer(address(0xdead), lock.value); } - function _withdraw( - Controller controller, - Fund fund, - Recipient recipient - ) internal { + function _withdraw(Controller controller, Fund fund, AccountId id) internal { Lock memory lock = _locks[controller][fund]; require(lock.status() == LockStatus.Unlocked, VaultFundNotUnlocked()); - Account memory account = _accounts[controller][fund][recipient]; + Account memory account = _accounts[controller][fund][id]; account.update(lock.expiry); uint128 amount = account.balance.available + account.balance.designated; @@ -270,9 +264,10 @@ abstract contract VaultBase { _locks[controller][fund] = lock; } - delete _accounts[controller][fund][recipient]; + delete _accounts[controller][fund][id]; - _token.safeTransfer(Recipient.unwrap(recipient), amount); + (address owner, ) = Accounts.decodeId(id); + _token.safeTransfer(owner, amount); } function _checkLockInvariant(Lock memory lock) private pure { diff --git a/test/Vault.tests.js b/test/Vault.tests.js index 90252bc..711dc4c 100644 --- a/test/Vault.tests.js +++ b/test/Vault.tests.js @@ -1,6 +1,6 @@ const { expect } = require("chai") const { ethers } = require("hardhat") -const { randomBytes } = ethers.utils +const { randomBytes, hexlify } = ethers.utils const { currentTime, advanceTimeTo, @@ -34,10 +34,33 @@ describe("Vault", function () { await revert() }) + describe("account ids", function () { + let address + let discriminator + + beforeEach(async function () { + address = holder.address + discriminator = hexlify(randomBytes(12)) + }) + + it("encodes the account holder and a discriminator in an account id", async function () { + const account = await vault.encodeAccountId(address, discriminator) + const decoded = await vault.decodeAccountId(account) + expect(decoded[0]).to.equal(address) + expect(decoded[1]).to.equal(discriminator) + }) + }) + describe("when a fund has no lock set", function () { + let account + + beforeEach(async function () { + account = await vault.encodeAccountId(holder.address, randomBytes(12)) + }) + it("does not have any balances", async function () { - const balance = await vault.getBalance(fund, holder.address) - const designated = await vault.getDesignatedBalance(fund, holder.address) + const balance = await vault.getBalance(fund, account) + const designated = await vault.getDesignatedBalance(fund, account) expect(balance).to.equal(0) expect(designated).to.equal(0) }) @@ -64,11 +87,13 @@ describe("Vault", function () { describe("when a fund is locked", function () { let expiry let maximum + let account beforeEach(async function () { const beginning = (await currentTime()) + 10 expiry = beginning + 80 maximum = beginning + 100 + account = await vault.encodeAccountId(holder.address, randomBytes(12)) await setAutomine(false) await setNextBlockTimestamp(beginning) await vault.lock(fund, expiry, maximum) @@ -104,8 +129,8 @@ describe("Vault", function () { it("does not delete lock when no tokens remain", async function () { await token.connect(controller).approve(vault.address, 30) - await vault.deposit(fund, holder.address, 30) - await vault.burnAccount(fund, holder.address) + await vault.deposit(fund, account, 30) + await vault.burnAccount(fund, account) expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Locked) expect(await vault.getLockExpiry(fund)).to.not.equal(0) }) @@ -114,26 +139,29 @@ describe("Vault", function () { describe("depositing", function () { const amount = 1000 + let account + beforeEach(async function () { + account = await vault.encodeAccountId(holder.address, randomBytes(12)) await setAutomine(true) }) it("accepts deposits of tokens", async function () { await token.connect(controller).approve(vault.address, amount) - await vault.deposit(fund, holder.address, amount) - const balance = await vault.getBalance(fund, holder.address) + await vault.deposit(fund, account, amount) + const balance = await vault.getBalance(fund, account) expect(balance).to.equal(amount) }) it("keeps custody of tokens that are deposited", async function () { await token.connect(controller).approve(vault.address, amount) - await vault.deposit(fund, holder.address, amount) + await vault.deposit(fund, account, amount) expect(await token.balanceOf(vault.address)).to.equal(amount) }) it("deposit fails when tokens cannot be transferred", async function () { await token.connect(controller).approve(vault.address, amount - 1) - const depositing = vault.deposit(fund, holder.address, amount) + const depositing = vault.deposit(fund, account, amount) await expect(depositing).to.be.revertedWith( "ERC20InsufficientAllowance" ) @@ -141,22 +169,33 @@ describe("Vault", function () { it("adds multiple deposits to the balance", async function () { await token.connect(controller).approve(vault.address, amount) - await vault.deposit(fund, holder.address, amount / 2) - await vault.deposit(fund, holder.address, amount / 2) - const balance = await vault.getBalance(fund, holder.address) + await vault.deposit(fund, account, amount / 2) + await vault.deposit(fund, account, amount / 2) + const balance = await vault.getBalance(fund, account) expect(balance).to.equal(amount) }) + it("separates deposits from different accounts with the same holder", async function () { + const address = holder.address + const account1 = await vault.encodeAccountId(address, randomBytes(12)) + const account2 = await vault.encodeAccountId(address, randomBytes(12)) + await token.connect(controller).approve(vault.address, 3) + await vault.deposit(fund, account1, 1) + await vault.deposit(fund, account2, 2) + expect(await vault.getBalance(fund, account1)).to.equal(1) + expect(await vault.getBalance(fund, account2)).to.equal(2) + }) + it("separates deposits from different funds", async function () { const fund1 = randomBytes(32) const fund2 = randomBytes(32) await vault.lock(fund1, expiry, maximum) await vault.lock(fund2, expiry, maximum) await token.connect(controller).approve(vault.address, 3) - await vault.deposit(fund1, holder.address, 1) - await vault.deposit(fund2, holder.address, 2) - expect(await vault.getBalance(fund1, holder.address)).to.equal(1) - expect(await vault.getBalance(fund2, holder.address)).to.equal(2) + await vault.deposit(fund1, account, 1) + await vault.deposit(fund2, account, 2) + expect(await vault.getBalance(fund1, account)).to.equal(1) + expect(await vault.getBalance(fund2, account)).to.equal(2) }) it("separates deposits from different controllers", async function () { @@ -170,65 +209,63 @@ describe("Vault", function () { await token.mint(controller2.address, 1000) await token.connect(controller1).approve(vault.address, 1) await token.connect(controller2).approve(vault.address, 2) - await vault1.deposit(fund, holder.address, 1) - await vault2.deposit(fund, holder.address, 2) - expect(await vault1.getBalance(fund, holder.address)).to.equal(1) - expect(await vault2.getBalance(fund, holder.address)).to.equal(2) + await vault1.deposit(fund, account, 1) + await vault2.deposit(fund, account, 2) + expect(await vault1.getBalance(fund, account)).to.equal(1) + expect(await vault2.getBalance(fund, account)).to.equal(2) }) }) describe("designating", function () { const amount = 1000 + let account, account2 + beforeEach(async function () { + account = await vault.encodeAccountId(holder.address, randomBytes(12)) + account2 = await vault.encodeAccountId(holder2.address, randomBytes(12)) await token.connect(controller).approve(vault.address, amount) - await vault.deposit(fund, holder.address, amount) + await vault.deposit(fund, account, amount) }) - it("can designate tokens for a single recipient", async function () { + it("can designate tokens for the account holder", async function () { await setAutomine(true) - await vault.designate(fund, holder.address, amount) - expect(await vault.getDesignatedBalance(fund, holder.address)).to.equal( - amount - ) + await vault.designate(fund, account, amount) + expect(await vault.getDesignatedBalance(fund, account)).to.equal(amount) }) it("can designate part of the balance", async function () { await setAutomine(true) - await vault.designate(fund, holder.address, 10) - expect(await vault.getDesignatedBalance(fund, holder.address)).to.equal( - 10 - ) + await vault.designate(fund, account, 10) + expect(await vault.getDesignatedBalance(fund, account)).to.equal(10) }) it("adds up designated tokens", async function () { await setAutomine(true) - await vault.designate(fund, holder.address, 10) - await vault.designate(fund, holder.address, 10) - expect(await vault.getDesignatedBalance(fund, holder.address)).to.equal( - 20 - ) + await vault.designate(fund, account, 10) + await vault.designate(fund, account, 10) + expect(await vault.getDesignatedBalance(fund, account)).to.equal(20) }) it("does not change the balance", async function () { await setAutomine(true) - await vault.designate(fund, holder.address, 10) - expect(await vault.getBalance(fund, holder.address)).to.equal(amount) + await vault.designate(fund, account, 10) + expect(await vault.getBalance(fund, account)).to.equal(amount) }) it("cannot designate more than the undesignated balance", async function () { await setAutomine(true) - await vault.designate(fund, holder.address, amount) - await expect( - vault.designate(fund, holder.address, 1) - ).to.be.revertedWith("InsufficientBalance") + await vault.designate(fund, account, amount) + await expect(vault.designate(fund, account, 1)).to.be.revertedWith( + "InsufficientBalance" + ) }) it("cannot designate tokens that are flowing", async function () { - await vault.flow(fund, holder.address, holder2.address, 5) + await vault.flow(fund, account, account2, 5) setAutomine(true) - await vault.designate(fund, holder.address, 500) - const designating = vault.designate(fund, holder.address, 1) + await vault.designate(fund, account, 500) + const designating = vault.designate(fund, account, 1) await expect(designating).to.be.revertedWith("InsufficientBalance") }) }) @@ -236,67 +273,65 @@ describe("Vault", function () { describe("transfering", function () { const amount = 1000 - let address1 - let address2 - let address3 + let account1, account2, account3 beforeEach(async function () { + account1 = await vault.encodeAccountId(holder.address, randomBytes(12)) + account2 = await vault.encodeAccountId(holder2.address, randomBytes(12)) + account3 = await vault.encodeAccountId(holder3.address, randomBytes(12)) await token.connect(controller).approve(vault.address, amount) - await vault.deposit(fund, holder.address, amount) - address1 = holder.address - address2 = holder2.address - address3 = holder3.address + await vault.deposit(fund, account1, amount) }) it("can transfer tokens from one recipient to the other", async function () { await setAutomine(true) - await vault.transfer(fund, address1, address2, amount) - expect(await vault.getBalance(fund, address1)).to.equal(0) - expect(await vault.getBalance(fund, address2)).to.equal(amount) + await vault.transfer(fund, account1, account2, amount) + expect(await vault.getBalance(fund, account1)).to.equal(0) + expect(await vault.getBalance(fund, account2)).to.equal(amount) }) it("can transfer part of a balance", async function () { await setAutomine(true) - await vault.transfer(fund, address1, address2, 10) - expect(await vault.getBalance(fund, address1)).to.equal(amount - 10) - expect(await vault.getBalance(fund, address2)).to.equal(10) + await vault.transfer(fund, account1, account2, 10) + expect(await vault.getBalance(fund, account1)).to.equal(amount - 10) + expect(await vault.getBalance(fund, account2)).to.equal(10) }) it("can transfer out funds that were transfered in", async function () { await setAutomine(true) - await vault.transfer(fund, address1, address2, amount) - await vault.transfer(fund, address2, address3, amount) - expect(await vault.getBalance(fund, address2)).to.equal(0) - expect(await vault.getBalance(fund, address3)).to.equal(amount) + await vault.transfer(fund, account1, account2, amount) + await vault.transfer(fund, account2, account3, amount) + expect(await vault.getBalance(fund, account2)).to.equal(0) + expect(await vault.getBalance(fund, account3)).to.equal(amount) }) it("can transfer to self", async function () { await setAutomine(true) - await vault.transfer(fund, address1, address1, amount) - expect(await vault.getBalance(fund, address1)).to.equal(amount) + await vault.transfer(fund, account1, account1, amount) + expect(await vault.getBalance(fund, account1)).to.equal(amount) }) it("does not transfer more than the balance", async function () { await setAutomine(true) await expect( - vault.transfer(fund, address1, address2, amount + 1) + vault.transfer(fund, account1, account2, amount + 1) ).to.be.revertedWith("InsufficientBalance") }) it("does not transfer designated tokens", async function () { await setAutomine(true) - await vault.designate(fund, holder.address, 1) + await vault.designate(fund, account1, 1) await expect( - vault.transfer(fund, holder.address, holder2.address, amount) + vault.transfer(fund, account1, account2, amount) ).to.be.revertedWith("InsufficientBalance") }) it("does not transfer tokens that are flowing", async function () { - await vault.flow(fund, address1, address2, 5) + await vault.flow(fund, account1, account2, 5) setAutomine(true) - await vault.transfer(fund, address1, address2, 500) + await vault.transfer(fund, account1, account2, 500) await expect( - vault.transfer(fund, address1, address2, 1) + vault.transfer(fund, account1, account2, 1) ).to.be.revertedWith("InsufficientBalance") }) }) @@ -304,151 +339,149 @@ describe("Vault", function () { describe("flowing", function () { const deposit = 1000 - let address1 - let address2 - let address3 + let account1, account2, account3 beforeEach(async function () { + account1 = await vault.encodeAccountId(holder.address, randomBytes(12)) + account2 = await vault.encodeAccountId(holder2.address, randomBytes(12)) + account3 = await vault.encodeAccountId(holder3.address, randomBytes(12)) await token.connect(controller).approve(vault.address, deposit) - await vault.deposit(fund, holder.address, deposit) - address1 = holder.address - address2 = holder2.address - address3 = holder3.address + await vault.deposit(fund, account1, deposit) }) - async function getBalance(recipient) { - return await vault.getBalance(fund, recipient) + async function getBalance(account) { + return await vault.getBalance(fund, account) } it("moves tokens over time", async function () { - await vault.flow(fund, address1, address2, 2) + await vault.flow(fund, account1, account2, 2) mine() const start = await currentTime() await advanceTimeTo(start + 2) - expect(await getBalance(address1)).to.equal(deposit - 4) - expect(await getBalance(address2)).to.equal(4) + expect(await getBalance(account1)).to.equal(deposit - 4) + expect(await getBalance(account2)).to.equal(4) await advanceTimeTo(start + 4) - expect(await getBalance(address1)).to.equal(deposit - 8) - expect(await getBalance(address2)).to.equal(8) + expect(await getBalance(account1)).to.equal(deposit - 8) + expect(await getBalance(account2)).to.equal(8) }) - it("can move tokens to several different recipients", async function () { - await vault.flow(fund, address1, address2, 1) - await vault.flow(fund, address1, address3, 2) + it("can move tokens to several different accounts", async function () { + await vault.flow(fund, account1, account2, 1) + await vault.flow(fund, account1, account3, 2) await mine() const start = await currentTime() await advanceTimeTo(start + 2) - expect(await getBalance(address1)).to.equal(deposit - 6) - expect(await getBalance(address2)).to.equal(2) - expect(await getBalance(address3)).to.equal(4) + expect(await getBalance(account1)).to.equal(deposit - 6) + expect(await getBalance(account2)).to.equal(2) + expect(await getBalance(account3)).to.equal(4) await advanceTimeTo(start + 4) - expect(await getBalance(address1)).to.equal(deposit - 12) - expect(await getBalance(address2)).to.equal(4) - expect(await getBalance(address3)).to.equal(8) + expect(await getBalance(account1)).to.equal(deposit - 12) + expect(await getBalance(account2)).to.equal(4) + expect(await getBalance(account3)).to.equal(8) }) - it("allows flows to be diverted to other recipient", async function () { - await vault.flow(fund, address1, address2, 3) - await vault.flow(fund, address2, address3, 1) + it("allows flows to be diverted to other account", async function () { + await vault.flow(fund, account1, account2, 3) + await vault.flow(fund, account2, account3, 1) await mine() const start = await currentTime() await advanceTimeTo(start + 2) - expect(await getBalance(address1)).to.equal(deposit - 6) - expect(await getBalance(address2)).to.equal(4) - expect(await getBalance(address3)).to.equal(2) + expect(await getBalance(account1)).to.equal(deposit - 6) + expect(await getBalance(account2)).to.equal(4) + expect(await getBalance(account3)).to.equal(2) await advanceTimeTo(start + 4) - expect(await getBalance(address1)).to.equal(deposit - 12) - expect(await getBalance(address2)).to.equal(8) - expect(await getBalance(address3)).to.equal(4) + expect(await getBalance(account1)).to.equal(deposit - 12) + expect(await getBalance(account2)).to.equal(8) + expect(await getBalance(account3)).to.equal(4) }) it("allows flow to be reversed back to the sender", async function () { - await vault.flow(fund, address1, address2, 3) - await vault.flow(fund, address2, address1, 3) + await vault.flow(fund, account1, account2, 3) + await vault.flow(fund, account2, account1, 3) await mine() const start = await currentTime() await advanceTimeTo(start + 2) - expect(await getBalance(address1)).to.equal(deposit) - expect(await getBalance(address2)).to.equal(0) + expect(await getBalance(account1)).to.equal(deposit) + expect(await getBalance(account2)).to.equal(0) await advanceTimeTo(start + 4) - expect(await getBalance(address1)).to.equal(deposit) - expect(await getBalance(address2)).to.equal(0) + expect(await getBalance(account1)).to.equal(deposit) + expect(await getBalance(account2)).to.equal(0) }) it("can change flows over time", async function () { - await vault.flow(fund, address1, address2, 1) - await vault.flow(fund, address1, address3, 2) + await vault.flow(fund, account1, account2, 1) + await vault.flow(fund, account1, account3, 2) await mine() const start = await currentTime() setNextBlockTimestamp(start + 4) - await vault.flow(fund, address3, address2, 1) + await vault.flow(fund, account3, account2, 1) await mine() - expect(await getBalance(address1)).to.equal(deposit - 12) - expect(await getBalance(address2)).to.equal(4) - expect(await getBalance(address3)).to.equal(8) + expect(await getBalance(account1)).to.equal(deposit - 12) + expect(await getBalance(account2)).to.equal(4) + expect(await getBalance(account3)).to.equal(8) await advanceTimeTo(start + 8) - expect(await getBalance(address1)).to.equal(deposit - 24) - expect(await getBalance(address2)).to.equal(12) - expect(await getBalance(address3)).to.equal(12) + expect(await getBalance(account1)).to.equal(deposit - 24) + expect(await getBalance(account2)).to.equal(12) + expect(await getBalance(account3)).to.equal(12) await advanceTimeTo(start + 12) - expect(await getBalance(address1)).to.equal(deposit - 36) - expect(await getBalance(address2)).to.equal(20) - expect(await getBalance(address3)).to.equal(16) + expect(await getBalance(account1)).to.equal(deposit - 36) + expect(await getBalance(account2)).to.equal(20) + expect(await getBalance(account3)).to.equal(16) }) - it("designates tokens that flow for the recipient", async function () { - await vault.flow(fund, address1, address2, 3) + it("designates tokens that flow into the account", async function () { + await vault.flow(fund, account1, account2, 3) await mine() const start = await currentTime() await advanceTimeTo(start + 7) - expect(await vault.getDesignatedBalance(fund, address2)).to.equal(21) + expect(await vault.getDesignatedBalance(fund, account2)).to.equal(21) }) it("designates tokens that flow back to the sender", async function () { - await vault.flow(fund, address1, address1, 3) + await vault.flow(fund, account1, account1, 3) await mine() const start = await currentTime() await advanceTimeTo(start + 7) - expect(await vault.getBalance(fund, address1)).to.equal(deposit) - expect(await vault.getDesignatedBalance(fund, address1)).to.equal(21) + expect(await vault.getBalance(fund, account1)).to.equal(deposit) + expect(await vault.getDesignatedBalance(fund, account1)).to.equal(21) }) it("flows longer when lock is extended", async function () { - await vault.flow(fund, address1, address2, 2) + await vault.flow(fund, account1, account2, 2) await mine() const start = await currentTime() await vault.extendLock(fund, maximum) await mine() await advanceTimeTo(maximum) const total = (maximum - start) * 2 - expect(await getBalance(address1)).to.equal(deposit - total) - expect(await getBalance(address2)).to.equal(total) + expect(await getBalance(account1)).to.equal(deposit - total) + expect(await getBalance(account2)).to.equal(total) await advanceTimeTo(maximum + 10) - expect(await getBalance(address1)).to.equal(deposit - total) - expect(await getBalance(address2)).to.equal(total) + expect(await getBalance(account1)).to.equal(deposit - total) + expect(await getBalance(account2)).to.equal(total) }) it("rejects flow when insufficient available tokens", async function () { setAutomine(true) await expect( - vault.flow(fund, address1, address2, 11) + vault.flow(fund, account1, account2, 11) ).to.be.revertedWith("InsufficientBalance") }) it("rejects total flows exceeding available tokens", async function () { - await vault.flow(fund, address1, address2, 10) + await vault.flow(fund, account1, account2, 10) setAutomine(true) await expect( - vault.flow(fund, address1, address2, 1) + vault.flow(fund, account1, account2, 1) ).to.be.revertedWith("InsufficientBalance") }) it("cannot flow designated tokens", async function () { - await vault.designate(fund, address1, 500) - await vault.flow(fund, address1, address2, 5) + await vault.designate(fund, account1, 500) + await vault.flow(fund, account1, account2, 5) setAutomine(true) await expect( - vault.flow(fund, address1, address2, 1) + vault.flow(fund, account1, account2, 1) ).to.be.revertedWith("InsufficientBalance") }) }) @@ -457,111 +490,119 @@ describe("Vault", function () { const dead = "0x000000000000000000000000000000000000dead" const amount = 1000 + let account1, account2, account3 + beforeEach(async function () { + account1 = await vault.encodeAccountId(holder.address, randomBytes(12)) + account2 = await vault.encodeAccountId(holder2.address, randomBytes(12)) + account3 = await vault.encodeAccountId(holder3.address, randomBytes(12)) await setAutomine(true) await token.connect(controller).approve(vault.address, amount) - await vault.deposit(fund, holder.address, amount) + await vault.deposit(fund, account1, amount) }) describe("burn designated", function () { const designated = 100 beforeEach(async function () { - await vault.designate(fund, holder.address, designated) + await vault.designate(fund, account1, designated) }) it("burns a number of designated tokens", async function () { - await vault.burnDesignated(fund, holder.address, 10) - expect( - await vault.getDesignatedBalance(fund, holder.address) - ).to.equal(designated - 10) - expect(await vault.getBalance(fund, holder.address)).to.equal( - amount - 10 + await vault.burnDesignated(fund, account1, 10) + expect(await vault.getDesignatedBalance(fund, account1)).to.equal( + designated - 10 ) + expect(await vault.getBalance(fund, account1)).to.equal(amount - 10) }) it("can burn all of the designated tokens", async function () { - await vault.burnDesignated(fund, holder.address, designated) - expect( - await vault.getDesignatedBalance(fund, holder.address) - ).to.equal(0) - expect(await vault.getBalance(fund, holder.address)).to.equal( + await vault.burnDesignated(fund, account1, designated) + expect(await vault.getDesignatedBalance(fund, account1)).to.equal(0) + expect(await vault.getBalance(fund, account1)).to.equal( amount - designated ) }) it("moves burned tokens to address 0xdead", async function () { const before = await token.balanceOf(dead) - await vault.burnDesignated(fund, holder.address, 10) + await vault.burnDesignated(fund, account1, 10) const after = await token.balanceOf(dead) expect(after - before).to.equal(10) }) it("can burn designated when tokens are flowing", async function () { - await vault.flow(fund, holder.address, holder2.address, 5) - await expect(vault.burnDesignated(fund, holder.address, designated)) - .not.to.be.reverted + await vault.flow(fund, account1, account2, 5) + await expect(vault.burnDesignated(fund, account1, designated)).not.to + .be.reverted }) it("cannot burn more than all designated tokens", async function () { await expect( - vault.burnDesignated(fund, holder.address, designated + 1) + vault.burnDesignated(fund, account1, designated + 1) ).to.be.revertedWith("InsufficientBalance") }) }) - describe("burn holder", function () { - it("can burn an holder", async function () { - await vault.burnAccount(fund, holder.address) - expect(await vault.getBalance(fund, holder.address)).to.equal(0) + describe("burn account", function () { + it("can burn an account", async function () { + await vault.burnAccount(fund, account1) + expect(await vault.getBalance(fund, account1)).to.equal(0) }) it("also burns the designated tokens", async function () { - await vault.designate(fund, holder.address, 10) - await vault.burnAccount(fund, holder.address) - expect( - await vault.getDesignatedBalance(fund, holder.address) - ).to.equal(0) + await vault.designate(fund, account1, 10) + await vault.burnAccount(fund, account1) + expect(await vault.getDesignatedBalance(fund, account1)).to.equal(0) }) - it("moves holder tokens to address 0xdead", async function () { - await vault.designate(fund, holder.address, 10) + it("moves account tokens to address 0xdead", async function () { + await vault.designate(fund, account1, 10) const before = await token.balanceOf(dead) - await vault.burnAccount(fund, holder.address) + await vault.burnAccount(fund, account1) const after = await token.balanceOf(dead) expect(after - before).to.equal(amount) }) + it("does not burn tokens from other accounts with the same holder", async function () { + const account1a = await vault.encodeAccountId( + holder.address, + randomBytes(12) + ) + await vault.transfer(fund, account1, account1a, 10) + await vault.burnAccount(fund, account1) + expect(await vault.getBalance(fund, account1a)).to.equal(10) + }) + it("cannot burn tokens that are flowing", async function () { - await vault.flow(fund, holder.address, holder2.address, 5) - const burning1 = vault.burnAccount(fund, holder.address) + await vault.flow(fund, account1, account2, 5) + const burning1 = vault.burnAccount(fund, account1) await expect(burning1).to.be.revertedWith("FlowNotZero") - const burning2 = vault.burnAccount(fund, holder2.address) + const burning2 = vault.burnAccount(fund, account2) await expect(burning2).to.be.revertedWith("FlowNotZero") }) it("can burn tokens that are no longer flowing", async function () { - await vault.flow(fund, holder.address, holder2.address, 5) - await vault.flow(fund, holder2.address, holder.address, 5) - await expect(vault.burnAccount(fund, holder.address)).not.to.be - .reverted + await vault.flow(fund, account1, account2, 5) + await vault.flow(fund, account2, account1, 5) + await expect(vault.burnAccount(fund, account1)).not.to.be.reverted }) }) describe("burn fund", function () { it("can burn an entire fund", async function () { - await vault.transfer(fund, holder.address, holder2.address, 10) - await vault.transfer(fund, holder.address, holder3.address, 10) + await vault.transfer(fund, account1, account2, 10) + await vault.transfer(fund, account1, account3, 10) await vault.burnFund(fund) expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Burned) - expect(await vault.getBalance(fund, holder.address)).to.equal(0) - expect(await vault.getBalance(fund, holder2.address)).to.equal(0) - expect(await vault.getBalance(fund, holder3.address)).to.equal(0) + expect(await vault.getBalance(fund, account1)).to.equal(0) + expect(await vault.getBalance(fund, account2)).to.equal(0) + expect(await vault.getBalance(fund, account3)).to.equal(0) }) it("moves all tokens in the fund to address 0xdead", async function () { - await vault.transfer(fund, holder.address, holder2.address, 10) - await vault.transfer(fund, holder.address, holder3.address, 10) + await vault.transfer(fund, account1, account2, 10) + await vault.transfer(fund, account1, account3, 10) const before = await token.balanceOf(dead) await vault.burnFund(fund) const after = await token.balanceOf(dead) @@ -569,7 +610,7 @@ describe("Vault", function () { }) it("can burn fund when tokens are flowing", async function () { - await vault.flow(fund, holder.address, holder2.address, 5) + await vault.flow(fund, account1, account2, 5) await expect(vault.burnFund(fund)).not.to.be.reverted }) }) @@ -578,24 +619,26 @@ describe("Vault", function () { describe("withdrawing", function () { const amount = 1000 + let account1, account2 + beforeEach(async function () { + account1 = await vault.encodeAccountId(holder.address, randomBytes(12)) + account2 = await vault.encodeAccountId(holder2.address, randomBytes(12)) await setAutomine(true) await token.connect(controller).approve(vault.address, amount) - await vault.deposit(fund, holder.address, amount) + await vault.deposit(fund, account1, amount) }) it("does not allow withdrawal before lock expires", async function () { await setNextBlockTimestamp(expiry - 1) - const withdrawing = vault.withdraw(fund, holder.address) + const withdrawing = vault.withdraw(fund, account1) await expect(withdrawing).to.be.revertedWith("FundNotUnlocked") }) it("disallows withdrawal for everyone in the fund", async function () { - const address1 = holder.address - const address2 = holder2.address - await vault.transfer(fund, address1, address2, amount / 2) - let withdrawing1 = vault.withdraw(fund, address1) - let withdrawing2 = vault.withdraw(fund, address2) + await vault.transfer(fund, account1, account2, amount / 2) + let withdrawing1 = vault.withdraw(fund, account1) + let withdrawing2 = vault.withdraw(fund, account2) await expect(withdrawing1).to.be.revertedWith("FundNotUnlocked") await expect(withdrawing2).to.be.revertedWith("FundNotUnlocked") }) @@ -605,11 +648,15 @@ describe("Vault", function () { describe("when a fund lock is expiring", function () { let expiry let maximum + let account1, account2, account3 beforeEach(async function () { const beginning = (await currentTime()) + 10 expiry = beginning + 80 maximum = beginning + 100 + account1 = await vault.encodeAccountId(holder.address, randomBytes(12)) + account2 = await vault.encodeAccountId(holder2.address, randomBytes(12)) + account3 = await vault.encodeAccountId(holder3.address, randomBytes(12)) await setAutomine(false) await setNextBlockTimestamp(beginning) await vault.lock(fund, expiry, maximum) @@ -640,23 +687,23 @@ describe("Vault", function () { it("deletes lock when no tokens remain", async function () { await token.connect(controller).approve(vault.address, 30) - await vault.deposit(fund, holder.address, 30) - await vault.transfer(fund, holder.address, holder2.address, 20) - await vault.transfer(fund, holder2.address, holder3.address, 10) + await vault.deposit(fund, account1, 30) + await vault.transfer(fund, account1, account2, 20) + await vault.transfer(fund, account2, account3, 10) // some designated tokens are burned - await vault.designate(fund, holder2.address, 10) - await vault.burnDesignated(fund, holder2.address, 5) + await vault.designate(fund, account2, 10) + await vault.burnDesignated(fund, account2, 5) // some holder is burned - await vault.burnAccount(fund, holder2.address) + await vault.burnAccount(fund, account2) await expire() // some tokens are withdrawn - await vault.withdraw(fund, holder.address) + await vault.withdraw(fund, account1) expect(await vault.getLockStatus(fund)).to.equal(LockStatus.Unlocked) expect(await vault.getLockExpiry(fund)).not.to.equal(0) // remainder of the tokens are withdrawn by recipient await vault .connect(holder3) - .withdrawByRecipient(controller.address, fund) + .withdrawByRecipient(controller.address, fund, account3) expect(await vault.getLockStatus(fund)).to.equal(LockStatus.NoLock) expect(await vault.getLockExpiry(fund)).to.equal(0) }) @@ -667,37 +714,37 @@ describe("Vault", function () { beforeEach(async function () { await token.connect(controller).approve(vault.address, deposit) - await vault.deposit(fund, holder.address, deposit) + await vault.deposit(fund, account1, deposit) }) it("stops flows when lock expires", async function () { - await vault.flow(fund, holder.address, holder2.address, 2) + await vault.flow(fund, account1, account2, 2) await mine() const start = await currentTime() const total = (expiry - start) * 2 let balance1, balance2 await advanceTimeTo(expiry) - balance1 = await vault.getBalance(fund, holder.address) - balance2 = await vault.getBalance(fund, holder2.address) + balance1 = await vault.getBalance(fund, account1) + balance2 = await vault.getBalance(fund, account2) expect(balance1).to.equal(deposit - total) expect(balance2).to.equal(total) await advanceTimeTo(expiry + 10) - balance1 = await vault.getBalance(fund, holder.address) - balance2 = await vault.getBalance(fund, holder2.address) + balance1 = await vault.getBalance(fund, account1) + balance2 = await vault.getBalance(fund, account2) expect(balance1).to.equal(deposit - total) expect(balance2).to.equal(total) }) it("allows flowing tokens to be withdrawn", async function () { - await vault.flow(fund, holder.address, holder2.address, 2) + await vault.flow(fund, account1, account2, 2) await mine() const start = await currentTime() const total = (expiry - start) * 2 await advanceTimeTo(expiry + 10) balance1Before = await token.balanceOf(holder.address) balance2Before = await token.balanceOf(holder2.address) - await vault.withdraw(fund, holder.address) - await vault.withdraw(fund, holder2.address) + await vault.withdraw(fund, account1) + await vault.withdraw(fund, account2) await mine() balance1After = await token.balanceOf(holder.address) balance2After = await token.balanceOf(holder2.address) @@ -712,15 +759,15 @@ describe("Vault", function () { beforeEach(async function () { setAutomine(true) await token.connect(controller).approve(vault.address, amount) - await vault.deposit(fund, holder.address, amount) + await vault.deposit(fund, account1, amount) await token.connect(controller).approve(vault.address, amount) - await vault.deposit(fund, holder2.address, amount) + await vault.deposit(fund, account2, amount) }) it("allows controller to withdraw for a recipient", async function () { await expire() const before = await token.balanceOf(holder.address) - await vault.withdraw(fund, holder.address) + await vault.withdraw(fund, account1) const after = await token.balanceOf(holder.address) expect(after - before).to.equal(amount) }) @@ -730,68 +777,79 @@ describe("Vault", function () { const before = await token.balanceOf(holder.address) await vault .connect(holder) - .withdrawByRecipient(controller.address, fund) + .withdrawByRecipient(controller.address, fund, account1) const after = await token.balanceOf(holder.address) expect(after - before).to.equal(amount) }) it("empties the balance when withdrawing", async function () { await expire() - await vault.withdraw(fund, holder.address) - expect(await vault.getBalance(fund, holder.address)).to.equal(0) + await vault.withdraw(fund, account1) + expect(await vault.getBalance(fund, account1)).to.equal(0) + }) + + it("does not withdraw other accounts from the same holder", async function () { + const account1a = await vault.encodeAccountId( + holder.address, + randomBytes(12) + ) + await vault.transfer(fund, account1, account1a, 10) + await expire() + await vault.withdraw(fund, account1) + expect(await vault.getBalance(fund, account1a)).to.equal(10) }) it("allows designated tokens to be withdrawn", async function () { - await vault.designate(fund, holder.address, 10) + await vault.designate(fund, account1, 10) await expire() const before = await token.balanceOf(holder.address) - await vault.withdraw(fund, holder.address) + await vault.withdraw(fund, account1) const after = await token.balanceOf(holder.address) expect(after - before).to.equal(amount) }) it("does not withdraw designated tokens more than once", async function () { - await vault.designate(fund, holder.address, 10) + await vault.designate(fund, account1, 10) await expire() - await vault.withdraw(fund, holder.address) + await vault.withdraw(fund, account1) const before = await token.balanceOf(holder.address) - await vault.withdraw(fund, holder.address) + await vault.withdraw(fund, account1) const after = await token.balanceOf(holder.address) expect(after).to.equal(before) }) it("can withdraw funds that were transfered in", async function () { - await vault.transfer(fund, holder.address, holder3.address, amount) + await vault.transfer(fund, account1, account3, amount) await expire() const before = await token.balanceOf(holder3.address) - await vault.withdraw(fund, holder3.address) + await vault.withdraw(fund, account3) const after = await token.balanceOf(holder3.address) expect(after - before).to.equal(amount) }) it("cannot withdraw funds that were transfered out", async function () { - await vault.transfer(fund, holder.address, holder3.address, amount) + await vault.transfer(fund, account1, account3, amount) await expire() const before = await token.balanceOf(holder.address) - await vault.withdraw(fund, holder.address) + await vault.withdraw(fund, account1) const after = await token.balanceOf(holder.address) expect(after).to.equal(before) }) it("cannot withdraw more than once", async function () { await expire() - await vault.withdraw(fund, holder.address) + await vault.withdraw(fund, account1) const before = await token.balanceOf(holder.address) - await vault.withdraw(fund, holder.address) + await vault.withdraw(fund, account1) const after = await token.balanceOf(holder.address) expect(after).to.equal(before) }) it("cannot withdraw burned tokens", async function () { - await vault.burnAccount(fund, holder.address) + await vault.burnAccount(fund, account1) await expire() const before = await token.balanceOf(holder.address) - await vault.withdraw(fund, holder.address) + await vault.withdraw(fund, account1) const after = await token.balanceOf(holder.address) expect(after).to.equal(before) }) @@ -811,12 +869,14 @@ describe("Vault", function () { const amount = 1000 let expiry + let account beforeEach(async function () { expiry = (await currentTime()) + 100 + account = await vault.encodeAccountId(holder.address, randomBytes(12)) await token.connect(controller).approve(vault.address, amount) await vault.lock(fund, expiry, expiry) - await vault.deposit(fund, holder.address, amount) + await vault.deposit(fund, account, amount) await vault.burnFund(fund) }) @@ -837,7 +897,7 @@ describe("Vault", function () { }) it("cannot withdraw", async function () { - const withdrawing = vault.withdraw(fund, holder.address) + const withdrawing = vault.withdraw(fund, account) await expect(withdrawing).to.be.revertedWith("FundNotUnlocked") }) @@ -846,6 +906,13 @@ describe("Vault", function () { }) function testFundThatIsNotLocked() { + let account, account2 + + beforeEach(async function () { + account = await vault.encodeAccountId(holder.address, randomBytes(12)) + account2 = await vault.encodeAccountId(holder2.address, randomBytes(12)) + }) + it("does not allow extending of lock", async function () { await expect( vault.extendLock(fund, (await currentTime()) + 1) @@ -855,37 +922,37 @@ describe("Vault", function () { it("does not allow depositing of tokens", async function () { const amount = 1000 await token.connect(controller).approve(vault.address, amount) - await expect( - vault.deposit(fund, holder.address, amount) - ).to.be.revertedWith("FundNotLocked") + await expect(vault.deposit(fund, account, amount)).to.be.revertedWith( + "FundNotLocked" + ) }) it("does not allow designating tokens", async function () { - await expect(vault.designate(fund, holder.address, 0)).to.be.revertedWith( + await expect(vault.designate(fund, account, 0)).to.be.revertedWith( "FundNotLocked" ) }) it("does not allow transfer of tokens", async function () { await expect( - vault.transfer(fund, holder.address, holder2.address, 0) + vault.transfer(fund, account, account2, 0) ).to.be.revertedWith("FundNotLocked") }) it("does not allow new token flows to start", async function () { - await expect( - vault.flow(fund, holder.address, holder2.address, 0) - ).to.be.revertedWith("FundNotLocked") + await expect(vault.flow(fund, account, account2, 0)).to.be.revertedWith( + "FundNotLocked" + ) }) it("does not allow burning of designated tokens", async function () { - await expect( - vault.burnDesignated(fund, holder.address, 1) - ).to.be.revertedWith("FundNotLocked") + await expect(vault.burnDesignated(fund, account, 1)).to.be.revertedWith( + "FundNotLocked" + ) }) it("does not allow burning of accounts", async function () { - await expect(vault.burnAccount(fund, holder.address)).to.be.revertedWith( + await expect(vault.burnAccount(fund, account)).to.be.revertedWith( "FundNotLocked" ) }) @@ -942,21 +1009,26 @@ describe("Vault", function () { describe("when the vault is paused", function () { let expiry let maximum + let account1, account2 beforeEach(async function () { expiry = (await currentTime()) + 80 maximum = (await currentTime()) + 100 + account1 = await vault.encodeAccountId(holder.address, randomBytes(12)) + account2 = await vault.encodeAccountId(holder2.address, randomBytes(12)) await vault.lock(fund, expiry, maximum) await token.approve(vault.address, 1000) - await vault.deposit(fund, holder.address, 1000) - await vault.designate(fund, holder.address, 100) + await vault.deposit(fund, account1, 1000) + await vault.designate(fund, account1, 100) await vault.connect(owner).pause() }) it("only allows a recipient to withdraw itself", async function () { await advanceTimeTo(expiry) await expect( - vault.connect(holder).withdrawByRecipient(controller.address, fund) + vault + .connect(holder) + .withdrawByRecipient(controller.address, fund, account1) ).not.to.be.reverted }) @@ -976,39 +1048,39 @@ describe("Vault", function () { it("does not allow depositing of tokens", async function () { await token.approve(vault.address, 100) - await expect( - vault.deposit(fund, holder.address, 100) - ).to.be.revertedWith("EnforcedPause") + await expect(vault.deposit(fund, account1, 100)).to.be.revertedWith( + "EnforcedPause" + ) }) it("does not allow designating tokens", async function () { - await expect( - vault.designate(fund, holder.address, 10) - ).to.be.revertedWith("EnforcedPause") + await expect(vault.designate(fund, account1, 10)).to.be.revertedWith( + "EnforcedPause" + ) }) it("does not allow transfer of tokens", async function () { await expect( - vault.transfer(fund, holder.address, holder2.address, 10) + vault.transfer(fund, account1, account2, 10) ).to.be.revertedWith("EnforcedPause") }) it("does not allow new token flows to start", async function () { await expect( - vault.flow(fund, holder.address, holder2.address, 1) + vault.flow(fund, account1, account2, 1) ).to.be.revertedWith("EnforcedPause") }) it("does not allow burning of designated tokens", async function () { await expect( - vault.burnDesignated(fund, holder.address, 10) + vault.burnDesignated(fund, account1, 10) ).to.be.revertedWith("EnforcedPause") }) it("does not allow burning of accounts", async function () { - await expect( - vault.burnAccount(fund, holder.address) - ).to.be.revertedWith("EnforcedPause") + await expect(vault.burnAccount(fund, account1)).to.be.revertedWith( + "EnforcedPause" + ) }) it("does not allow burning an entire fund", async function () { @@ -1017,7 +1089,7 @@ describe("Vault", function () { it("does not allow a controller to withdraw for a recipient", async function () { await advanceTimeTo(expiry) - await expect(vault.withdraw(fund, holder.address)).to.be.revertedWith( + await expect(vault.withdraw(fund, account1)).to.be.revertedWith( "EnforcedPause" ) })