Skip to content

Commit

Permalink
Merge pull request #70 from euler-xyz/product-lines-basic
Browse files Browse the repository at this point in the history
Product lines basic
  • Loading branch information
dglowinski authored Mar 25, 2024
2 parents 57bef8f + a2f23b9 commit c908f35
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 5 deletions.
111 changes: 111 additions & 0 deletions src/ProductLines/BaseProductLine.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.0;

import {IERC20, IEVault, IGovernance} from "../EVault/IEVault.sol";
import {GenericFactory} from "../GenericFactory/GenericFactory.sol";
import {RevertBytes} from "../EVault/shared/lib/RevertBytes.sol";

import "../EVault/shared/Constants.sol";

/// @notice Base contract for product line contracts, which deploy pre-configured EVaults through a GenericFactory
abstract contract BaseProductLine {
// Constants

uint256 constant REENTRANCYLOCK__UNLOCKED = 1;
uint256 constant REENTRANCYLOCK__LOCKED = 2;

address public immutable vaultFactory;
address public immutable evc;

// State

uint256 private reentrancyLock;

mapping(address vault => bool created) public vaultLookup;
address[] public vaultList;

// Events

event Genesis();
event VaultCreated(address indexed vault, address indexed asset, bool upgradeable);

// Errors

error E_Reentrancy();
error E_BadQuery();

// Modifiers

modifier nonReentrant() {
if (reentrancyLock != REENTRANCYLOCK__UNLOCKED) revert E_Reentrancy();

reentrancyLock = REENTRANCYLOCK__LOCKED;
_;
reentrancyLock = REENTRANCYLOCK__UNLOCKED;
}

// Interface

constructor(address vaultFactory_, address evc_) {
vaultFactory = vaultFactory_;
evc = evc_;

emit Genesis();
}

function makeNewVaultInternal(bool upgradeable, address asset, address oracle, address unitOfAccount)
internal
returns (IEVault)
{
address newVault =
GenericFactory(vaultFactory).createProxy(upgradeable, abi.encodePacked(asset, oracle, unitOfAccount));

vaultLookup[newVault] = true;
vaultList.push(newVault);

if (isEVCCompatible(asset)) {
uint32 flags = IEVault(newVault).configFlags();
IEVault(newVault).setConfigFlags(flags | CFG_EVC_COMPATIBLE_ASSET);
}

emit VaultCreated(newVault, asset, upgradeable);

return IEVault(newVault);
}

// Getters

function getVaultListLength() external view returns (uint256) {
return vaultList.length;
}

function getVaultListSlice(uint256 start, uint256 end) external view returns (address[] memory list) {
if (end == type(uint256).max) end = vaultList.length;
if (end < start || end > vaultList.length) revert E_BadQuery();

list = new address[](end - start);
for (uint256 i; i < end - start; ++i) {
list[i] = vaultList[start + i];
}
}

function getTokenName(address asset) internal view returns (string memory) {
// Handle MKR like tokens returning bytes32
(bool success, bytes memory data) = address(asset).staticcall(abi.encodeWithSelector(IERC20.name.selector));
if (!success) RevertBytes.revertBytes(data);
return data.length == 32 ? string(data) : abi.decode(data, (string));
}

function getTokenSymbol(address asset) internal view returns (string memory) {
// Handle MKR like tokens returning bytes32
(bool success, bytes memory data) = address(asset).staticcall(abi.encodeWithSelector(IERC20.symbol.selector));
if (!success) RevertBytes.revertBytes(data);
return data.length == 32 ? string(data) : abi.decode(data, (string));
}

function isEVCCompatible(address asset) private view returns (bool) {
(bool success, bytes memory data) = asset.staticcall(abi.encodeCall(IGovernance.EVC, ()));
return success && data.length >= 32 && abi.decode(data, (address)) == address(evc);
}
}
51 changes: 51 additions & 0 deletions src/ProductLines/Core.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.0;

import "./BaseProductLine.sol";

