Skip to content
Open
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
149 changes: 149 additions & 0 deletions contracts/ZeroDAOTokenV3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

// Slight modifiations from base Open Zeppelin Contracts
// Consult /oz/README.md for more information
import "./oz/ERC20Upgradeable.sol";
import "./oz/ERC20SnapshotUpgradeable.sol";
import "./oz/ERC20PausableUpgradeable.sol";

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

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

contract ZeroDAOTokenV3 is
OwnableUpgradeable,
ERC20Upgradeable,
ERC20PausableUpgradeable,
ERC20SnapshotUpgradeable
{
using SafeERC20 for IERC20;

event AuthorizedSnapshotter(address account);
event DeauthorizedSnapshotter(address account);
event ERC20TokenWithdrawn(
IERC20 indexed token,
address indexed to,
uint256 indexed amount
);

// Mapping which stores all addresses allowed to snapshot
mapping(address => bool) authorizedToSnapshot;

function initialize(
string memory name,
string memory symbol
) public initializer {
__ERC20_init(name, symbol);
}

// Call this on the implementation contract (not the proxy)
function initializeImplementation() public initializer {}
Comment thread
JamesEarle marked this conversation as resolved.

/**
* Utility function to transfer tokens to many addresses at once.
* @param recipients The addresses to send tokens to
* @param amount The amount of tokens to send
* @return Boolean if the transfer was a success
*/
function transferBulk(
address[] calldata recipients,
uint256 amount
) external returns (bool) {
address sender = _msgSender();

uint256 total = amount * recipients.length;
require(
_balances[sender] >= total,
"ERC20: transfer amount exceeds balance"
);

_balances[sender] -= total;

for (uint256 i = 0; i < recipients.length; ++i) {
address recipient = recipients[i];
require(recipient != address(0), "ERC20: transfer to the zero address");

// Note: _beforeTokenTransfer isn't called here
// This function emulates what it would do

_balances[recipient] += amount;

emit Transfer(sender, recipient, amount);
}

return true;
}

/**
* Utility function to transfer tokens to many addresses at once.
* @param sender The address to send the tokens from
* @param recipients The addresses to send tokens to
* @param amount The amount of tokens to send
* @return Boolean if the transfer was a success
*/
function transferFromBulk(
address sender,
address[] calldata recipients,
uint256 amount
) external returns (bool) {
uint256 total = amount * recipients.length;
require(
_balances[sender] >= total,
"ERC20: transfer amount exceeds balance"
);

// Ensure enough allowance
uint256 currentAllowance = _allowances[sender][_msgSender()];
require(
currentAllowance >= total,
"ERC20: transfer total exceeds allowance"
);
_approve(sender, _msgSender(), currentAllowance - total);

_balances[sender] -= total;

for (uint256 i = 0; i < recipients.length; ++i) {
address recipient = recipients[i];
require(recipient != address(0), "ERC20: transfer to the zero address");

// Note: _beforeTokenTransfer isn't called here
// This function emulates what it would do (paused and snapshot)

_balances[recipient] += amount;

emit Transfer(sender, recipient, amount);
}

return true;
}

function _beforeTokenTransfer(
address from,
address to,
uint256 amount
)
internal
virtual
override(
ERC20PausableUpgradeable,
ERC20SnapshotUpgradeable,
ERC20Upgradeable
)
{
super._beforeTokenTransfer(from, to, amount);
}

function _transfer(
address from,
address to,
uint256 amount
) internal override {
super._transfer(from, to, amount);

if (to == address(this)) {
_burn(to, amount);
}
}
}
18 changes: 8 additions & 10 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,11 @@ const config: HardhatUserConfig = {
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
runs: 200,
},
},
},
{ version: "0.6.0", settings: {} }

