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

feat: Incorporate upgrade deployment changes from PR #11 Coauthored by @doublo54 #15

Merged
merged 3 commits into from
Nov 15, 2023
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
56 changes: 56 additions & 0 deletions contracts/LockUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

// Import this file to use console.log
// import "hardhat/console.sol";

/* solhint-disable not-rely-on-time */
contract LockUpgradeable is Initializable {
/// -----------------------------------------------------------------------
/// Storage variables
/// -----------------------------------------------------------------------

uint256 public unlockTime;
address payable public owner;

/// -----------------------------------------------------------------------
/// Events
/// -----------------------------------------------------------------------

/// @notice Emitted when the contract is withdrawn
/// @param amount The amount of wei withdrawn
/// @param when The timestamp of the block when the withdraw happened
event Withdrawal(uint256 amount, uint256 when);

/// -----------------------------------------------------------------------
/// Constructor
/// -----------------------------------------------------------------------

function initialize(uint256 _unlockTime, address payable _owner) public initializer {
require(block.timestamp < _unlockTime, "Unlock time should be in the future");

Check notice

Code scanning / Semgrep

Semgrep Finding: rules.solidity.performance.use-short-revert-string

Shortening revert strings to fit in 32 bytes will decrease gas costs for deployment and gas costs when the revert condition has been met.

Check notice

Code scanning / Semgrep

Semgrep Finding: rules.solidity.performance.use-custom-error-not-require

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

unlockTime = _unlockTime;
owner = _owner;
}

receive() external payable {}

/// -----------------------------------------------------------------------
/// Public functions
/// -----------------------------------------------------------------------

/// @notice Withdraw all the funds
function withdraw() public {
// Uncomment this line to print a log in your terminal
// console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp);

require(block.timestamp >= unlockTime, "You can't withdraw yet");

Check notice

Code scanning / Semgrep

Semgrep Finding: rules.solidity.performance.use-custom-error-not-require

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.
require(msg.sender == owner, "You aren't the owner");

Check notice

Code scanning / Semgrep

Semgrep Finding: rules.solidity.performance.use-custom-error-not-require

Consider using custom errors as they are more gas efficient while allowing developers to describe the error in detail using NatSpec.

emit Withdrawal(address(this).balance, block.timestamp);

owner.transfer(address(this).balance);
}
}
12 changes: 12 additions & 0 deletions contracts/external/OpenZeppelinImports.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// contracts/Dummy.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

/// @dev This contract enables hardhat to compile the builds for upgradeable deployments
contract OpenZeppelinImports {
// No implementation required
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@
"typescript": ">=4.5.0"
},
"dependencies": {
"@openzeppelin/contracts": "^4.8.3",
"@openzeppelin/contracts-upgradeable": "^4.8.3",
"@openzeppelin/contracts": "^5.0.0",
"@openzeppelin/contracts-upgradeable": "^5.0.0",
"@openzeppelin/hardhat-upgrades": "^1.22.1"
}
}
}
133 changes: 125 additions & 8 deletions scripts/deploy/DeployManager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ContractFactory, Signer, utils } from 'ethers'
import { network, run } from 'hardhat'
// https://hardhat.org/hardhat-runner/plugins/nomiclabs-hardhat-etherscan#using-programmatically
import { BigNumber, BigNumberish, Contract, ContractFactory, Signer, utils } from 'ethers'
import { network, run, ethers } from 'hardhat'
import { logger } from '../../hardhat/utils/logger'
import fs from 'fs'
import { DEPLOYMENTS_BASE_DIR } from './deploy.config'
import { ProxyAdmin, TransparentUpgradeableProxy } from '../../typechain-types'

