Skip to content

Commit

Permalink
Improve test fixtures (#64)
Browse files Browse the repository at this point in the history
Depends on: #58

Improve test fixtures used in Acre contract tests:
- use Acre and TBTC contracts deployed with hardhat development scripts
(#58)
- use unnamed signers as stakers, to not overlap with named signers
defined in `hardhat.config.ts`. `ethers.getSigners()` returns all
signers, regardless of whether it is a Hardhat named or unnamed account

---
This PR contains changes from #62
which were unintentionally merged to this branch.
  • Loading branch information
dimpar authored Dec 13, 2023
2 parents f8a2b11 + 790c768 commit 574cec8
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 14 deletions.
3 changes: 2 additions & 1 deletion core/.solhint.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"extends": "thesis",
"plugins": []
"plugins": [],
"rules": {}
}
69 changes: 69 additions & 0 deletions core/contracts/Dispatcher.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/access/Ownable.sol";

/// @title Dispatcher
/// @notice Dispatcher is a contract that routes TBTC from stBTC (Acre) to
/// a given vault and back. Vaults supply yield strategies with TBTC that
/// generate yield for Bitcoin holders.
contract Dispatcher is Ownable {
error VaultAlreadyAuthorized();
error VaultUnauthorized();

struct VaultInfo {
bool authorized;
}

/// @notice Authorized Yield Vaults that implement ERC4626 standard. These
/// vaults deposit assets to yield strategies, e.g. Uniswap V3
/// WBTC/TBTC pool. Vault can be a part of Acre ecosystem or can be
/// implemented externally. As long as it complies with ERC4626
/// standard and is authorized by the owner it can be plugged into
/// Acre.
address[] public vaults;
mapping(address => VaultInfo) public vaultsInfo;

event VaultAuthorized(address indexed vault);
event VaultDeauthorized(address indexed vault);

constructor() Ownable(msg.sender) {}

/// @notice Adds a vault to the list of authorized vaults.
/// @param vault Address of the vault to add.
function authorizeVault(address vault) external onlyOwner {
if (vaultsInfo[vault].authorized) {
revert VaultAlreadyAuthorized();
}

vaults.push(vault);
vaultsInfo[vault].authorized = true;

emit VaultAuthorized(vault);
}

/// @notice Removes a vault from the list of authorized vaults.
/// @param vault Address of the vault to remove.
function deauthorizeVault(address vault) external onlyOwner {
if (!vaultsInfo[vault].authorized) {
revert VaultUnauthorized();
}

vaultsInfo[vault].authorized = false;

for (uint256 i = 0; i < vaults.length; i++) {
if (vaults[i] == vault) {
vaults[i] = vaults[vaults.length - 1];
// slither-disable-next-line costly-loop
vaults.pop();
break;
}
}

emit VaultDeauthorized(vault);
}

function getVaults() external view returns (address[] memory) {
return vaults;
}
}
22 changes: 22 additions & 0 deletions core/deploy/02_deploy_acre_router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"

const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { getNamedAccounts, deployments } = hre
const { deployer } = await getNamedAccounts()

await deployments.deploy("Dispatcher", {
from: deployer,
args: [],
log: true,
waitConfirmations: 1,
})

// TODO: Add Etherscan verification
// TODO: Add Tenderly verification
}

export default func

func.tags = ["Dispatcher"]
func.dependencies = ["Acre"]
22 changes: 22 additions & 0 deletions core/deploy/22_transfer_ownership_acre_router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"

const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { getNamedAccounts, deployments } = hre
const { deployer, governance } = await getNamedAccounts()
const { log } = deployments

log(`transferring ownership of Dispatcher contract to ${governance}`)

await deployments.execute(
"Dispatcher",
{ from: deployer, log: true, waitConfirmations: 1 },
"transferOwnership",
governance,
)
}

export default func

func.tags = ["TransferOwnershipAcreRouter"]
func.dependencies = ["Dispatcher"]
5 changes: 4 additions & 1 deletion core/scripts/fetch_external_artifacts.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#! /bin/bash
set -eou pipefail

ROOT_DIR="$(realpath "$(dirname $0)/../")"
ROOT_DIR=$(
cd "$(dirname $0)/../"
pwd -P
)
TMP_DIR=${ROOT_DIR}/tmp/external-artifacts
EXTERNAL_ARTIFACTS_DIR=${ROOT_DIR}/external