{ version: "0.6.0", settings: {} },
],
},
paths: {
Expand Down Expand Up @@ -87,11 +86,10 @@ const config: HardhatUserConfig = {
chainId: 11155111,
urls: {
apiURL: "https://api-sepolia.etherscan.io/api",
browserURL: "https://sepolia.etherscan.io"
}
}

]
browserURL: "https://sepolia.etherscan.io",
},
},
],
},
};
export default config;
16 changes: 13 additions & 3 deletions scripts/deployWILD.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as hre from "hardhat";
import { ERC20Mock, ERC20Mock__factory, ZeroDAOToken, ZeroDAOToken__factory, ZeroDAOTokenV2, ZeroDAOTokenV2__factory } from "../typechain";
import {
ERC20Mock,
ERC20Mock__factory,
ZeroDAOToken,
ZeroDAOToken__factory,
ZeroDAOTokenV2,
ZeroDAOTokenV2__factory,
} from "../typechain";
import { getLogger } from "../utilities";

const logger = getLogger("scripts::deployWILD");
Expand Down Expand Up @@ -39,11 +46,14 @@ async function main() {
"wilder-prod"
);

const wildToken: ZeroDAOToken = await deployWILDTx.deployed() as ZeroDAOToken;
const wildToken: ZeroDAOToken = (await deployWILDTx.deployed()) as ZeroDAOToken;
logger.info(`Deployed WILD Token to: ${wildToken.address}`);

const mockFactory = new ERC20Mock__factory(deployer);
const mockToken: ERC20Mock = await mockFactory.deploy("MOCK TOKEN", "MOCK") as ERC20Mock;
const mockToken: ERC20Mock = (await mockFactory.deploy(
"MOCK TOKEN",
"MOCK"
)) as ERC20Mock;

logger.info(`Deployed MOCK Token to: ${mockToken.address}`);
}
Expand Down
30 changes: 17 additions & 13 deletions scripts/upgrade/01-deploy-v1.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
import * as hre from "hardhat";
import { deployFundTransfer } from "../../test/helpers/deploy-fund-transfer";
import { DEFAULT_MOCK_TOKEN_AMOUNT, DEFAULT_MOCK_TOKEN_DECIMALS } from "../../test/helpers/constants";
import {
DEFAULT_MOCK_TOKEN_AMOUNT,
DEFAULT_MOCK_TOKEN_DECIMALS,
} from "../../test/helpers/constants";
import { getLogger } from "../../utilities";

