EOLink 0.2.1: Connecting with AAVE

If you like this content consider tipping in ETH, LINK or an ERC20 stablecoin at this address: 0xf9f6849230cBe10200B43dB103511b778898e71C

Code associated with this post is available on Github (for this post, see Tag v0.0.1)

A central component of the new “Brightlink” project is the ability to hold donor funds, denominated in DAI, in escrow. However, it is also desirable that the project turns a profit without decrementing the donated funds. The mechanism for this is that while the donated funds are held in escrow in the contract, they are actually pushed into an aave pool where the donated DAI accrues interest. When a payout from the contract is triggered, this is all unwound and the original donated funds are made available to send to the recipient. In this post, I will start working on the BrightLink smart contract, with the aim of creating a constructor and a set of functions that accept DAI, deposit into Aave, receive aDAI, and then unwind completely such that the initial DAI balances are restored. This “closed loop” will demonstrate the monetary system that will underpin the BrightLink project in isolation, without any of the wider project code.

As for the FloodInsurance project, this will be built in brownie and tested on the Kovan testnet. If you would like to review setting up brownie, MetaMask, Python etc please review the earlier posts in this series (EOLink 0.1.X..)

Aave - Crunchbase Company Profile & Funding

What is Aave

Aave is a set of smart contracts that allow users to deposit or borrow digital assets. Users who deposits assets are “liquidity providers” because they make their tokens available for other users to borrow, thereby making that token “liquid”. Liquidity providers benefit by accruing interest on their deposited tokens while they are locked away in a pool. Borrowers have the option of keeping their loan long-term (in which case they must deposit assets with greater value than those borrowed) or returning it within a single block of transactions (a “flash loan” that does not require deposited collateral). When a user deposits tokens in an Aave pool, they receive a debt token in return. These debt tokens have the same symbol as the deposited asset with an “a” prepended to it (e.g. the debt token received for deposited DAI is aDAI). These debt tokens act as proof of deposit, and as such they can be traded back into the Aave pool to retrieve the original deposit. Interest is paid out in the form of additional debt tokens, so that more of the original asset can be withdrawn than was deposited, according to the interest rate and the length of time the asset remained in the pool.

However, the user does not have to simply hold their debt tokens and wait for them to passively accrue interest from the Aave pool. Those debt tokens can also be deposited in secondary pools, where they are swapped for secondary debt tokens that accrue more interest. Sometimes this can be repeated to create derivative tokens that are several levels abstracted from the original deposit, albeit accruing risk along with interest. This is called yield farming, and it provides a convenient way for a user to profit from a bulk of capital without decrementing it or making it illiquid. This makes it an ideal strategy for the BrightLink project, since donated funds will sit dormant for periods of time while end-users act to alter their environments, hoping to trigger a payout. While that is happening, the contract owner parks the funds in an Aave pool and the debt tokens are returned to their wallet, where they can choose to yield farm, or simply allow the aave pool to generate passive income. In this example, the donated capital will be in DAI, the debt token will be aDAI. Potential yield farming options include depositing aDAI in the Curve Aave pool, although that will not be shown here. When a payout is triggered, the debt tokens are traded back into DAI. The original capital is unchanged, but some profit has been generated for the protocol.

Aave is generally accessed via its front-end app (app.aave.com) but it is also open source code that can be interacted with programmatically, including from inside other smart contracts. First, we need to grab some testnet funds. We need Kovan DAI and Kovan ETH…

Testnet Funds

The contract built in this post requires testnet ETH to pay for transaction gas and testnet DAI as the currency for payouts. Kovan ETH is easily available via the Kovan ETH faucet (as described here).

Kovan DAI can be accessed from Aave’s faucet. This can be found here. On the landing page, click on the link to AMM v2, then MORE >> faucet. Click DAI, and then watch 10000 DAI land in the connected wallet. The contract addresses for the various tokens and their Aave debt token counterparts is here.

Interfaces

