Aave Integration

Mainnet fork showing how Oval can be used in Aave.

A number of useful unit tests have been created to show how Oval can integrate into Aave v2, Aave v3, and Compound v2. These tests illustrate the minimal required changes needed to these protocols by simply swapping from Chainlink to Oval within the protocol's oracle flow.

Running the Aave integration mainnet fork test

To demonstrate the integration, we will run a mainnet fork test for Aave v3. We have written a script that works by forking Ethereum mainnet before a ETH/USD Chainlink price feed update that catalyzed a liquidation on mainnet. You can find this liquidation in question here and you can use a useful tool like Eigenpi to see the actions of the liquidation here.

To run the script locally you need two things first:

  1. Install foundry. You can get it here

  2. A mainnet RPC URL. Infura will work for this.

Then, run the following commands to clone the quickstart and install the dependencies.

git clone https://github.com/UMAprotocol/oval-quickstart.git
cd oval-quickstart
forge install
forge build

1. Executing the tests

Next, we can run the fork tests. Before doing so we need to set an environment variable so that forge can access historic Ethereum state through the fork. To do this run the following. Note that if you'd set this value within your .env file from one of the other Oval tutorials you can skip this step.

export RPC_MAINNET=https://mainnet.infura.io/v3/YOUR_INFURA_KEY

Next, we execute the tests with the following command:

forge test

Breakdown of tests

The mainnet fork tests illustrate a number of key Oval + Aave function touch points. each are discussed below and walkthrough the key parts of the code.

1. Running the sample Aave liquidations in a forked environment

The first test shows that the sample liquidation in question can be executed within the forked environment. The code below shows the liquidation action:

function testCanExecuteStandardLiquidation() public {
    seedLiquidator();
    //Show that we can execute the liquidation within the fork. Roll to right after the oracle update and execute.
    vm.rollFork(liquidationTx);
    vm.prank(liquidator);
    lendingPool.liquidationCall(address(collateralAsset), address(usdcDebtAsset), user, type(uint256).max, false);

    assertTrue(usdcDebtAsset.balanceOf(liquidator) < amountToMintToLiquidator); // Some amount of USDC spent on the liquidation
    assertTrue(collateralAsset.balanceOf(liquidator) > 0); // Some amount of WETH received from the liquidation

    assertTrue(isPositionHealthy()); // Health factor should be greater than 1 after liquidation.
    }

Breaking down this code we can see the following actions done:

  1. seedLiquidator funds the liquidator wallet.

  2. vm.rollFork moves the fork to right before the liquidation action.

  3. lendingPool.liquidationCall is done right after pranking the liquidator. This is the call to the Aave market that will liquidate the position.

  4. The following assertTrue calls show:

    1. The liquidator spent USDC to repay the liquidated debt

    2. The liquidator received some amount of margin collateral, WETH, for the liquidation

  5. Lastly, the position has a positive health factor after the liquidation.

The second test shows that Chainlink can be replaced with Oval and the liquidation flow continues to work as expected. Importantly, this test showcases that until the unlockLatestValue function is called on Oval the integration will receive a stale value. This is important as it shows that only the winner of the auction (the account who calls right after the unlockLatestValue call) can access the new price. The code for this test is as follows:

function testCanReplaceSourceAndExecuteLiquidation() public {
    seedLiquidator();
    createOvalAndUnlock(); // Deploy an Oval contract and update it.
    setOvalAsAaveSource(); // Update Aave to use Oval contract as its oracle.
    updateChainlinkToLatestValue(); // Roll after the chainlink update. However, dont update Oval instance.

    // Even though chainlink is up to date, Oval is not. This means an attempted liquidation
    // will fail because Oval price is stale. Only once Oval is updated can the liquidation go through.
    vm.prank(liquidator);
    vm.expectRevert(bytes("45")); // 45 corresponds with position health being above 1.
    lendingPool.liquidationCall(address(collateralAsset), address(usdcDebtAsset), user, type(uint256).max, false);

    //Now, unlock Oval and show that the liquidation can be executed.
    vm.prank(permissionedUnlocker);
    oval.unlockLatestValue();
    (, int256 latestAnswer,, uint256 latestTimestamp,) = sourceChainlinkOracle.latestRoundData();
    assertTrue(oval.latestAnswer() == latestAnswer && oval.latestTimestamp() == latestTimestamp);
    assertTrue(aaveOracle.getAssetPrice(address(collateralAsset)) == uint256(oval.latestAnswer()));
    assertFalse(isPositionHealthy()); // Post update but pre-liquidation position should be underwater.

    vm.prank(liquidator); // Run the liquidation from the liquidator.
    lendingPool.liquidationCall(address(collateralAsset), address(usdcDebtAsset), user, type(uint256).max, false);
    assertTrue(isPositionHealthy()); // Post liquidation position should be healthy again.
}

