Skip to content

Commit

Permalink
WIP Dispatcher and funds routing
Browse files Browse the repository at this point in the history
This is WIP code to check how dispatching to vaults could work
  • Loading branch information
nkuba committed Dec 8, 2023
1 parent 2bbf761 commit bd0559a
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 6 deletions.
20 changes: 18 additions & 2 deletions core/contracts/Acre.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
pragma solidity ^0.8.21;

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

import "./Dispatcher.sol";

/// @title Acre
/// @notice This contract implements the ERC-4626 tokenized vault standard. By
Expand All @@ -14,12 +17,15 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
/// of yield-bearing vaults. This contract facilitates the minting and
/// burning of shares (stBTC), which are represented as standard ERC20
/// tokens, providing a seamless exchange with tBTC tokens.
contract Acre is ERC4626 {
contract Acre is ERC4626, Ownable {
event StakeReferral(bytes32 indexed referral, uint256 assets);

Dispatcher public dispatcher;

constructor(
IERC20 tbtc
) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") {}
) ERC4626(tbtc) ERC20("Acre Staked Bitcoin", "stBTC") Ownable(msg.sender) {}


/// @notice Stakes a given amount of tBTC token and mints shares to a
/// receiver.
Expand All @@ -43,4 +49,14 @@ contract Acre is ERC4626 {

return shares;
}

function upgradeDispatcher(Dispatcher _newDispatcher) public onlyOwner {
if (address(dispatcher) != address(0)) {
IERC20(asset()).approve(address(dispatcher), 0);
}

dispatcher = _newDispatcher;

IERC20(asset()).approve(address(dispatcher), type(uint256).max);
}
}
34 changes: 34 additions & 0 deletions core/contracts/Dispatcher.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import "./Router.sol";

contract Dispatcher is Router, Ownable {
using SafeERC20 for IERC20;

Acre acre;

constructor(Acre _acre) Ownable(msg.sender) {
acre = _acre;
}

function assetsHolder() public virtual override returns (address){
return address(acre);
}

function sharesHolder() public virtual override returns (address){
return address(this);
}

function migrateShares(IERC4626[] calldata _vaults) public onlyOwner {
address newDispatcher = address(acre.dispatcher());

for (uint i=0; i<_vaults.length; i++) {
_vaults[i].transfer(newDispatcher, _vaults[i].balanceOf(address(this)));
}
}
}
67 changes: 67 additions & 0 deletions core/contracts/Router.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./Acre.sol";


// TODO: Consider deploying ERC4626RouterBase from the ERC4626 Alliance.
// TODO: Think about adding reentrancy guard
// TODO: Add ACL

abstract contract Router {
using SafeERC20 for IERC20;

/// @notice thrown when amount of assets received is below the min set by caller
error MinAmountError();

/// @notice thrown when amount of shares received is below the min set by caller
error MinSharesError();

/// @notice thrown when amount of assets received is above the max set by caller
error MaxAmountError();

/// @notice thrown when amount of shares received is above the max set by caller
error MaxSharesError();


function assetsHolder() public virtual returns (address);
function sharesHolder() public virtual returns (address);

function deposit(
IERC4626 vault,
uint256 amount,
uint256 minSharesOut
) public returns (uint256 sharesOut) {
IERC20(vault.asset()).safeTransferFrom(assetsHolder(), address(this), amount);

IERC20(vault.asset()).approve(address(vault), amount);

if ((sharesOut = vault.deposit(amount, sharesHolder())) < minSharesOut) {
revert MinSharesError();
}
}


function withdraw(
IERC4626 vault,
uint256 amount,
uint256 maxSharesOut
) public returns (uint256 sharesOut) {
if ((sharesOut = vault.withdraw(amount, assetsHolder(), sharesHolder())) > maxSharesOut) {
revert MaxSharesError();
}
}

function redeem(
IERC4626 vault,
uint256 shares,
uint256 minAmountOut
) public returns (uint256 amountOut) {
if ((amountOut = vault.redeem(shares, assetsHolder(), sharesHolder())) < minAmountOut) {
revert MinAmountError();
}
}
}
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) {}
}
24 changes: 24 additions & 0 deletions core/deploy/02_deploy_dispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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()

