Skip to content

Commit

Permalink
ApxEth plugin and scripts (#1143)
Browse files Browse the repository at this point in the history
Co-authored-by: Taylor Brent <[email protected]>
  • Loading branch information
julianmrodri and tbrent authored Jul 30, 2024
1 parent c5b08d9 commit 0c04a2f
Show file tree
Hide file tree
Showing 18 changed files with 760 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- run: yarn install --immutable
- run: yarn devchain &
env:
MAINNET_RPC_URL: https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161
MAINNET_RPC_URL: https://eth-mainnet.alchemyapi.io/v2/${{ secrets.ALCHEMY_MAINNET_KEY }}
FORK_NETWORK: mainnet
- run: yarn deploy:run --network localhost
env:
Expand Down
6 changes: 6 additions & 0 deletions common/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ export interface ITokens {
steakPYUSD?: string
Re7WETH?: string

pxETH?: string
apxETH?: string

// Ethena
USDe?: string
sUSDe?: string
Expand Down Expand Up @@ -225,6 +228,8 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
stETH: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84',
wstETH: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0',
rETH: '0xae78736Cd615f374D3085123A210448E74Fc6393',
pxETH: '0x04C154b66CB340F3Ae24111CC767e0184Ed00Cc6',
apxETH: '0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6',
cUSDCv3: '0xc3d688B66703497DAA19211EEdff47f25384cdc3',
wcUSDCv3: '0x27F2f159Fe990Ba83D57f39Fd69661764BEbf37a',
ONDO: '0xfAbA6f8e4a5E8Ab82F62fe7C39859FA577269BE3',
Expand Down Expand Up @@ -277,6 +282,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
cbETH: '0xf017fcb346a1885194689ba23eff2fe6fa5c483b', // cbETH/ETH
frxETH: '0xc58f3385fbc1c8ad2c0c9a061d7c13b141d7a5df', // frxETH/ETH
pyUSD: '0x8f1dF6D7F2db73eECE86a18b4381F4707b918FB1',
apxETH: '0x19219BC90F48DeE4d5cF202E09c438FAacFd8Bea', // apxETH/ETH
USDe: '0xa569d910839Ae8865Da8F8e70FfFb0cBA869F961',
},
AAVE_INCENTIVES: '0xd784927Ff2f95ba542BfC824c8a8a98F3495f6b5',
Expand Down
2 changes: 1 addition & 1 deletion contracts/plugins/assets/lido/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Summary

This plugin allows `wstETH` holders use their tokens as collateral in the Reverse Protocol.
This plugin allows `wstETH` holders use their tokens as collateral in the Reserve Protocol.

As described in the [Lido Site](https://docs.lido.fi/guides/steth-integration-guide#wsteth) , `wstETH` is a LSD (Liquid staking derivatives) which enables users to sell or transfer stacked ETH even before withdrawal being enabled.

Expand Down
74 changes: 74 additions & 0 deletions contracts/plugins/assets/pirex-eth/ApxEthCollateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "@openzeppelin/contracts/utils/math/Math.sol";
import "../../../libraries/Fixed.sol";
import "../AppreciatingFiatCollateral.sol";
import "../OracleLib.sol";
import "./vendor/IApxETH.sol";

/**
* @title apxETH Collateral
* @notice Collateral plugin for Dinero apxETH (Pirex-ETH)
* tok = apxETH
* ref = pxETH (pegged to ETH 1:1)
* tar = ETH
* UoA = USD
*/
contract ApxEthCollateral is AppreciatingFiatCollateral {
using OracleLib for AggregatorV3Interface;
using FixLib for uint192;

AggregatorV3Interface public immutable targetPerTokChainlinkFeed;
uint48 public immutable targetPerTokChainlinkTimeout;

/// @param config.chainlinkFeed {UoA/target} price of ETH in USD terms
/// @param _targetPerTokChainlinkFeed {target/tok} price of apxETH in ETH terms
constructor(
CollateralConfig memory config,
uint192 revenueHiding,
AggregatorV3Interface _targetPerTokChainlinkFeed,
uint48 _targetPerTokChainlinkTimeout
) AppreciatingFiatCollateral(config, revenueHiding) {
require(config.defaultThreshold != 0, "defaultThreshold zero");
require(address(_targetPerTokChainlinkFeed) != address(0), "missing targetPerTok feed");
require(_targetPerTokChainlinkTimeout != 0, "targetPerTokChainlinkTimeout zero");

targetPerTokChainlinkFeed = _targetPerTokChainlinkFeed;
targetPerTokChainlinkTimeout = _targetPerTokChainlinkTimeout;
maxOracleTimeout = uint48(Math.max(maxOracleTimeout, _targetPerTokChainlinkTimeout));
}

/// Can revert, used by other contract functions in order to catch errors
/// @return low {UoA/tok} The low price estimate
/// @return high {UoA/tok} The high price estimate
/// @return pegPrice {target/ref} The actual price observed in the peg
function tryPrice()
external
view
override
returns (
uint192 low,
uint192 high,
uint192 pegPrice
)
{
uint192 targetPerTok = targetPerTokChainlinkFeed.price(targetPerTokChainlinkTimeout);

// {UoA/tok} = {UoA/target} * {target/tok}
uint192 p = chainlinkFeed.price(oracleTimeout).mul(targetPerTok);
uint192 err = p.mul(oracleError, CEIL);

high = p + err;
low = p - err;
// assert(low <= high); obviously true just by inspection

// {target/ref} = {target/tok} / {ref/tok}
pegPrice = targetPerTok.div(underlyingRefPerTok());
}

/// @return {ref/tok} Quantity of whole reference units per whole collateral tokens
function underlyingRefPerTok() public view override returns (uint192) {
return _safeWrap(IApxETH(address(erc20)).assetsPerShare());
}
}
29 changes: 29 additions & 0 deletions contracts/plugins/assets/pirex-eth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Pirex apxETH (pxETH) Collateral Plugin

## Summary

This plugin allows `apxETH` holders use their tokens as collateral in the Reserve Protocol.

As described in the [Dinero Site](https://dineroismoney.com/docs/pirex-eth-overview), Pirex ETH is an Ethereum liquid staking solution that consists of two tokens, `pxETH` and `apxETH`.

Upon depositing ETH into the Dinero protocol through Pirex ETH, users receive `pxETH` - a liquid wrapper for staked ETH. However, the pxETH token itself does not earn any rewards. Users can deposit to Dinero's auto-compounding vaults to obtain `apxETH`, which is focused on maximizing their staking yields. Each `apxETH` benefits from staking rewards from more than one staked ETH, amplifying the yield for apxETH users.

`apxETH` will accrue revenue from **staking rewards** into itself by **increasing** the exchange rate of `pxETH` per `apxETH`.

`pxETH` contract: <https://etherscan.io/address/0x04C154b66CB340F3Ae24111CC767e0184Ed00Cc6#code>

`apxETH` contract: <https://etherscan.io/address/0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6#code>

## Implementation

### Units

| tok | ref | target | UoA |
| ------ | ----- | ------ | --- |
| apxETH | pxETH | ETH | USD |

### Functions

#### refPerTok {ref/tok}

This function returns rate of `pxETH/apxETH`, getting from [assetsPerShare()](https://etherscan.io/token/0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6#readContract) function in wstETH contract.
13 changes: 13 additions & 0 deletions contracts/plugins/assets/pirex-eth/vendor/IApxETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

// External interface for apxETH
interface IApxETH is IERC20Metadata {
function assetsPerShare() external view returns (uint256);

function setWithdrawalPenalty(uint256 penalty) external;

function notifyRewardAmount() external;
}
14 changes: 14 additions & 0 deletions contracts/plugins/mocks/ApxEthMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

import "./ERC20Mock.sol";

contract ApxEthMock is ERC20Mock {
uint256 public assetsPerShare;

constructor() ERC20Mock("Mock ApxETH", "ApxEth") {}

function setAssetsPerShare(uint256 mockValue) external {
assetsPerShare = mockValue;
}
}
4 changes: 3 additions & 1 deletion scripts/addresses/1-tmp-assets-collateral.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"cvxCrvUSDUSDC": "0x9Fc0F31e2D26C437461a9eEBfe858d17e2611Ea5",
"cvxCrvUSDUSDT": "0x69c6597690B8Df61D15F201519C03725bdec40c1",
"sfrxETH": "0x4c891fCa6319d492866672E3D2AfdAAA5bDcfF67",
"apxETH": "0x05ffDaAA2aF48e1De1CE34d633db018a28e3B3F5",
"sUSDe": "0x35081Ca24319835e5f759163F7e75eaB753e0b7E"
},
"erc20s": {
Expand Down Expand Up @@ -119,6 +120,7 @@
"sfrxETH": "0xac3E018457B222d93114458476f3E3416Abbe38F",
"CRV": "0xD533a949740bb3306d119CC777fa900bA034cd52",
"CVX": "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B",
"apxETH": "0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6",
"sUSDe": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497"
}
}
}
2 changes: 2 additions & 0 deletions scripts/addresses/mainnet-3.4.0/1-tmp-assets-collateral.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"cvxCrvUSDUSDC": "0x9Fc0F31e2D26C437461a9eEBfe858d17e2611Ea5",
"cvxCrvUSDUSDT": "0x69c6597690B8Df61D15F201519C03725bdec40c1",
"sfrxETH": "0x4c891fCa6319d492866672E3D2AfdAAA5bDcfF67",
"apxETH": "0x05ffDaAA2aF48e1De1CE34d633db018a28e3B3F5",
"sUSDe": "0x35081Ca24319835e5f759163F7e75eaB753e0b7E"
},
"erc20s": {
Expand Down Expand Up @@ -119,6 +120,7 @@
"sfrxETH": "0xac3E018457B222d93114458476f3E3416Abbe38F",
"CRV": "0xD533a949740bb3306d119CC777fa900bA034cd52",
"CVX": "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B",
"apxETH": "0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6",
"sUSDe": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497"
}
}
1 change: 1 addition & 0 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ async function main() {
'phase2-assets/collaterals/deploy_steakpyusd.ts',
'phase2-assets/collaterals/deploy_bbusdt.ts',
'phase2-assets/collaterals/deploy_re7weth.ts',
'phase2-assets/collaterals/deploy_apxeth.ts',
'phase2-assets/collaterals/deploy_USDe.ts',
'phase2-assets/assets/deploy_crv.ts',
'phase2-assets/assets/deploy_cvx.ts'
Expand Down
95 changes: 95 additions & 0 deletions scripts/deployment/phase2-assets/collaterals/deploy_apxeth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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 { priceTimeout, combinedError } from '../../utils'
import { ApxEthCollateral } from '../../../../typechain'
import {
ETH_ORACLE_ERROR,
ETH_ORACLE_TIMEOUT,
APXETH_ORACLE_ERROR,
APXETH_ORACLE_TIMEOUT,
DELAY_UNTIL_DEFAULT,
} from '../../../../test/plugins/individual-collateral/pirex-eth/constants'
import { ContractFactory } from 'ethers'

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 = <IAssetCollDeployments>getDeploymentFile(assetCollDeploymentFilename)

const deployedCollateral: string[] = []

/******** Deploy ApxETH Collateral - apxETH **************************/

const ApxEthCollateralFactoryCollateralFactory: ContractFactory =
await hre.ethers.getContractFactory('ApxEthCollateral')

const oracleError = combinedError(ETH_ORACLE_ERROR, APXETH_ORACLE_ERROR) // 0.5% & 1%

const collateral = <ApxEthCollateral>await ApxEthCollateralFactoryCollateralFactory.connect(
deployer
).deploy(
{
priceTimeout: priceTimeout.toString(),
chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ETH,
oracleError: oracleError.toString(),
erc20: networkConfig[chainId].tokens.apxETH,
maxTradeVolume: fp('1e6').toString(), // $1m,
oracleTimeout: ETH_ORACLE_TIMEOUT.toString(), // 1 hr,
targetName: hre.ethers.utils.formatBytes32String('ETH'),
defaultThreshold: fp('0.02').add(APXETH_ORACLE_ERROR).toString(), // 3%
delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), // 72h
},
fp('1e-4').toString(), // revenueHiding = 0.01%
networkConfig[chainId].chainlinkFeeds.apxETH, // targetPerTokChainlinkFeed
APXETH_ORACLE_TIMEOUT.toString() // targetPerTokChainlinkTimeout - 24h
)
await collateral.deployed()
await (await collateral.refresh()).wait()
expect(await collateral.status()).to.equal(CollateralStatus.SOUND)

