Skip to content

Commit

Permalink
Drafting integration flows for assets
Browse files Browse the repository at this point in the history
  • Loading branch information
dimpar committed Dec 12, 2023
1 parent 54a1562 commit c0485d9
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 7 deletions.
31 changes: 31 additions & 0 deletions core/contracts/Acre.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./Dispatcher.sol";

/// @title Acre
/// @notice This contract implements the ERC-4626 tokenized vault standard. By
Expand All @@ -15,6 +17,12 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
/// burning of shares (stBTC), which are represented as standard ERC20
/// tokens, providing a seamless exchange with tBTC tokens.
contract Acre is ERC4626 {
using SafeERC20 for IERC20;

Dispatcher public dispatcher;

error ZeroAddress();

event StakeReferral(bytes32 indexed referral, uint256 assets);

constructor(
Expand Down Expand Up @@ -43,4 +51,27 @@ contract Acre is ERC4626 {

return shares;
}

// TODO: add onlyOwner
function updateDispatcher(Dispatcher _dispatcher) external {
if (address(_dispatcher) == address(0)) {
revert ZeroAddress();
}
dispatcher = _dispatcher;
}

// TODO: Add maintainerOnly or maintainerOrOwner?
// We should decide if this function should be called by a bot (maintainer)
// only. Leaving as is for now for testing purposes.
function depositToVault(address vault, uint256 amount, uint256 minSharesOut) external {
IERC20(asset()).safeIncreaseAllowance(address(dispatcher), amount);
// TODO: check if the dispatcher is set
dispatcher.depositToVault(vault, amount, minSharesOut);
}

// TODO: same question as for depositToVault
function redeemFromVault(address vault, uint256 shares, uint256 minAssetsOut) external {
// TODO: implement
dispatcher.redeemFromVault(vault, shares, minAssetsOut);
}
}
19 changes: 15 additions & 4 deletions core/contracts/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ contract Dispatcher is Router, Ownable {

error VaultAlreadyAuthorized();
error VaultUnauthorized();
error CallerUnauthorized(string reason);

struct VaultInfo {
bool authorized;
Expand Down Expand Up @@ -78,27 +79,37 @@ contract Dispatcher is Router, Ownable {
return vaults;
}

// TODO: add documentation
/// @notice Routes tBTC from stBTC (Acre) to a vault.
/// @param vault Address of the vault to route the assets to.
/// @param amount Amount of tBTC to deposit.
/// @param minSharesOut Minimum amount of shares to receive by Acre.
function depositToVault(
address vault,
uint256 amount,
uint256 minSharesOut
) public {
require(msg.sender == address(stBTC), "stBTC only");
if (msg.sender != address(stBTC)) {
revert CallerUnauthorized("Acre only");
}
if (!vaultsInfo[vault].authorized) {
revert VaultUnauthorized();
}

deposit(IERC4626(vault), address(stBTC), amount, minSharesOut);
}

// TODO: add documentation
/// @notice Routes tBTC from a vault to stBTC (Acre).
/// @param vault Address of the vault to collect the assets from.
/// @param shares Amount of shares to collect.
/// @param minAssetsOut Minimum amount of TBTC to receive.
function redeemFromVault(
address vault,
uint256 shares,
uint256 minAssetsOut
) public {
require(msg.sender == address(stBTC), "stBTC only");
if (msg.sender != address(stBTC)) {
revert CallerUnauthorized("Acre only");
}
if (!vaultsInfo[vault].authorized) {
revert VaultUnauthorized();
}
Expand Down
12 changes: 12 additions & 0 deletions core/contracts/test/TestERC4626.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

contract TestERC4626 is ERC4626 {
constructor(
IERC20 asset,
string memory tokenName,
string memory tokenSymbol
) ERC4626(asset) ERC20(tokenName, tokenSymbol) {}
}
27 changes: 27 additions & 0 deletions core/deploy/00_resolve_testing_erc4626.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"

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

const tBTC = await deployments.get("TBTC")

if (hre.network.tags.allowStubs) {
log("deploying Mock ERC4626 Vault")

await deployments.deploy("Vault", {
contract: "TestERC4626",
from: deployer,
args: [tBTC.address, "MockVault", "MV"],
log: true,
waitConfirmations: 1,
})
}
}

export default func

func.tags = ["TestERC4626"]
func.dependencies = ["TBTC"]
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {

const tTBC = await deployments.get("TBTC")
const stBTC = await deployments.get("Acre")

Check failure on line 10 in core/deploy/02_deploy_dispatcher.ts

View workflow job for this annotation

GitHub Actions / core-format

Delete `··`
await deployments.deploy("Dispatcher", {
from: deployer,
args: [stBTC.address, tTBC.address],
log: true,
waitConfirmations: 1,
})
const dispatcher = await deployments.get("Dispatcher")

// TODO: move to a separate script
await deployments.execute(
"Acre",
{ from: deployer, log: true, waitConfirmations: 1 },
"updateDispatcher",
dispatcher.address,
)

// TODO: Add Etherscan verification
// TODO: Add Tenderly verification
Expand Down
5 changes: 3 additions & 2 deletions core/test/helpers/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { deployments } from "hardhat"

import { getDeployedContract } from "./contract"

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

// eslint-disable-next-line import/prefer-default-export
export async function deployment() {
Expand All @@ -11,6 +11,7 @@ export async function deployment() {
const tbtc: TestERC20 = await getDeployedContract("TBTC")
const acre: Acre = await getDeployedContract("Acre")
const dispatcher: Dispatcher = await getDeployedContract("Dispatcher")
const vault: TestERC4626 = await getDeployedContract("Vault")

return { tbtc, acre, dispatcher }
return { tbtc, acre, dispatcher, vault }
}
93 changes: 93 additions & 0 deletions core/test/integration/AssetFlows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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 { Acre, Dispatcher, TestERC4626, TestERC20 } from "../../typechain"
import { deployment } from "../helpers/context"
import { getNamedSigner, getUnnamedSigner } from "../helpers/signer"
import { to1e18 } from "../utils"

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

return { acre, dispatcher, governance, thirdParty, vault, tbtc }
}

describe.only("Integration Test - Asset Flows", () => {

Check failure on line 22 in core/test/integration/AssetFlows.test.ts

View workflow job for this annotation

GitHub Actions / core-format

describe.only not permitted
let snapshot: SnapshotRestorer

let dispatcher: Dispatcher
let acre: Acre
let vault: TestERC4626
let tbtc: TestERC20
let governance: HardhatEthersSigner
let thirdParty: HardhatEthersSigner

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

Check failure on line 33 in core/test/integration/AssetFlows.test.ts

View workflow job for this annotation

GitHub Actions / core-format

Insert `⏎·····`

await dispatcher.connect(governance).authorizeVault(vault.getAddress())
await tbtc.mint(acre.getAddress(), to1e18(100000))
})

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

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


Check failure on line 47 in core/test/integration/AssetFlows.test.ts

View workflow job for this annotation

GitHub Actions / core-format

Delete `⏎`
describe("depositToVault", () => {
const tbtcToDeposit = to1e18(100)
const shares = to1e18(100)

context("when caller is not Acre (stBTC)", () => {
it("should revert when depositing to a vault", async () => {
await expect(
dispatcher.connect(thirdParty).depositToVault(vault.getAddress(), tbtcToDeposit, shares),

Check failure on line 55 in core/test/integration/AssetFlows.test.ts

View workflow job for this annotation

GitHub Actions / core-format

Replace `.connect(thirdParty)` with `⏎············.connect(thirdParty)⏎············`
).to.be.revertedWithCustomError(

Check failure on line 56 in core/test/integration/AssetFlows.test.ts

View workflow job for this annotation

GitHub Actions / core-format

Replace `.to.be.revertedWithCustomError(⏎··········dispatcher,⏎··········"CallerUnauthorized",` with `⏎··········.to.be.revertedWithCustomError(dispatcher,·"CallerUnauthorized")`
dispatcher,
"CallerUnauthorized",
).withArgs("Acre only")

Check failure on line 59 in core/test/integration/AssetFlows.test.ts

View workflow job for this annotation

GitHub Actions / core-format

Replace `)` with `··`
})
})

context("when caller is Acre (stBTC)", () => {
it("should revert when vault is unauthorized", async () => {
const randomAddress = await ethers.Wallet.createRandom().getAddress()
await expect(
acre.depositToVault(randomAddress, tbtcToDeposit, shares),
).to.be.revertedWithCustomError(

Check failure on line 68 in core/test/integration/AssetFlows.test.ts

View workflow job for this annotation

GitHub Actions / core-format

Replace `⏎··········dispatcher,⏎··········"VaultUnauthorized",⏎········` with `dispatcher,·"VaultUnauthorized"`
dispatcher,
"VaultUnauthorized",
)
})

it("should be able to deposit to an authorized vault", async () => {
await acre.depositToVault(vault.getAddress(), tbtcToDeposit, shares)

expect(await tbtc.balanceOf(vault.getAddress())).to.equal(tbtcToDeposit)
})

it("should be able to receive vault's shares", async () => {
await acre.depositToVault(vault.getAddress(), tbtcToDeposit, shares)

expect(await vault.balanceOf(acre.getAddress())).to.equal(shares)
})

// TODO: add more tests
})
})

describe("redeemFromVault", () => {
// TODO: add tests
})
})

0 comments on commit c0485d9

Please sign in to comment.