EVC Basics Part 3 - Testing our base contracts

Yield DevYield Dev
5 min read

Now that we have implemented the minimal required contracts for our lending market, we can begin setting up some simple test to explore how the EVC and the vaults work in practice.

Following along with the previous two tutorials, we should have a forge project with the following src/ directory.


-src/
    -EVCClient.sol
    -VaultBase.sol
    -VaultSimple.sol
    -VaultSimpleBorrowable

We will start off by testing the deposit only VaultSimple.sol The initial setup will involve importing the EVC contract and a MockERC20 for testing as well as our VaultSimple contract. In the testy setUp() we will deploy everything and then run a simple test to ensure its metadata is properly defined.

Additionally we define a test user, Alice, which will hold 1000 units of our mock token for depositing

// test_vault_simple.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import { Test, console } from "forge-std/Test.sol";
import "evc/EthereumVaultConnector.sol";
import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol";
import { VaultSimple } from "../src/VaultSimple.sol";

contract VaultSimpleTest is Test {
    IEVC evc;
    MockERC20 collateralAsset;
    VaultSimple vault;
    address alice = address(0x69);
    uint256 aliceDeposit = 1000;

    function setUp() public {
        evc = new EthereumVaultConnector();
        collateralAsset = new MockERC20("Collateral Asset", "CA", 18);
        vault = new VaultSimple(address(evc), collateralAsset, "Simple Collateral Vault", "SCV");

        collateralAsset.mint(alice, aliceDeposit);
        vm.prank(alice);
        collateralAsset.approve(address(vault), aliceDeposit);
    }

    // Test vault deployed with metadata
    function test_deployed_vault() public {
        assertEq(vault.name(), "Simple Collateral Vault");
        assertEq(vault.symbol(), "SCV");
    }

}

Next, we will test out a simple deposit which, according to ERC4626, should simply wrap alice's tokens in our vault's shares at a 1:1 ratio.

    // Test Deposit 
    function test_deposit() public {
        vm.prank(alice);
        vault.deposit(aliceDeposit, alice); // direct call to deposit is rerouted through the EVC via it's modifier logic

        assertEq(vault.balanceOf(alice), aliceDeposit); // 1:1 share to asset caclulation in this case
        assertEq(collateralAsset.balanceOf(alice), 0);
        assertEq(vault.totalAssets(), aliceDeposit);
    }

Further, we will test the withdraw functionality,

    // Test Withdraw
    function test_withdraw() public {
        vm.prank(alice);
        vault.deposit(aliceDeposit, alice);

        vm.prank(alice);
        vault.withdraw(shares, alice, alice); // withdraw a certain amount of assets for the vaults shares
        assertEq(vault.balanceOf(alice), 0);
        assertEq(collateralAsset.balanceOf(alice), shares);
    }

mint() and redeem() are similar operations to deposit withdraw and are also defined in ERC4626. Where deposit/withdraw take the amount of tokens to put into, or remove from the vault as arguments, the mint/ redeem functions take the amount of shares worth of tokens to be removed or added to the vault. We test these functions as well.

    // Test mint and redeem 
    function test_mint_redeem() public {
        vm.prank(alice);
        vault.mint(aliceDeposit, alice); // here put the amount of shares we want to mint as an argument

        assertEq(vault.balanceOf(alice), aliceDeposit); // 1:1 share to asset caclulation in this case
        assertEq(collateralAsset.balanceOf(alice), 0);
        assertEq(vault.totalAssets(), aliceDeposit); 

        uint256 shares = vault.balanceOf(alice);

        vm.prank(alice);
        vault.redeem(shares, alice, alice); // redeem a certain amount of shares for the underlying asset
    }

Thus far, we have only tested the basic functionality inherited from ERC4626 vaults. In this next example we will see how the EVC facilitates lending interactions in our VaultSimpleBorrowable.

Setting up our tests, similarly to our VaultSimple test, We deploy an EVC contract along with our borrowable vault.

contract VaultSimpleBorrowableTest is Test {
    IEVC evc;
    MockERC20 borrowableAsset;
    VaultSimpleBorrowable vault;
    address alice = address(0x69);
    uint256 aliceDeposit = 1000;
    function setUp() public {
        evc = new EthereumVaultConnector();
        borrowableAsset = new MockERC20("Borrowable Asset", "CA", 18);
        vault = new VaultSimpleBorrowable(address(evc), borrowableAsset, "Simple Borrowable Vault", "SBV");

        borrowableAsset.mint(alice, aliceDeposit);
        vm.prank(alice);
        borrowableAsset.approve(address(vault), type(uint256).max);
    }
}