There are two three interfaces required for this contract, one relating to the currency tokens used to transact and two relating to the aave protocol. This contract will transact using DAI and the debt tokens issued by aave will be aDAI. These are both ERC20 tokens, so the ERC20 interface is required by the contract. Then, the token definitions can inherit from these interfaces. The ERC20 interface can be imported from OpenZeppelin. In brownie-config.yaml there is a remapping between the sequence “@openzeppelin” and a specific repository where contracts can be downloaded from. Then, in the Solidity contract the interface can be imported using:

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

The other interfaces define the properties of the Aave lending pool. The specific lending pool changes addresses periodically when updates are made by the aave team, meaning a contract with the pool address hardcoded into it will quickly become obselete. Instead, Aave provide a fixed address for a contract called LendingPoolAddressesProvider that retrieves the most up-to-date lending pool address. LendingPoolAddressesProvider can then be imported into a new contract and used to dynamically load the most recent lending pool address, giving longevity to the new contract. This means both the lending pool interface and the lending pool address provider interfaces are both needed as imports into the new contract. I chose to use Aave’s version 2 protocol. I downloaded the source code from the Aave github and saved them locally in the project /interfaces folder and imported them from there.

import "./interfaces/ILendingPoolAddressesProviderV2.sol";
import "./interfaces/ILendingPoolV2.sol";

contract

With the interfaces saved in the project’s ./interfaces directory, the real building can begin. After the pragma statement and imports, the first task is to define the contract and give it a name, then define the variables with contract-level scope. I like to organize the variables by data type starting with numeric types then addresses then variables that inherit from interfaces because it is readable and looks neat.

contract BrightLink {

    uint256 depositedFunds;
    address private owner;
    address dai_address = 0xFf795577d9AC8bD7D90Ee22b6C1703490b6512FD;
    address adai_address = 0xdCf0aF9e59C002FA3AA091a46196b37530FD48a8;
    address poolAddress;
    IERC20 dai = IERC20(dai_address);
    IERC20 adai = IERC20(adai_address);
    ILendingPoolV2 lendingPool;
    ILendingPoolAddressesProviderV2 provider;

}

uint256 depositedFunds will be used to track the amount of DAI transferred from the contract owner’s wallet to the contract to enable tracking of the intitial capital.

address owner will be instantiated with the contract owner’s wallet address (via msg.sender) so that the modifier “onlyOwner” can be used to restrict access to some functions to the contract owner only.

address dai_address is the address for the Kovan DAI token contract, obtained from this list provided in json format by Aave. Same goes for the debt token aDAI.

address poolAddress will take the address of the Aave lending pool returned by the lending pool address provider.

Then there are four variables that inherit from interfaces. First are the two tokens, DAI and aDAI that are instantiated here as ERC20 tokens using the IERC20 interface and the contract addresses provided a few lines earlier. Then, the lending pool itself is defined as an instance of the lending pol interface which will be instantiated with the address provided by the address-provider. Finally, the address-provider is defined as an instance of the address-provider interface.

Those are all the necessary contract-level variables, so next task is to define the constructor function.

constructor() public{
        
    owner = msg.sender;
    depositedFunds = 0;
    provider = ILendingPoolAddressesProviderV2(address(
                   0x88757f2f99175387aB4C6a4b3067c77A695b0349)); 
    poolAddress = provider.getLendingPool();
    lendingPool = ILendingPoolV2(poolAddress);

    }

The constructor is a function that runs immediately when the contract is deployed. I think of the constructor as a way to set the landscape for the other functions to operate within. Here, I first instantiated the variable “owner” with the address assigned to msg.sender. The syntax “msg.sender” is used to store the address of whomever initiates a transaction, in this case the contract deployment. Having this variable instantiation inside the constructor ensures that any other function that is only accessible to “owner” is in fact only accessible to the contract owner.

The variable “depositedFunds” is set to zero as no funds have yet been deposited at the time of the deployment. This variable will be made to increase when a deposit is made.

