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
1 change: 1 addition & 0 deletions config/deploy/BaseConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ contract BaseConfig {
AggregatorV3Interface aUsdt;
AggregatorV3Interface aUsdc;
AggregatorV3Interface mNav;
AggregatorV3Interface cappedMNav;
}

Config public config;
Expand Down
3 changes: 2 additions & 1 deletion config/deploy/Mainnet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ contract Mainnet is BaseConfig {
ustb: AggregatorV3Interface(0x289B5036cd942e619E1Ee48670F98d214E745AAC),
aUsdt: AggregatorV3Interface(0x380adC857Cd3d0531C0821B5D52F34737C4eCDC4),
aUsdc: AggregatorV3Interface(0x95CCDE4C1bb3d56639d22185aa2f95EcfebD7F22),
mNav: AggregatorV3Interface(0xC28198Df9aee1c4990994B35ff51eFA4C769e534)
mNav: AggregatorV3Interface(0xC28198Df9aee1c4990994B35ff51eFA4C769e534),
cappedMNav: AggregatorV3Interface(0x0000000000000000000000000000000000000000)
}),
users: Users({
admin: 0x343ACce723339D5A417411D8Ff57fde8886E91dc,
Expand Down
3 changes: 2 additions & 1 deletion config/deploy/Sepolia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ contract Sepolia is BaseConfig {
ustb: AggregatorV3Interface(0x732d3C7515356eAB22E3F3DcA183c5c65102d518),
aUsdc: AggregatorV3Interface(address(0)),
aUsdt: AggregatorV3Interface(address(0)),
mNav: AggregatorV3Interface(address(0))
mNav: AggregatorV3Interface(address(0)),
cappedMNav: AggregatorV3Interface(address(0))
}),
users: Users({
admin: 0xb2522DC238DEA8a821dEcE38a1d46eC5C4708256,
Expand Down
5 changes: 5 additions & 0 deletions script/v2/usd/DeploySwapManager.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ contract DeploySwapManager is Configurable, DeploymentUtils, Script {
})
);

config.levelContracts.swapManager.addOracle(address(config.tokens.usdc), address(config.oracles.usdc));
config.levelContracts.swapManager.addOracle(address(config.tokens.wrappedM), address(config.oracles.cappedMNav));
config.levelContracts.swapManager.setHeartBeat(address(config.tokens.usdc), 1 days);
config.levelContracts.swapManager.setHeartBeat(address(config.tokens.wrappedM), 26 hours);

// Transfer ownership to admin timelock
config.levelContracts.swapManager.transferOwnership(address(config.levelContracts.adminTimelock));

Expand Down
8 changes: 4 additions & 4 deletions src/v2/common/libraries/VaultLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ library VaultLib {

emit DepositToM0(address(vault), baseCollateral, amount, deposited);

return deposited;
return amount;
}

/// @notice Withdraws assets from M
Expand Down Expand Up @@ -584,7 +584,7 @@ library VaultLib {

emit StakeToAaveUmbrella(address(vault), address(_config.baseCollateral), amount, staked_);

return staked_;
return amount;
}

/// @notice Unstakes waTokens from Aave Umbrella
Expand Down Expand Up @@ -629,14 +629,14 @@ library VaultLib {
bytes memory tokensRaw = vault.manage(
address(stataToken),
abi.encodeWithSignature(
"redeem(uint256,address,address)", wrappedATokens, address(vault), address(vault)
"redeem(uint256,address,address)", waTokenAmount, address(vault), address(vault)
),
0
);

uint256 tokens = abi.decode(tokensRaw, (uint256));

emit UnstakeFromAaveUmbrella(address(vault), address(_config.baseCollateral), amount, tokens);
emit UnstakeFromAaveUmbrella(address(vault), address(_config.baseCollateral), amount, wrappedATokens);

return tokens;
} else {
Expand Down
54 changes: 50 additions & 4 deletions src/v2/usd/SwapManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ pragma solidity ^0.8.19;

import {IUniswapV3Pool} from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import {ISwapRouter} from "@level/src/v2/interfaces/uniswap/ISwapRouter.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

import {SwapManagerStorage} from "./SwapManagerStorage.sol";
import {Initializable} from "@openzeppelin-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {AuthUpgradeable} from "@level/src/v2/auth/AuthUpgradeable.sol";
import {SwapConfig} from "./SwapManagerStorage.sol";
import {PauserGuardedUpgradable} from "@level/src/v2/common/guard/PauserGuardedUpgradable.sol";
import {OracleLib} from "@level/src/v2/common/libraries/OracleLib.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

/**
* .-==+=======+:
Expand Down Expand Up @@ -61,6 +64,22 @@ contract SwapManager is SwapManagerStorage, Initializable, UUPSUpgradeable, Auth
swapConfigs[tokenIn][tokenOut] = config;
}

/// @notice Adds an oracle to the swap manager
/// @param token The address of the token to add an oracle for
/// @param oracle The address of the oracle to add
/// @dev Only callable by authorized addresses
function addOracle(address token, address oracle) public requiresAuth {
oracles[token] = oracle;
}

/// @notice Sets the heart beat for a token
/// @param token The address of the token to set the heart beat for
/// @param heartBeat The heart beat to set
/// @dev Only callable by authorized addresses
function setHeartBeat(address token, uint256 heartBeat) public requiresAuth {
heartbeats[token] = heartBeat;
}

/// @notice Executes a token swap using the configured parameters
/// @param tokenIn The address of the input token
/// @param tokenOut The address of the output token
Expand All @@ -86,12 +105,39 @@ contract SwapManager is SwapManagerStorage, Initializable, UUPSUpgradeable, Auth
uint128 liquidity = IUniswapV3Pool(config.pool).liquidity();
require(liquidity > 0, "SwapManager: No liquidity");

IERC20Metadata tokenInMetadata = IERC20Metadata(tokenIn);
IERC20Metadata tokenOutMetadata = IERC20Metadata(tokenOut);

// Transfer & approve
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
IERC20(tokenIn).approve(address(swapRouter), amountIn);
tokenInMetadata.transferFrom(msg.sender, address(this), amountIn);
tokenInMetadata.approve(address(swapRouter), amountIn);

(int256 inputTokenPrice, uint256 inputPriceDecimals) =
OracleLib.getPriceAndDecimals(oracles[tokenIn], heartbeats[tokenIn]);

(int256 outputTokenPrice, uint256 outputPriceDecimals) =
OracleLib.getPriceAndDecimals(oracles[tokenOut], heartbeats[tokenOut]);

require(inputTokenPrice > 0 && outputTokenPrice > 0, "SwapManager: Invalid oracle price");

uint8 tokenInDecimals = tokenInMetadata.decimals();
uint8 tokenOutDecimals = tokenOutMetadata.decimals();

// Normalize amountIn to 1e18
uint256 amountInNormalized = Math.mulDiv(amountIn, 1e18, 10 ** tokenInDecimals);

// Normalize oracle prices to 1e18
uint256 inputPriceNormalized = Math.mulDiv(uint256(inputTokenPrice), 1e18, 10 ** inputPriceDecimals);
uint256 outputPriceNormalized = Math.mulDiv(uint256(outputTokenPrice), 1e18, 10 ** outputPriceDecimals);

// Calculate expected output amount (in 1e18 units)
uint256 expectedOutNormalized = (amountInNormalized * inputPriceNormalized) / outputPriceNormalized;

// Scale expectedOut back to tokenOut decimals
uint256 adjustedOutAmount = Math.mulDiv(expectedOutNormalized, 10 ** tokenOutDecimals, 1e18);

// Calculate slippage min out
uint256 minOut = (amountIn * (10_000 - config.slippageBps) + 10_000 - 1) / 10_000; // Round up
uint256 minOut = (adjustedOutAmount * (10_000 - config.slippageBps) + 10_000 - 1) / 10_000; // Round up

// Build params
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
Expand Down
4 changes: 4 additions & 0 deletions src/v2/usd/SwapManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ abstract contract SwapManagerStorage {
/// @notice The Uniswap V3 SwapRouter contract instance
ISwapRouter public swapRouter;

// collateral token address to chainlink oracle address map
mapping(address => address) public oracles;
mapping(address => uint256) public heartbeats;

/// @notice Mapping of token pairs to their swap configurations
/// @dev First address is token0, second address is token1
mapping(address => mapping(address => SwapConfig)) public swapConfigs;
Expand Down
74 changes: 65 additions & 9 deletions test/v2/integration/VaultManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import {IERC4626Oracle} from "@level/src/v2/interfaces/level/IERC4626Oracle.sol"
import {AggregatorV3Interface} from "@level/src/v2/interfaces/AggregatorV3Interface.sol";
import {IAllowListV2} from "@level/src/v2/interfaces/superstate/IAllowListV2.sol";
import {UpgradeVaultManager} from "@level/script/v2/usd/UpgradeVaultManager.s.sol";
import {DeploySwapManager} from "@level/script/v2/usd/DeploySwapManager.s.sol";
import {SwapManager} from "@level/src/v2/usd/SwapManager.sol";
import {IERC4626StataToken} from "@level/src/v2/interfaces/aave/IERC4626StataToken.sol";
import {IERC4626StakeToken} from "@level/src/v2/interfaces/aave/IERC4626StakeToken.sol";
import {CappedOneDollarOracle} from "@level/src/v2/oracles/CappedOneDollarOracle.sol";
import {AaveUmbrellaOracle} from "@level/src/v2/oracles/AaveUmbrellaOracle.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {SwapConfig} from "@level/src/v2/usd/SwapManager.sol";

contract VaultManagerMainnetTests is Utils, Configurable {
using SafeTransferLib for ERC20;
Expand Down Expand Up @@ -53,15 +55,14 @@ contract VaultManagerMainnetTests is Utils, Configurable {

function setUp() public {
forkMainnet(22664895);
initConfig(1);

deployer = vm.createWallet("deployer");
strategist = vm.createWallet("strategist");

DeploySwapManager deploySwapManager = new DeploySwapManager();

vm.prank(deployer.addr);
deploySwapManager.setUp_(1, deployer.privateKey);
config = deploySwapManager.run();
vm.startPrank(deployer.addr);
_deploySwapManager();
vm.stopPrank();

_upgradeVaultManager();

Expand Down Expand Up @@ -148,14 +149,67 @@ contract VaultManagerMainnetTests is Utils, Configurable {
}
}

function _deploySwapManager() internal {
// Replicate SwapManager deploy script
CappedOneDollarOracle mNavOracle = new CappedOneDollarOracle(address(config.oracles.mNav));
config.oracles.cappedMNav = AggregatorV3Interface(address(mNavOracle));

bytes memory constructorArgs = abi.encodeWithSignature(
"initialize(address,address,address)",
deployer.addr,
address(config.periphery.uniswapV3Router),
address(config.levelContracts.pauserGuard)
);

SwapManager _swapManager = new SwapManager();
ERC1967Proxy _swapManagerProxy = new ERC1967Proxy(address(_swapManager), constructorArgs);

config.levelContracts.swapManager = SwapManager(address(_swapManagerProxy));
config.levelContracts.swapManager.setAuthority(config.levelContracts.rolesAuthority);

config.levelContracts.swapManager.setSwapConfig(
address(config.tokens.usdc),
address(config.tokens.wrappedM),
SwapConfig({
pool: 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, // wM/USDC pool
fee: 100, //0.01%
tickLower: -10,
tickUpper: 10,
slippageBps: 5, //0.05%
active: true
})
);

config.levelContracts.swapManager.setSwapConfig(
address(config.tokens.wrappedM),
address(config.tokens.usdc),
SwapConfig({
pool: 0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288, // wM/USDC pool
fee: 100, //0.01%
tickLower: -10,
tickUpper: 10,
slippageBps: 5, //0.05%
active: true
})
);

config.levelContracts.swapManager.addOracle(address(config.tokens.usdc), address(config.oracles.usdc));
config.levelContracts.swapManager.addOracle(address(config.tokens.wrappedM), address(config.oracles.cappedMNav));
config.levelContracts.swapManager.setHeartBeat(address(config.tokens.usdc), 1 days);
config.levelContracts.swapManager.setHeartBeat(address(config.tokens.wrappedM), 26 hours);

// Transfer ownership to admin timelock
config.levelContracts.swapManager.transferOwnership(address(config.levelContracts.adminTimelock));
}

function _upgradeVaultManager() internal {
VaultManager impl = new VaultManager();
vm.prank(address(config.levelContracts.adminTimelock));
config.levelContracts.vaultManager.upgradeToAndCall(address(impl), "");
}

function _setupTreasuriesForTests() internal {
CappedOneDollarOracle mNavOracle = new CappedOneDollarOracle(address(config.oracles.mNav));
CappedOneDollarOracle mNavOracle = CappedOneDollarOracle(address(config.oracles.cappedMNav));

StrategyConfig memory ustbConfig = StrategyConfig({
category: StrategyCategory.SUPERSTATE,
Expand Down Expand Up @@ -852,6 +906,9 @@ contract VaultManagerMainnetTests is Utils, Configurable {
)
);

_mockChainlinkCall(address(config.oracles.mNav), 105e6); // 1.05 USD per M
_mockChainlinkCall(address(config.oracles.usdc), 1e8); // 1 USD per USDC

vm.startPrank(strategist.addr);
vaultManager.deposit(address(config.tokens.usdc), address(config.tokens.wrappedM), deposit);

Expand All @@ -863,8 +920,6 @@ contract VaultManagerMainnetTests is Utils, Configurable {
"Wrong amount of wrapped M"
);

_mockChainlinkCall(address(config.oracles.mNav), 105e6); // 1.05 USD per M

// Check assets in strategy
assertApproxEqRel(
_getAssetsInStrategy(address(config.tokens.usdc), address(config.tokens.wrappedM)),
Expand Down Expand Up @@ -899,6 +954,7 @@ contract VaultManagerMainnetTests is Utils, Configurable {
);

_mockChainlinkCall(address(config.oracles.mNav), 105e6); // 1.05 USD per M
_mockChainlinkCall(address(config.oracles.usdc), 1e8); // 1 USD per USDC

vm.startPrank(strategist.addr);
vaultManager.deposit(address(config.tokens.usdc), address(config.tokens.wrappedM), deposit);
Expand Down