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
124 changes: 124 additions & 0 deletions VERIFICATION_LOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Bytecode Verification Log — FiatToken (NeoX Mainnet)

**Date**: 2026-08-14
**Network**: NeoX Mainnet (chainId: 47763)
**RPC**: `https://mainnet-1.rpc.banelabs.org`
**Script**: `scripts/verifyMainnetTokenBytecode.ts` (official Circle script)
**Verification mode**: `full` (constructor + full runtime bytecode + metadata hash)
**Command**:
```bash
forge build --skip scripts
./node_modules/.bin/hardhat run scripts/verifyMainnetTokenBytecode.ts --network mainnet
```
**Compiler**: Solc 0.6.12+commit.27d51765 (evm target: istanbul, optimizer 10,000,000 runs)
**Result**: ✅ EXIT CODE 0 — all three contracts verified in **full** mode.

---

## Deployment addresses (2026-08-14, fresh redeploy)

| Contract | Address | Creation Tx |
|---|---|---|
| SignatureChecker (Library) | `0xD99628bD77EfAe2DA7d8adA524BAa0d2FB28A1ca` | `0x2849eb500fbba6fa7653d4f621896698dc57a65cdcb6a8c5c84cbf51a4c4f499` |
| FiatTokenV2_2 (Implementation) | `0x2BDCDA0d8a5D9015908E52b53C282902147dB63e` | `0xe53dcce8dea7d2690edb4a2133a91d5e3ad853300b3ac75826896c0021a443c9` |
| FiatTokenProxy | `0x028447684F3fFb6d27042D6c3Da7B088332c6576` | `0xbb351846a192b416fdf71d5da611159908901d0ec0d2d003778ffcb65532645d` |
| MasterMinter | `0xC126Fa40260f5a55bA8c2C3ccBBBD247CBA6ddF9` | `0x9337cbeb039c8e5603e5727b2a79c59380a210fd13c444ff45aa2d7d95393314` |

---

## Verification results

```
Verifying on chain bytecode for: FiatTokenV2_2

verification complete - supplied constructor code is consistent with local version.
verification complete - supplied full runtime bytecode is consistent with local version.

Verifying on chain bytecode for: FiatTokenProxy

verification complete - supplied constructor code is consistent with local version.
verification complete - supplied full runtime bytecode is consistent with local version.

Verifying on chain bytecode for: SignatureChecker

verification complete - supplied constructor code is consistent with local version.
verification complete - supplied full runtime bytecode is consistent with local version.
```

| Contract | Constructor | Full Runtime | Metadata Hash | Result |
|---|---|---|---|---|
| FiatTokenV2_2 | ✅ matches | ✅ matches | ✅ matches | ✅ PASS |
| FiatTokenProxy | ✅ matches | ✅ matches | ✅ matches | ✅ PASS |
| SignatureChecker | ✅ matches | ✅ matches | ✅ matches | ✅ PASS |

**Exit code**: 0

---

## Block explorer verification (xexplorer — Blockscout)

All four contracts are publicly verified on https://xexplorer.neo.org via `forge verify-contract` (blockscout verifier, RPC `https://mainnet-1.rpc.banelabs.org`):

| Contract | Explorer status |
|---|---|
| SignatureChecker | ✅ Verified (`contracts/util/SignatureChecker.sol`) |
| FiatTokenV2_2 | ✅ Verified (`contracts/v2/FiatTokenV2_2.sol`) |
| FiatTokenProxy | ✅ Verified (`contracts/v1/FiatTokenProxy.sol`) |
| MasterMinter | ✅ Verified (`contracts/minting/MasterMinter.sol`) |

## Notes

- Fresh redeploy of the whole set on mainnet; `SignatureChecker` was deployed first via `forge create` (unlinked), then `FiatTokenV2_2`/`FiatTokenProxy`/`MasterMinter` were deployed with `--libraries contracts/util/SignatureChecker.sol:SignatureChecker:0xD99628bD77EfAe2DA7d8adA524BAa0d2FB28A1ca`.
- Verification artifacts carry `settings.libraries.SignatureChecker = 0xD99628bD77EfAe2DA7d8adA524BAa0d2FB28A1ca`, matching the on-chain linkage.
- Role assignments: proxy admin = SimpleMultiSig `0x0F378b9433c674Bc5021908b7a4150C6B0C3704E`; token owner / pauser / blacklister / MM owner = Gnosis Safe `0xc81C12BF1fd79DF32Cd7C914c653164bb17B7dB0`.
- No tokens minted: `totalSupply() == 0`, zero Transfer/Mint events since deployment.

