From ff699dbfb1ce1b181ba39d54e382c3e251cba84a Mon Sep 17 00:00:00 2001 From: Sean Schulte Date: Fri, 15 Aug 2025 21:45:36 -0500 Subject: [PATCH] feat: add tokemak autopool collateral plugin for autoETH/autoUSD --- common/configuration.ts | 6 ++ .../assets/tokemak/AutopoolCollateral.sol | 25 +++++ contracts/plugins/assets/tokemak/README.md | 22 +++++ scripts/deploy.ts | 2 + .../collaterals/deploy_autoETH.ts | 92 +++++++++++++++++++ .../collaterals/deploy_autoUSD.ts | 92 +++++++++++++++++++ .../collateral-plugins/verify_autoETH.ts | 59 ++++++++++++ .../collateral-plugins/verify_autoUSD.ts | 59 ++++++++++++ scripts/verify_etherscan.ts | 2 + .../tokemak/constants.ts | 15 +++ 10 files changed, 374 insertions(+) create mode 100644 contracts/plugins/assets/tokemak/AutopoolCollateral.sol create mode 100644 contracts/plugins/assets/tokemak/README.md create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_autoETH.ts create mode 100644 scripts/deployment/phase2-assets/collaterals/deploy_autoUSD.ts create mode 100644 scripts/verification/collateral-plugins/verify_autoETH.ts create mode 100644 scripts/verification/collateral-plugins/verify_autoUSD.ts create mode 100644 test/plugins/individual-collateral/tokemak/constants.ts diff --git a/common/configuration.ts b/common/configuration.ts index d5164fa63..360dc377e 100644 --- a/common/configuration.ts +++ b/common/configuration.ts @@ -115,6 +115,10 @@ export interface ITokens { // Mountain USDM?: string wUSDM?: string + + // Tokemak + autoETH?: string + autoUSD?: string } export type ITokensKeys = Array @@ -256,6 +260,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = { sdUSDCUSDCPlus: '0x9bbF31E99F30c38a5003952206C31EEa77540BeF', USDe: '0x4c9edd5852cd905f086c759e8383e09bff1e68b3', sUSDe: '0x9D39A5DE30e57443BfF2A8307A4256c8797A3497', + autoETH: '0x0A2b94F6871c1D7A32Fe58E1ab5e6deA2f114E56', + autoUSD: '0xa7569A44f348d3D70d8ad5889e50F78E33d80D35', }, chainlinkFeeds: { RSR: '0x759bBC1be8F90eE6457C44abc7d443842a976d02', diff --git a/contracts/plugins/assets/tokemak/AutopoolCollateral.sol b/contracts/plugins/assets/tokemak/AutopoolCollateral.sol new file mode 100644 index 000000000..395a67d12 --- /dev/null +++ b/contracts/plugins/assets/tokemak/AutopoolCollateral.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity 0.8.19; + +import { CollateralConfig } from "../AppreciatingFiatCollateral.sol"; +import { ERC4626FiatCollateral } from "../ERC4626FiatCollateral.sol"; + +/** + * @title Tokemak Autopool Collateral + * @notice Collateral plugin for autoETH/autoUSD (Tokemak) + * tok = autoETH|autoUSD (ERC4626 vault) + * ref = WETH|USDC + * tar = ETH|USD + * UoA = USD + */ + +contract AutopoolCollateral is ERC4626FiatCollateral { + /// config.erc20 must be autoETH/autoUSD + /// @param config.chainlinkFeed Feed units: {UoA/ref} + /// @param revenueHiding {1} A value like 1e-6 that represents the maximum refPerTok to hide + constructor(CollateralConfig memory config, uint192 revenueHiding) + ERC4626FiatCollateral(config, revenueHiding) + { + require(config.defaultThreshold != 0, "defaultThreshold zero"); + } +} diff --git a/contracts/plugins/assets/tokemak/README.md b/contracts/plugins/assets/tokemak/README.md new file mode 100644 index 000000000..ab66adba0 --- /dev/null +++ b/contracts/plugins/assets/tokemak/README.md @@ -0,0 +1,22 @@ +# Tokemak Autopool Collateral Plugin + +## Summary + +These plugins allow Tokemak Autopilot users, ie holders of `autoETH` or `autoUSD`, to use their tokens as collateral in the Reserve Protocol. + +[Tokemak Autopilot](https://docs.tokemak.xyz/autopilot/autopilot-tl-dr) is an automated liquidity aggregator, which seeks out the optimum yield for non-volatile blue-chip liquidity pools, compounds rewards, and rebalances to maintain the optimum allocation. What DEX aggregators did for traders, Autopilot does for LPs. + +`autoETH` earns the **liquidity rate** for ETH LSTs. `autoUSD` earns the **liquidity rate** for USD-stablecoins. + +`autoETH` contract: + +`autoUSD` contract: + +## Implementation + +### Units + +| tok | ref | target | UoA | +| ------ | ----- | ------ | --- | +| autoETH | WETH | ETH | USD | +| autoUSD | USDC | USD | USD | diff --git a/scripts/deploy.ts b/scripts/deploy.ts index fa2a1988f..2f16862e0 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -87,6 +87,8 @@ async function main() { 'phase2-assets/collaterals/deploy_re7weth.ts', 'phase2-assets/collaterals/deploy_apxeth.ts', 'phase2-assets/collaterals/deploy_USDe.ts', + 'phase2-assets/collaterals/deploy_autoETH.ts', + 'phase2-assets/collaterals/deploy_autoUSD.ts', 'phase2-assets/assets/deploy_crv.ts', 'phase2-assets/assets/deploy_cvx.ts', 'phase2-assets/collaterals/deploy_pyusd.ts' diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_autoETH.ts b/scripts/deployment/phase2-assets/collaterals/deploy_autoETH.ts new file mode 100644 index 000000000..619588f23 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_autoETH.ts @@ -0,0 +1,92 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { AutopoolCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' +import { + DELAY_UNTIL_DEFAULT, + PRICE_TIMEOUT, + ORACLE_TIMEOUT, + ORACLE_ERROR, +} from '../../../../test/plugins/individual-collateral/tokemak/constants' + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy autoETH Collateral - autoETH **************************/ + let collateral: AutopoolCollateral + + const AutopoolCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'AutopoolCollateral' + ) + + collateral = await AutopoolCollateralFactory.connect(deployer).deploy( + { + priceTimeout: PRICE_TIMEOUT.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.WETH, + oracleError: ORACLE_ERROR.toString(), + erc20: networkConfig[chainId].tokens.autoETH, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: ORACLE_TIMEOUT.toString(), // 24 hr + targetName: hre.ethers.utils.formatBytes32String('ETH'), + defaultThreshold: fp('0.01').add(ORACLE_ERROR).toString(), // ~1.5% + delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), // 72h + }, + fp('1e-6').toString() + ) + + await collateral.deployed() + + console.log( + `Deployed autoETH Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.autoETH = collateral.address + assetCollDeployments.erc20s.autoETH = networkConfig[chainId].tokens.autoETH + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/deployment/phase2-assets/collaterals/deploy_autoUSD.ts b/scripts/deployment/phase2-assets/collaterals/deploy_autoUSD.ts new file mode 100644 index 000000000..e3f4db157 --- /dev/null +++ b/scripts/deployment/phase2-assets/collaterals/deploy_autoUSD.ts @@ -0,0 +1,92 @@ +import fs from 'fs' +import hre from 'hardhat' +import { getChainId } from '../../../../common/blockchain-utils' +import { networkConfig } from '../../../../common/configuration' +import { fp } from '../../../../common/numbers' +import { expect } from 'chai' +import { CollateralStatus } from '../../../../common/constants' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, + getDeploymentFilename, + fileExists, +} from '../../common' +import { AutopoolCollateral } from '../../../../typechain' +import { ContractFactory } from 'ethers' +import { + DELAY_UNTIL_DEFAULT, + PRICE_TIMEOUT, + ORACLE_TIMEOUT, + ORACLE_ERROR, +} from '../../../../test/plugins/individual-collateral/tokemak/constants' + +async function main() { + // ==== Read Configuration ==== + const [deployer] = await hre.ethers.getSigners() + + const chainId = await getChainId(hre) + + console.log(`Deploying Collateral to network ${hre.network.name} (${chainId}) + with burner account: ${deployer.address}`) + + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + // Get phase1 deployment + const phase1File = getDeploymentFilename(chainId) + if (!fileExists(phase1File)) { + throw new Error(`${phase1File} doesn't exist yet. Run phase 1`) + } + // Check previous step completed + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + const assetCollDeployments = getDeploymentFile(assetCollDeploymentFilename) + + const deployedCollateral: string[] = [] + + /******** Deploy autoUSD Collateral - autoUSD **************************/ + let collateral: AutopoolCollateral + + const AutopoolCollateralFactory: ContractFactory = await hre.ethers.getContractFactory( + 'AutopoolCollateral' + ) + + collateral = await AutopoolCollateralFactory.connect(deployer).deploy( + { + priceTimeout: PRICE_TIMEOUT.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDC, + oracleError: ORACLE_ERROR.toString(), + erc20: networkConfig[chainId].tokens.autoUSD, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: ORACLE_TIMEOUT.toString(), // 24 hr + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(ORACLE_ERROR).toString(), // ~1.5% + delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), // 72h + }, + fp('1e-6').toString() + ) + + await collateral.deployed() + + console.log( + `Deployed autoUSD Collateral to ${hre.network.name} (${chainId}): ${collateral.address}` + ) + await (await collateral.refresh()).wait() + expect(await collateral.status()).to.equal(CollateralStatus.SOUND) + + assetCollDeployments.collateral.autoUSD = collateral.address + assetCollDeployments.erc20s.autoUSD = networkConfig[chainId].tokens.autoUSD + deployedCollateral.push(collateral.address.toString()) + + fs.writeFileSync(assetCollDeploymentFilename, JSON.stringify(assetCollDeployments, null, 2)) + + console.log(`Deployed collateral to ${hre.network.name} (${chainId}) + New deployments: ${deployedCollateral} + Deployment file: ${assetCollDeploymentFilename}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_autoETH.ts b/scripts/verification/collateral-plugins/verify_autoETH.ts new file mode 100644 index 000000000..d453b427d --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_autoETH.ts @@ -0,0 +1,59 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { fp } from '../../../common/numbers' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { + PRICE_TIMEOUT, + ORACLE_ERROR, + ORACLE_TIMEOUT, + DELAY_UNTIL_DEFAULT, +} from '../../../test/plugins/individual-collateral/tokemak/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + /******** Verify autoETH COllateral **************************/ + await verifyContract( + chainId, + deployments.collateral.autoETH, + [ + { + priceTimeout: PRICE_TIMEOUT.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.WETH, + oracleError: ORACLE_ERROR.toString(), + erc20: networkConfig[chainId].tokens.autoETH, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: ORACLE_TIMEOUT.toString(), + targetName: hre.ethers.utils.formatBytes32String('ETH'), + defaultThreshold: fp('0.01').add(ORACLE_ERROR).toString(), + delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), + }, + fp('1e-6').toString(), + ], + 'contracts/plugins/assets/tokemak/AutopoolCollateral.sol:AutopoolCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verification/collateral-plugins/verify_autoUSD.ts b/scripts/verification/collateral-plugins/verify_autoUSD.ts new file mode 100644 index 000000000..175311aab --- /dev/null +++ b/scripts/verification/collateral-plugins/verify_autoUSD.ts @@ -0,0 +1,59 @@ +import hre from 'hardhat' +import { getChainId } from '../../../common/blockchain-utils' +import { developmentChains, networkConfig } from '../../../common/configuration' +import { fp } from '../../../common/numbers' +import { + getDeploymentFile, + getAssetCollDeploymentFilename, + IAssetCollDeployments, +} from '../../deployment/common' +import { verifyContract } from '../../deployment/utils' +import { + PRICE_TIMEOUT, + ORACLE_ERROR, + ORACLE_TIMEOUT, + DELAY_UNTIL_DEFAULT, +} from '../../../test/plugins/individual-collateral/tokemak/constants' + +let deployments: IAssetCollDeployments + +async function main() { + // ********** Read config ********** + const chainId = await getChainId(hre) + if (!networkConfig[chainId]) { + throw new Error(`Missing network configuration for ${hre.network.name}`) + } + + if (developmentChains.includes(hre.network.name)) { + throw new Error(`Cannot verify contracts for development chain ${hre.network.name}`) + } + + const assetCollDeploymentFilename = getAssetCollDeploymentFilename(chainId) + deployments = getDeploymentFile(assetCollDeploymentFilename) + + /******** Verify autoUSD COllateral **************************/ + await verifyContract( + chainId, + deployments.collateral.autoUSD, + [ + { + priceTimeout: PRICE_TIMEOUT.toString(), + chainlinkFeed: networkConfig[chainId].chainlinkFeeds.USDC, + oracleError: ORACLE_ERROR.toString(), + erc20: networkConfig[chainId].tokens.autoUSD, + maxTradeVolume: fp('1e6').toString(), // $1m, + oracleTimeout: ORACLE_TIMEOUT.toString(), + targetName: hre.ethers.utils.formatBytes32String('USD'), + defaultThreshold: fp('0.01').add(ORACLE_ERROR).toString(), + delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), + }, + fp('1e-6').toString(), + ], + 'contracts/plugins/assets/tokemak/AutopoolCollateral.sol:AutopoolCollateral' + ) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/verify_etherscan.ts b/scripts/verify_etherscan.ts index 1266c5ce2..65991037a 100644 --- a/scripts/verify_etherscan.ts +++ b/scripts/verify_etherscan.ts @@ -79,6 +79,8 @@ async function main() { 'collateral-plugins/verify_re7weth.ts', 'collateral-plugins/verify_apxeth.ts', 'collateral-plugins/verify_USDe.ts', + 'collateral-plugins/verify_autoETH.ts', + 'collateral-plugins/verify_autoUSD.ts', 'collateral-plugins/verify_pyusd.ts' ) } else if (chainId == '8453' || chainId == '84531') { diff --git a/test/plugins/individual-collateral/tokemak/constants.ts b/test/plugins/individual-collateral/tokemak/constants.ts new file mode 100644 index 000000000..85f35edb3 --- /dev/null +++ b/test/plugins/individual-collateral/tokemak/constants.ts @@ -0,0 +1,15 @@ +import { bn, fp } from '../../../../common/numbers' +import { networkConfig } from '../../../../common/configuration' + +// Mainnet Addresses +export const autoETH = networkConfig['31337'].tokens.autoETH as string +export const autoUSD = networkConfig['31337'].tokens.autoUSD as string + +export const PRICE_TIMEOUT = bn('604800') // 1 week +export const ORACLE_TIMEOUT = bn(86400) // 24h +export const ORACLE_ERROR = fp('0.005') // 0.5% +export const DEFAULT_THRESHOLD = fp('0.05') // 5% +export const DELAY_UNTIL_DEFAULT = bn(259200) // 72h +export const MAX_TRADE_VOL = bn(1000) + +export const FORK_BLOCK = 23150675