Skip to content

Commit

Permalink
Merge pull request #106 from Ion-Protocol/jun/vault-rounding-error
Browse files Browse the repository at this point in the history
Vault Deployment
  • Loading branch information
junkim012 authored Jun 7, 2024
2 parents d31aeb2 + 70c0db5 commit a073854
Show file tree
Hide file tree
Showing 16 changed files with 1,751 additions and 222 deletions.
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,18 @@
"10_AdminTransfer:deployment:deploy:tenderly": "forge script script/deploy/10_AdminTransfer.s.sol --rpc-url $TENDERLY_RPC_URL --private-key $PRIVATE_KEY --slow",
"10_AdminTransfer:deployment:deploy:sepolia": "forge script script/deploy/10_AdminTransfer.s.sol --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --slow",
"10_AdminTransfer:deployment:deploy:mainnet": "forge script script/deploy/10_AdminTransfer.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow",

"11_RedeploySpotOracle:deployment:test": "forge test --mc RedeploySpotOracleTest --nmp \"\"",
"11_RedeploySpotOracle:deployment:deploy:anvil": "forge script script/deploy/11_RedeploySpotOracle.s.sol --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY --slow",
"11_RedeploySpotOracle:deployment:deploy:tenderly": "forge script script/deploy/11_RedeploySpotOracle.s.sol --rpc-url $TENDERLY_RPC_URL --private-key $PRIVATE_KEY --slow",
"11_RedeploySpotOracle:deployment:deploy:sepolia": "forge script script/deploy/11_RedeploySpotOracle.s.sol --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY --slow",
"11_RedeploySpotOracle:deployment:deploy:mainnet": "forge script script/deploy/11_RedeploySpotOracle.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow"
"11_RedeploySpotOracle:deployment:deploy:mainnet": "forge script script/deploy/11_RedeploySpotOracle.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow",
"VaultBytecode:deployment:deploy:tenderly": "forge script script/deploy/vault/DeployVaultBytecode.s.sol --rpc-url $TENDERLY_RPC_URL --private-key $PRIVATE_KEY --slow",
"VaultBytecode:deployment:deploy:mainnet": "forge script script/deploy/vault/DeployVaultBytecode.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow",
"VaultFactory:deployment:deploy:anvil": "forge script script/deploy/vault/DeployVaultFactory.s.sol --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY --slow",
"VaultFactory:deployment:deploy:tenderly": "forge script script/deploy/vault/DeployVaultFactory.s.sol --rpc-url $TENDERLY_RPC_URL --private-key $PRIVATE_KEY --slow",
"VaultFactory:deployment:deploy:mainnet": "forge script script/deploy/vault/DeployVaultFactory.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow",
"Vault:deployment:deploy:tenderly": "forge script script/deploy/vault/DeployVault.s.sol --rpc-url $TENDERLY_RPC_URL --private-key $PRIVATE_KEY --slow",
"Vault:deployment:deploy:mainnet": "forge script script/deploy/vault/DeployVault.s.sol --rpc-url $MAINNET_RPC_URL --private-key $PRIVATE_KEY --slow"
},
"dependencies": {
"date-fns": "^2.30.0",
Expand Down
3 changes: 3 additions & 0 deletions script/Base.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ValidateInterface } from "./ValidateInterface.s.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

import { Script, stdJson } from "forge-std/Script.sol";
import { console2 } from "forge-std/console2.sol";

abstract contract BaseScript is Script, ValidateInterface {
using stdJson for string;
Expand Down Expand Up @@ -41,6 +42,8 @@ abstract contract BaseScript is Script, ValidateInterface {
mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC });
(broadcaster,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 });
}

console2.log("broadcaster", broadcaster);
}

