-
Notifications
You must be signed in to change notification settings - Fork 2
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
Handling ERC4626 vaults in AcreRouter #62
Changes from all commits
32b0f4d
3246552
a3ba20f
072f854
07ff2a0
9a9c513
2ae92ec
fd4a287
1c85545
2bbf761
aa133de
9e0ffd9
b56c0ec
d7e8ac6
01086fc
f477e45
d0bd5dd
cf0d47d
b131f82
e94c5fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
{ | ||
"extends": "thesis", | ||
"plugins": [] | ||
"plugins": [], | ||
"rules": {} | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wonder if we need this array. From the dapp/subgprah perspective, we can get the vault address from an event. Not sure about the contracts perspective, maybe other contracts will need this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This array will track all the vaults within Acre ecosystem. |
||
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; | ||
} | ||
} |
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"] |
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"] | ||
nkuba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
func.dependencies = ["Dispatcher"] |
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) | ||
}) | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a warning here. We should define errors below the events declaration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I'll change that in the following PR