/*
This is a TypeScript class called `DeployManager` that is used to deploy contracts, verify them and save the deployment details to a file. The class has the following methods:
Expand Down Expand Up @@ -42,8 +44,23 @@ interface ContractDetails {
verificationCommand: string
}

interface DeployContractOptions {
name?: string
upgradeableProxy?: boolean
}

interface UpgradeableDeployContractOptions extends DeployContractOptions {
proxyAdminAddress?: string
proxyAdminOwner?: string
}

/**
* A class to deploy contracts, verify them and save the deployment details to a file.
*
* See docs at top of file for more details.
*/
export class DeployManager {
signer?: Signer
private signer?: Signer
baseDir: string
deployedContracts: ContractDetails[] = []

Expand All @@ -62,28 +79,44 @@ export class DeployManager {
return instance
}

async getSigner(): Promise<Signer> {
if (!this.signer) {
// NOTE: Defaults to the first signer if not provided
return (await ethers.getSigners())[0]
}
return this.signer
}

setSigner(signer: Signer) {
this.signer = signer
}

// -----------------------------------------------------------------------------------------------
// Deployments
// -----------------------------------------------------------------------------------------------
async deployContractFromFactory<C extends ContractFactory>(
contract: C,
params: Parameters<C['deploy']>,
name = 'Contract' // TODO: Provide better fallback naming
params: Parameters<C['deploy']>, // NOTE: For upgradeable proxy
{
name = 'Contract', // Default contract name if not provided
}: DeployContractOptions = {}
): Promise<ReturnType<C['deploy']>> {
logger.logHeader(`Deploying ${name}`, `🚀`)
// Get the balance of the account before deployment
const balanceBefore = await this.signer?.getBalance()
const balanceBeforeInEther = utils.formatEther(balanceBefore || 0)
logger.log(`Balance before deployment: ${balanceBeforeInEther} ETH`, `💰`)
// Deploy contract with signer if available
const contractInstance = this.signer
? await contract.connect(this.signer).deploy(...params)
: await contract.deploy(...params)
let encodedConstructorArgs = ''
const contractInstance = await contract.connect(await this.getSigner()).deploy(...params)
try {
encodedConstructorArgs = contractInstance.interface.encodeDeploy(params)
} catch {
// NOTE: The encode fails when the deploy options are passed in. So we pop the last element and try again.
params.pop()
encodedConstructorArgs = contractInstance.interface.encodeDeploy(params)
}

await contractInstance.deployed()

logger.success(`Deployed ${name} at ${contractInstance.address}`)
Expand All @@ -110,6 +143,90 @@ export class DeployManager {
return contractInstance as ReturnType<C['deploy']>
}

// -----------------------------------------------------------------------------------------------
// Upgradeable Deployments
// -----------------------------------------------------------------------------------------------
async deployProxyAdmin(adminAddress: string): Promise<ProxyAdmin> {
logger.log(`Deploying Proxy Admin`, `🚀`)
const ProxyAdminFactory = await ethers.getContractFactory('ProxyAdmin')
const proxyAdmin = await this.deployContractFromFactory(ProxyAdminFactory, [adminAddress], { name: 'ProxyAdmin' })
return proxyAdmin
}

async deployTransparentProxy(
implementationAddress: string,
proxyAdminAddress: string,
initializerData: string
): Promise<TransparentUpgradeableProxy> {
logger.log(`Deploying Transparent Proxy`, `🚀`)
const TransparentUpgradeableProxyFactory = await ethers.getContractFactory(
'TransparentUpgradeableProxy',
this.signer
)
const transparentProxy = await this.deployContractFromFactory(
TransparentUpgradeableProxyFactory,
[implementationAddress, proxyAdminAddress, initializerData],
{
name: 'TransparentUpgradeableProxy',
}
)

return transparentProxy
}

// Method to deploy an upgradeable contract with a TransparentUpgradeableProxy
async deployUpgradeableContract<C extends ContractFactory>(
contract: C,
// NOTE: The main deploy method passes in constructors, but this passes in initializer params
// params: Parameters<C['deploy']>,
initializerParams: (string | BigNumberish)[],
{ name = 'Contract', proxyAdminAddress, proxyAdminOwner }: UpgradeableDeployContractOptions = {}
): Promise<{
implementationThroughProxy: ReturnType<C['attach']> // Returns the interface of the implementation, at the proxy address.
proxyAdmin: ProxyAdmin
transparentProxy: TransparentUpgradeableProxy
implementation: ReturnType<C['deploy']>
}> {
logger.log(`Deploying upgradeable ${name}`, `🚀`)
// Deploy the logic/implementation contract
// NOTE: Assumes that no constructor arguments are passed in
const implementation = await this.deployContractFromFactory(contract, [] as any, {
name,
})

// Deploy the ProxyAdmin if not provided
let proxyAdmin
if (!proxyAdminAddress) {
const admin = proxyAdminOwner ? proxyAdminOwner : await (await this.getSigner()).getAddress()
logger.log(`deployUpgradeableContract:: Proxy Admin not passed. Deploying ProxyAdmin with owner: ${admin}`, '⚠️')
proxyAdmin = await this.deployProxyAdmin(admin)
proxyAdminAddress = proxyAdmin.address
} else {
proxyAdmin = (await ethers.getContractAt('ProxyAdmin', proxyAdminAddress)) as ProxyAdmin
}

// Encode the initializer function call
const initializerData = contract.interface.encodeFunctionData('initialize', initializerParams)
// Deploy the TransparentUpgradeableProxy contract
const transparentProxy = await this.deployTransparentProxy(
implementation.address,
proxyAdminAddress,
initializerData
)
// Return the proxy contract as an instance of the implementation contract
const implementationThroughProxy = (await contract.attach(transparentProxy.address)) as ReturnType<C['attach']>

return {
implementationThroughProxy,
proxyAdmin,
transparentProxy,
implementation,
}
}

// -----------------------------------------------------------------------------------------------
// Verification
// -----------------------------------------------------------------------------------------------
async verifyContracts() {
for (const contract of this.deployedContracts) {
logger.logHeader(`Verifying ${contract.name} at ${contract.address}`, ` 🔍`)
Expand Down
23 changes: 20 additions & 3 deletions scripts/deploy/deployLock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ethers, network } from 'hardhat'
import { getDeployConfig, DeployableNetworks } from './deploy.config'
import { DeployManager } from './DeployManager'
import { logger } from '../../hardhat/utils'

/**
* // NOTE: This is an example of the default hardhat deployment approach.
Expand All @@ -11,7 +12,9 @@ async function main() {
const currentNetwork = network.name as DeployableNetworks
// Optionally pass in accounts to be able to use them in the deployConfig
const accounts = await ethers.getSigners()
const { wNative, adminAddress } = getDeployConfig(currentNetwork, accounts)
// NOTE: For importing deployment params by network
// const { wNative, adminAddress } = getDeployConfig(currentNetwork, accounts)

// Optionally pass in signer to deploy contracts
const deployManager = await DeployManager.create(accounts[0])

Expand All @@ -26,9 +29,23 @@ async function main() {
const lock = await deployManager.deployContractFromFactory(
Lock,
[unlockTime, accounts[0].address, { value: lockedAmount }],
lockContractName // Pass in contract name to log contract
{
name: lockContractName, // Pass in contract name to log contract
}
)
logger.log(`Lock with 1 ETH deployed to: ${lock.address}`, '🔒')

const lockUpgradableContractName = 'LockUpgradeable'
const LockUpgradable = await ethers.getContractFactory(lockUpgradableContractName)
// TODO: This isn't passing value to the constructor due to how upgradeable contracts work
const lockUpgradable = await deployManager.deployUpgradeableContract(
LockUpgradable,
[unlockTime, accounts[0].address],
{
name: lockUpgradableContractName, // Pass in contract name to log contract
}
)
console.log('Lock with 1 ETH deployed to:', lock.address)
logger.log(`LockUpgradeable to: ${lock.address}`, '🔒')

await deployManager.verifyContracts()
}
Expand Down
4 changes: 2 additions & 2 deletions solhint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ module.exports = {
'func-param-name-mixedcase': 'error',
'modifier-name-mixedcase': 'error',
'private-vars-leading-underscore': ['error', { strict: false }],
ordering: 'error',
ordering: 'warn',

// Security Rules
'compiler-version': [
SOLC_COMPILER_VERSIONS.length == 1 ? 'error' : 'warn',
'warn',
SOLC_COMPILER_VERSIONS[0],
// NOTE: Custom value added in template to support exporting multiple compiler versions
SOLC_COMPILER_VERSIONS,
Expand Down
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -809,15 +809,15 @@
table "^6.8.0"
undici "^5.14.0"

"@openzeppelin/contracts-upgradeable@^4.8.3":
version "4.9.2"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.2.tgz#a817c75688f8daede420052fbcb34e52482e769e"
integrity sha512-siviV3PZV/fHfPaoIC51rf1Jb6iElkYWnNYZ0leO23/ukXuvOyoC/ahy8jqiV7g+++9Nuo3n/rk5ajSN/+d/Sg==
"@openzeppelin/contracts-upgradeable@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.0.0.tgz#859c00c55f04b6dda85b3c88bce507d65019888f"
integrity sha512-D54RHzkOKHQ8xUssPgQe2d/U92mwaiBDY7qCCVGq6VqwQjsT3KekEQ3bonev+BLP30oZ0R1U6YC8/oLpizgC5Q==

"@openzeppelin/contracts@^4.8.3":
version "4.9.2"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.2.tgz#1cb2d5e4d3360141a17dbc45094a8cad6aac16c1"
integrity sha512-mO+y6JaqXjWeMh9glYVzVu8HYPGknAAnWyxTRhGeckOruyXQMNnlcW6w/Dx9ftLeIQk6N+ZJFuVmTwF7lEIFrg==
"@openzeppelin/contracts@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.0.tgz#ee0e4b4564f101a5c4ee398cd4d73c0bd92b289c"
integrity sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw==

"@openzeppelin/hardhat-upgrades@^1.22.1":
version "1.26.0"
Expand Down