---

## Historical: NeoX Testnet (2026-08-06)

**Date**: 2026-08-06
**Network**: NeoX Testnet (chainId: 12227332)
**RPC**: `https://testnet-1.rpc.banelabs.org`

### Deployment addresses (2026-08-06)

| Contract | Address | Creation Tx |
|---|---|---|
| SignatureChecker (Library) | `0x518e55b099E7fc78Cc59b45fe40a1B23C65dFd5B` | `0xad246bd3f0ecc4becf93000b2f4dcb0dbc8351014074e59e2c0a51db68e74044` |
| FiatTokenV2_2 (Implementation) | `0x9EED3835f283A3575f4f0d2EC98d26Be9F41244D` | `0xeeac8e2a3cc5c7b6fea1bda862a2cf4b630ed0c494dd0df4fe49b3cb1674cc84` |
| FiatTokenProxy | `0x6bc037Fa5421d7E53dA211A548d6539CBe12c8f3` | `0xbcd01e23197c8cfb1b195c004845dee68ef77ad458a5229fb1a0288091113dca` |
| MasterMinter | `0x4007CA68eFdD1f4BA1151c068FA6ddaB256fACca` | `0x333b80e735571c972d373980d92193b6a7fbb407da0ab7d1a869653548a1a4dc` |

### Verification results

```
Verifying on chain bytecode for: FiatTokenV2_2

verification complete - supplied constructor code is consistent with local version.
verification complete - supplied full runtime bytecode is consistent with local version.

Verifying on chain bytecode for: FiatTokenProxy

verification complete - supplied constructor code is consistent with local version.
verification complete - supplied full runtime bytecode is consistent with local version.

Verifying on chain bytecode for: SignatureChecker

verification complete - supplied constructor code is consistent with local version.
verification complete - supplied full runtime bytecode is consistent with local version.
```

| Contract | Constructor | Full Runtime | Metadata Hash | Result |
|---|---|---|---|---|
| FiatTokenV2_2 | ✅ matches | ✅ matches | ✅ matches | ✅ PASS |
| FiatTokenProxy | ✅ matches | ✅ matches | ✅ matches | ✅ PASS |
| SignatureChecker | ✅ matches | ✅ matches | ✅ matches | ✅ PASS |

**Exit code**: 0

### Notes