/// @notice Contract deploying EVaults, forming the `Core` product line, which are upgradeable and fully governed.
contract Core is BaseProductLine {
// Constants

bool public constant UPGRADEABLE = true;

// State

address public governor;
address public feeReceiver;

// Errors

error E_Unauthorized();

// Interface

constructor(address vaultFactory_, address evc_, address governor_, address feeReceiver_)
BaseProductLine(vaultFactory_, evc_)
{
governor = governor_;
feeReceiver = feeReceiver_;
}

modifier governorOnly() {
if (msg.sender != governor) revert E_Unauthorized();
_;
}

function createVault(address asset, address oracle, address unitOfAccount)
external
governorOnly
returns (address)
{
IEVault vault = makeNewVaultInternal(UPGRADEABLE, asset, oracle, unitOfAccount);

vault.setName(string.concat("Core vault: ", getTokenName(asset)));
vault.setSymbol(string.concat("e", getTokenSymbol(asset)));

vault.setFeeReceiver(feeReceiver);
vault.setGovernorAdmin(governor);

return address(vault);
}
}
47 changes: 47 additions & 0 deletions src/ProductLines/Escrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.0;

import "./BaseProductLine.sol";
import "../EVault/shared/Constants.sol";

/// @notice Contract deploying EVaults, forming the `Escrow` product line, which are non-upgradeable
/// non-governed, don't allow borrowing and only allow one instance per asset.
contract Escrow is BaseProductLine {
// Constants

bool public constant UPGRADEABLE = false;

// State

mapping(address asset => address vault) public assetLookup;

// Errors

error E_AlreadyCreated();

// Interface

constructor(address vaultFactory_, address evc_) BaseProductLine(vaultFactory_, evc_) {}

function createVault(address asset) external returns (address) {
if (assetLookup[asset] != address(0)) revert E_AlreadyCreated();

IEVault vault = makeNewVaultInternal(UPGRADEABLE, asset, address(0), address(0));

assetLookup[asset] = address(vault);

vault.setName(string.concat("Escrow vault: ", getTokenName(asset)));
vault.setSymbol(string.concat("e", getTokenSymbol(asset)));

vault.setDisabledOps(
OP_BORROW | OP_REPAY | OP_LOOP | OP_DELOOP | OP_PULL_DEBT | OP_CONVERT_FEES | OP_LIQUIDATE | OP_TOUCH
| OP_ACCRUE_INTEREST
);

// Renounce governorship
vault.setGovernorAdmin(address(0));

return address(vault);
}
}
15 changes: 12 additions & 3 deletions test/unit/evault/EVaultTestBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import {IEVault, IERC20} from "src/EVault/IEVault.sol";
import {TypesLib} from "src/EVault/shared/types/Types.sol";
import {Base} from "src/EVault/shared/Base.sol";

import {Core} from "src/ProductLines/Core.sol";
import {Escrow} from "src/ProductLines/Escrow.sol";

import {EthereumVaultConnector} from "ethereum-vault-connector/EthereumVaultConnector.sol";