const acre = await deployments.get("Acre")

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

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

export default func

func.tags = ["Dispatcher"]
func.dependencies = ["Acre"]
2 changes: 0 additions & 2 deletions core/deploy/21_transfer_ownership_acre.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,3 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
export default func

func.tags = ["TransferOwnershipAcre"]
// TODO: Enable once Acre extends Ownable
func.skip = async () => true
21 changes: 21 additions & 0 deletions core/deploy/22_transfer_ownership_dispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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 AcreRouter contract to ${governance}`)

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

export default func

func.tags = ["TransferOwnershipDispatcher"]
109 changes: 109 additions & 0 deletions core/test/Dispatcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"
import { expect } from "chai"

import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"

import { ethers } from "hardhat"
import { deployment } from "./helpers/context"
import { getNamedSigner, getUnnamedSigner } from "./helpers/signer"

import { to1e18 } from "./utils"

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

async function fixture() {
const { tbtc, acre, dispatcher } = await deployment()

const { governance } = await getNamedSigner()

const [staker1] = await getUnnamedSigner()

await acre
.connect(governance)
.upgradeDispatcher(await dispatcher.getAddress())

const vault: TestERC4626 = await ethers.deployContract("TestERC4626", [
await tbtc.getAddress(),
"Test Vault Token",
"vToken",
])
await vault.waitForDeployment()

return { acre, tbtc, dispatcher, vault, staker1 }
}

describe("Dispatcher", () => {
const staker1Amount = to1e18(1000)

let acre: Acre
let tbtc: TestERC20
let dispatcher: Dispatcher
let vault: TestERC4626

let staker1: HardhatEthersSigner

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

it("test deposit and withdraw", async () => {
// Mint tBTC for staker.
await tbtc.mint(staker1.address, staker1Amount)

// Stake tBTC in Acre.
await tbtc.approve(await acre.getAddress(), staker1Amount)
await acre.connect(staker1).deposit(staker1Amount, staker1.address)

expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal(
staker1Amount,
)

const vaultDepositAmount = to1e18(500)
const expectedSharesDeposit = vaultDepositAmount

await dispatcher.deposit(
await vault.getAddress(),
vaultDepositAmount,
expectedSharesDeposit,
)

expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal(
staker1Amount - vaultDepositAmount,
)
expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0)
expect(await tbtc.balanceOf(await vault.getAddress())).to.be.equal(
vaultDepositAmount,
)

expect(await vault.balanceOf(await acre.getAddress())).to.be.equal(0)
expect(await vault.balanceOf(await dispatcher.getAddress())).to.be.equal(
expectedSharesDeposit,
)

// // Simulate Vault generating yield.
// const yieldAmount = to1e18(200)
// await tbtc.mint(await vault.getAddress(), yieldAmount)

// Partial withdrawal.
const amountToWithdraw1 = to1e18(300)
const expectedSharesWithdraw = to1e18(300)
await dispatcher.withdraw(
await vault.getAddress(),
amountToWithdraw1,
expectedSharesWithdraw,
)

expect(await vault.balanceOf(await acre.getAddress())).to.be.equal(0)
expect(await vault.balanceOf(await dispatcher.getAddress())).to.be.equal(
expectedSharesDeposit - expectedSharesWithdraw,
)

expect(await tbtc.balanceOf(await acre.getAddress())).to.be.equal(
staker1Amount - vaultDepositAmount + amountToWithdraw1,
)
expect(await tbtc.balanceOf(await dispatcher.getAddress())).to.be.equal(0)
expect(await tbtc.balanceOf(await vault.getAddress())).to.be.equal(
vaultDepositAmount - amountToWithdraw1,
)
})
})
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, AcreRouter, TestERC20 } from "../../typechain"
import type { Acre, AcreRouter, Dispatcher, TestERC20 } 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 acreRouter: AcreRouter = await getDeployedContract("AcreRouter")
const dispatcher: Dispatcher = await getDeployedContract("Dispatcher")

return { tbtc, acre, acreRouter }
return { tbtc, acre, acreRouter, dispatcher }
}

0 comments on commit bd0559a

Please sign in to comment.