console.log(`Deployed ApxETH to ${hre.network.name} (${chainId}): ${collateral.address}`)

assetCollDeployments.collateral.apxETH = collateral.address
assetCollDeployments.erc20s.apxETH = networkConfig[chainId].tokens.apxETH
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
})
63 changes: 63 additions & 0 deletions scripts/verification/collateral-plugins/verify_apxeth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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 {
ETH_ORACLE_TIMEOUT,
ETH_ORACLE_ERROR,
DELAY_UNTIL_DEFAULT,
APXETH_ORACLE_ERROR,
APXETH_ORACLE_TIMEOUT,
} from '../../../test/plugins/individual-collateral/pirex-eth/constants'
import { priceTimeout, verifyContract, combinedError } from '../../deployment/utils'

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 = <IAssetCollDeployments>getDeploymentFile(assetCollDeploymentFilename)

/******** Verify ApxETH - apxETH **************************/
const oracleError = combinedError(ETH_ORACLE_ERROR, APXETH_ORACLE_ERROR) // 0.5% & 1%
await verifyContract(
chainId,
deployments.collateral.apxETH,
[
{
priceTimeout: priceTimeout.toString(),
chainlinkFeed: networkConfig[chainId].chainlinkFeeds.ETH,
oracleError: oracleError.toString(), // 0.5% & 1%,
erc20: networkConfig[chainId].tokens.apxETH,
maxTradeVolume: fp('1e6').toString(), // $1m,
oracleTimeout: ETH_ORACLE_TIMEOUT.toString(), // 1 hr,
targetName: hre.ethers.utils.formatBytes32String('ETH'),
defaultThreshold: fp('0.02').add(APXETH_ORACLE_ERROR).toString(), // 3%
delayUntilDefault: DELAY_UNTIL_DEFAULT.toString(), // 72h
},
fp('1e-4'), // revenueHiding = 0.01%
networkConfig[chainId].chainlinkFeeds.apxETH, // targetPerTokChainlinkFeed
APXETH_ORACLE_TIMEOUT.toString(), // targetPerTokChainlinkTimeout
],
'contracts/plugins/assets/pirex-eth/ApxEthCollateral.sol:ApxEthCollateral'
)
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})
1 change: 1 addition & 0 deletions scripts/verify_etherscan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ async function main() {
'collateral-plugins/verify_sfrax_eth.ts',
'collateral-plugins/verify_steakusdc.ts',
'collateral-plugins/verify_re7weth.ts',
'collateral-plugins/verify_apxeth.ts',
'collateral-plugins/verify_USDe.ts'
)
} else if (chainId == '8453' || chainId == '84531') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ const opts = {
itChecksTargetPerRefDefaultUp: it.skip,
itChecksRefPerTokDefault: it.skip,
itChecksPriceChanges: it,
itHasRevenueHiding: it.skip, // implemnted in this file
itHasRevenueHiding: it.skip, // implemented in this file
itChecksNonZeroDefaultThreshold: it,
resetFork,
collateralName: 'SFraxEthCollateral',
Expand Down
Loading

0 comments on commit 0c04a2f

Please sign in to comment.