import {TestERC20} from "../../mocks/TestERC20.sol";
Expand All @@ -46,6 +49,9 @@ contract EVaultTestBase is AssertionsCustomTypes, Test, DeployPermit2 {
address permit2;
GenericFactory public factory;

Core public coreProductLine;
Escrow public escrowProductLine;

Base.Integrations integrations;
Dispatch.DeployedModules modules;

Expand Down Expand Up @@ -102,14 +108,17 @@ contract EVaultTestBase is AssertionsCustomTypes, Test, DeployPermit2 {
vm.prank(admin);
factory.setImplementation(evaultImpl);

coreProductLine = new Core(address(factory), address(evc), address(this), feeReceiver);
escrowProductLine = new Escrow(address(factory), address(evc));

assetTST = new TestERC20("Test Token", "TST", 18, false);
assetTST2 = new TestERC20("Test Token 2", "TST2", 18, false);

eTST = IEVault(factory.createProxy(true, abi.encodePacked(address(assetTST), address(oracle), unitOfAccount)));
eTST = IEVault(coreProductLine.createVault(address(assetTST), address(oracle), unitOfAccount));
eTST.setInterestRateModel(address(new IRMTestDefault()));

eTST2 = IEVault(factory.createProxy(true, abi.encodePacked(address(assetTST2), address(oracle), unitOfAccount)));
eTST2.setInterestRateModel(address(new IRMTestDefault()));
eTST2 = IEVault(coreProductLine.createVault(address(assetTST2), address(oracle), unitOfAccount));
eTST.setInterestRateModel(address(new IRMTestDefault()));
}

uint32 internal constant SYNTH_VAULT_DISABLED_OPS = OP_MINT | OP_REDEEM | OP_SKIM | OP_LOOP | OP_DELOOP;
Expand Down
4 changes: 2 additions & 2 deletions test/unit/evault/modules/Token/views.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import "../../EVaultTestBase.t.sol";

contract ERC20Test_views is EVaultTestBase {
function test_basicViews() public view {
assertEq(eTST.name(), "Unnamed Euler Vault");
assertEq(eTST.symbol(), "UNKNOWN");
assertEq(eTST.name(), "Core vault: Test Token");
assertEq(eTST.symbol(), "eTST");
assertEq(eTST.decimals(), assetTST.decimals());
}
}
15 changes: 15 additions & 0 deletions test/unit/productLines/productLines.base.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.0;

import "../evault/EVaultTestBase.t.sol";

contract ProductLine_Base is EVaultTestBase {
function test_ProductLine_Base_lookup() public view {
assertEq(coreProductLine.vaultLookup(address(eTST)), true);
assertEq(coreProductLine.vaultLookup(vm.addr(100)), false);
assertEq(coreProductLine.getVaultListLength(), 2);
assertEq(coreProductLine.getVaultListSlice(0, type(uint256).max)[0], address(eTST));
assertEq(coreProductLine.getVaultListSlice(0, type(uint256).max)[1], address(eTST2));
}
}
22 changes: 22 additions & 0 deletions test/unit/productLines/productLines.core.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.0;

import "../evault/EVaultTestBase.t.sol";

contract ProductLine_Core is EVaultTestBase {
function test_ProductLine_Core_basicViews() public view {
assertEq(factory.getProxyConfig(address(eTST)).upgradeable, true);

assertEq(eTST.unitOfAccount(), unitOfAccount);
assertEq(eTST.oracle(), address(oracle));
assertEq(eTST.feeReceiver(), feeReceiver);
assertEq(eTST.governorAdmin(), address(this));
}

function test_ProductLine_Core_EVCCompatibility() public {
assertEq(eTST.configFlags(), 0);
IEVault nested = IEVault(coreProductLine.createVault(address(eTST), address(oracle), unitOfAccount));
assertEq(nested.configFlags(), CFG_EVC_COMPATIBLE_ASSET);
}
}
30 changes: 30 additions & 0 deletions test/unit/productLines/productLines.escrow.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.0;

import "src/ProductLines/Escrow.sol";
import "../evault/EVaultTestBase.t.sol";

contract ProductLine_Escrow is EVaultTestBase {
uint32 constant ESCROW_DISABLED_OPS = OP_BORROW | OP_REPAY | OP_LOOP | OP_DELOOP | OP_PULL_DEBT | OP_CONVERT_FEES
| OP_LIQUIDATE | OP_TOUCH | OP_ACCRUE_INTEREST;

function test_ProductLine_Escrow_basicViews() public {
IEVault escrowTST = IEVault(escrowProductLine.createVault(address(assetTST)));

assertEq(factory.getProxyConfig(address(escrowTST)).upgradeable, false);

assertEq(escrowTST.name(), "Escrow vault: Test Token");
assertEq(escrowTST.symbol(), "eTST");
assertEq(escrowTST.unitOfAccount(), address(0));
assertEq(escrowTST.oracle(), address(0));
assertEq(escrowTST.disabledOps(), ESCROW_DISABLED_OPS);
}

function test_ProductLine_Escrow_RevertWhenAlreadyCreated() public {
escrowProductLine.createVault(address(assetTST));

vm.expectRevert(Escrow.E_AlreadyCreated.selector);
escrowProductLine.createVault(address(assetTST));
}
}

0 comments on commit c908f35

Please sign in to comment.