vault: pausing and unpausing

This commit is contained in:
Mark Spanbroek 2025-02-12 14:54:28 +01:00
parent 15e7ae855d
commit 4c46f2d1a0
2 changed files with 163 additions and 13 deletions

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "./vault/VaultBase.sol";
/// A vault provides a means for smart contracts to control allocation of ERC20
@ -43,9 +45,8 @@ import "./vault/VaultBase.sol";
/// - burning tokens in a fund ensures that these tokens can no longer be
/// extracted by an attacker
///
contract Vault is VaultBase {
constructor(IERC20 token) VaultBase(token) {}
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.
/// This includes available and designated tokens. Available tokens can be
@ -87,7 +88,11 @@ contract Vault is VaultBase {
/// Locks the fund until the expiry timestamp. The lock expiry can be extended
/// later, but no more than the maximum timestamp.
function lock(Fund fund, Timestamp expiry, Timestamp maximum) public {
function lock(
Fund fund,
Timestamp expiry,
Timestamp maximum
) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_lock(controller, fund, expiry, maximum);
}
@ -96,7 +101,7 @@ contract Vault is VaultBase {
/// the existing expiry, but no later than the maximum timestamp that was
/// provided when locking the fund.
/// Only allowed when the lock has not unlocked yet.
function extendLock(Fund fund, Timestamp expiry) public {
function extendLock(Fund fund, Timestamp expiry) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_extendLock(controller, fund, expiry);
}
@ -105,7 +110,11 @@ contract Vault is VaultBase {
/// of the recipient. ERC20 tokens are transfered from the caller to the vault
/// contract.
/// Only allowed when the fund is locked.
function deposit(Fund fund, Recipient recipient, uint128 amount) public {
function deposit(
Fund fund,
Recipient recipient,
uint128 amount
) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_deposit(controller, fund, recipient, amount);
}
@ -118,7 +127,7 @@ contract Vault is VaultBase {
Fund fund,
Recipient recipient,
uint128 amount
) public {
) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_designate(controller, fund, recipient, amount);
}
@ -131,7 +140,7 @@ contract Vault is VaultBase {
Recipient from,
Recipient to,
uint128 amount
) public {
) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_transfer(controller, fund, from, to, amount);
}
@ -148,14 +157,18 @@ contract Vault is VaultBase {
Recipient from,
Recipient to,
TokensPerSecond rate
) public {
) 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.
/// Only allowed when the fund is locked.
function burnDesignated(Fund fund, Recipient recipient, uint128 amount) public {
function burnDesignated(
Fund fund,
Recipient recipient,
uint128 amount
) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_burnDesignated(controller, fund, recipient, amount);
}
@ -163,14 +176,14 @@ contract Vault is VaultBase {
/// Burns all tokens from the account of the recipient.
/// 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 {
function burnAccount(Fund fund, Recipient recipient) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_burnAccount(controller, fund, recipient);
}
/// Burns all tokens from all accounts in a fund.
/// Only allowed when the fund is locked.
function burnFund(Fund fund) public {
function burnFund(Fund fund) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_burnFund(controller, fund);
}
@ -181,7 +194,7 @@ contract Vault is VaultBase {
/// The recipient 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 {
function withdraw(Fund fund, Recipient recipient) public whenNotPaused {
Controller controller = Controller.wrap(msg.sender);
_withdraw(controller, fund, recipient);
}
@ -193,4 +206,12 @@ contract Vault is VaultBase {
Recipient recipient = Recipient.wrap(msg.sender);
_withdraw(controller, fund, recipient);
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
}

View File

@ -894,4 +894,133 @@ describe("Vault", function () {
await expect(vault.burnFund(fund)).to.be.revertedWith("FundNotLocked")
})
}
describe("pausing", function () {
let owner
let owner2
let other
beforeEach(async function () {
;[owner, owner2, other] = await ethers.getSigners()
})
it("allows the vault to be paused by the owner", async function () {
await expect(vault.connect(owner).pause()).not.to.be.reverted
})
it("allows the vault to be unpaused by the owner", async function () {
await vault.connect(owner).pause()
await expect(vault.connect(owner).unpause()).not.to.be.reverted
})
it("does not allow pause to be called by others", async function () {
await expect(vault.connect(other).pause()).to.be.revertedWith(
"UnauthorizedAccount"
)
})
it("does not allow unpause to be called by others", async function () {
await vault.connect(owner).pause()
await expect(vault.connect(other).unpause()).to.be.revertedWith(
"UnauthorizedAccount"
)
})
it("allows the ownership to change", async function () {
await vault.connect(owner).pause()
await vault.connect(owner).transferOwnership(owner2.address)
await expect(vault.connect(owner2).unpause()).not.to.be.reverted
})
it("allows the ownership to be renounced", async function () {
await vault.connect(owner).renounceOwnership()
await expect(vault.connect(owner).pause()).to.be.revertedWith(
"UnauthorizedAccount"
)
})
describe("when the vault is paused", function () {
let expiry
let maximum
beforeEach(async function () {
expiry = (await currentTime()) + 80
maximum = (await currentTime()) + 100
await vault.lock(fund, expiry, maximum)
await token.approve(vault.address, 1000)
await vault.deposit(fund, account.address, 1000)
await vault.designate(fund, account.address, 100)
await vault.connect(owner).pause()
})
it("only allows a recipient to withdraw itself", async function () {
await advanceTimeTo(expiry)
await expect(
vault.connect(account).withdrawByRecipient(controller.address, fund)
).not.to.be.reverted
})
it("does not allow funds to be locked", async function () {
const fund = randomBytes(32)
const expiry = (await currentTime()) + 100
await expect(vault.lock(fund, expiry, expiry)).to.be.revertedWith(
"EnforcedPause"
)
})
it("does not allow extending of lock", async function () {
await expect(vault.extendLock(fund, maximum)).to.be.revertedWith(
"EnforcedPause"
)
})
it("does not allow depositing of tokens", async function () {
await token.approve(vault.address, 100)
await expect(
vault.deposit(fund, account.address, 100)
).to.be.revertedWith("EnforcedPause")
})
it("does not allow designating tokens", async function () {
await expect(
vault.designate(fund, account.address, 10)
).to.be.revertedWith("EnforcedPause")
})
it("does not allow transfer of tokens", async function () {
await expect(
vault.transfer(fund, account.address, account2.address, 10)
).to.be.revertedWith("EnforcedPause")
})
it("does not allow new token flows to start", async function () {
await expect(
vault.flow(fund, account.address, account2.address, 1)
).to.be.revertedWith("EnforcedPause")
})
it("does not allow burning of designated tokens", async function () {
await expect(
vault.burnDesignated(fund, account.address, 10)
).to.be.revertedWith("EnforcedPause")
})
it("does not allow burning of accounts", async function () {
await expect(
vault.burnAccount(fund, account.address)
).to.be.revertedWith("EnforcedPause")
})
it("does not allow burning an entire fund", async function () {
await expect(vault.burnFund(fund)).to.be.revertedWith("EnforcedPause")
})
it("does not allow a controller to withdraw for a recipient", async function () {
await advanceTimeTo(expiry)
await expect(vault.withdraw(fund, account.address)).to.be.revertedWith(
"EnforcedPause"
)
})
})
})
})