Next, we use the Aave address-provider to find the most recent lending pool contract. The address-provider is fixed and is hard-coded in this constructor function. The address-provider at that address is a LendingPoolAddressesProviderV2 object assigned to variable “provider”. The latest pool address is then retrieved by calling the getLendingPool() function of that address-provider object. The lending pool itself is then instantiated using that returned address, as an instance of the ILendingPool interface. After this, the Aave lending pool is available to transact with from inside the contract via the lendingPool variable.

That is all I put in the constructor, so it is time to build the set of specific functions that will enable transactions between user, contract and Aave pool. First, a simple public view function that returns the DAI and aDAI balance of the contract. This is necessary because a manual transaction from the donor to the contract is required to fund the contract, and tracking of the various balances is critical to the correct currency flow through the project – residual funds sitting in the contract could skew the initial capital investment and profit calculations later on.

function checkBalance() public view returns (uint256 dai_balance, uint256 adai_balance) {
    // dai is held here, but aDai is sent to the owner's wallet 

        dai_balance = dai.balanceOf(address(this));
        adai_balance = adai.balanceOf(address(this));

    }

Next, assuming that there has been an initial transfer of DAI from the user to the contract, those funds should be committed to the Aave pool. Before the funds are deposited into the pool, the balance of DAI in the contract is stored in the depositedFunds variable to help keep track of the funds being moved. Then, there is some approval required for DAI transfers, then to move the funds it is simply a case of calling the deposit() function of the lendinPool object. The arguments for the deposit() function are the address of the token (DAI), the amount to deposit (in this case, the contract balance), then the address where the debt tokens should be held (in this case it is the contract, although they can also be retrieved into the owner’s wallet by a separate manual transaction) and a referral code which in this test environment can just be set to 0.

function depositFundsToAave() public onlyOwner{
	
    depositedFunds += dai.balanceOf(address(this));
    uint16 referral = 0;
    // Deposit dai and hold aDAI in contract.
    // pool requires approval to move DAI
    dai.approve(poolAddress,100000e18);
    dai.approve(address(this),100000e18);
    lendingPool.deposit(dai_address, dai.balanceOf(address(this)), address(this), referral);
        
    }

Now, in order to view the deposited amount, another simple public view function.

function viewDeposits() public view returns (uint256 deposits){
        
    deposits = depositedFunds;
    
    }

Once the deposit has been made to the Aave pool, the contract holds the debt tokens aDAI. The interest on the deposited DAI accrues in the form of additional aDAI tokens. The longer the DAI sits in the pool, the higher the aDAI balance of the contract. The difference between the initial deposited DAI and the contract’s aDAI balance is the profit. This is also useful information to return as a public view function.

function viewProfit() public view returns(uint256 profit){

    profit = adai.balanceOf(address(this)) - depositedFunds;

    }

So we now have functionality to add DAI to the contract, move that DAI into the Aave lending pool in return for aDAI, and query the various balances. When this project is larger than just the financial transactions, it will pause for some amount of time with the system resting in this state, gradually occurring aDAI. Meanwhile, a real world organisation will be brightening their environment in a way that can be detected remotely using our app (to be built later). If the app returns a valid result, a payout to the incentivized organization is required, meaning the deposits must be withdrawn from the Aave pool in exchange for the aDAI tokens held in the contract. It is also possible that the aDAI tokens had been withdrawn from the contract to the owner’s wallet and deposited in a secondary pool, in which case this would also need to be unwound and the aDAI returned to the contract. This is outside the scope of this post but yield farming via the Curve aave pool might be incorporated into this contract later. Anyway, we can progress assuming the aDAI is in the contract. The task is to swap it for DAI and return the balance to the owner’s wallet. Later, a second account representing the incentivized organization will be included and the payment separated out into release of initial capital to the organization’s wallet and profit sent to the owner’s wallet. But for now, we just want to send all the accumulated DAI to the owner’s wallet to close the loop and demonstrate successful withdrawal from the Aave pool. This is achieved by calling the withdraw() function of the lendingPool object, after approving the pool and contract to move aDAI. The arguments passed to the withdraw() function are the token address for DAI (the currency we want to withdraw to), the amount (in this case the total amount of aDAI in the contract) and the destination (in this case the DAI should be held in the contract as it is still considered to be in escrow, just not in the lending pool).

