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

Market Reserve Automation #456

Open
wants to merge 53 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
8231339
init: market automation
ElliotFriedman Jan 8, 2025
25df713
remove: reserve registry
ElliotFriedman Jan 9, 2025
b19b3ac
add: auction delay and guardian
ElliotFriedman Jan 9, 2025
666b48d
test: auction delay and guardian
ElliotFriedman Jan 9, 2025
8311c90
remove unneeded check from constructor
ElliotFriedman Jan 9, 2025
8abac49
Merge branch 'main' of github.com:moonwell-fi/moonwell-contracts-v2 i…
ElliotFriedman Jan 9, 2025
485cf8a
remove unused import
ElliotFriedman Jan 9, 2025
f241b98
end -> start time natspec
ElliotFriedman Jan 9, 2025
148d6e2
naming: clarify variable and function names
ElliotFriedman Jan 9, 2025
b3b4db0
add: erc20 holding deposit integration test
ElliotFriedman Jan 9, 2025
7671520
add: purchase failure tests + erc20 holding deposit tests
ElliotFriedman Jan 9, 2025
27bbc5d
add: reserve automation integration tests
ElliotFriedman Jan 9, 2025
162c355
add: reserve automation deploy script
ElliotFriedman Jan 10, 2025
bf2fd20
refactor init params
ElliotFriedman Jan 10, 2025
6e33739
refactor constructor params, add additional checks on deployment
ElliotFriedman Jan 10, 2025
310fb6b
refactor: make validate and deploy function external to ease testing
ElliotFriedman Jan 10, 2025
7498cdb
make setup virtual for test
ElliotFriedman Jan 10, 2025
fd39fb2
add tests for newly deployed reserve automation contracts directly fr…
ElliotFriedman Jan 10, 2025
e51aed2
fix: change buffer updated time based on amount of buffer taken
ElliotFriedman Jan 11, 2025
9ca8c21
add new integration test
ElliotFriedman Jan 11, 2025
408375d
give mock erc20 token functionality
ElliotFriedman Jan 11, 2025
56be0b6
fix error message
ElliotFriedman Jan 11, 2025
b1f3034
fix test issues
ElliotFriedman Jan 11, 2025
6ecb3f6
increment timestamp to fix accrue interest error
ElliotFriedman Jan 11, 2025
0f56033
increase wait time accruing interest
ElliotFriedman Jan 12, 2025
3f2bf19
add new tests, make testSetup view
ElliotFriedman Jan 14, 2025
06bdea2
remove sale start time check, allow guardian to cancel many times
ElliotFriedman Jan 15, 2025
0105662
checkpoint: add premium and discount logic + other changes like cachi…
ElliotFriedman Jan 16, 2025
8ed09c0
add _addReserves function
ElliotFriedman Jan 16, 2025
4dc46f4
add fuzz test around discount application throughout period
ElliotFriedman Jan 16, 2025
de5f26b
fix discount, and start and end timestamps to be inclusive
ElliotFriedman Jan 17, 2025
376098d
fmt
ElliotFriedman Jan 17, 2025
61d64b2
fix failing tests
ElliotFriedman Jan 17, 2025
baa073f
uncomment code, fix deployment
ElliotFriedman Jan 18, 2025
671026c
add period start and end tests as well as chainlink price caching
ElliotFriedman Jan 18, 2025
635955c
check mToken and reserve asset match
ElliotFriedman Jan 18, 2025
23d4861
organize import
ElliotFriedman Jan 18, 2025
f85ea52
cancel auction sends all funds back to mToken by adding reserves
ElliotFriedman Jan 18, 2025
0eb75db
add tests around start and end time bounds, as well as moving through…
ElliotFriedman Jan 18, 2025
81807c0
add price caching test
ElliotFriedman Jan 18, 2025
6865e3c
Merge branch 'main' of github.com:moonwell-fi/moonwell-contracts-v2 i…
ElliotFriedman Jan 18, 2025
0e0e65d
fix underlying mismatch
ElliotFriedman Jan 18, 2025
f5c8f84
add MWethDelegatorOwner
ElliotFriedman Jan 19, 2025
693779e
address feedback, make internal function private, name mapping variables
ElliotFriedman Jan 21, 2025
2057280
Merge pull request #466 from moonwell-fi/feat/mweth-owner
ElliotFriedman Jan 22, 2025
40f3d94
address pr feedback
ElliotFriedman Jan 25, 2025
84a8a1e
Merge branch 'main' into feat/market-reserve-automation
ElliotFriedman Jan 27, 2025
2f39550
update saleinitiated function
ElliotFriedman Jan 27, 2025
1a12311
test final second of final period
ElliotFriedman Jan 27, 2025
dd6ac45
Merge branch 'feat/market-reserve-automation' of github.com:moonwell-…
ElliotFriedman Jan 27, 2025
605e4c1
update ci run
ElliotFriedman Jan 28, 2025
f4bc3ec
rename addresses -> _addresses
ElliotFriedman Jan 28, 2025
d40879e
make post proposal integration test, add _testUpperLowerBoundsPremium…
ElliotFriedman Jan 28, 2025
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
24 changes: 24 additions & 0 deletions .github/workflows/base-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,30 @@ jobs:
max_attempts: 3
command: time forge test --match-contract LiveSystem -vvv --ffi

