Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve test fixtures #64

Merged
merged 29 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
32b0f4d
Handling ERC4626 vaults in AcreRouter
dimpar Dec 4, 2023
3246552
Adding pnpm-lock.yaml
dimpar Dec 4, 2023
a3ba20f
Replacing OZ upgradable lib to a non-upgradable one
dimpar Dec 6, 2023
072f854
Drafting tests for AcreRouter
dimpar Dec 6, 2023
cd91d07
Add helpers to get deployed contract
nkuba Dec 5, 2023
4bf5442
Add helpers for hardhat signers
nkuba Dec 5, 2023
16676e9
Improve test fixture used in Acre test
nkuba Dec 5, 2023
07ff2a0
Merge remote-tracking branch 'origin/test-fixtures' into vaults-acre-…
dimpar Dec 7, 2023
9a9c513
Adding AcreRouter to deployed context
dimpar Dec 7, 2023
2ae92ec
Adding AcreRouter deployment scripts
dimpar Dec 7, 2023
fd4a287
Refactoring tests adjusting for helper methods
dimpar Dec 7, 2023
1c85545
Adding func-visibility rule
dimpar Dec 7, 2023
2bbf761
Referencing correct vault names in tests
dimpar Dec 7, 2023
cc54053
Merge remote-tracking branch 'origin/main' into test-fixtures
nkuba Dec 8, 2023
3a05a45
Replace realpath with another resolution
nkuba Dec 8, 2023
aa133de
Removing a rule that already exist in parent config
dimpar Dec 11, 2023
9e0ffd9
Renaming AcreRouter -> Dispatcher
dimpar Dec 11, 2023
b56c0ec
Changing a comment for vaults array
dimpar Dec 11, 2023
d7e8ac6
Renaming Vault -> VaultInfo struct
dimpar Dec 11, 2023
01086fc
Approve->authorize for vaults and functions around it
dimpar Dec 11, 2023
f477e45
Replacing require err messages with custom errors
dimpar Dec 11, 2023
d0bd5dd
Renaming events and returning the entire vaults array
dimpar Dec 11, 2023
cf0d47d
Adding func.dependencies for Dispatcher ownership transfer
dimpar Dec 11, 2023
b131f82
Refactoring Dispatcher tests:
dimpar Dec 11, 2023
e94c5fc
Simplifying tests
dimpar Dec 11, 2023
5cdfff9
Handling ERC4626 vaults in AcreRouter (#62)
nkuba Dec 13, 2023
e21a50b
Improve signers mapping
nkuba Dec 13, 2023
2831ef6
Don't return awaited promise in getUnnamedSigner
nkuba Dec 13, 2023
790c768
Merge remote-tracking branch 'origin/main' into test-fixtures
nkuba Dec 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading