Skip to content
Merged
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
31 changes: 31 additions & 0 deletions docs/MIP-R35.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# MIP-R35: Moonriver Collateral Factor Changes

### **Summary**
This proposal suggests changing the collateral factors on the Moonriver markets further to continue the current market wind-down. For those unfamiliar, Moonriver price feeds will be deprecated in early January, and as a result, Moonwell is winding down these markets to prevent a possible issue in the future. Recently, collateral factor, reward variables, and supply and borrow caps were already modified to start the wind down process, this is the next reasonable conclusion.

The following markets will have their collateral factors modified: xcKSM, MOVR, and FRAX. This proposal details the specific percentage changes to these variables.

### **Background**
A few weeks ago, the Moonwell community received knowledge that Chainlink is deprecating its price feeds across the Moonriver network. Out of concern for Moonwell's users, we believe the best course of actions is to slowly deprecate the respective markets affected on Moonwell's Moonriver implementation: xcKSM, FRAX, and MOVR.

### **Proposal**

If this proposal is successful, the following markets will have their current collateral factors changed to the proposed collateral factors:

| Market | Current Collateral Factor | Proposed Collateral Factor |
|--------|----------------------------|-----------------------------|
| xcKSM | 50% | 25% |
| MOVR | 50% | 25% |
| FRAX | 40% | 25% |