reserve-automation-integration-tests:
name: Reserve Automation Integration Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: recursive

- name: Setup Environment
uses: ./.github/actions

- name: Give write permissions
run: chmod -R +x proposals/mips/

- name: Reserve Automation Integration Tests
uses: nick-fields/retry@v3
with:
polling_interval_seconds: 30
retry_wait_seconds: 60
timeout_minutes: 50
max_attempts: 3
command: time forge test --match-contract 'ReserveAutomationLiveIntegrationTest|ERC20HoldingDepositLiveIntegrationTest' -vvv --fork-url base

fee-splitter-xwell-integration-tests:
name: Fee Splitter/xWell Integration Test
runs-on: ubuntu-latest
Expand Down
14 changes: 14 additions & 0 deletions proposals/mips/mip-reserve-automation/8453.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
"MOONWELL_USDC",
"MOONWELL_USDBC",
"MOONWELL_DAI",
"MOONWELL_WETH",
"MOONWELL_cbETH",
"MOONWELL_wstETH",
"MOONWELL_rETH",
"MOONWELL_AERO",
"MOONWELL_weETH",
"MOONWELL_cbBTC",
"MOONWELL_EURC",
"MOONWELL_wrsETH"
]
285 changes: 285 additions & 0 deletions proposals/mips/mip-reserve-automation/reserveAutomationDeploy.sol
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on the current repo folder structure I believe it's makes more sense to keep this file inside /script and rename to DeployReserveAutomation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chainink oracle, market and underlying address names could be placed in a json config file so we could easily deploy the reserve automation to any chain

Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.19;

import {ITransparentUpgradeableProxy} from "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ProxyAdmin} from "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol";
import {Script} from "@forge-std/Script.sol";
import {Test} from "@forge-std/Test.sol";
import {console} from "@forge-std/console.sol";

import "@protocol/utils/ChainIds.sol";

import {AutomationDeploy} from "@protocol/market/AutomationDeploy.sol";
import {ReserveAutomation} from "@protocol/market/ReserveAutomation.sol";
import {ERC20HoldingDeposit} from "@protocol/market/ERC20HoldingDeposit.sol";
import {ChainIds, BASE_FORK_ID} from "@utils/ChainIds.sol";
import {AllChainAddresses as Addresses} from "@proposals/Addresses.sol";

/// how to run locally:
/// forge script proposals/mips/mip-reserve-automation/reserveAutomationDeploy.sol:ReserveAutomationDeploy --rpc-url base
contract ReserveAutomationDeploy is Script, Test {
using ChainIds for uint256;

/// @notice the name of the proposal
string public constant NAME = "Reserve Automation Deployment";

AutomationDeploy private _deployer;
Addresses internal _addresses;

function setUp() public virtual {
_addresses = new Addresses();
}

/// @notice array of mToken names to deploy automation for
function _getMTokens(
uint256 chainId
) internal view returns (string[] memory) {
string memory file = vm.readFile(
string.concat(
"./proposals/mips/mip-reserve-automation/",
string.concat(vm.toString(chainId), ".json")
)
);
string[] memory mTokens = abi.decode(vm.parseJson(file), (string[]));
return mTokens;
}

function run() public {
vm.startBroadcast();

deploy(_addresses);

vm.stopBroadcast();

_addresses.printAddresses();

validate(_addresses);
}

function deploy(Addresses addresses) public {
address temporalGov = addresses.getAddress("TEMPORAL_GOVERNOR");
address pauseGuardian = addresses.getAddress("PAUSE_GUARDIAN");
address xWellProxy = addresses.getAddress("xWELL_PROXY");

_deployer = new AutomationDeploy();

/// Deploy ERC20HoldingDeposit for xWELL
address holdingDeposit = _deployer.deployERC20HoldingDeposit(
xWellProxy,
temporalGov
);

addresses.addAddress("RESERVE_WELL_HOLDING_DEPOSIT", holdingDeposit);

/// Deploy ReserveAutomation for each mToken
string[] memory mTokens = _getMTokens(block.chainid);
for (uint256 i = 0; i < mTokens.length; i++) {
string memory mTokenName = mTokens[i];
string memory underlyingName = _getUnderlyingName(mTokenName);
string memory oracleName = _getOracleName(underlyingName);
string memory reserveAutomationIdentifier = string.concat(
"RESERVE_AUTOMATION_",
_stripMoonwellPrefix(mTokenName)
);

/// avoid redeploying a contract that already exists
if (!addresses.isAddressSet(reserveAutomationIdentifier)) {
ReserveAutomation.InitParams memory params = ReserveAutomation
.InitParams({
recipientAddress: holdingDeposit,
wellToken: xWellProxy,
reserveAsset: addresses.getAddress(underlyingName),
wellChainlinkFeed: addresses.getAddress(
"CHAINLINK_WELL_USD"
),
reserveChainlinkFeed: addresses.getAddress(oracleName),
owner: temporalGov,
mTokenMarket: addresses.getAddress(mTokenName),
guardian: pauseGuardian
});

address automation = _deployer.deployReserveAutomation(params);

addresses.addAddress(reserveAutomationIdentifier, automation);
}
}
}

function validate(Addresses addresses) public view {
address temporalGov = addresses.getAddress("TEMPORAL_GOVERNOR");
address pauseGuardian = addresses.getAddress("PAUSE_GUARDIAN");
address xWellProxy = addresses.getAddress("xWELL_PROXY");
address holdingDeposit = addresses.getAddress(
"RESERVE_WELL_HOLDING_DEPOSIT"
);

/// Validate ERC20HoldingDeposit
assertEq(
ERC20HoldingDeposit(holdingDeposit).token(),
xWellProxy,
"incorrect holding deposit token"
);
assertEq(
ERC20HoldingDeposit(holdingDeposit).owner(),
temporalGov,
"incorrect holding deposit owner"
);

/// Validate ReserveAutomation for each mToken
string[] memory mTokens = _getMTokens(block.chainid);
for (uint256 i = 0; i < mTokens.length; i++) {
string memory mTokenName = mTokens[i];
string memory underlyingName = _getUnderlyingName(mTokenName);
string memory oracleName = _getOracleName(underlyingName);

address automation = addresses.getAddress(
string.concat(
"RESERVE_AUTOMATION_",
_stripMoonwellPrefix(mTokenName)
)
);

ReserveAutomation reserve = ReserveAutomation(automation);

assertEq(
reserve.owner(),
temporalGov,
string.concat("incorrect owner for ", mTokenName)
);
assertEq(
reserve.guardian(),
pauseGuardian,
string.concat("incorrect guardian for ", mTokenName)
);
assertEq(
reserve.recipientAddress(),
holdingDeposit,
string.concat("incorrect recipient address for ", mTokenName)
);
assertEq(
reserve.wellToken(),
xWellProxy,
string.concat("incorrect well token for ", mTokenName)
);
assertEq(
reserve.reserveAsset(),
addresses.getAddress(underlyingName),
string.concat("incorrect reserve asset for ", mTokenName)
);
assertEq(
reserve.wellChainlinkFeed(),
addresses.getAddress("CHAINLINK_WELL_USD"),
string.concat("incorrect well chainlink feed for ", mTokenName)
);
assertEq(
reserve.reserveChainlinkFeed(),
addresses.getAddress(oracleName),
string.concat(
"incorrect reserve chainlink feed for ",
mTokenName
)
);
assertEq(
reserve.mTokenMarket(),
addresses.getAddress(mTokenName),
string.concat("incorrect mToken market for ", mTokenName)
);
}
}

/// @notice Helper function to get the underlying token name from an mToken name
/// @param mTokenName The name of the mToken
/// @return The name of the underlying token
function _getUnderlyingName(
string memory mTokenName
) internal pure returns (string memory) {
string memory token = _stripMoonwellPrefix(mTokenName);
if (keccak256(bytes(token)) == keccak256(bytes("USDBC"))) {
return "USDBC";
} else if (keccak256(bytes(token)) == keccak256(bytes("USDC"))) {
return "USDC";
} else if (keccak256(bytes(token)) == keccak256(bytes("DAI"))) {
return "DAI";
} else if (keccak256(bytes(token)) == keccak256(bytes("WETH"))) {
return "WETH";
} else if (keccak256(bytes(token)) == keccak256(bytes("cbETH"))) {
return "cbETH";
} else if (keccak256(bytes(token)) == keccak256(bytes("wstETH"))) {
return "wstETH";
} else if (keccak256(bytes(token)) == keccak256(bytes("rETH"))) {
return "rETH";
} else if (keccak256(bytes(token)) == keccak256(bytes("AERO"))) {
return "AERO";
} else if (keccak256(bytes(token)) == keccak256(bytes("weETH"))) {
return "weETH";
} else if (keccak256(bytes(token)) == keccak256(bytes("cbBTC"))) {
return "cbBTC";
} else if (keccak256(bytes(token)) == keccak256(bytes("EURC"))) {
return "EURC";
} else if (keccak256(bytes(token)) == keccak256(bytes("wrsETH"))) {
return "wrsETH";
} else if (keccak256(bytes(token)) == keccak256(bytes("WELL"))) {
return "xWELL_PROXY";
} else {
revert("unknown mToken");
}
}

/// @notice Helper function to get the oracle name for a token
/// @param tokenName The name of the token
/// @return The name of the oracle for the token
function _getOracleName(
string memory tokenName
) internal pure returns (string memory) {
if (
keccak256(bytes(tokenName)) == keccak256(bytes("USDBC")) ||
keccak256(bytes(tokenName)) == keccak256(bytes("USDC"))
) {
return "USDC_ORACLE";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("DAI"))) {
return "DAI_ORACLE";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("WETH"))) {
return "CHAINLINK_ETH_USD";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("cbETH"))) {
return "cbETH_ORACLE";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("wstETH"))) {
return "CHAINLINK_WSTETH_STETH_COMPOSITE_ORACLE";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("rETH"))) {
return "CHAINLINK_RETH_ETH_COMPOSITE_ORACLE";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("AERO"))) {
return "CHAINLINK_AERO_ORACLE";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("weETH"))) {
return "CHAINLINK_WEETH_ETH_COMPOSITE_ORACLE";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("cbBTC"))) {
return "CHAINLINK_BTC_USD";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("EURC"))) {
return "CHAINLINK_EURC_USD";
} else if (keccak256(bytes(tokenName)) == keccak256(bytes("wrsETH"))) {
return "CHAINLINK_wrsETH_COMPOSITE_ORACLE";
} else if (
keccak256(bytes(tokenName)) == keccak256(bytes("xWELL_PROXY"))
) {
return "CHAINLINK_WELL_USD";
} else {
revert("unknown token");
}
}