Expand Down
24 changes: 12 additions & 12 deletions core/test/Acre.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import {
SnapshotRestorer,
loadFixture,
takeSnapshot,
loadFixture,
} from "@nomicfoundation/hardhat-toolbox/network-helpers"
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"
import { ethers } from "hardhat"
import { expect } from "chai"
import { ContractTransactionResponse, ZeroAddress } from "ethers"
import type { TestERC20, Acre } from "../typechain"

import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"
import type { SnapshotRestorer } from "@nomicfoundation/hardhat-toolbox/network-helpers"
import { deployment } from "./helpers/context"
import { getUnnamedSigner } from "./helpers/signer"

import { to1e18 } from "./utils"

async function acreFixture() {
const [staker1, staker2] = await ethers.getSigners()
import type { Acre, TestERC20 } from "../typechain"

const TestERC20 = await ethers.getContractFactory("TestERC20")
const tbtc = await TestERC20.deploy()
async function fixture() {
const { tbtc, acre } = await deployment()

const Acre = await ethers.getContractFactory("Acre")
const acre = await Acre.deploy(await tbtc.getAddress())
const [staker1, staker2] = await getUnnamedSigner()

const amountToMint = to1e18(100000)
tbtc.mint(staker1, amountToMint)
Expand All @@ -33,7 +33,7 @@ describe("Acre", () => {
let staker2: HardhatEthersSigner

before(async () => {
;({ acre, tbtc, staker1, staker2 } = await loadFixture(acreFixture))
;({ acre, tbtc, staker1, staker2 } = await loadFixture(fixture))
})

describe("stake", () => {
Expand Down
156 changes: 156 additions & 0 deletions core/test/Dispatcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { ethers } from "hardhat"
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"
import { expect } from "chai"
import {
SnapshotRestorer,
takeSnapshot,
loadFixture,
} from "@nomicfoundation/hardhat-toolbox/network-helpers"
import type { Dispatcher } from "../typechain"
import { deployment } from "./helpers/context"
import { getNamedSigner, getUnnamedSigner } from "./helpers/signer"

async function fixture() {
const { dispatcher } = await deployment()
const { governance } = await getNamedSigner()
const [thirdParty] = await getUnnamedSigner()

return { dispatcher, governance, thirdParty }
}

describe("Dispatcher", () => {
let snapshot: SnapshotRestorer

let dispatcher: Dispatcher
let governance: HardhatEthersSigner
let thirdParty: HardhatEthersSigner
let vaultAddress1: string
let vaultAddress2: string
let vaultAddress3: string
let vaultAddress4: string

before(async () => {
;({ dispatcher, governance, thirdParty } = await loadFixture(fixture))

vaultAddress1 = await ethers.Wallet.createRandom().getAddress()
vaultAddress2 = await ethers.Wallet.createRandom().getAddress()
vaultAddress3 = await ethers.Wallet.createRandom().getAddress()
vaultAddress4 = await ethers.Wallet.createRandom().getAddress()
})

beforeEach(async () => {
snapshot = await takeSnapshot()
})

afterEach(async () => {
await snapshot.restore()
})

describe("authorizeVault", () => {
context("when caller is not a governance account", () => {
it("should revert when adding a vault", async () => {
await expect(
dispatcher.connect(thirdParty).authorizeVault(vaultAddress1),
).to.be.revertedWithCustomError(
dispatcher,
"OwnableUnauthorizedAccount",
)
})
})

context("when caller is a governance account", () => {
it("should be able to authorize vaults", async () => {
await dispatcher.connect(governance).authorizeVault(vaultAddress1)
await dispatcher.connect(governance).authorizeVault(vaultAddress2)
await dispatcher.connect(governance).authorizeVault(vaultAddress3)

expect(await dispatcher.vaults(0)).to.equal(vaultAddress1)
expect(await dispatcher.vaultsInfo(vaultAddress1)).to.be.equal(true)

expect(await dispatcher.vaults(1)).to.equal(vaultAddress2)
expect(await dispatcher.vaultsInfo(vaultAddress2)).to.be.equal(true)

expect(await dispatcher.vaults(2)).to.equal(vaultAddress3)
expect(await dispatcher.vaultsInfo(vaultAddress3)).to.be.equal(true)
})

it("should not be able to authorize the same vault twice", async () => {
await dispatcher.connect(governance).authorizeVault(vaultAddress1)
await expect(
dispatcher.connect(governance).authorizeVault(vaultAddress1),
).to.be.revertedWithCustomError(dispatcher, "VaultAlreadyAuthorized")
})

it("should emit an event when adding a vault", async () => {
await expect(
dispatcher.connect(governance).authorizeVault(vaultAddress1),
)
.to.emit(dispatcher, "VaultAuthorized")
.withArgs(vaultAddress1)
})
})
})

describe("deauthorizeVault", () => {
beforeEach(async () => {
await dispatcher.connect(governance).authorizeVault(vaultAddress1)
await dispatcher.connect(governance).authorizeVault(vaultAddress2)
await dispatcher.connect(governance).authorizeVault(vaultAddress3)
})

context("when caller is not a governance account", () => {
it("should revert when adding a vault", async () => {
await expect(
dispatcher.connect(thirdParty).deauthorizeVault(vaultAddress1),
).to.be.revertedWithCustomError(
dispatcher,
"OwnableUnauthorizedAccount",
)
})
})

context("when caller is a governance account", () => {
it("should be able to authorize vaults", async () => {
await dispatcher.connect(governance).deauthorizeVault(vaultAddress1)

// Last vault replaced the first vault in the 'vaults' array
expect(await dispatcher.vaults(0)).to.equal(vaultAddress3)
expect(await dispatcher.vaultsInfo(vaultAddress1)).to.be.equal(false)
expect((await dispatcher.getVaults()).length).to.equal(2)

await dispatcher.connect(governance).deauthorizeVault(vaultAddress2)

// Last vault (vaultAddress2) was removed from the 'vaults' array
expect(await dispatcher.vaults(0)).to.equal(vaultAddress3)
expect((await dispatcher.getVaults()).length).to.equal(1)
expect(await dispatcher.vaultsInfo(vaultAddress2)).to.be.equal(false)

await dispatcher.connect(governance).deauthorizeVault(vaultAddress3)
expect((await dispatcher.getVaults()).length).to.equal(0)
expect(await dispatcher.vaultsInfo(vaultAddress3)).to.be.equal(false)
})

it("should be able to deauthorize a vault and authorize it again", async () => {
await dispatcher.connect(governance).deauthorizeVault(vaultAddress1)
expect(await dispatcher.vaultsInfo(vaultAddress1)).to.be.equal(false)

await dispatcher.connect(governance).authorizeVault(vaultAddress1)
expect(await dispatcher.vaultsInfo(vaultAddress1)).to.be.equal(true)
})

it("should not be able to deauthorize a vault that is not authorized", async () => {
await expect(
dispatcher.connect(governance).deauthorizeVault(vaultAddress4),
).to.be.revertedWithCustomError(dispatcher, "VaultUnauthorized")
})

it("should emit an event when removing a vault", async () => {
await expect(
dispatcher.connect(governance).deauthorizeVault(vaultAddress1),
)
.to.emit(dispatcher, "VaultDeauthorized")
.withArgs(vaultAddress1)
})
})
})
})
16 changes: 16 additions & 0 deletions core/test/helpers/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { deployments } from "hardhat"

import { getDeployedContract } from "./contract"

import type { Acre, Dispatcher, TestERC20 } from "../../typechain"

// eslint-disable-next-line import/prefer-default-export
export async function deployment() {
await deployments.fixture()

const tbtc: TestERC20 = await getDeployedContract("TBTC")
const acre: Acre = await getDeployedContract("Acre")
const dispatcher: Dispatcher = await getDeployedContract("Dispatcher")

return { tbtc, acre, dispatcher }
}
22 changes: 22 additions & 0 deletions core/test/helpers/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ethers } from "ethers"
import { deployments } from "hardhat"

import type { BaseContract } from "ethers"
import { getUnnamedSigner } from "./signer"

/**
* Get instance of a contract from Hardhat Deployments.
* @param deploymentName Name of the contract deployment.
* @returns Deployed Ethers contract instance.
*/
// eslint-disable-next-line import/prefer-default-export
export async function getDeployedContract<T extends BaseContract>(
deploymentName: string,
): Promise<T> {
const { address, abi } = await deployments.get(deploymentName)

// Use default unnamed signer from index 0 to initialize the contract runner.
const [defaultSigner] = await getUnnamedSigner()

return new ethers.BaseContract(address, abi, defaultSigner) as T
}
3 changes: 3 additions & 0 deletions core/test/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./context"
export * from "./contract"
export * from "./signer"
Loading

0 comments on commit 574cec8

Please sign in to comment.