-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of github.com:thesis/acre into currency-balance
- Loading branch information
Showing
24 changed files
with
521 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
{ | ||
"extends": "thesis", | ||
"plugins": [] | ||
"plugins": [], | ||
"rules": {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./context" | ||
export * from "./contract" | ||
export * from "./signer" |
Oops, something went wrong.