/// @notice Helper function to strip the MOONWELL_ prefix from a token name
/// @param mTokenName The name of the mToken
/// @return The token name without the MOONWELL_ prefix
function _stripMoonwellPrefix(
string memory mTokenName
) internal pure returns (string memory) {
bytes memory mTokenBytes = bytes(mTokenName);
bytes memory prefix = bytes("MOONWELL_");
require(mTokenBytes.length > prefix.length, "invalid mToken name");

bytes memory result = new bytes(mTokenBytes.length - prefix.length);
for (uint256 i = prefix.length; i < mTokenBytes.length; i++) {
result[i - prefix.length] = mTokenBytes[i];
}

return string(result);
}
}
48 changes: 48 additions & 0 deletions src/market/AutomationDeploy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
pragma solidity =0.8.19;

import {MErc20Storage} from "@protocol/MTokenInterfaces.sol";
import {ReserveAutomation} from "@protocol/market/ReserveAutomation.sol";
import {ERC20HoldingDeposit} from "@protocol/market/ERC20HoldingDeposit.sol";

contract AutomationDeploy {
function deployReserveAutomation(
ReserveAutomation.InitParams memory params
) public returns (address) {
require(params.wellToken.code.length > 0, "wellToken must be set");
require(params.reserveAsset.code.length > 0, "mToken must be set");
require(
params.wellChainlinkFeed.code.length > 0,
"wellChainlinkFeed must be set"
);
require(
params.reserveChainlinkFeed.code.length > 0,
"reserveChainlinkFeed must be set"
);
require(
params.recipientAddress.code.length > 0,
"recipientAddress must be set"
);
require(
params.mTokenMarket.code.length > 0,
"mTokenMarket must be set"
);
require(
MErc20Storage(params.mTokenMarket).underlying() ==
params.reserveAsset,
"reserveUnderlying must match mToken underlying"
);
require(params.owner != address(0), "owner must be set");
require(params.guardian != address(0), "guardian must be set");

ReserveAutomation automation = new ReserveAutomation(params);
return address(automation);
}

function deployERC20HoldingDeposit(
address token,
address owner
) public returns (address) {
require(token.code.length > 0, "token must be set");
anajuliabit marked this conversation as resolved.
Show resolved Hide resolved
return address(new ERC20HoldingDeposit(token, owner));
}
}
Loading
Loading