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
2 changes: 2 additions & 0 deletions common/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export interface ITokens {
yvCurveUSDCcrvUSD?: string
wsuperOETHb?: string
wOETH?: string
wOUSD?: string

pyUSD?: string
aEthPyUSD?: string
Expand Down Expand Up @@ -331,6 +332,7 @@ export const networkConfig: { [key: string]: INetworkConfig } = {
USDS: '0xdC035D45d973E3EC169d2276DDab16f1e407384F',
sUSDS: '0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD',
wOETH: '0xDcEe70654261AF21C44c093C300eD3Bb97b78192',
wOUSD: '0xD2af830E8CBdFed6CC11Bab697bB25496ed6FA62',
wcvx3Pool: '0x24CDc6b4Edd3E496b7283D94D93119983A61056a', // our wrapper
wcvxPayPool: '0x511daB8150966aFfE15F0a5bFfBa7F4d2b62DEd4', // our wrapper
wcvxCrvUSDUSDC: '0x6ad24C0B8fD4B594C6009A7F7F48450d9F56c6b8', // our wrapper
Expand Down
19 changes: 19 additions & 0 deletions common/ousd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { utils } from 'ethers'
import { networkConfig } from './configuration'
import { fp } from './numbers'

// Ethereum mainnet only. Shared by deployment, verification, and tests.
export const ousdCollateralConfig = {
erc20: networkConfig['1'].tokens.wOUSD!,
targetName: utils.formatBytes32String('USD'),
priceTimeout: 604800,
// Required by Asset's constructor, but never read by OUSDCollateral.
chainlinkFeed: networkConfig['1'].chainlinkFeeds.USDC!,
oracleTimeout: 1, // Delay before inherited price decay, plus ORACLE_TIMEOUT_BUFFER
oracleError: fp('0.005'), // Pricing margin only; does not cover an OUSD depeg
maxTradeVolume: fp('1e6'),
defaultThreshold: 0, // No depeg detection
delayUntilDefault: 86400,
}

export const ousdRevenueHiding = fp('1e-4')
56 changes: 56 additions & 0 deletions contracts/plugins/assets/origin/OUSD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Origin Dollar collateral

`OUSDCollateral` holds **wOUSD** on Ethereum mainnet. Its reference is OUSD; its target
and unit of account are USD. It inherits the linear ERC-4626 conversion and revenue
hiding from `ERC4626FiatCollateral`.

## Pricing and defaults

The plugin **assumes 1 OUSD = 1 USD**. Its central USD/wOUSD price is the current
OUSD/wOUSD conversion ratio; `oracleError` widens this into low/high price estimates.
It reads no external price feed and cannot detect an OUSD depeg. The pricing margin
is not a bound on possible depeg losses.

A decrease below the exposed reference ratio causes an immediate, irreversible hard
default. Small decreases within revenue hiding are tolerated. Backing losses that
do not lower the wrapper ratio are not detected by this mechanism.

`chainlinkFeed` is retained because the inherited constructor requires a nonzero
address, but is never called. `oracleTimeout` must also be positive: it still enters
the inherited saved-price decay delay (`oracleTimeout + 300 seconds`) if wrapper
pricing fails. A wrapper conversion revert with a reason causes a hard default;
empty reverts propagate, following the parent contract's handling of possible
out-of-gas failures.

The shared configuration in `common/ousd.ts` uses a 0.5% pricing margin, 0.01% revenue
hiding, zero default threshold, one-second oracle timeout, seven-day price decay,
24-hour delay until default and $1m maximum trade volume. These are deployment
defaults, not a collateral risk assessment.

## Integration

Reserve transfers wOUSD directly on RToken redemption and sells wOUSD during
recollateralization. This plugin does not call Origin's withdrawal queue or monitor
capital pauses. Exit liquidity and the underlying strategies remain economic risks.

The [Origin contract registry](https://docs.originprotocol.com/registry/contracts/ousd-registry)
identifies wOUSD at `0xD2af830E8CBdFed6CC11Bab697bB25496ed6FA62` and OUSD at
`0x2A8e1E676Ec238d8A992307B495b45B3fEAa5e86`.
[WrappedOusd](https://github.com/OriginProtocol/origin-dollar/blob/master/contracts/contracts/token/WrappedOusd.sol)
inherits Origin's WOETH wrapper implementation.

Deploy using `scripts/deployment/phase2-assets/collaterals/deploy_origin_ousd.ts`
after the standard deployment prerequisites; verify using
`scripts/verification/collateral-plugins/verify_ousd.ts`. Both consume the same
constructor configuration and support Ethereum mainnet only.

## Tests

```sh
FORK= yarn hardhat test test/plugins/OUSDCollateral.test.ts
PROTO_IMPL=1 FORK=1 FORK_NETWORK=mainnet FORK_BLOCK=22164000 yarn hardhat test test/plugins/individual-collateral/origin/OUSDCollateral.test.ts
```

The fork suite requires an archive-capable `MAINNET_RPC_URL`. It uses real OUSD
transferred from the Curve OUSD/3CRV pool and wraps it through wOUSD's deposit
interface. It covers the ratio, transfers, registration, issuance and redemption.
45 changes: 45 additions & 0 deletions contracts/plugins/assets/origin/OUSDCollateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.28;

import "../ERC4626FiatCollateral.sol";
import "../../../libraries/Fixed.sol";

/**
* @title Origin Dollar Collateral for Mainnet
* @notice tok = wOUSD, ref = OUSD, tar = USD, UoA = USD
* @dev WARNING: Assumes 1 OUSD = 1 USD. This plugin does not detect OUSD depegs or
* backing losses that do not reduce the wrapper's OUSD/wOUSD exchange rate.
*/
contract OUSDCollateral is ERC4626FiatCollateral {
using FixLib for uint192;

/// @param config.chainlinkFeed Ignored, but must be nonzero for Asset validation
/// @param config.oracleTimeout Delays saved-price decay on wrapper failure; no feed expiry
/// @param config.oracleError Pricing margin, not protection against an OUSD depeg
/// @param revenueHiding {1} Maximum fraction of refPerTok to hide
// solhint-disable no-empty-blocks
constructor(CollateralConfig memory config, uint192 revenueHiding)
ERC4626FiatCollateral(config, revenueHiding)
{}

// solhint-enable no-empty-blocks

/// @return low {UoA/tok} The low price estimate
/// @return high {UoA/tok} The high price estimate
/// @return pegPrice {target/ref} Assumes 1 USD/OUSD
function tryPrice()
external
view
override
returns (
uint192 low,
uint192 high,
uint192 pegPrice
)
{
// {UoA/tok} = {ref/tok}, assuming 1 USD/OUSD
uint192 p = underlyingRefPerTok();
uint192 err = p.mul(oracleError, CEIL);
return (p - err, p + err, FIX_ONE);
}
}
2 changes: 2 additions & 0 deletions contracts/plugins/assets/origin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ On base:
### claimRewards()

There are no rewards to claim from `wOETH` and `wsuperOETH`, all yield is already included in the ERC4626 assets appreciation.

For the fixed-peg wOUSD collateral plugin, see [Origin Dollar collateral](./OUSD.md).
42 changes: 42 additions & 0 deletions contracts/plugins/mocks/ERC4626Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.28;

import "@openzeppelin/contracts/utils/math/Math.sol";
import "./ERC20Mock.sol";

/// @dev Configurable ERC4626 pricing surface; minting is independent of assets for tests.
contract ERC4626Mock is ERC20Mock {
address public immutable asset;
uint8 private immutable shareDecimals;
uint256 public assetsPerShare;
uint8 public revertMode;

constructor(
address asset_,
uint8 shareDecimals_,
uint256 assetsPerShare_
) ERC20Mock("Mock vault", "VAULT") {
asset = asset_;
shareDecimals = shareDecimals_;
assetsPerShare = assetsPerShare_;
}

function decimals() public view override returns (uint8) {
return shareDecimals;
}

function setAssetsPerShare(uint256 value) external {
assetsPerShare = value;
}

function setRevertMode(uint8 value) external {
revertMode = value;
}

function convertToAssets(uint256 shares) external view returns (uint256) {
require(revertMode != 1, "wrapper unavailable");
// solhint-disable-next-line reason-string
if (revertMode == 2) revert();
return Math.mulDiv(shares, assetsPerShare, 10**shareDecimals);
}
}
43 changes: 43 additions & 0 deletions scripts/deployment/phase2-assets/collaterals/deploy_origin_ousd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import fs from 'fs'
import hre from 'hardhat'
import { getChainId } from '../../../../common/blockchain-utils'
import { CollateralStatus } from '../../../../common/constants'
import { ousdCollateralConfig, ousdRevenueHiding } from '../../../../common/ousd'
import {
fileExists,
getAssetCollDeploymentFilename,
getDeploymentFile,
getDeploymentFilename,
IAssetCollDeployments,
} from '../../common'

async function main() {
const chainId = await getChainId(hre)
if (chainId !== '1') throw new Error(`Unsupported chainId: ${chainId}`)

const phase1File = getDeploymentFilename(chainId)
if (!fileExists(phase1File)) throw new Error(`${phase1File} doesn't exist yet. Run phase 1`)

const filename = getAssetCollDeploymentFilename(chainId)
const deployments = getDeploymentFile(filename) as IAssetCollDeployments
const [deployer] = await hre.ethers.getSigners()
console.log(`Deploying wOUSD collateral on ${hre.network.name} with ${deployer.address}`)

const factory = await hre.ethers.getContractFactory('OUSDCollateral', deployer)
const collateral = await factory.deploy(ousdCollateralConfig, ousdRevenueHiding)
await collateral.deployed()
await (await collateral.refresh()).wait()
if ((await collateral.status()) !== CollateralStatus.SOUND) {
throw new Error('wOUSD collateral is not SOUND')
}

deployments.collateral.wOUSD = collateral.address
deployments.erc20s.wOUSD = ousdCollateralConfig.erc20
fs.writeFileSync(filename, JSON.stringify(deployments, null, 2))
console.log(`Deployed wOUSD collateral: ${collateral.address}; saved to ${filename}`)
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})
33 changes: 33 additions & 0 deletions scripts/verification/collateral-plugins/verify_ousd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import hre from 'hardhat'
import { getChainId } from '../../../common/blockchain-utils'
import { developmentChains } from '../../../common/configuration'
import { ousdCollateralConfig, ousdRevenueHiding } from '../../../common/ousd'
import {
getAssetCollDeploymentFilename,
getDeploymentFile,
IAssetCollDeployments,
} from '../../deployment/common'
import { verifyContract } from '../../deployment/utils'

async function main() {
const chainId = await getChainId(hre)
if (chainId !== '1' || developmentChains.includes(hre.network.name)) {
throw new Error(`Unsupported verification network: ${hre.network.name} (${chainId})`)
}
const deployments = getDeploymentFile(
getAssetCollDeploymentFilename(chainId)
) as IAssetCollDeployments
if (!deployments.collateral.wOUSD) throw new Error('Missing wOUSD collateral deployment')

await verifyContract(
Number(chainId),
deployments.collateral.wOUSD,
[ousdCollateralConfig, ousdRevenueHiding],
'contracts/plugins/assets/origin/OUSDCollateral.sol:OUSDCollateral'
)
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})
Loading