/**
* Script 01: Deploy V1 Token Contract
*
*
* This script performs the initial setup for testing the WILD token upgrade process.
* It serves as the first step in a multi-step upgrade testing workflow.
*
*
* Operations performed:
* - Deploy the ZeroDAOToken (V1) as an upgradeable proxy
* - Deploy an ERC20Mock token for testing purposes
* - Mint mock tokens to the ZeroDAOToken contract
* - Transfer ownership of both the Proxy and ProxyAdmin to a specified address
*
*
* Environment Variables Required:
* - OWNER_ADDRESS: The address to transfer ownership to (optional for hardhat network)
*
*
* Output:
* - Creates a JSON file with deployment details: `01-deploy-v1-${network}.json`
*/
const main = async () => {
const logger = getLogger("01-deploy-v1");

logger.info("Executing...");

const [deployer] = await hre.ethers.getSigners();
const outputFile = `01-deploy-v1-${hre.network.name}.json`;

// The address of the new owner to be transferred to for WILD as well as ProxyAdmin
// If not using hardhat, this should be a Safe address
let newOwnerAddress = process.env.OWNER_ADDRESS;
const newOwnerAddress = process.env.OWNER_ADDRESS;

if (!newOwnerAddress) {
throw new Error("No address given for transfer ownership call");
Expand All @@ -37,14 +45,10 @@ const main = async () => {
DEFAULT_MOCK_TOKEN_DECIMALS
);

logger.info(`Deploying ZeroDAOToken to ${hre.network.name}`);

// Deploy V1 token contract with mock token for testing
await deployFundTransfer(
deployer,
newOwnerAddress,
outputFile,
amount,
true
);
await deployFundTransfer(deployer, newOwnerAddress, outputFile, amount, true);
};

main()
Expand Down
30 changes: 19 additions & 11 deletions scripts/upgrade/02-deploy-v2.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,49 @@
import * as hre from "hardhat";
import { deployV2 } from "../../test/helpers/deploy-v2";
import { ZeroDAOTokenV2, ZeroDAOTokenV2__factory } from "../../typechain";
import { getLogger } from "../../utilities";
import { initImpl } from "../../test/helpers/init-impl";

/**
* Script 02: Deploy V2 Implementation Contract
*
*
* This script deploys the ZeroDAOTokenV2 implementation contract that will be used
* for upgrading the existing V1 proxy contract. This is step 2 in the upgrade process.
*
*
* Note: This only deploys the implementation contract, not a proxy. The actual upgrade
* happens separately using the proxy upgrade mechanism.
*
*
* Operations performed:
* - Deploy ZeroDAOTokenV2 implementation contract
* - Save deployment details to output file
*
*
* Prerequisites:
* - V1 contract should already be deployed (from script 01)
* - Deployer account should have sufficient funds for deployment
*
*
* Output:
* - Creates a JSON file with implementation address: `02-deploy-v2-${network}.json`
*
*
* Production Notes:
* - Ensure proper addresses are configured
* - Deployer account needs funds for deployment
* - For mainnet, proposer should be configured on Safe multisig
*/
const main = async () => {
const logger = getLogger("02-deploy-v2");

logger.info("Executing...");

const [deployer] = await hre.ethers.getSigners();
const outputFile = `02-deploy-v2-${hre.network.name}.json`;

logger.info(`Deploying ZeroDAOTokenV2 to ${hre.network.name}`);

// Deploy V2 implementation contract
await deployV2(
deployer,
outputFile,
true
);
const contract: ZeroDAOTokenV2 = await deployV2(deployer, outputFile, true);

// Calling to initalize the implementation contract
await initImpl(new ZeroDAOTokenV2__factory(deployer), contract.address);
};

main()
Expand Down
25 changes: 10 additions & 15 deletions scripts/upgrade/03-read-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,43 @@ import { ZeroDAOToken__factory } from "../../typechain";

/**
* Script 03: Read Contract State (Pre-Upgrade)
*
*
* This script reads and captures the current state of the ZeroDAOToken contract
* before performing an upgrade. This creates a baseline snapshot that can be
* compared against the post-upgrade state to ensure data integrity.
*
*
* Purpose:
* - Capture contract storage state before upgrade
* - Create a reference point for state comparison
* - Ensure upgrade doesn't corrupt existing data
*
*
* Usage Pattern:
* 1. Run this script BEFORE upgrade (captures pre-upgrade state)
* 2. Perform the contract upgrade
* 3. Run script 04 to compare pre/post upgrade states
*
*
* Environment Variables Required:
* - TOKEN_ADDRESS: Address of the deployed ZeroDAOToken contract
*
*
* Output:
* - Creates a JSON file with contract state: `03-read-state-${network}.json`
*
*
* @note This script should be executed twice in the upgrade workflow:
* once BEFORE upgrading and once AFTER upgrading (via script 04)
*/
const main = async () => {
const [creator] = await hre.ethers.getSigners();
const outputFile = `03-read-state-${hre.network.name}.json`;
const outputFile = `03-read-state-v1-to-v2-${hre.network.name}.json`;

// Get the token address from initial deployment in step 1
let tokenAddress = process.env.TOKEN_ADDRESS;
const tokenAddress = process.env.TOKEN_ADDRESS;
if (!tokenAddress) {
throw Error("No token address present in env");
}

// Read and save current contract state
await readState(
creator,
tokenAddress,
outputFile,
false
);
}
await readState(creator, tokenAddress, outputFile, false);
};

main()
.then(() => {
Expand Down
Loading