Skip to content
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

Timelock for the Bridge and other critical components #829

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions solidity/contracts/Timelock.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.17;

import "@openzeppelin/contracts/governance/TimelockController.sol";

contract Timelock is TimelockController {
constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors
) TimelockController(minDelay, proposers, executors, address(0)) {}
}
50 changes: 50 additions & 0 deletions solidity/deploy/42_deploy_timelock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { HardhatRuntimeEnvironment } from "hardhat/types"
import { DeployFunction } from "hardhat-deploy/types"

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

const timelock = await deploy("Timelock", {
from: deployer,
args: [
86400, // 24h governance delay
[governance], // Threshold Council multisig as a proposer
// All current signers from the Threshold Council multisig as executors
// plus the Threshold Council multisig itself. The last one is here in
// case Threshold Council multisig rotates the owners but forgets to
// update the Timelock contract.
// See https://app.safe.global/settings/setup?safe=eth:0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f
[
"0x2844a0d6442034D3027A05635F4224d966C54fD7",
"0xf35dEE924F483Bc234F09cbfbc8B4488fD06be20",
"0x739730cCb2a34cc83D3e30645002C52bA4B06167",
"0xe989805835093e37E6b12dCddF718e0481024573",
"0x1Ba899530A89fAb245De9ff6cc23534F4a8A4e58",
"0x75ed7b219a737134f00255e331a36a706BD2ae2C",
"0xcE3778528fC73D46685069D455bbCcE16A6e22Af",
"0x35B46702C5d1CD36194217Fb92F72B563eFf851A",
"0xf791EfdF778a3Ca9cc193fFbe57Da33d1596E854",
governance,
],
],
log: true,
waitConfirmations: 1,
})

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

if (hre.network.tags.tenderly) {
await hre.tenderly.verify({
name: "Timelock",
address: timelock.address,
})
}
}

export default func

func.tags = ["Timelock"]
89 changes: 89 additions & 0 deletions solidity/test/Timelock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { helpers, waffle, upgrades } from "hardhat"
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"
import { expect } from "chai"
import type { Bridge, TBTCVault, Timelock, ProxyAdmin } from "../typechain"

import bridgeFixture from "./fixtures/bridge"

const { createSnapshot, restoreSnapshot } = helpers.snapshot

describe("Timelock", () => {
let governance: SignerWithAddress
let governanceSigner: SignerWithAddress

let bridge: Bridge
let tbtcVault: TBTCVault
let timelock: Timelock
let proxyAdmin: ProxyAdmin

const zeroBytes32 =
"0x0000000000000000000000000000000000000000000000000000000000000000"
const timelockDelay = 86400 // 24h governance delay

before(async () => {
const { esdm } = await helpers.signers.getNamedSigners()
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;({ governance, bridge, tbtcVault } = await waffle.loadFixture(
bridgeFixture
))

// One of the Threshold Council signers
governanceSigner = await helpers.account.impersonateAccount(
"0x2844a0d6442034D3027A05635F4224d966C54fD7",
{
from: governance,
value: 10,
}
)

timelock = (await helpers.contracts.getContract("Timelock")) as Timelock
proxyAdmin = (await upgrades.admin.getInstance()) as ProxyAdmin

await proxyAdmin.connect(esdm).transferOwnership(timelock.address)
})

context("when upgrading Bridge implementation via Timelock", async () => {
let expectedNewImplementation: string

before(async () => {
await createSnapshot()

// We need an existing contract. Otherwise, ProxyAdmin.upgrade will
// revert. Obviously, in a real world, it does not make sense to upgrade
// Bridge implementation address to point to the vault contract but we
// just want to confirm switching the implementation address works.
expectedNewImplementation = tbtcVault.address

const upgradeTxData = await proxyAdmin.interface.encodeFunctionData(
"upgrade",
[bridge.address, expectedNewImplementation]
)

await timelock
.connect(governance)
.schedule(
proxyAdmin.address,
0,
upgradeTxData,
zeroBytes32,
zeroBytes32,
timelockDelay
)
await helpers.time.increaseTime(timelockDelay)
await timelock
.connect(governanceSigner)
.execute(proxyAdmin.address, 0, upgradeTxData, zeroBytes32, zeroBytes32)
})

after(async () => {
await restoreSnapshot()
})

it("should switch the implementation address", async () => {
const newImplementation = await upgrades.erc1967.getImplementationAddress(
bridge.address
)
expect(newImplementation).to.equal(expectedNewImplementation)
})
})
})
Loading