function WithdrawFundsFromAave() public onlyOwner {
    // swap aDAI in contract for DAI
    // pool requires approval to move aDAI
    adai.approve(poolAddress,100000e18);
    adai.approve(address(this), 100000e18);
    lendingPool.withdraw(dai_address, adai.balanceOf(address(this)), address(this));
    
    }

Finally, the DAI released from the Aave pool can be transferred out of the contract back to the owner wallet. This should close the loop for DAI, from owner wallet back to owner wallet via the contract, aave pool and contract again. However, parking the DAI in the Aave pool should have generated some profit, so the returned amount should be greater than the initial DAI deposited. The function below transfers any remaining balance in either DAI or aDAI back to the owner.

function retrieveDAI() public onlyOwner{
        
    // send DAI from contract back to owner
    if (dai.balanceOf(address(this)) >0){
        require(dai.transfer(owner, dai.balanceOf(address(this))));
    }

    // send aDAI from contract back to owner
    if (adai.balanceOf(address(this)) > 0){
        require(adai.transfer(owner, adai.balanceOf(address(this))));
    }
    
}

The main functions of the contract are then complete. However, you may have noticed that several of these functions had restricted permissions and were only accessible to the owner, as determined by the modifier “onlyOwner”. This modifier is not built-in to Solidity, so it must be defined within the contract.

modifier onlyOwner(){
    require(owner==msg.sender);
    _;
    }
    

This Solidity contract for moving DAI into and out of an Aave lending pool is now complete and ready to be deployed to Kovan for testing. There are no arguments required by the constructor function, so deployment is as simple as:

$ brownie console --network kovan
$ account = accounts.load('main')
$ contract = BrightLink.deploy({'from':account}) 

testing

With the contract deployed it is time to test the functions to make sure they conform to their expected behaviour. To do this, I first defined the testing fixtures in a file called ./tests/conftest.py. These are the functions that need to run in order to do any testing of the contract – the background configuration that remain constant throughout the tests and is required for individual unit testing to work. These fixtures were:

checkNetwork(): a quick check to make sure the user is testing on Kovan

getDeployedContract(): function to create instance of the contract to test (requires deployment address)

set_deposit_amount(): function to define how much DAI should be donated into the contract

load_account(): function to load owner’s wallet into brownie

The file conftest.py looks like this:

import pytest

from brownie import (
    Contract,
    accounts,
    network,
)

@pytest.fixture
def checkNetwork():
    assert network.show_active() == 'kovan'
    return


@pytest.fixture(scope="module")
def getDeployedContract():
    return Contract(<contract address>)

@pytest.fixture(scope='module')
def set_deposit_amount():
    return 2000e18

@pytest.fixture(scope='module')
def load_account():
    account1 = accounts.load('main')
    return account1

There were initially five actions that I wanted to test, which I defined in five unit tests. First, an initial test to check the initial balances to ensure there was not residual DAI or aDAI in any of the relevant wallets that could skew the calculations down the line. Then, does the initial deposit go through correctly, as indicated by the contract balance being equal to the deposit amount defined in the fixtures.

Next, check the funds are deployed into the Aave pool correctly. This is done by calling the contract’s depositFundsToAave() function then querying the various DAI and aDAI balances to ensure they are consistent with expectation (after the function call there should be no DAI in the contract, but the aDAI balance should be >= the deposit amount). Next, there should be instant interest accrual, as represented by the aDAI contract balance exceeding the initial DAI balance. Next, we test unwinding the investment – first by withdrawing from the Aave pool, which should wipe out the aDAI balance but reinstate the DAI balance in the contract, plus interest. Finally, test withdrawing DAI back to the owner’s wallet – after this final function call there should be no funds in the contract at all and the owner’s wallet should have the initial DAI plus some profit.


