Skip to content

Commit

Permalink
Initial implementation of Bitcoin depositor (tBTC Depositor) (#91)
Browse files Browse the repository at this point in the history
The Acre Bitcoin Depositor contract is an implementation of a depositor
contract mentioned in
[RFC-11](https://github.com/keep-network/tbtc-v2/blob/main/docs/rfc/rfc-11.adoc).

This contract serves as an integrator of tBTC Bridge and Acre stBTC
staking contract.

Bitcoin deposits revealed via the tBTC Depositor contract will be
automatically staked in Acre after the tBTC minting process is completed
on the tBTC network side.

The contract is based on the `AbstractTBTCDepositor`
(keep-network/tbtc-v2#778).

The staking process consists of two steps:
- `initializeStake` - this step reveals a deposit to tBTC Bridge to
start the minting process
- `finalizeStake` - this step should be called after tBTC minting on the
tBTC Bridge side is completed, and tBTC token is transferred to the tBTC
Depositor contract, this step calculates the approximate amount of
minted tBTC tokens, and stakes them in stBTC contract.

The functions are unprotected, meaning anyone can call them (e.g. bots).
This solution will be used to enable a gasless minting experience for
the users, where only a funding Bitcoin transaction will be required
from them.

### tBTC Minting Fees

The complexity comes with the actual minted tBTC amount calculation as
the tBTC network fees are not unambiguously predictable.
The stake finalization step calculates the amount to stake in Acre by
deducting approximate tBTC network minting fees from the initial funding
transaction amount.

The calculation is performed in
`AbstractTBTCDepositor._calculateTbtcAmount` function.

The amount to stake is calculated d:
`amount = depositAmount - depositTreasuryFee - optimisticMintingFee -
depositTxMaxFee`

These calculations are approximate and can leave some imbalance in the
Depositor contract, as:
- `depositTreasuryFee` - this is a precise value, snapshotted at the
moment of deposit reveal,
- `optimisticMintingFee` - this is an optimistic minting fee calculated
at the moment of
completion notification, there is a very low possibility that the fee
was updated in the tBTC Vault contract between tBTC was minted and
completion notification was submitted,
- `depositTxMaxFee` - this is the maximum transaction fee that can be
deducted on Bitcoin transaction sweeping, in most cases it will be
higher than the actual deducted amount, and will grow the reserve in the
depositor contract.

For the great majority of the deposits, such an algorithm will return a
tbtcAmount slightly lesser than the actual amount of TBTC minted for the
deposit. This will cause some TBTC to be left in the contract and ensure
there is enough liquidity to finalize the deposit. However, in some rare
cases, where the actual values of those fees change between the deposit
minting and finalization, the tbtcAmount returned by this function may
be greater than the actual amount of TBTC minted for the deposit.
If this happens and the reserve coming from previous deposits leftovers
does not provide enough liquidity, the deposit will have to wait for
finalization until the reserve is refilled by subsequent deposits or a
manual top-up.
The Acre governance is responsible for handling such cases.


#### Fee changes simulation

Please see the simulation performed by @lukasz-zimnoch for fee changes
analysis:

<details>
<summary>
Fee changes simulation
</summary>

**Case 1 - Deposit is optimistically minted (currently 98.6% of mainnet
deposits)**

Let's say a deposit is 100 BTC, `treasury_fee = 5%`, `opt_minting_fee =
3%` and `max_tx_fee = 0.01 BTC`. The actually minted amount will be
`(100 BTC * 0.95) * 0.97 = 92.15 TBTC` and this will be in direct
control of the depositor contract. The actual `tx_fee` remains as debt
after the deposit is swept and we know it is lower than `max_tx_fee =
0.01 BTC`

The depositor contract does the finalization AFTER the deposit is
optimistically minted. If it uses the `tbtc_net_amount =
btc_gross_amount - treasury_fee - opt_minting_fee - max_tx_fee`
equation, it will compute `tbtc_net_amount = 100 BTC - 5 BTC - 2.85 BTC
- 0.01 BTC = 92.14 TBTC`. Note that `92.15 - 92.14 = 0.01 TBTC` is in
reserve to cover the `tx_fee` debt which is more than enough.

Now, consider fee changes. Fee changes matters only if they occur
between deposit mint and deposit finalization. Let's suppose the
depositor contract received the aforementioned `92.15 TBTC`. This is the
outcome if particular fees change between deposit mint and finalization:

- If `treasury_fee` changes, it doesn't matter as we used the real
snapshotted value

- If `opt_minting_fee` increases, let's say to 5%, the `tbtc_net_amount
= 100 BTC - 5 BTC - 4.75 BTC - 0.01 BTC = 90.24 TBTC`. That means `92.15
- 90.24 = 1.91 TBTC` stays in reserve

- If `opt_minting_fee` decreases, let's say to 1%, the `tbtc_net_amount
= 100 BTC - 5 BTC - 0.95 BTC - 0.01 BTC = 94.04 TBTC`. The loss is
`92.15 - 94.04 = -1.89 TBTC`. If there is a reserve, we take from there.
If not, we can either pay the full balance 92.15 or wait for the gov or
next deposits to fill the gap (new deposits won't have this error)

- If `max_tx_fee` increases to `0.05 BTC`, the `tbtc_net_amount = 100
BTC - 5 BTC - 2.85 BTC - 0.05 BTC = 92.10 BTC`. That means `92.15 -
90.10 = 0.05 TBTC` stays in reserve. We know that actual `tx_fee`
accrued as debt will be lower so we are covered. The corner case here is
`max_tx_fee` increases AFTER deposit finalization. This can make the
real debt slightly uncovered but only if the actual tx_fee exceeds the
old value of `max_tx_fee`

- If `max_tx_fee` decreases to `0.002 BTC`, the `tbtc_net_amount = 100
BTC - 5 BTC - 2.85 BTC - 0.002 BTC = 92.148 BTC`. That means `92.15 -
92.148 = 0.002 TBTC` stays in reserve. We know that actual `tx_fee`
accrued as debt will be lower so we are covered. The corner case here is
`max_tx_fee` decreases AFTER deposit finalization. However, this is not
a problem as the reserve will still be greater than the debt (we used
old `max_tx_fee` to cut the reserve while the debt is below the new
`max_tx_fee` value)

-----

As you can see, almost all cases cause the positive imbalance and
reserve increase. There are only two cases that cause negative
imbalance:
- `opt_minting_fee` decreases between deposit minting and finalization
- `max_tx_fee` increases after deposit finalization

The first case requires a very unfortunate timing. It becomes a real
problem only when the reserve is not enough to cover the loss. We can
decrease the probability by keeping the delay between minting and
finalization as short as possible.

The second case is similar. In practice, `max_tx_fee` never changed and
the only reason to do so would be a global change of fee levels on
Bitcoin. Moreover, the `max_tx_fee` is just a cap and the actual fee is
always lower as tBTC clients estimate it according to the network
circumstances. Last but not least, this becomes a real problem only in
case a deposit is not optimistically minted but swept instead (currently
1.4% of mainnet deposits went this way) so part of their minted amount
is used to repaid the accrued debt. It also requires that the reserve is
not enough to cover the loss.


**Case 2 - Deposit is not optimistically minted but swept (currently
1.4% of mainnet deposits)**

Let's say a deposit is `100 BTC`, `treasury_fee = 5%`, `opt_minting_fee
= 3%` and `max_tx_fee = 0.01 BTC` but the `actual tx_fee = 0.005 BTC`.
The actually minted amount will be `100 BTC * 0.95 - 0.005 BTC = 94.995
TBTC` (`opt_minting_fee` is not accrued here) and this will be in direct
control of the depositor contract.

The depositor contract does the finalization AFTER the deposit is swept.
If it uses the `tbtc_net_amount = btc_gross_amount - treasury_fee -
opt_minting_fee - max_tx_fee` equation, it will compute `tbtc_net_amount
= 100 BTC - 5 BTC - 2.85 BTC - 0.01 BTC = 92.14 TBTC`. Note that `94.995
- 92.14 = 2.855` TBTC stays in reserve.

Now, consider fee changes. Fee changes matter only if they occur between
deposit sweep and deposit finalization. Let's suppose the depositor
contract received the aforementioned `94.995 TBTC`. This is the outcome
if particular fees change between deposit sweep and finalization:

- If treasury_fee changes, it doesn't matter as we used the real
snapshotted value

- If `opt_minting_fee` increases, let's say to 5%, the `tbtc_net_amount
= 100 BTC - 5 BTC - 4.75 BTC - 0.01 BTC = 90.24 TBTC`. That means
`94.995 - 90.24 = 4.755 TBTC` stays in reserve

- If `opt_minting_fee` decreases, let's say to 1%, the `tbtc_net_amount
= 100 BTC - 5 BTC - 0.95 BTC - 0.01 BTC = 94.04 TBTC`. That means
`94.995 - 94.04 = 0.955 TBTC` stays in reserve

- If `max_tx_fee` increases to `0.05 BTC`, the `tbtc_net_amount = 100
BTC - 5 BTC - 2.85 BTC - 0.05 BTC = 92.10 BTC`. That means `94.995 -
92.10 = 2.895 TBTC` stays in reserve.

- If max_tx_fee decreases to `0.002 BTC`, the `tbtc_net_amount = 100 BTC
- 5 BTC - 2.85 BTC - 0.002 BTC = 92.148 BTC`. That means `94.995 -
92.148 = 2.847 TBTC` stays in reserve.

As you can see, fee changes in this case do not cause a negative
imbalance at all. The only risk is that this deposit is partially used
to repay previous debts. However, we are balancing this unlikely case by
always deducting the `opt_minting_fee` fee which commits to the reserve.
The risk that the reserve won't be enough is low here.

</details>

### Maximum Stake Limit

The Acre contract has a limit for the maximum amount of deposits it can
accept. Due to an asynchronous manner of staking where Bitcoin has to be
first bridged to tBTC via tBTC Bridge, the limit for deposits may be
reached in the meantime of stake request initialization and
finalization. In such cases, the stake request has to wait until more
deposits are accepted by the Acre contract (the maximum limit is
increased, or funds are withdrawn from the Acre contract, making space
for new deposits).

In this PR we added a path where stake requests that are unable to be
finalized will be added to a queue and finalized later. If the user is
not willing to wait anymore for the stake request to be finalized, they
can recall the tBTC stake request and withdraw to their wallet the
liquid tBTC that got minted.

This solution should be improved further to not allow the user to reduce
the possibility of such a situation happening. Different possibilities
are being explored as part of #191.

-----

Refs: #60
Depends on: keep-network/tbtc-v2#760
  • Loading branch information
dimpar authored Mar 6, 2024
2 parents 8226f97 + e4d3062 commit 3ba69e2
Show file tree
Hide file tree
Showing 31 changed files with 2,463 additions and 132 deletions.
5 changes: 3 additions & 2 deletions core/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
"test/**"
]
}
]
],
"@typescript-eslint/no-use-before-define": "off"
},
"overrides": [
{
"files": ["deploy/*.ts"],
"files": ["deploy/**/*.ts", "test/**/*.ts"],
"rules": {
"@typescript-eslint/unbound-method": "off"
}
Expand Down
524 changes: 524 additions & 0 deletions core/contracts/AcreBitcoinDepositor.sol

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions core/contracts/Dispatcher.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/interfaces/IERC4626.sol";
import "./Router.sol";
Expand All @@ -11,7 +11,7 @@ import "./stBTC.sol";
/// @notice Dispatcher is a contract that routes tBTC from stBTC to
/// yield vaults and back. Vaults supply yield strategies with tBTC that
/// generate yield for Bitcoin holders.
contract Dispatcher is Router, Ownable {
contract Dispatcher is Router, Ownable2Step {
using SafeERC20 for IERC20;

/// Struct holds information about a vault.
Expand Down
13 changes: 0 additions & 13 deletions core/contracts/TbtcDepositor.sol

This file was deleted.

11 changes: 8 additions & 3 deletions core/contracts/stBTC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.21;

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

/// @title stBTC
Expand All @@ -17,7 +17,7 @@ import "./Dispatcher.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 stBTC is ERC4626, Ownable {
contract stBTC is ERC4626, Ownable2Step {
using SafeERC20 for IERC20;

/// Dispatcher contract that routes tBTC from stBTC to a given vault and back.
Expand All @@ -26,8 +26,13 @@ contract stBTC is ERC4626, Ownable {
/// Address of the treasury wallet, where fees should be transferred to.
address public treasury;

/// Minimum amount for a single deposit operation.
/// Minimum amount for a single deposit operation. The value should be set
/// low enough so the deposits routed through Bitcoin Depositor contract won't
/// be rejected. It means that minimumDepositAmount should be lower than
/// tBTC protocol's depositDustThreshold reduced by all the minting fees taken
/// before depositing in the Acre contract.
uint256 public minimumDepositAmount;

/// Maximum total amount of tBTC token held by Acre protocol.
uint256 public maximumTotalAssets;

Expand Down
79 changes: 79 additions & 0 deletions core/contracts/test/AcreBitcoinDepositorHarness.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-3.0-only
/* solhint-disable func-name-mixedcase */
pragma solidity ^0.8.21;

import {AcreBitcoinDepositor} from "../AcreBitcoinDepositor.sol";
import {MockBridge, MockTBTCVault} from "@keep-network/tbtc-v2/contracts/test/TestTBTCDepositor.sol";
import {IBridge} from "@keep-network/tbtc-v2/contracts/integrator/IBridge.sol";
import {IBridgeTypes} from "@keep-network/tbtc-v2/contracts/integrator/IBridge.sol";

import {TestERC20} from "./TestERC20.sol";

/// @dev A test contract to expose internal function from AcreBitcoinDepositor contract.
/// This solution follows Foundry recommendation:
/// https://book.getfoundry.sh/tutorials/best-practices#internal-functions
contract AcreBitcoinDepositorHarness is AcreBitcoinDepositor {
constructor(
address bridge,
address tbtcVault,
address tbtcToken,
address stbtc
) AcreBitcoinDepositor(bridge, tbtcVault, tbtcToken, stbtc) {}

function exposed_finalizeBridging(
uint256 depositKey
) external returns (uint256 amountToStake, address staker) {
return finalizeBridging(depositKey);
}
}

/// @dev A test contract to stub tBTC Bridge contract.
contract BridgeStub is MockBridge {}

/// @dev A test contract to stub tBTC Vault contract.
contract TBTCVaultStub is MockTBTCVault {
TestERC20 public immutable tbtc;
IBridge public immutable bridge;

/// @notice Multiplier to convert satoshi to TBTC token units.
uint256 public constant SATOSHI_MULTIPLIER = 10 ** 10;

constructor(TestERC20 _tbtc, IBridge _bridge) {
tbtc = _tbtc;
bridge = _bridge;
}

function finalizeOptimisticMintingRequest(
uint256 depositKey
) public override {
IBridgeTypes.DepositRequest memory deposit = bridge.deposits(
depositKey
);

uint256 amountSubTreasury = (deposit.amount - deposit.treasuryFee) *
SATOSHI_MULTIPLIER;

uint256 omFee = optimisticMintingFeeDivisor > 0
? (amountSubTreasury / optimisticMintingFeeDivisor)
: 0;

// The deposit transaction max fee is in the 1e8 satoshi precision.
// We need to convert them to the 1e18 TBTC precision.
// slither-disable-next-line unused-return
(, , uint64 depositTxMaxFee, ) = bridge.depositParameters();
uint256 txMaxFee = depositTxMaxFee * SATOSHI_MULTIPLIER;

uint256 amountToMint = amountSubTreasury - omFee - txMaxFee;

finalizeOptimisticMintingRequestWithAmount(depositKey, amountToMint);
}

function finalizeOptimisticMintingRequestWithAmount(
uint256 depositKey,
uint256 amountToMint
) public {
MockTBTCVault.finalizeOptimisticMintingRequest(depositKey);

tbtc.mint(bridge.deposits(depositKey).depositor, amountToMint);
}
}
2 changes: 1 addition & 1 deletion core/contracts/test/TestERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.21;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract TestERC20 is ERC20 {
constructor() ERC20("Test Token", "TEST") {}
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}

function mint(address account, uint256 value) external {
_mint(account, value);
Expand Down
35 changes: 35 additions & 0 deletions core/deploy/00_resolve_tbtc_bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { DeployFunction } from "hardhat-deploy/types"
import type {
HardhatNetworkConfig,
HardhatRuntimeEnvironment,
} from "hardhat/types"
import { isNonZeroAddress } from "../helpers/address"
import { waitConfirmationsNumber } from "../helpers/deployment"

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

const bridge = await deployments.getOrNull("Bridge")

if (bridge && isNonZeroAddress(bridge.address)) {
log(`using Bridge contract at ${bridge.address}`)
} else if ((hre.network.config as HardhatNetworkConfig)?.forking?.enabled) {
throw new Error("deployed Bridge contract not found")
} else {
log("deploying Bridge contract stub")

await deployments.deploy("Bridge", {
contract: "BridgeStub",
args: [],
from: deployer,
log: true,
waitConfirmations: waitConfirmationsNumber(hre),
})
}
}

export default func

func.tags = ["TBTC", "Bridge"]
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"
import type {
HardhatNetworkConfig,
HardhatRuntimeEnvironment,
} from "hardhat/types"
import { isNonZeroAddress } from "../helpers/address"
import { waitConfirmationsNumber } from "../helpers/deployment"

const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { getNamedAccounts, deployments } = hre
Expand All @@ -11,20 +15,24 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {

if (tbtc && isNonZeroAddress(tbtc.address)) {
log(`using TBTC contract at ${tbtc.address}`)
} else if (!hre.network.tags.allowStubs) {
} else if (
!hre.network.tags.allowStubs ||
(hre.network.config as HardhatNetworkConfig)?.forking?.enabled
) {
throw new Error("deployed TBTC contract not found")
} else {
log("deploying TBTC contract stub")

await deployments.deploy("TBTC", {
contract: "TestERC20",
args: ["Test tBTC", "TestTBTC"],
from: deployer,
log: true,
waitConfirmations: 1,
waitConfirmations: waitConfirmationsNumber(hre),
})
}
}

export default func

func.tags = ["TBTC"]
func.tags = ["TBTC", "TBTCToken"]
42 changes: 42 additions & 0 deletions core/deploy/00_resolve_tbtc_vault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { DeployFunction } from "hardhat-deploy/types"
import type {
HardhatNetworkConfig,
HardhatRuntimeEnvironment,
} from "hardhat/types"
import { isNonZeroAddress } from "../helpers/address"
import { waitConfirmationsNumber } from "../helpers/deployment"

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

const tbtcVault = await deployments.getOrNull("TBTCVault")

if (tbtcVault && isNonZeroAddress(tbtcVault.address)) {
log(`using TBTCVault contract at ${tbtcVault.address}`)
} else if (
!hre.network.tags.allowStubs ||
(hre.network.config as HardhatNetworkConfig)?.forking?.enabled
) {
throw new Error("deployed TBTCVault contract not found")
} else {
log("deploying TBTCVault contract stub")

const tbtc = await deployments.get("TBTC")
const bridge = await deployments.get("Bridge")

await deployments.deploy("TBTCVault", {
contract: "TBTCVaultStub",
args: [tbtc.address, bridge.address],
from: deployer,
log: true,
waitConfirmations: waitConfirmationsNumber(hre),
})
}
}

export default func

func.tags = ["TBTC", "TBTCVault"]
func.dependencies = ["TBTCToken", "Bridge"]
4 changes: 2 additions & 2 deletions core/deploy/02_deploy_dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const tbtc = await deployments.get("TBTC")
const stbtc = await deployments.get("stBTC")

await deployments.deploy("Dispatcher", {
const dispatcher = await deployments.deploy("Dispatcher", {
from: deployer,
args: [stbtc.address, tbtc.address],
log: true,
waitConfirmations: waitConfirmationsNumber(hre),
})

if (hre.network.tags.etherscan) {
await helpers.etherscan.verify(stbtc)
await helpers.etherscan.verify(dispatcher)
}

// TODO: Add Tenderly verification
Expand Down
35 changes: 35 additions & 0 deletions core/deploy/03_deploy_acre_bitcoin_depositor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { DeployFunction } from "hardhat-deploy/types"
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import { waitConfirmationsNumber } from "../helpers/deployment"

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

const bridge = await deployments.get("Bridge")
const tbtcVault = await deployments.get("TBTCVault")
const tbtc = await deployments.get("TBTC")
const stbtc = await deployments.get("stBTC")

const depositor = await deployments.deploy("AcreBitcoinDepositor", {
contract:
process.env.HARDHAT_TEST === "true"
? "AcreBitcoinDepositorHarness"
: "AcreBitcoinDepositor",
from: deployer,
args: [bridge.address, tbtcVault.address, tbtc.address, stbtc.address],
log: true,
waitConfirmations: waitConfirmationsNumber(hre),
})

if (hre.network.tags.etherscan) {
await helpers.etherscan.verify(depositor)
}

// TODO: Add Tenderly verification
}

export default func

func.tags = ["AcreBitcoinDepositor"]
func.dependencies = ["TBTC", "stBTC"]
9 changes: 9 additions & 0 deletions core/deploy/21_transfer_ownership_stbtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
"transferOwnership",
governance,
)

if (hre.network.name !== "mainnet") {
await deployments.execute(
"stBTC",
{ from: governance, log: true, waitConfirmations: 1 },
"acceptOwnership",
)
}
}

export default func

func.tags = ["TransferOwnershipStBTC"]
func.dependencies = ["stBTC"]
8 changes: 8 additions & 0 deletions core/deploy/22_transfer_ownership_dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
"transferOwnership",
governance,
)

if (hre.network.name !== "mainnet") {
await deployments.execute(
"Dispatcher",
{ from: governance, log: true, waitConfirmations: 1 },
"acceptOwnership",
)
}
}

export default func
Expand Down
Loading

0 comments on commit 3ba69e2

Please sign in to comment.