Smart contracts and deployment scripts for the HYBOND system.
The repository is built on Hardhat and uses OpenZeppelin UUPS proxies for the main upgradeable contracts.
Primary contracts in this repo:
HYBONDtokenAssetRegistryExpressPriceOracleGovernanceTimelock(see Timelock Governance)
The current PriceOracle uses:
- dual deviation guardrails (
relativeMaxDeviationandabsoluteMaxDeviation) - staged updates via
proposePrice()andconfirmPrice() - a 1-day expiry for pending price proposals
Express charges a daily management fee to mgtFeeTo via updateEpoch. Fee accounting uses two counters:
totalMgtFeeMinted— cumulative fees ever minted. Monotonic; never decremented. Anchors historical dilution in thesharesPerTokendenominator even after fee tokens are burned.totalMgtFeeUnclaimed— currently live (unredeemed) fee tokens. Decremented when fee tokens move from the pending redeem queue to the final redeem queue.
Share accounting:
circulatingSupply = totalSupply − totalRedeemQueueShares − totalMgtFeeUnclaimed
sharesPerToken = circulatingSupply / (circulatingSupply + totalMgtFeeMinted)
The ratio moves only on fee accrual and new user deposits. Fee redemptions (request → pending→final → burn) leave both circulatingSupply and sharesPerToken unchanged.
On-chain guards:
requestRedeemcalled bymgtFeeTooverrides the caller-supplied amount tototalMgtFeeUnclaimed(full live balance). Prevents fee-share provenance desync.updateMgtFeeTorequirestotalMgtFeeUnclaimed == 0AND empty redeem queues.requestDepositand_calculateMintAmountboth revertDrainedInstancewhencirculatingSupply() == 0 && totalMgtFeeMinted > 0— a drained instance with historical fees is terminal; redeploy instead of reusing.
The full list lives in the comment block at contracts/extension/Express.sol near the mgtFeeTo declaration. Operators must:
- Transfer HYBOND shares from
mgtFeeToonly to theExpresscontract viarequestRedeem. - Move any non-fee shares that land on
mgtFeeToto a quarantine wallet; do not redeem them frommgtFeeTo. - Never ban
mgtFeeTowhile it holds live fees or has in-flight fee redeems. - Drain both redeem queues before rotating
mgtFeeTo(also enforced on-chain). - Keep
mgtFeeTounbanned for the lifetime of the pool — futureupdateEpochmints would fail on a banned recipient. - Keep
mgtFeeToand any fee-redeem receiver KYC-listed through settlement. - Do not change
convertRedeemRequestsDelay,redeemFeeRate,depositFeeRate,priceOracle,maxStalePeriod, ortrimDecimalswhile any queue is non-empty — these values are read live at processing time and changes retroactively affect queued entries.
- Node.js 18+ recommended
- npm
npm installCreate a .env file for network access and verification:
PRIVATE_KEY=your_private_key
ALCHEMY_KEY=your_alchemy_api_key
ETHERSCAN_KEY=your_etherscan_api_keyDeployment parameters are stored in:
The config format is hierarchical. Each top-level key is a contract or shared section, and that section contains its parameters.
Example:
{
"Common": {
"admin": "0x..."
},
"PriceOracle": {
"decimals": 8,
"relativeMaxDeviation": 500,
"absoluteMaxDeviation": 1000,
"initPrice": "1",
"referencePrice": "1",
"operator": "0x..."
}
}Current top-level sections:
CommonHYBONDAssetRegistryPriceOracleExpressTimelock
Notes:
hardhatandlocalhostdeployments useconfig/sepolia.jsonas the default config source.- Several address fields are set to the zero address in the sample configs. In the deploy scripts, those fields fall back to
deployerfor admin/operator/fee-recipient style values. - The
Timelocksection is the exception: itsproposerandexecutorare not allowed to fall back todeployer. They must be set to the real ops multisig before deploying — the deploy script hard-fails on a zero-address proposer/executor. - Real deployment addresses should be filled in before deploying to testnet or mainnet.
npm run compileRun all tests:
npm testUseful subsets:
npm run test:unit
npm run test:integration
npm run coverageThe repo uses hardhat-deploy tags.
Full deployment is a three-phase sequence:
- Core contracts (
hybond_all) — token, asset registry, price oracle, Express. - Timelock (
timelock) — deployed separately; depends on the core contracts. Not part ofhybond_all, so it must be run as its own step. - Governance migration — hand roles/ownership to the timelock (see Timelock Governance).
# 1. Core system
npx hardhat deploy --network sepolia --tags hybond_all
# 2. Timelock (set config/<network>.json Timelock proposer/executor first)
npm run timelock:deploy:sepolia # dry-run
EXECUTE=true npm run timelock:deploy:sepolia # deploy + verify
# 3. Migrate governance to the timelock
npm run timelock:migrate:sepolia # dry-run plan
EXECUTE=true npm run timelock:migrate:sepoliaIndividual deploy scripts:
deploy/00_deploy_hybond_all.tsdeploy/02_deploy_hybond_token.tsdeploy/03_deploy_asset_registry.tsdeploy/04_deploy_express.tsdeploy/05_deploy_price_oracle.tsdeploy/06_deploy_timelock.ts
Examples (individual tags):
npx hardhat deploy --network sepolia --tags hybond_all
npx hardhat deploy --network sepolia --tags hybond
npx hardhat deploy --network sepolia --tags asset_registry
npx hardhat deploy --network sepolia --tags express
npx hardhat deploy --network sepolia --tags price_oracle
npx hardhat deploy --network sepolia --tags timelockNote: the hybond_all tag deploys the core system only — it does not include the timelock
or the governance migration. Those are phases 2 and 3 above.
deploy/05_deploy_price_oracle.ts reads from the PriceOracle and Common config sections.
Initializer arguments:
(decimals, relativeMaxDeviation, absoluteMaxDeviation, initPrice, referencePrice, admin)
After deployment the script:
- grants
OPERATOR_ROLEif needed - does not auto-grant
CONFIRMER_ROLE - does not auto-grant
UPGRADE_ROLE
Those roles must be granted separately if your operational flow requires them.
GovernanceTimelock is a thin re-export of OpenZeppelin's TimelockController. It becomes
the governance root: after migration it holds DEFAULT_ADMIN_ROLE + UPGRADE_ROLE on
HYBOND/Express/PriceOracle, DEFAULT_ADMIN_ROLE on KycManager, and ownership of
AssetRegistry. Critical config and all upgrades then go through a 24h announce-before-execute
delay; daily operations stay instant.
Design:
- One shared timelock per network, self-administered (
admin = address(0)). minDelay= 86400 (24h), uniform for config changes and upgrades.- Proposer = canceller = executor = the ops multisig (no separate guardian).
Behind the 24h delay: UUPS upgrades; the Express pointer setters
(updateAssetRegistry, updatePriceOracle, updateTreasury, updateRedeemAsset,
setKycManager); PriceOracle deviation/reference-price setters; AssetRegistry
config + ownership; and all grantRole/revokeRole (since DEFAULT_ADMIN_ROLE administers
every role).
Accepted residual risk — the Express economic setters stay instant (fee rates,
updateTxFeeTo, updateMgtFeeTo, updateMgtFeeRate, updateOffchainShares, deviation bps).
They are deliberately not timelocked, so a compromised ops multisig can divert deposit
fees or inflate management-fee tokens without the delay. The security of those paths rests on
the multisig signer policy, not the timelock. This is a conscious trade-off; do not assume the
timelock protects fund flows it does not.
Add a Timelock section to config/<network>.json and set the real ops multisig:
{
"Timelock": {
"minDelay": 86400,
"proposer": "0x<opsMultisig>",
"executor": "0x<opsMultisig>"
}
}The zero-address placeholders in the sample configs are rejected by the deploy script.
Scripts are dry-run by default (print the plan, broadcast nothing). Set EXECUTE=true to send
transactions. .env decryption is wrapped via dotenvx in the npm aliases.
# 1. Deploy the timelock (dry-run first, then execute)
npm run timelock:deploy:sepolia # dry-run
EXECUTE=true npm run timelock:deploy:sepolia # deploy + verify
# 2. Hand governance to the timelock (dry-run first, then execute)
npm run timelock:migrate:sepolia # dry-run plan
EXECUTE=true npm run timelock:migrate:sepoliaAliases exist for sepolia, bsc-testnet, eth (mainnet), and bnb (bsc_mainnet).
The lower-level hardhat-deploy tag also deploys the same contract to the same artifact:
npx hardhat deploy --network sepolia --tags timelock (no dry-run gate, no .env decryption;
prefer the npm alias for real rollouts).
- AccessControl contracts (
HYBOND,Express,PriceOracle,KycManager) migrate synchronously (the deployer still holds admin): grant timelock → verify → revoke oldUPGRADE_ROLEholders → renounce the deployer'sDEFAULT_ADMIN_ROLElast → assert the timelock is the sole admin/upgrader. A preflight guard aborts before any mutation if a second (non-deployer) admin exists, sincerenounceRoleis self-call-only. - AssetRegistry (
Ownable2Step) cannot be finished synchronously — accepting ownership must run through the timelock's 24h cycle, submitted by the ops multisig. The migrate script sendstransferOwnership(timelock)and prints theschedule()/execute()calldata for the multisig to submit (schedule now, execute after 24h). Governance is not fully timelocked untilAssetRegistry.owner() == timelock.
The operator runbook (schedule/execute flow, queue-drain windows, compromised-key response,
no-op-expiry monitoring) lives in docs/DAILY_OPERATIONS_WORKFLOW.md; the deployment/migration
checklist is in docs/DEPLOYMENT_TIMELOCK.md.
npm run format
npm run format:checkUseful commands:
npm run verify:generate-json
npm run verify:all- The deployment scripts are network-config driven; prefer updating
config/*.jsonover editing hardcoded values in scripts. - For upgradeable contracts, do not change storage layout unless you are intentionally introducing a new deployment baseline and understand the upgrade implications.
Deployed contract addresses for all networks (mainnet + testnet) are in deployed-addresses.md.
Independent security audit reports are in audits/.
This project is released under the MIT License. While the MIT License does not require it, if you use this software in your own product or service, we kindly ask that you include visible attribution to OpenEden (e.g. in your documentation, UI, or credits) and link to openeden.com.