import pytest
import time
from brownie import (

    interface
)

def check_initial_balances():

    dai = interface.IERC20('0xFf795577d9AC8bD7D90Ee22b6C1703490b6512FD')
    adai = interface.IERC20('0xdCf0aF9e59C002FA3AA091a46196b37530FD48a8')
    contract = getDeployedContract

    assert dai.balanceOf(contract) ==0
    assert adai.balanceOf(contract)==0

    return

def test_initial_deposit(set_deposit_amount, getDeployedContract, load_account):

    dai = interface.IERC20('0xFf795577d9AC8bD7D90Ee22b6C1703490b6512FD')
    contract = getDeployedContract

    initialBalance = dai.balanceOf(contract)
    dai.transfer(contract,set_deposit_amount,{'from':load_account})
    
    time.sleep(10)
    finalBalance = dai.balanceOf(contract)

    assert set_deposit_amount > 0
    assert finalBalance == initialBalance + set_deposit_amount

    return


def test_move_funds_to_aave(set_deposit_amount, getDeployedContract, load_account):

    dai = interface.IERC20('0xFf795577d9AC8bD7D90Ee22b6C1703490b6512FD')
    adai = interface.IERC20('0xdCf0aF9e59C002FA3AA091a46196b37530FD48a8')
    contract = getDeployedContract

    initialDaiBalance = dai.balanceOf(contract)
    initialAdaiBalance = adai.balanceOf(contract)

    assert initialDaiBalance == set_deposit_amount
    assert initialAdaiBalance == 0
    
    contract.depositFundsToAave({'from':load_account})

    time.sleep(10)

    finalDaiBalance = dai.balanceOf(contract)
    finalAdaiBalance = adai.balanceOf(contract)

    assert finalDaiBalance == 0
    assert finalAdaiBalance >= set_deposit_amount
    
    return

def test_profit(set_deposit_amount, getDeployedContract):

    adai = interface.IERC20('0xdCf0aF9e59C002FA3AA091a46196b37530FD48a8')
    contract = getDeployedContract

    assert adai.balanceOf(contract) > set_deposit_amount

    return

def test_withdrawal_from_aave(set_deposit_amount, getDeployedContract, load_account):

    dai = interface.IERC20('0xFf795577d9AC8bD7D90Ee22b6C1703490b6512FD')
    adai = interface.IERC20('0xdCf0aF9e59C002FA3AA091a46196b37530FD48a8')
    contract = getDeployedContract

    contract.WithdrawFundsFromAave({'from': load_account})

    assert dai.balanceOf(contract) >= set_deposit_amount
    assert adai.balanceOf(contract) == 0

    return

def test_withdrawal_from_contract(set_deposit_amount, getDeployedContract, load_account):

    dai = interface.IERC20('0xFf795577d9AC8bD7D90Ee22b6C1703490b6512FD')
    adai = interface.IERC20('0xdCf0aF9e59C002FA3AA091a46196b37530FD48a8')
    contract = getDeployedContract

    initialBalance = dai.balanceOf(load_account)
    initialContractBalance = dai.balanceOf(contract)

    contract.retrieveDAI({'from':load_account})
    time.sleep(10)

    assert dai.balanceOf(contract) == 0
    assert adai.balanceOf(contract) == 0
    assert adai.balanceOf(load_account) == 0
    assert dai.balanceOf(load_account) == (initialBalance + initialContractBalance)

    return

An if everything has gone according to plan, running

$ brownie test --network kovan

will collect 5 items and gradually accumulate 5 green lights indicating the tests have passed.

Summary

This post has demonstrated the creation of a Solidity contract that takes a DAI deposit, moves it to Aave, withdraws it with a small profit and returns the total DAI balance back to the owner’s wallet. Next step is to introduce a second user, representing the incentivized organisation aiming to be rewarded with a fat DAI transfer.

Thanks for reading, please comment with any suggestions or feedback.

One thought on “EOLink 0.2.1: Connecting with AAVE

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s