First thing to note is that we constructed this vault as a simple borrowable vault using its own deposits as collateral for its own assets at 90% of its value. We did this for simplicity to demonstrate borrowing functionality. In a later post we will extend this contract to a more real world scenario where we can borrow from this vault with our initial VaultSimple deposits as collateral.

First, our test will have Alice deposit into the vault and than borrow 90% of those tokens and subsequently repay those same tokens. Doing this we will see how the EVC comes into play in these interaction.

After Alice has deposited, in order that she may borrow from the vault she must take two actions via the EVC. First, calling the evc.enableController(alice, vault) which enables the vault to control Alice's account through the EVC according the terms of the vaults collateral requirements. This effectively gives the vault a lien on Alice's available collaterals. Second, Alice must opt-in to allowing the EVC to use certain deposits as collateral for various actions, this is done by calling evc.enableCollateral(alice, vault). In summary, Alice must authorize the EVC to, both use specific vaults as collateral and allow a specific vault to control that collateral.

    function test_borrow_repay() public {
        vm.prank(alice);
        uint256 shares = vault.deposit(aliceDeposit, alice);

        assertEq(vault.balanceOf(alice), aliceDeposit);
        assertEq(borrowableAsset.balanceOf(alice), 0);
        assertEq(vault.totalAssets(), aliceDeposit);

        vm.prank(alice);
        evc.enableController(alice, address(vault));

        vm.prank(alice);
        evc.enableCollateral(alice, address(vault));

        vm.prank(alice);
        vault.borrow((aliceDeposit * 9) / 10, alice);

        vm.prank(alice);
        vault.repay((aliceDeposit * 9) / 10, alice);

        assertEq(borrowableAsset.balanceOf(alice), 0);
        assertEq(vault.balanceOf(alice), shares);
        assertEq(vault.debtOf(alice), 0);

    }

Recall that the callThroughEVC function modifiers on the repay/borrow function reroute these direct function calls to be made through the EVC. However, the EVC itself exposes some powerful functionality which we can use directly to call this contract with better UX. One such feature is the EVC batch allowing us to use the EVC as a multicall combining all these calls into a single transaction.

To do this we create a list of IEVC.BatchItem structs containing the encoded transaction execution data which the EVC with authenticate and execute in a single transaction.

    function test_borrow_with_evc_batch() public {

        IEVC.BatchItem[] memory items = new IEVC.BatchItem[](4);
        items[0] = IEVC.BatchItem({
            targetContract: address(vault),
            onBehalfOfAccount: alice,
            value: 0, 
            data: abi.encodeWithSelector(VaultSimple.deposit.selector, aliceDeposit, alice)
        });
        items[1] = IEVC.BatchItem({
            targetContract: address(evc),
            onBehalfOfAccount: address(0),
            value: 0, 
            data: abi.encodeWithSelector(IEVC.enableController.selector, alice, address(vault))
        });
        items[2] = IEVC.BatchItem({
            targetContract: address(evc),
            onBehalfOfAccount: address(0),
            value: 0, 
            data: abi.encodeWithSelector(IEVC.enableCollateral.selector, alice, address(vault))
        });
        items[3] = IEVC.BatchItem({
            targetContract: address(vault),
            onBehalfOfAccount: alice,
            value: 0, 
            data: abi.encodeWithSelector(VaultSimpleBorrowable.borrow.selector, (aliceDeposit * 9) / 10, alice)
        });

        vm.prank(alice);
        evc.batchSimulation(items);
        vm.prank(alice);
        evc.batch(items);

        assertEq(borrowableAsset.balanceOf(alice), (aliceDeposit * 9) / 10);
        assertEq(vault.maxWithdraw(alice), aliceDeposit - (aliceDeposit * 9) / 10);
        assertEq(vault.debtOf(alice), (aliceDeposit * 9) / 10);
    }

By implementing these tests, we have seen how the EVC is used to authenticate and mediate interactions between a user's account and our vaults.

In the next post we will further extend our VaultSimpleBorrowable to illustrate how the EVC is used to mediate interactions between vaults. Thus, completing our simple lending market.

0
Subscribe to my newsletter

Read articles from Yield Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yield Dev
Yield Dev