Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wUSDM as collateral to USDC market on Arbitrum #78

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
86 changes: 86 additions & 0 deletions contracts/pricefeeds/WUSDMPriceFeed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.15;

import "../vendor/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "../vendor/mountain/IMountainRateProvider.sol";
import "../IPriceFeed.sol";

/**
* @title wUSDM price feed
* @notice A custom price feed that calculates the price for wUSDM / USD
* @author Compound
*/
contract WUSDMPriceFeed is IPriceFeed {
/** Custom errors **/
error BadDecimals();
error InvalidInt256();

/// @notice Version of the price feed
uint public constant override version = 1;
Fixed Show fixed Hide fixed

/// @notice Description of the price feed
string public constant override description = "Custom price feed for wUSDM / USD";
Fixed Show fixed Hide fixed

/// @notice Number of decimals for returned prices
uint8 public immutable override decimals;

/// @notice Number of decimals for the wUSDM / USDM rate provider
uint8 wUSDMToUSDMPriceFeedDecimals;

Check warning on line 28 in contracts/pricefeeds/WUSDMPriceFeed.sol

View workflow job for this annotation

GitHub Actions / Contract linter

Explicitly mark visibility of state

/// @notice Number of decimals for the USDM / USD price feed
uint8 USDMToUSDPriceFeedDecimals;

Check warning on line 31 in contracts/pricefeeds/WUSDMPriceFeed.sol

View workflow job for this annotation

GitHub Actions / Contract linter

Explicitly mark visibility of state

/// @notice Mountain wUSDM / USDM rate provider
address public immutable wUSDMToUSDMRateProvider;

/// @notice Chainlink USDM / USD price feed
address public immutable USDMToUSDPriceFeed;

/// @notice Combined scale of the two underlying price feeds
int public immutable combinedScale;

/// @notice Scale of this price feed
int public immutable priceFeedScale;

/**
* @notice Construct a new wUSDM / USD price feed
* @param wUSDMToUSDMPriceFeed_ The address of the wUSDM / USDM price feed to fetch prices from
* @param USDMToUSDPriceFeed_ The address of the USDM / USD price feed to fetch prices from
* @param decimals_ The number of decimals for the returned prices
**/
constructor(address wUSDMToUSDMPriceFeed_, address USDMToUSDPriceFeed_, uint8 decimals_) {
wUSDMToUSDMRateProvider = wUSDMToUSDMPriceFeed_;
USDMToUSDPriceFeed = USDMToUSDPriceFeed_;
wUSDMToUSDMPriceFeedDecimals = IMountainRateProvider(wUSDMToUSDMPriceFeed_).decimals();
USDMToUSDPriceFeedDecimals = AggregatorV3Interface(USDMToUSDPriceFeed_).decimals();
combinedScale = signed256(10 ** (wUSDMToUSDMPriceFeedDecimals + USDMToUSDPriceFeedDecimals));

if (decimals_ > 18) revert BadDecimals();
decimals = decimals_;
priceFeedScale = int256(10 ** decimals);
}
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed

/**
* @notice wUSDM price for the latest round
* @return roundId Round id from the USDM / USD price feed
* @return answer Latest price for wUSDM / USD
* @return startedAt Timestamp when the round was started; passed on from the USDM / USD price feed
* @return updatedAt Timestamp when the round was last updated; passed on from the USDM / USD price feed
* @return answeredInRound Round id in which the answer was computed; passed on from the USDM / USD price feed
**/
function latestRoundData() override external view returns (uint80, int256, uint256, uint256, uint80) {
uint256 wUSDMToUSDMPrice = IMountainRateProvider(wUSDMToUSDMRateProvider).convertToAssets(10**wUSDMToUSDMPriceFeedDecimals);
(uint80 roundId_, int256 USDMToUSDPrice, uint256 startedAt_, uint256 updatedAt_, uint80 answeredInRound_) = AggregatorV3Interface(USDMToUSDPriceFeed).latestRoundData();

// We return the round data of the USDM / USD price feed because of its shorter heartbeat (1hr vs 24hr)
if (wUSDMToUSDMPrice <= 0 || USDMToUSDPrice <= 0) return (roundId_, 0, startedAt_, updatedAt_, answeredInRound_);

int256 price = signed256(wUSDMToUSDMPrice) * USDMToUSDPrice * priceFeedScale / combinedScale;
return (roundId_, price, startedAt_, updatedAt_, answeredInRound_);
}

function signed256(uint256 n) internal pure returns (int256) {
if (n > uint256(type(int256).max)) revert InvalidInt256();
return int256(n);
}
}
7 changes: 7 additions & 0 deletions contracts/vendor/mountain/IMountainRateProvider.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IMountainRateProvider {
function decimals() external view returns (uint8);
function convertToAssets(uint256) external view returns (uint256);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { expect } from 'chai';
import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager';
import { migration } from '../../../../plugins/deployment_manager/Migration';
import { exp, proposal } from '../../../../src/deploy';
import { applyL1ToL2Alias, estimateL2Transaction } from '../../../../scenario/utils/arbitrumUtils';
import { ethers } from 'ethers';

const WUSDM_ADDRESS = '0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812';
const WUSDM_TO_USDM_PRICE_FEED_ADDRESS = '0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812';
const USDM_TO_USD_PRICE_FEED_ADDRESS = '0x24EA2671671c33D66e9854eC06e42E5D3ac1f764';

let newPriceFeedAddress: string;

export default migration('1727427904_add_wusdm_as_collateral', {
async prepare(deploymentManager: DeploymentManager) {
const _wUSDMPriceFeed = await deploymentManager.deploy(
'wUSDM:priceFeed',
'pricefeeds/WUSDMPriceFeed.sol',
[
WUSDM_TO_USDM_PRICE_FEED_ADDRESS, // wUSDM / USDM price feed
USDM_TO_USD_PRICE_FEED_ADDRESS, // USDM / USD price feed
8 // decimals
]
);
return { wUSDMPriceFeedAddress: _wUSDMPriceFeed.address };
},

enact: async (deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager, { wUSDMPriceFeedAddress }) => {
const trace = deploymentManager.tracer();
const {
bridgeReceiver,
timelock: l2Timelock,
comet,
cometAdmin,
configurator
} = await deploymentManager.getContracts();

const {
arbitrumInbox,
timelock,
governor
} = await govDeploymentManager.getContracts();

newPriceFeedAddress = wUSDMPriceFeedAddress;

const wUSDM = await deploymentManager.existing(
'wUSDM',
WUSDM_ADDRESS,
'arbitrum',
'contracts/ERC20.sol:ERC20'
);

const wUSDMPriceFeed = await deploymentManager.existing(
'wUSDM:priceFeed',
wUSDMPriceFeedAddress,
'arbitrum'
);

const wUSDMAssetConfig = {
asset: wUSDM.address,
priceFeed: wUSDMPriceFeed.address,
decimals: 18n,
borrowCollateralFactor: exp(0.88, 18),
liquidateCollateralFactor: exp(0.90, 18),
liquidationFactor: exp(0.95, 18),
supplyCap: exp(4_500_000, 18),
};

const addAssetCalldata = ethers.utils.defaultAbiCoder.encode(
['address', 'tuple(address,address,uint8,uint64,uint64,uint64,uint128)'],
[comet.address,
[
wUSDMAssetConfig.asset,
wUSDMAssetConfig.priceFeed,
wUSDMAssetConfig.decimals,
wUSDMAssetConfig.borrowCollateralFactor,
wUSDMAssetConfig.liquidateCollateralFactor,
wUSDMAssetConfig.liquidationFactor,
wUSDMAssetConfig.supplyCap
]
]
);

const deployAndUpgradeToCalldata = ethers.utils.defaultAbiCoder.encode(
['address', 'address'],
[configurator.address, comet.address]
);

const l2ProposalData = ethers.utils.defaultAbiCoder.encode(
['address[]', 'uint256[]', 'string[]', 'bytes[]'],
[
[
configurator.address,
cometAdmin.address
],
[
0,
0
],
[
'addAsset(address,(address,address,uint8,uint64,uint64,uint64,uint128))',
'deployAndUpgradeTo(address,address)',
],
[
addAssetCalldata,
deployAndUpgradeToCalldata,
]
]
);

const createRetryableTicketGasParams = await estimateL2Transaction(
{
from: applyL1ToL2Alias(timelock.address),
to: bridgeReceiver.address,
data: l2ProposalData
},
deploymentManager
);
const refundAddress = l2Timelock.address;

const mainnetActions = [
// 1. Set Comet configuration and deployAndUpgradeTo USDC Comet on Arbitrum.
{
contract: arbitrumInbox,
signature: 'createRetryableTicket(address,uint256,uint256,address,address,uint256,uint256,bytes)',
args: [
bridgeReceiver.address, // address to,
0, // uint256 l2CallValue,
createRetryableTicketGasParams.maxSubmissionCost, // uint256 maxSubmissionCost,
refundAddress, // address excessFeeRefundAddress,
refundAddress, // address callValueRefundAddress,
createRetryableTicketGasParams.gasLimit, // uint256 gasLimit,
createRetryableTicketGasParams.maxFeePerGas, // uint256 maxFeePerGas,
l2ProposalData, // bytes calldata data
],
value: createRetryableTicketGasParams.deposit
},
];

const description = '# Add wUSDM as collateral into cUSDCv3 on Arbitrum\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes to add wUSDM into cUSDCv3 on Arbitrum network. This proposal takes the governance steps recommended and necessary to update a Compound III USDC market on Arbitrum. Simulations have confirmed the market’s readiness, as much as possible, using the [Comet scenario suite](https://github.com/compound-finance/comet/tree/main/scenario). The new parameters include setting the risk parameters based off of the [recommendations from Gauntlet](https://www.comp.xyz/t/list-wusdm-as-a-collateral-on-usdc-usdt-markets-on-arbitrum-and-ethereum/5590/3).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/931) and [forum discussion](https://www.comp.xyz/t/list-wusdm-as-a-collateral-on-usdc-usdt-markets-on-arbitrum-and-ethereum/5590/3).\n\n\n## Proposal Actions\n\nThe first proposal action adds wUSDM to the USDC Comet on Arbitrum. This sends the encoded `addAsset` and `deployAndUpgradeTo` calls across the bridge to the governance receiver on Arbitrum.';
const txn = await govDeploymentManager.retry(async () =>
trace(await governor.propose(...(await proposal(mainnetActions, description))))
);

const event = txn.events.find(event => event.event === 'ProposalCreated');

const [proposalId] = event.args;

trace(`Created proposal ${proposalId}.`);
},

async enacted(): Promise<boolean> {
return false;
},

async verify(deploymentManager: DeploymentManager) {
const { comet, configurator } = await deploymentManager.getContracts();

const wUSDMAssetIndex = Number(await comet.numAssets()) - 1;

const wUSDM = await deploymentManager.existing(
'wUSDM',
WUSDM_ADDRESS,
'arbitrum',
'contracts/ERC20.sol:ERC20'
);

const wUSDMAssetConfig = {
asset: wUSDM.address,
priceFeed: newPriceFeedAddress,
decimals: 18n,
borrowCollateralFactor: exp(0.88, 18),
liquidateCollateralFactor: exp(0.90, 18),
liquidationFactor: exp(0.95, 18),
supplyCap: exp(4_500_000, 18),
};

// 1. & 2. Compare wUSDM asset config with Comet and Configurator asset info
const cometWUSDMAssetInfo = await comet.getAssetInfoByAddress(WUSDM_ADDRESS);
expect(wUSDMAssetIndex).to.be.equal(cometWUSDMAssetInfo.offset);
expect(wUSDMAssetConfig.asset).to.be.equal(cometWUSDMAssetInfo.asset);
expect(exp(1, wUSDMAssetConfig.decimals)).to.be.equal(cometWUSDMAssetInfo.scale);
expect(wUSDMAssetConfig.borrowCollateralFactor).to.be.equal(cometWUSDMAssetInfo.borrowCollateralFactor);
expect(wUSDMAssetConfig.liquidateCollateralFactor).to.be.equal(cometWUSDMAssetInfo.liquidateCollateralFactor);
expect(wUSDMAssetConfig.liquidationFactor).to.be.equal(cometWUSDMAssetInfo.liquidationFactor);
expect(wUSDMAssetConfig.supplyCap).to.be.equal(cometWUSDMAssetInfo.supplyCap);

const configuratorWUSDMAssetConfig = (await configurator.getConfiguration(comet.address)).assetConfigs[wUSDMAssetIndex];
expect(wUSDMAssetConfig.asset).to.be.equal(configuratorWUSDMAssetConfig.asset);
expect(wUSDMAssetConfig.decimals).to.be.equal(configuratorWUSDMAssetConfig.decimals);
expect(wUSDMAssetConfig.borrowCollateralFactor).to.be.equal(configuratorWUSDMAssetConfig.borrowCollateralFactor);
expect(wUSDMAssetConfig.liquidateCollateralFactor).to.be.equal(configuratorWUSDMAssetConfig.liquidateCollateralFactor);
expect(wUSDMAssetConfig.liquidationFactor).to.be.equal(configuratorWUSDMAssetConfig.liquidationFactor);
expect(wUSDMAssetConfig.supplyCap).to.be.equal(configuratorWUSDMAssetConfig.supplyCap);
},
});
8 changes: 8 additions & 0 deletions deployments/arbitrum/usdc/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,12 @@ export default {
}
}
},
ERC1967Proxy: {
artifact: 'contracts/ERC20.sol:ERC20',
delegates: {
field: {
slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
}
}
},
};
3 changes: 2 additions & 1 deletion src/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ export const WHALES = {
'0xee3273f6d29ddfff08ffd9d513cff314734f01a2', // COMP whale
'0x9e786a8fc88ee74b758b125071d45853356024c3', // COMP whale
'0xd93f76944e870900779c09ddf1c46275f9d8bf9b', // COMP whale
'0xe68ee8a12c611fd043fb05d65e1548dc1383f2b9' // native USDC whale
'0xe68ee8a12c611fd043fb05d65e1548dc1383f2b9', // native USDC whale
'0x56CC5A9c0788e674f17F7555dC8D3e2F1C0313C0', // wUSDM whale
],
base: [
'0x6D3c5a4a7aC4B1428368310E4EC3bB1350d01455', // USDbC whale
Expand Down
Loading