Breaking down this code we can see the following actions done:

  1. seedLiquidator to give them funds to run the liquidation.

  2. createOvalAndUnlock deploys the Oval instance and update it to contain the latest value from chainlink. This Oval instance is pointed to the associated Chainlink contract Aave is using for this market.

  3. setOvalAsAaveSource changes the Aave configuration to use the Oval instance as its oracle for this price feed.

  4. updateChainlinkToLatestValue rolls forward the fork to the point past where Chainlink is updated. Note, though, that Oval will still return an old value at this point as no auction has been run and lockWindow has not yet passed.

  5. Next, we enter into the liquidation flow. First the test tries to run a liquidation without having run an Oval auction. This is shown by checking that the liquidationCall reverts with an error code of 45, indicating that the position cant be liquidated due to having a health above 1. This means that the Chainlink update has not yet been applied and the position is not yet liquidatable.

  6. The subsequent assertions show that Chainlink has updated but Oval is still returning old values.

  7. Next, the Oval auction is run. This is done form the permissionedUnlockers account by calling unlockLatestValue on the Oval contract. This action would occur within an auction normally.

  8. The liquidation is now attempted again. This time the liquidation goes through as the Oval contract releases the latest value.

 function testOvalGracefullyFallsBackToSourceIfNoUnlockApplied() public {
    seedLiquidator();
    createOvalAndUnlock(); // Deploy an Oval contract and update it.
    setOvalAsAaveSource(); // Update Aave to use Oval contract as its oracle.
    updateChainlinkToLatestValue(); // Roll after the chainlink update. However, dont update Oval instance.

    // Even though the Chainlink is up to date, Oval is not. This means an attempted liquidation
    // will fail because Oval price is stale. Only once Oval is updated can the liquidation go through.
    vm.prank(liquidator);
    vm.expectRevert(bytes("45")); // 45 corresponds with position health being above 1.
    lendingPool.liquidationCall(address(collateralAsset), address(usdcDebtAsset), user, type(uint256).max, false);

    // To show that we can gracefully fall back to the Chainlink, we will not unlock Oval and
    // rather advance time past the lock window. This will cause Oval to fall back to Chainlink
    // and the liquidation will succeed without Oval being unlocked.
    vm.warp(block.timestamp + oval.lockWindow() + 1);

    // We should see the accessors return the same values, even though the internal values are different.
    (, int256 latestAnswer,, uint256 latestTimestamp,) = sourceChainlinkOracle.latestRoundData();
    assertTrue(oval.latestAnswer() == latestAnswer && oval.latestTimestamp() == latestTimestamp);
    assertFalse(isPositionHealthy()); // Post update but pre-liquidation position should be underwater.

    // Now, run the liquidation. It should succeed without Oval being unlocked due to the fallback.
    vm.prank(liquidator);
    lendingPool.liquidationCall(address(collateralAsset), address(usdcDebtAsset), user, type(uint256).max, false);
    assertTrue(isPositionHealthy()); // Post liquidation position should be healthy again.
}

Breaking down this code we can see the following actions done:

  1. Similar to the previous code we start by seeding the liquidator, creating an oval instance, setting it as the source and updating the Chainlink value. We are now ready to show the core logic within this test.

  2. Next, we attempt a liquidation. Like before, it fails as the Oval price is returning a stale value.

  3. After this, we show that if you move just over the Oval lockWindow by using the vm.warp function the Oval contract will now gracefully unlock, releasing the latest price.

  4. This is shown by first showing that the latest value has been released and second by running the liquidation with liquidationCall and showing that the position is now healthy (liquidated).

Last updated