- Verification is performed in **full** mode (`verificationType: "full"` in `verification_artifacts/input.json`), which verifies the constructor code, the complete runtime bytecode, and the embedded metadata hash against the local compilation.
- The `SignatureChecker` library was compiled with `libraries` set to the address recorded by the compiler at build time; the on-chain deployment matches the local build exactly, including the metadata hash.
- `FiatTokenV2_2` and `FiatTokenProxy` were compiled with `libraries = [SignatureChecker:0x518e55b099E7fc78Cc59b45fe40a1B23C65dFd5B]`, matching the on-chain linkage.
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ remappings = [
"forge-std/=lib/forge-std/src",
"@openzeppelin/=node_modules/@openzeppelin/",
]
auto_detect_remappings = false
fs_permissions = [
{ access = "read-write", path = "blacklist.remote.json"},
{ access = "read-write", path = "test.blacklist.remote.json"}
Expand All @@ -37,6 +38,7 @@ extra-output-files = [
"metadata"
]


[rpc_endpoints]
testnet = "${TESTNET_RPC_URL}"
mainnet = "${MAINNET_RPC_URL}"
158 changes: 158 additions & 0 deletions scripts/deploy/deploy-neox-simple.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.6.12;

import "forge-std/console.sol";
import { Script } from "forge-std/Script.sol";
import { FiatTokenV2_2 } from "../../contracts/v2/FiatTokenV2_2.sol";
import { FiatTokenProxy } from "../../contracts/v1/FiatTokenProxy.sol";
import { MasterMinter } from "../../contracts/minting/MasterMinter.sol";

/**
* @title DeployNeoXSimple
* @notice Deploy FiatTokenV2_2 (USDC) to NeoX testnet.
* Deployer only deploys; proxy admin is handed to the existing
* SimpleMultiSig upgrade multisig (2-of-3); token owner / pauser /
* blacklister / MasterMinter owner all go to the Gnosis Safe.
*
* @dev Architecture:
* - Proxy admin : existing SimpleMultiSig upgrade multisig (2-of-3)
* -> upgradeTo() / changeAdmin() via multisig
* - Token owner : Gnosis Safe (pause/unpause/blacklist/transferOwnership)
* - Pauser : Gnosis Safe
* - Blacklister : Gnosis Safe
* - MM owner : Gnosis Safe (configureController/removeController)
* - Minter : configured manually via Gnosis Safe after deployment
* (configureController + configureMinter)
*
* NOTE: SignatureChecker library must be deployed FIRST and linked via
* foundry.toml `libraries` before running this script.
*
* Usage:
* 1. Fill .env with DEPLOYER_PRIVATE_KEY
* 2. Deploy SignatureChecker:
* forge create contracts/util/SignatureChecker.sol:SignatureChecker \
* --rpc-url neox-testnet --private-key $DEPLOYER_PRIVATE_KEY --broadcast
* 3. Update foundry.toml `libraries` with new SignatureChecker address
* 4. Run:
* forge script scripts/deploy/deploy-neox-simple.s.sol \
* --rpc-url neox-testnet --broadcast -vvv
*/
contract DeployNeoXSimple is Script {
// NeoX Mainnet upgrade multisig (Gnosis Safe) - proxy admin.
// NeoX Mainnet upgrade multisig (Gnosis Safe) — proxy admin.
address internal constant UPGRADE_MULTISIG = 0x0F378b9433c674Bc5021908b7a4150C6B0C3704E;
// Gnosis Safe multisig — token owner / pauser / blacklister / MM owner
address internal constant GNOSIS_SAFE = 0xc81C12BF1fd79DF32Cd7C914c653164bb17B7dB0;

function run() external {
uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
address deployer = vm.addr(deployerKey);

console.log("=============================================");
console.log(" DeployNeoXSimple - NeoX Testnet");
console.log("=============================================");
console.log(" Deployer: %s", deployer);
console.log(" UpgradeMultisig (proxy admin): %s", UPGRADE_MULTISIG);
console.log(" GnosisSafe (owner/pauser/blacklister): %s", GNOSIS_SAFE);
console.log("");

// ============================================
// Phase 1: Deploy contracts
// ============================================
console.log("[Phase 1] Deploying contracts...");

vm.startBroadcast(deployerKey);

// 1. Implementation — lock with zero-address init
FiatTokenV2_2 impl = new FiatTokenV2_2();
impl.initialize("", "", "", 0, address(1), address(1), address(1), address(1));
impl.initializeV2("");
impl.initializeV2_1(address(1));
impl.initializeV2_2(new address[](0), "");
console.log(" [1/4] Implementation: %s", address(impl));

// 2. Proxy (deployer = admin initially)
FiatTokenProxy proxy = new FiatTokenProxy(address(impl));
console.log(" [2/4] Proxy: %s", address(proxy));

// 3. MasterMinter bound to proxy
MasterMinter masterMinter = new MasterMinter(address(proxy));
console.log(" [3/4] MasterMinter: %s", address(masterMinter));

// 4. Transfer proxy admin to the existing upgrade multisig.
// After this, deployer is no longer admin and CAN call the
// proxy fallback to initialize the token.
proxy.changeAdmin(UPGRADE_MULTISIG);
console.log(" [4/4] Proxy admin -> UpgradeMultisig");

// 5. Transfer MasterMinter ownership to Gnosis Safe
masterMinter.transferOwnership(GNOSIS_SAFE);
console.log(" [5/5] MasterMinter owner -> Gnosis Safe");

vm.stopBroadcast();

// ============================================
// Phase 2: Initialize token through proxy
// deployer is no longer admin -> can call via fallback
// ============================================
console.log("");
console.log("[Phase 2] Initializing token (V1 -> V2 -> V2_1 -> V2_2)...");

FiatTokenV2_2 token = FiatTokenV2_2(address(proxy));

vm.startBroadcast(deployerKey);
token.initialize(
"USD Coin",
"USDC",
"USD",
6,
address(masterMinter),
GNOSIS_SAFE, // pauser
GNOSIS_SAFE, // blacklister
GNOSIS_SAFE // owner
);
token.initializeV2("USD Coin");
token.initializeV2_1(GNOSIS_SAFE); // V2_1 blacklister
token.initializeV2_2(new address[](0), "USDC");
vm.stopBroadcast();

console.log(" Token initialized");

// ============================================
// Phase 3: Verify deployment
// ============================================
console.log("");
console.log("[Phase 3] Verifying deployment...");

require(proxy.admin() == UPGRADE_MULTISIG, "FAIL: proxy admin != UpgradeMultisig");
require(token.owner() == GNOSIS_SAFE, "FAIL: token owner");
require(token.pauser() == GNOSIS_SAFE, "FAIL: pauser");
require(token.blacklister() == GNOSIS_SAFE, "FAIL: blacklister");
require(masterMinter.owner() == GNOSIS_SAFE, "FAIL: mm owner");
require(address(masterMinter.getMinterManager()) == address(proxy), "FAIL: mm manager");
require(keccak256(abi.encodePacked(token.name())) == keccak256(abi.encodePacked("USD Coin")), "FAIL: name");
require(keccak256(abi.encodePacked(token.symbol())) == keccak256(abi.encodePacked("USDC")), "FAIL: symbol");
require(keccak256(abi.encodePacked(token.currency())) == keccak256(abi.encodePacked("USD")), "FAIL: currency");
require(uint256(token.decimals()) == 6, "FAIL: decimals");
console.log(" [OK] All role assignments correct");

// ============================================
// Summary
// ============================================
console.log("");
console.log("=============================================");
console.log(" DEPLOYMENT SUCCESS");
console.log("=============================================");
console.log(" Implementation: %s", address(impl));
console.log(" Proxy: %s", address(proxy));
console.log(" MasterMinter: %s", address(masterMinter));
console.log(" ProxyAdmin: %s (SimpleMultiSig)", UPGRADE_MULTISIG);
console.log(" Token Owner: %s", GNOSIS_SAFE);
console.log(" Pauser: %s", GNOSIS_SAFE);
console.log(" Blacklister: %s", GNOSIS_SAFE);
console.log(" MasterMinter Owner: %s", GNOSIS_SAFE);
console.log(" NOTE: configureController + configureMinter + mint");
console.log(" must be executed manually via Gnosis Safe UI");
console.log("=============================================");
}
}
19 changes: 12 additions & 7 deletions scripts/hardhat/verifyOnChainBytecode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,14 +250,19 @@ export async function verifyOnChainBytecode(
// Library contract bytecode changes at deploy time. The contract address is embedded after the first push instruction
// https://docs.soliditylang.org/en/develop/contracts.html#call-protection-for-libraries
if (isLibrary) {
// Replace bytecode (4, 44) with contract address
const firstInstructionLength = 4;
// Forge v1.5+ prefixes artifact bytecode strings with "0x". Strip the
// prefix, substitute the embedded PUSH20 address on the raw hex body,
// then re-add the prefix so the comparison with on-chain bytecode
// (0x-prefixed) stays aligned. The PUSH20 opcode (0x73) is at body byte 0,
// followed by the 20-byte embedded address placeholder.
const prefix = expectedRuntimeBytecode.startsWith("0x") ? "0x" : "";
const body = prefix ? expectedRuntimeBytecode.slice(2) : expectedRuntimeBytecode;
const address = contractAddress.toLowerCase().slice(2);
expectedRuntimeBytecode =
expectedRuntimeBytecode.slice(0, firstInstructionLength) +
contractAddress.toLowerCase().slice(2) +
expectedRuntimeBytecode.slice(
firstInstructionLength + contractAddress.slice(2).length
);
prefix +
body.slice(0, 2) +
address +
body.slice(2 + address.length);
}

// ==== For contracts that use external libraries, replace embedded library address with locally compiled bytecode with actual library addresses from user input.
Expand Down
Loading