modifier broadcast() {
Expand Down
5 changes: 5 additions & 0 deletions script/ValidateInterface.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ abstract contract ValidateInterface {
function _validateInterfaceIonPool(IonPool ionPool) internal view {
require(address(ionPool).code.length > 0, "ionPool address must have code");
ionPool.balanceOf(address(this));
ionPool.totalSupply();
ionPool.GEM_JOIN_ROLE();
ionPool.LIQUIDATOR_ROLE();
ionPool.PAUSE_ROLE();
ionPool.calculateRewardAndDebtDistribution();
}

function _validateInterface(IERC20 ilkAddress) internal view {
Expand Down
163 changes: 163 additions & 0 deletions script/deploy/vault/DeployVault.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import { Vault } from "./../../../src/vault/Vault.sol";
import { VaultFactory } from "./../../../src/vault/VaultFactory.sol";
import { IIonPool } from "./../../../src/interfaces/IIonPool.sol";
import { IonPool } from "./../../../src/IonPool.sol";
import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";

import { BaseScript } from "./../../Base.s.sol";

import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { stdJson as StdJson } from "forge-std/StdJson.sol";

VaultFactory constant factory = VaultFactory(address(0));
/**
* Always use the factory to deploy a vault.
*/

contract DeployVault is BaseScript {
using EnumerableSet for EnumerableSet.AddressSet;
using StdJson for string;
using SafeCast for uint256;

string configPath = "./deployment-config/vault/DeployVault.json";
string config = vm.readFile(configPath);

VaultFactory factory = VaultFactory(config.readAddress(".factory"));

address baseAsset = config.readAddress(".baseAsset");

address feeRecipient = config.readAddress(".feeRecipient");
uint256 feePercentage = config.readUint(".feePercentage");

string name = config.readString(".name");
string symbol = config.readString(".symbol");

uint48 initialDelay = config.readUint(".initialDelay").toUint48();
address initialDefaultAdmin = config.readAddress(".initialDefaultAdmin");

bytes32 salt = config.readBytes32(".salt");

uint256 initialDeposit = config.readUint(".initialDeposit");

address[] marketsToAdd = config.readAddressArray(".marketsToAdd");
uint256[] allocationCaps = config.readUintArray(".allocationCaps");
address[] supplyQueue = config.readAddressArray(".supplyQueue");
address[] withdrawQueue = config.readAddressArray(".withdrawQueue");

IIonPool public constant IDLE = IIonPool(address(uint160(uint256(keccak256("IDLE_ASSET_HOLDINGS")))));

EnumerableSet.AddressSet marketsCheck;

/**
* Validate that the salt is msg.sender protected.
*/
function _validateSalt(bytes32 salt) internal {
if (address(bytes20(salt)) != broadcaster) {
revert("Invalid Salt");
}
}

/**
* No duplicates. No zero addresses. IonPool Interface.
*/
function _validateIonPoolArray(address[] memory ionPools) internal returns (IIonPool[] memory typedIonPools) {
typedIonPools = new IIonPool[](ionPools.length);

for (uint8 i = 0; i < ionPools.length; i++) {
address pool = ionPools[i];

require(pool != address(0), "zero address");

marketsCheck.add(pool);

// If not the IDLE address, then validate the IonPool interface.
if (pool != address(IDLE)) {
_validateInterfaceIonPool(IonPool(pool));
}

// Check for duplicates in this array
if (i != ionPools.length - 1) {
for (uint8 j = i + 1; j < ionPools.length; j++) {
require(ionPools[i] != ionPools[j], "duplicate");
}
}

typedIonPools[i] = IIonPool(pool);
}
}

function run() public broadcast returns (Vault vault) {
require(baseAsset != address(0), "baseAsset");

require(feeRecipient != address(0), "feeRecipient");
require(feePercentage <= 0.2e27, "feePercentage");

// require(initialDelay != 0, "initialDelay");
require(initialDefaultAdmin != address(0), "initialDefaultAdmin");

require(initialDeposit >= 1e3, "initialDeposit");
require(IERC20(baseAsset).balanceOf(broadcaster) >= initialDeposit, "sender balance");
// require(IERC20(baseAsset).allowance(broadcaster, address(factory)) >= initialDeposit, "sender allowance");

if (IERC20(baseAsset).allowance(broadcaster, address(factory)) < initialDeposit) {
IERC20(baseAsset).approve(address(factory), 1e9);
}

// The length of all the arrays must be the same.
require(marketsToAdd.length > 0);
require(allocationCaps.length > 0);
require(supplyQueue.length > 0);
require(withdrawQueue.length > 0);

uint256 marketsLength = marketsToAdd.length;

require(marketsToAdd.length == marketsLength, "array length");
require(allocationCaps.length == marketsLength, "array length");
require(supplyQueue.length == marketsLength, "array length");

_validateSalt(salt);

IIonPool[] memory typedMarketsToAdd = _validateIonPoolArray(marketsToAdd);
IIonPool[] memory typedSupplyQueue = _validateIonPoolArray(supplyQueue);
IIonPool[] memory typedWithdrawQueue = _validateIonPoolArray(withdrawQueue);

// If the length of the `uniqueMarketsCheck` set is greater than 4, that
// means not all of the IonPool arrays had the same set of markets.
// `_validateIonPoolArray` must be called before this.
require(marketsToAdd.length == marketsCheck.length(), "markets not consistent");

Vault.MarketsArgs memory marketsArgs = Vault.MarketsArgs({
marketsToAdd: typedMarketsToAdd,
allocationCaps: allocationCaps,
newSupplyQueue: typedSupplyQueue,
newWithdrawQueue: typedWithdrawQueue
});

vault = factory.createVault(
IERC20(baseAsset),
feeRecipient,
feePercentage,
name,
symbol,
initialDelay,
initialDefaultAdmin,
salt,
marketsArgs,
initialDeposit
);

require(vault.feeRecipient() == feeRecipient, "feeRecipient");
require(vault.feePercentage() == feePercentage, "feePercentage");
require(vault.defaultAdminDelay() == initialDelay, "initialDelay");
require(vault.defaultAdmin() == initialDefaultAdmin, "initialDefaultAdmin");
for (uint8 i = 0; i < marketsLength; i++) {
require(vault.supplyQueue(i) == typedSupplyQueue[i], "supplyQueue");
require(vault.withdrawQueue(i) == typedWithdrawQueue[i], "withdrawQueue");
}
require(vault.supportedMarketsLength() == marketsLength, "supportedMarkets");
}
}
20 changes: 20 additions & 0 deletions script/deploy/vault/DeployVaultBytecode.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import { VaultBytecode } from "./../../../src/vault/VaultBytecode.sol";
import { CREATEX } from "./../../../src/Constants.sol";

import { DeployScript } from "./../../Deploy.s.sol";

bytes32 constant SALT = 0xbcde1e1dd0bdb803514d8e000000000000000000000000000000000000000000;

contract DeployVaultBytecode is DeployScript {
function run() public broadcast returns (VaultBytecode vaultBytecode) {
bytes memory initCode = type(VaultBytecode).creationCode;

require(initCode.length > 0, "initCode");
require(SALT != bytes32(0), "salt");

vaultBytecode = VaultBytecode(CREATEX.deployCreate3(SALT, initCode));
}
}
21 changes: 21 additions & 0 deletions script/deploy/vault/DeployVaultFactory.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import { VaultFactory } from "./../../../src/vault/VaultFactory.sol";
import { CREATEX } from "./../../../src/Constants.sol";

import { DeployScript } from "./../../Deploy.s.sol";

// deploys to 0x0000000000d7dc416dfe993b0e3dd53ba3e27fc8
bytes32 constant SALT = 0x2f428c0d9f1d9e00034c85000000000000000000000000000000000000000000;

contract DeployVaultFactory is DeployScript {
function run() public broadcast returns (VaultFactory vaultFactory) {
bytes memory initCode = type(VaultFactory).creationCode;

require(initCode.length > 0, "initCode");
require(SALT != bytes32(0), "salt");

vaultFactory = VaultFactory(CREATEX.deployCreate3(SALT, initCode));
}
}
37 changes: 26 additions & 11 deletions src/vault/Vault.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.21;

import { IIonPool } from "./../interfaces/IIonPool.sol";
import { IIonPool } from "./../interfaces/IIonPool.sol";
import { RAY } from "./../libraries/math/WadRayMath.sol";

Expand Down Expand Up @@ -48,6 +47,7 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
error MarketsAndAllocationCapLengthMustBeEqual();
error IonPoolsArrayAndNewCapsArrayMustBeOfEqualLength();
error InvalidFeePercentage();
error InvalidFeeRecipient();
error MaxSupportedMarketsReached();

event UpdateSupplyQueue(address indexed caller, IIonPool[] newSupplyQueue);
Expand Down Expand Up @@ -114,6 +114,9 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
{
BASE_ASSET = _baseAsset;

if (_feePercentage > RAY) revert InvalidFeePercentage();
if (_feeRecipient == address(0)) revert InvalidFeeRecipient();

feePercentage = _feePercentage;
feeRecipient = _feeRecipient;

Expand Down Expand Up @@ -144,6 +147,7 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
* @param _feeRecipient The recipient address of the shares minted as fees.
*/
function updateFeeRecipient(address _feeRecipient) external onlyRole(OWNER_ROLE) {
if (_feeRecipient == address(0)) revert InvalidFeeRecipient();
feeRecipient = _feeRecipient;
}

Expand Down Expand Up @@ -459,21 +463,29 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy

for (uint256 i; i != supplyQueueLength;) {
IIonPool pool = supplyQueue[i];

uint256 depositable = pool == IDLE ? _zeroFloorSub(caps[pool], currentIdleDeposits) : _depositable(pool);

if (depositable != 0) {
uint256 toSupply = Math.min(depositable, assets);

// For the IDLE pool, decrement the accumulator at the end of this
// loop, but no external interactions need to be made as the assets
// are already on this contract' balance. If the pool supply
// reverts, simply skip to the next iteration.
if (pool != IDLE) {
try pool.supply(address(this), toSupply, new bytes32[](0)) {
assets -= toSupply;
// solhint-disable-next-line no-empty-blocks
} catch { }
// Early exit ok since this is the last remaining part of
// the user's requested amount and the deposit will
// normalize to zero. Note that this dust amount has already
// been transferred to the vault but is not a 'donation' as
// this amount was accounted for when calculating the amount
// of shares to mint.
uint256 normalizedSupply = toSupply.mulDiv(RAY, pool.supplyFactor());
if (toSupply == assets && normalizedSupply == 0) {
return;
} else {
// If this call reverts by trying to mint zero shares
// with a small supply amount, skip to the next
// iteration.
try pool.supply(address(this), toSupply, new bytes32[](0)) {
assets -= toSupply;
// solhint-disable-next-line no-empty-blocks
} catch { }
}
} else {
assets -= toSupply;
}
Expand Down Expand Up @@ -513,6 +525,9 @@ contract Vault is ERC4626, Multicall, AccessControlDefaultAdminRules, Reentrancy
// transfer. If the pool withdraw reverts, simply skip to the
// next iteration.
if (pool != IDLE) {
// This will never throw InvalidBurnAmount since
// `toWithdraw` is non-zero which means the normalized
// shares to burn inside the IonPool must be non-zero.
try pool.withdraw(address(this), toWithdraw) {
assets -= toWithdraw;
// solhint-disable-next-line no-empty-blocks
Expand Down
55 changes: 55 additions & 0 deletions src/vault/VaultBytecode.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.21;

import { Vault } from "./Vault.sol";
import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";

/**
* @title VaultBytecode
* @author Molecular Labs
* @notice The sole job of this contract is to deploy the embedded `Vault`
* contract's bytecode with the constructor args. `VaultFactory` handles rest of
* the verification and post-deployment logic.
*/
contract VaultBytecode {
error OnlyFactory();

address constant VAULT_FACTORY = 0x0000000000D7DC416dFe993b0E3dd53BA3E27Fc8;

/**
* @notice Deploys the embedded `Vault` bytecode with the given constructor
* args. Only the `VaultFactory` contract can call this function.
* @dev This contract was separated from `VaultFactory` to reduce the
* codesize of the factory contract.
* @param baseAsset The asset that is being lent out to IonPools.
* @param feeRecipient Address that receives the accrued manager fees.
* @param feePercentage Fee percentage to be set.
* @param name Name of the vault token.
* @param symbol Symbol of the vault token.
* @param initialDelay The initial delay for default admin transfers.
* @param initialDefaultAdmin The initial default admin for the vault.
* @param salt The salt used for CREATE2 deployment. The first 20 bytes must
* be the msg.sender.
* @param marketsArgs Arguments for the markets to be added to the vault.
*/
function deploy(
IERC20 baseAsset,
address feeRecipient,
uint256 feePercentage,
string memory name,
string memory symbol,
uint48 initialDelay,
address initialDefaultAdmin,
bytes32 salt,
Vault.MarketsArgs memory marketsArgs
)
external
returns (Vault vault)
{
if (msg.sender != VAULT_FACTORY) revert OnlyFactory();

vault = new Vault{ salt: salt }(
baseAsset, feeRecipient, feePercentage, name, symbol, initialDelay, initialDefaultAdmin, marketsArgs
);
}
}
Loading

0 comments on commit a073854

Please sign in to comment.