These recommendations were provided by Anthias Labs in their forum post: [Moonriver Chain Sunset](https://forum.moonwell.fi/t/moonriver-chain-sunset/2042)

### **Rationale**

Lowering the collateral factors on Moonriver assets is a necessary and responsible step to ensure a safe wind-down of the affected markets ahead of Chainlink's price feed deprecation. Once these feeds go offline, Moonwell would no longer be able to reliably determine asset values on Moonriver, creating a material risk of under-collateralized borrowing, inaccurate liquidations, and potential bad debt.

### **Voting Options**

- **For:** Approve collateral factor changes to the Moonriver markets.
- **Against:** Do not make collateral factor changes to the Moonriver markets.
- **Abstain:** No preference.
33 changes: 33 additions & 0 deletions docs/MIP-R37.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# MIP-R37: Moonriver Progressive Factor Reductions

### **Summary**
This proposal recommends reducing collateral factors on Moonriver markets from 15% to 7.5% as part of the ongoing wind-down of Moonriver lending activity. This will affect xcKSM, MOVR, and FRAX markets respectively.

Chainlink has announced plans to deprecate oracle feeds on Moonriver, materially increasing oracle risk for remaining positions. In advance of this deadline, Anthias Labs has recommended continuing to lower collateral factors toward zero to reduce protocol exposure and encourage orderly position closure.

This proposal represents the next step in that process.

### **Background**
A few weeks ago, the Moonwell community received knowledge that Chainlink is deprecating its price feeds across the Moonriver network. Out of concern for Moonwell's users, we believe the best course of actions is to slowly deprecate the respective markets affected on Moonwell's Moonriver implementation: xcKSM, FRAX, and MOVR.

### **Proposal**

If this proposal is successful, the following markets will have their current collateral factors changed to the proposed collateral factors:

| Market | Current Collateral Factor | Proposed Collateral Factor |
|--------|---------------------------|----------------------------|
| xcKSM | 15% | 7.5% |
| MOVR | 15% | 7.5% |
| FRAX | 14% | 7.5% |

These recommendations were provided by Anthias Labs in their forum post: [Anthias Recommendations](https://forum.moonwell.fi/t/anthias-labs-risk-parameter-recommendations/1759/9)

### **Rationale**

Lowering the collateral factors on Moonriver assets is a necessary and responsible step to ensure a safe wind-down of the affected markets ahead of Chainlink price feed deprecation. Once these feeds go offline, Moonwell would no longer be able to reliably determine asset values on Moonriver.

### **Voting Options**

- **For:** Approve collateral factor changes to the Moonriver markets.
- **Against:** Do not make collateral factor changes to the Moonriver markets.
- **Abstain:** No preference.
27 changes: 27 additions & 0 deletions test/apollo/mip-r35/assertCurrentExpectedState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {ethers} from "ethers";
import {ContractBundle} from "@moonwell-fi/moonwell.js";
import {STARTING_COLLATERAL_FACTORS} from "./vars";
import BigNumber from "bignumber.js";

export async function assertCurrentExpectedState(contracts: ContractBundle, provider: ethers.providers.JsonRpcProvider){
console.log("[+] Asserting collateral factors are in expected state BEFORE gov proposal passes")

const comptroller = contracts.COMPTROLLER.contract.connect(provider)

// Assert that the markets have the expected collateral factors before the proposal
for (const [ticker, expectedCF] of Object.entries(STARTING_COLLATERAL_FACTORS)) {
const market = contracts.MARKETS[ticker]
if (market) {
const marketData = await comptroller.markets(market.mTokenAddress)
const cfMantissa = new BigNumber(marketData.collateralFactorMantissa.toString())
const cfPercent = cfMantissa.div(1e18).times(100).toNumber()

if (Math.abs(cfPercent - expectedCF) > 0.01) {
throw new Error(`${ticker} CF expected ${expectedCF}% but found ${cfPercent}%`)
}
console.log(` ✅ ${ticker} Collateral Factor is ${cfPercent}%`)
}
}

console.log("[+] ✅ All collateral factors are in expected state before proposal")
}
18 changes: 18 additions & 0 deletions test/apollo/mip-r35/assertExpectedEndState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {ethers} from "ethers";
import {assertMarketCFEqualsPercent} from "../../../src";
import {ContractBundle} from "@moonwell-fi/moonwell.js";
import {COLLATERAL_FACTOR_CHANGES} from "./vars";

export async function assertExpectedEndState(contracts: ContractBundle, provider: ethers.providers.JsonRpcProvider){
console.log("[+] Asserting collateral factors are updated AFTER gov proposal passed")

// Assert that the markets have updated collateral factors after the proposal
for (const [ticker, expectedCF] of Object.entries(COLLATERAL_FACTOR_CHANGES)) {
const market = contracts.MARKETS[ticker]
if (market) {
await assertMarketCFEqualsPercent(provider, contracts, market, expectedCF)
}
}

console.log("[+] ✅ All collateral factors are updated after proposal")
}
47 changes: 47 additions & 0 deletions test/apollo/mip-r35/generateProposalData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {ethers} from "ethers";
import {BigNumber as EthersBigNumber} from "@ethersproject/bignumber/lib/bignumber";
import {addProposalToPropData, ProposalData} from "../../../src";
import {ContractBundle} from "@moonwell-fi/moonwell.js";
import {COLLATERAL_FACTOR_CHANGES} from "./vars";
import BigNumber from "bignumber.js";

function cfPercentToMantissa(newCFPercent: number){
return EthersBigNumber.from(
new BigNumber(newCFPercent)
.div(100)
.times(new BigNumber('1e18'))
.toFixed(0)
)
}

export async function generateProposalData(contracts: ContractBundle, provider: ethers.providers.JsonRpcProvider){
const comptroller = contracts.COMPTROLLER.contract.connect(provider)

const proposalData: ProposalData = {
targets: [],
values: [],
signatures: [],
callDatas: [],
}

// Add collateral factor changes for each market
const cfOrder = ['xcKSM', 'MOVR', 'FRAX'] // Fixed order
for (const marketTicker of cfOrder) {
const newCF = COLLATERAL_FACTOR_CHANGES[marketTicker]
if (newCF !== undefined) {
const market = contracts.MARKETS[marketTicker]
if (market) {
console.log(` Adding CF change for ${marketTicker}: ${newCF}% (mToken: ${market.mTokenAddress})`)
await addProposalToPropData(comptroller, '_setCollateralFactor',
[
market.mTokenAddress,
cfPercentToMantissa(newCF)
],
proposalData
)
}
}
}

return proposalData
}
78 changes: 78 additions & 0 deletions test/apollo/mip-r35/getCalldata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {ethers} from 'ethers'
import {Contracts} from '@moonwell-fi/moonwell.js'
import {generateProposalData} from "./generateProposalData"
import {RPC_URL, STARTING_COLLATERAL_FACTORS, COLLATERAL_FACTOR_CHANGES} from "./vars"
import * as fs from 'fs'
import * as path from 'path'

async function main() {
const contracts = Contracts.moonriver
const provider = new ethers.providers.JsonRpcProvider(RPC_URL)

console.log('\n===========================================')
console.log('MIP-R35: Moonriver Collateral Factor Changes')
console.log('===========================================\n')

console.log('Governor Address:', contracts.GOVERNOR.address)
console.log('Expected:', '0x2BE2e230e89c59c8E20E633C524AD2De246e7370')
console.log('Match:', contracts.GOVERNOR.address === '0x2BE2e230e89c59c8E20E633C524AD2De246e7370' ? '✅' : '❌')
console.log()

console.log('Generating proposal data...\n')
const proposalData = await generateProposalData(contracts, provider)

// Read the full proposal description from the markdown file
const markdownPath = path.join(__dirname, '../../../docs/MIP-R35.md')
const description = fs.readFileSync(markdownPath, 'utf-8')

console.log('✅ Loaded full markdown proposal from docs/MIP-R35.md')
console.log(`Description length: ${description.length} characters\n`)

console.log('=== PROPOSAL DESCRIPTION ===\n')
console.log(description)
console.log('\n=== END DESCRIPTION ===\n')

console.log('=== PROPOSAL DATA ===\n')
console.log(JSON.stringify(proposalData, null, 2))
console.log('\n=== SUMMARY ===')
console.log(`Total actions: ${proposalData.targets.length}`)
console.log(`Target (Comptroller): ${proposalData.targets[0]}`)
console.log('\nActions:')
const cfMarkets = ['xcKSM', 'MOVR', 'FRAX']
proposalData.signatures.forEach((sig, i) => {
console.log(` ${i + 1}. ${sig} - ${cfMarkets[i]} CF`)
})
console.log('\nCollateral Factor Changes:')
for (const market of cfMarkets) {
const oldCF = STARTING_COLLATERAL_FACTORS[market]
const newCF = COLLATERAL_FACTOR_CHANGES[market]
console.log(` - ${market}: ${oldCF}% → ${newCF}%`)
}

// Encode the propose function call
console.log('\n===========================================')
console.log('ENCODED PROPOSE CALLDATA')
console.log('===========================================\n')

const governor = contracts.GOVERNOR.contract.connect(provider)
const proposeCalldata = await governor.populateTransaction.propose(
proposalData.targets,
proposalData.values,
proposalData.signatures,
proposalData.callDatas,
description
)

console.log('To:', contracts.GOVERNOR.address)
console.log('Data:', proposeCalldata.data)
console.log('\nCalldata Length:', proposeCalldata.data!.length, 'characters')
console.log('\nYou can submit this transaction to the Governor contract at:')
console.log('https://moonriver.moonscan.io/address/' + contracts.GOVERNOR.address + '#writeContract')
}

main()
.then(() => process.exit(0))
.catch(error => {
console.error(error)
process.exit(1)
})
113 changes: 113 additions & 0 deletions test/apollo/mip-r35/mip-r35-verification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {ethers, BigNumber as EthersBigNumber} from 'ethers'
import {
passGovProposal,
setupDeployerAndEnvForGovernance,
sleep,
startGanache,
replaceXCAssetWithDummyERC20
} from "../../../src";

import {Contracts} from '@moonwell-fi/moonwell.js'
import {generateProposalData} from "./generateProposalData";
import {assertCurrentExpectedState} from "./assertCurrentExpectedState";
import {assertExpectedEndState} from "./assertExpectedEndState";
import {F_MOVR_GRANT, FORK_BLOCK, RPC_URL, COLLATERAL_FACTOR_CHANGES} from "./vars";

// Helper function to set MFAM balance directly using storage manipulation
async function setMFAMBalance(provider: ethers.providers.JsonRpcProvider, address: string, amount: EthersBigNumber) {
const govTokenAddress = Contracts.moonriver.GOV_TOKEN.address

// MFAM uses mapping(address => uint96) balances at slot 1 (not slot 0!)
// Storage slot = keccak256(abi.encode(address, slot))
const paddedAddress = ethers.utils.hexZeroPad(address, 32)
const paddedSlot = ethers.utils.hexZeroPad('0x01', 32)
const slot = ethers.utils.keccak256(paddedAddress + paddedSlot.slice(2))

// Set the balance - MFAM uses uint96, but we can set the full word
await provider.send('evm_setAccountStorageAt', [
govTokenAddress,
slot,
ethers.utils.hexZeroPad(amount.toHexString(), 32)
])
}

test("mip-r35-verification", async () => {
console.log("\n===========================================")
console.log("MIP-R35: Moonriver Collateral Factor Changes")
console.log("===========================================\n")
console.log("Summary: This proposal reduces collateral factors")
console.log("for all 3 Moonriver markets as part of the wind-down process.")
console.log("Collateral factors: xcKSM=25%, MOVR=25%, FRAX=25%\n")

const contracts = Contracts.moonriver

const forkedChainProcess = await startGanache(contracts,
FORK_BLOCK,
RPC_URL,
[F_MOVR_GRANT]
)

console.log("Waiting 5 seconds for chain to bootstrap...")
await sleep(5)

try {
const provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545')

// At recent blocks, F_MOVR_GRANT is empty. Inject MFAM tokens directly.
const quorumAmount = EthersBigNumber.from('50000000').mul(EthersBigNumber.from(10).pow(18)) // 50M MFAM
await setMFAMBalance(provider, F_MOVR_GRANT, quorumAmount)
console.log("[+] Injected 50M MFAM into F_MOVR_GRANT for governance testing")

// Mock xcKSM XC-20 precompile with a dummy ERC20
// Ganache doesn't properly handle XC-20 precompiles, causing symbol() to fail
await replaceXCAssetWithDummyERC20(
provider,
contracts.MARKETS['FRAX'], // Clone from FRAX (regular ERC20)
contracts.MARKETS['xcKSM'] // Replace xcKSM XC-20 precompile
)
// Set the symbol storage slot to "xcKSM" (slot 4 for OpenZeppelin ERC20)
const symbolBytes = ethers.utils.formatBytes32String('xcKSM')
const symbolWithLength = EthersBigNumber.from(symbolBytes).add('xcKSM'.length * 2).toHexString()
await provider.send('evm_setAccountStorageAt', [
contracts.MARKETS['xcKSM'].tokenAddress,
ethers.utils.hexZeroPad('0x04', 32), // slot 4 = _symbol
ethers.utils.hexZeroPad(symbolWithLength, 32)
])
console.log("[+] Mocked xcKSM XC-20 precompile with dummy ERC20 for Ganache testing")

// Note: Using current on-chain collateral factors (pre-MIP-R33)
// xcKSM=59%, MOVR=60%, FRAX=50%

await setupDeployerAndEnvForGovernance(
contracts,
provider,
F_MOVR_GRANT,
FORK_BLOCK,
5_000_000 // Quorum adjusts up just short of this amount when the proposal is submitted
)

await assertCurrentExpectedState(contracts, provider)

// Generate new proposal data
const proposalData = await generateProposalData(contracts, provider)

console.log("\n[+] Proposal Summary:")
console.log(` - Total actions: ${proposalData.targets.length}`)
console.log(` - Markets: xcKSM, MOVR, FRAX`)
console.log(` - Collateral factors: xcKSM=25%, MOVR=25%, FRAX=25%\n`)

// Pass the proposal
await passGovProposal(contracts, provider, proposalData)

// Assert that our end state is as desired
await assertExpectedEndState(contracts, provider)

console.log("\n✅ MIP-R35 verification complete!")
console.log("Collateral factors reduced on all 3 Moonriver markets\n")
} finally {
// Kill our child chain.
console.log("Shutting down Ganache chain. PID", forkedChainProcess.pid!)
process.kill(-forkedChainProcess.pid!)
console.log("Ganache chain stopped.")
}
});
26 changes: 26 additions & 0 deletions test/apollo/mip-r35/vars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export * from '../base-vars'

// Fork block - use a recent block after MIP-R33 has passed
// MIP-R33 set CF to: xcKSM=50%, MOVR=50%, FRAX=40%
// We need to use a block after that proposal executed
export const FORK_BLOCK = 14_090_000

// RPC URL - use environment variable or fallback to public RPC
export const RPC_URL = process.env.MOONRIVER_RPC_URL || 'https://rpc.api.moonriver.moonbeam.network'

// Starting collateral factors (current on-chain values at block 14,090,000)
// Note: MIP-R33 will change these to xcKSM=50%, MOVR=50%, FRAX=40%
// But we test from current state for now
export const STARTING_COLLATERAL_FACTORS: { [market: string]: number } = {
'xcKSM': 59, // Current on-chain value
'MOVR': 60, // Current on-chain value
'FRAX': 50, // Current on-chain value
}

// Collateral factor changes - new collateral factors for each market
// Target values: xcKSM=25%, MOVR=25%, FRAX=25%
export const COLLATERAL_FACTOR_CHANGES: { [market: string]: number } = {
'xcKSM': 25, // 25% (down from 50%)
'MOVR': 25, // 25% (down from 50%)
'FRAX': 25, // 25% (down from 40%)
}
Loading
Loading