Writing new integrations

Writing an integration is a process that requires writing data that describes assets that use that integration and code to deal with fetching price and generating transactions to enter/exit a position. In general terms, these steps are fundamental:

  • Devising a name for the asset's type

  • Adding asset descriptions

  • Adding ABIs for all relevant contracts

  • Writing a concrete InterfaceStrategy for the integration

  • Writing end-to-end tests

Some optional actions:

  • Writing scripts to populate and update assets according to external sources

  • Writing unit tests for individual parts of the integration

Create a class for new asset type

The main element in order to create an integration supporting a new asset type is a class, namely a strategy to deal with all operations that involve that asset type.

The structure of this class is specified in InterfaceStrategy. The stub of an implementation looks like this:

import {
  FetchPriceDataParams,
  GetPriceParams,
  GenerateStepParams,
} from "./InterfaceStrategy";
import { InterfaceStrategy } from "./InterfaceStrategy";

export class MyNewDepositStrategy extends InterfaceStrategy {
  fetchPriceData({ provider, assetStore, asset }: FetchPriceDataParams) {
  }

  getPrice({ assetStore, asset, requestTree }: GetPriceParams) {
  }

  async generateStep({
    assetAllocation,
    assetStore,
    walletAddress,
    chainId,
    value,
    currentAllocation,
    routerOperation,
  }: GenerateStepParams) {
}

fetchPriceData

This function takes the following parameters:

The expected return is a RequestTree object that returns Promises for all data that needs to be fetched in order to calculate that asset's price.

Example #1: asset that depends on a linked asset

An asset that merely depends on the price data of the underlying asset can do so using fetchPriceData passing the same provider and assetStore, but specifying the linked asset.

In other words, this asset doesn't need to fetcch any data on its own, but delegates it down to its linked asset.

// This examples is from the AaveV2 integration

fetchPriceData({ provider, assetStore, asset }: FetchPriceDataParams) {
  const linkedAsset = assetStore.getAssetById(asset.linkedAssets[0].assetId);

  const requestTree: RequestTree = fetchPriceData({
    provider,
    assetStore,
    asset: linkedAsset,
  });
  return requestTree;
}

Example #2: asset that needs fetching some data

Another case is an asset that needs to fetch some data about itself in order to subsequently calculate the price.

// This example is from the Beefy integration

fetchPriceData({ provider, assetStore, asset }: FetchPriceDataParams) {
  const linkedAsset = assetStore.getAssetById(asset.linkedAssets[0].assetId);

  const pool = new Contract(asset.address, IBeefyVaultV6, provider);

  let requestTree: RequestTree = {};

  requestTree[asset.address] = {};
  requestTree[asset.address].underlyingAmount = () => pool.balance();
  requestTree[asset.address].supply = () => pool.totalSupply();

  const fetchedData = fetchPriceData({
    provider,
    assetStore,
    asset: linkedAsset,
  });
  requestTree = {
    ...requestTree,
    ...fetchedData,
  };
  return requestTree;
}

In the above example. the requestTree will be resolved to get:

  • underlyingAmount from the pool (invoked using its balance function)

  • supply from the pool (invoked using its totalSupply function)

  • All requestData required by underlying asset.

getPrice

The getPrice function takes three parameters:

  • assetStore: AssetStore

  • asset: Asset

  • requestTree: RequestTree - with the resolved promises for all fetched assets.

The expected return is a number with the price.

Example #1: asset that depends on a linked asset

An asset that depends on the price data of the underlying asset just has to invoke and getPrice for that linked asset.

// This examples is from the AaveV2 integration

getPrice({ assetStore, asset, requestTree }: GetPriceParams) {
  const linkedAsset = assetStore.getAssetById(asset.linkedAssets[0].assetId);

  return getPrice({ assetStore, asset: linkedAsset, requestTree });
}

Example #2: asset that needed fetching data

If the getPrice requires using data that was requested on the previous step, this data will be available on the requestTree parameter, with the same key (generally asset.address) and key (whatever it was called on fetchPriceData).

// This example is from the Beefy integration

getPrice({ assetStore, asset, requestTree }: GetPriceParams) {
  const linkedAsset = assetStore.getAssetById(asset.linkedAssets[0].assetId);

  const amount = getAmount({
    amount: requestTree[asset.address].underlyingAmount,
    decimals: linkedAsset.decimals,
  });
  const supply = getAmount({
    amount: requestTree[asset.address].supply,
    decimals: asset.decimals,
  });

  return (
    (amount * getPrice({ assetStore, asset: linkedAsset, requestTree })) /
    supply
  );
}

generateStep

This step, the core of the transaction generation, is where information regarding the operation is processed to generate all the steps required. Each step is a call to a smart contract.

These are the steps that will typically be performed:

  • Get all assets relevant to the operation. This is done using the assetStore.

  • Determine which stores will be needed. This is done using routerOperation.stores.findOrInitializeStoreIdx.

  • Evaluate whether the position is being entered or exited. This is done by evaluating the value of assetAllocation.fraction.

  • Update the estimated allocation for all assets using currentAllocation.updateFraction.

  • Add steps for each smart contract interation required using routerOperation.addStep.

Example #1

// This examples is from the AaveV2 integration

async generateStep({
  assetAllocation,
  assetStore,
  walletAddress,
  chainId,
  value,
  currentAllocation,
  routerOperation,
}: GenerateStepParams) {
  const asset = assetStore.getAssetById(assetAllocation.assetId);
  if (asset.linkedAssets.length != 1) {
    throw new Error(
      `AaveV2DepositStrategy: asset ${asset.id} should have exactly one linked asset`
    );
  }
  const linkedAsset = assetStore.getAssetById(asset.linkedAssets[0].assetId);

  let lendingPoolAddress;
  let incentivesControllerAddress;
  if (chainId === 137) {
    lendingPoolAddress = "0x8dFf5E27EA6b7AC08EbFdf9eB090F32ee9a30fcf";
    incentivesControllerAddress =
      "0x357D51124f59836DeD84c8a1730D72B749d8BC23";
  } else {
    throw new Error(
      `AaveV2DepositStrategy: not implemented for chain ${chainId}`
    );
  }

  const storeNumberAToken = routerOperation.stores.findOrInitializeStoreIdx({
    assetId: asset.id,
  });
  const storeNumberToken = routerOperation.stores.findOrInitializeStoreIdx({
    assetId: linkedAsset.id,
  });

  if (assetAllocation.fraction > 0) {
    const currentFraction = currentAllocation.getAssetById({
      assetId: linkedAsset.id,
    }).fraction;
    const newFraction = asset.linkedAssets[0].fraction / currentFraction;
    const variation = currentFraction * newFraction;

    currentAllocation.updateFraction({
      assetId: linkedAsset.id,
      delta: -variation,
    });
    currentAllocation.updateFraction({
      assetId: asset.id,
      delta: variation,
    });

    routerOperation.addStep({
      stepAddress: linkedAsset.address,
      encodedFunctionData: IERC20.encodeFunctionData("approve", [
        lendingPoolAddress,
        MAGIC_REPLACERS[0],
      ]),
      storeOperations: [
        {
          storeOpType: StoreOpType.RetrieveStoreAssignCall,
          storeNumber: storeNumberToken,
          offsetReplacer: { replacer: MAGIC_REPLACERS[0], occurrence: 0 },
          fraction: Math.round(FRACTION_MULTIPLIER * newFraction),
        },
      ],
    });

    routerOperation.addStep({
      stepAddress: lendingPoolAddress,
      encodedFunctionData: LendingPool.encodeFunctionData("deposit", [
        linkedAsset.address, // assetIn
        MAGIC_REPLACERS[0], // amount
        walletAddress, // onBehalfOf
        0, // referralCode
      ]),
      storeOperations: [
        {
          storeOpType: StoreOpType.RetrieveStoreAssignCallSubtract,
          storeNumber: storeNumberToken,
          offsetReplacer: { replacer: MAGIC_REPLACERS[0], occurrence: 0 },
          fraction: Math.round(newFraction * FRACTION_MULTIPLIER),
        },
        {
          storeOpType: StoreOpType.AddStoreToStore,
          storeNumber: storeNumberAToken,
          secondaryStoreNumber: storeNumberToken,
          fraction: Math.round(newFraction * FRACTION_MULTIPLIER),
        },
      ],
    });
  } else if (assetAllocation.fraction < 0) {
    const storeNumberReward = routerOperation.stores.findOrInitializeStoreIdx(
      {
        tmpStoreName: `${asset.id} reward store`,
      }
    );

    const currentFraction = currentAllocation.getAssetById({
      assetId: asset.id,
    }).fraction;
    const newFraction = -assetAllocation.fraction / currentFraction;
    const variation = newFraction * currentFraction;

    asset.linkedAssets.map((la, i) => {
      currentAllocation.updateFraction({
        assetId: la.assetId,
        delta: variation * la.fraction,
      });
      currentAllocation.updateFraction({
        assetId: asset.id,
        delta: -variation * la.fraction,
      });
    });

    routerOperation.addStep({
      stepAddress: lendingPoolAddress,
      encodedFunctionData: LendingPool.encodeFunctionData("withdraw", [
        linkedAsset.address, // asset
        MAGIC_REPLACERS[0], // amount
        walletAddress, // to
      ]),
      encodedFunctionResult: LendingPool.encodeFunctionResult("withdraw", [
        MAGIC_REPLACERS[0],
      ]),
      storeOperations: [
        {
          storeOpType: StoreOpType.RetrieveStoreAssignCallSubtract,
          storeNumber: storeNumberAToken,
          offsetReplacer: { replacer: MAGIC_REPLACERS[0], occurrence: 0 },
          fraction: Math.round(newFraction * FRACTION_MULTIPLIER),
        },
        {
          storeOpType: StoreOpType.RetrieveResultAddStore,
          storeNumber: storeNumberToken,
          offsetReplacer: { replacer: MAGIC_REPLACERS[0], occurrence: 0 },
          fraction: FRACTION_MULTIPLIER,
        },
      ],
    });

    routerOperation.addStep({
      stepAddress: incentivesControllerAddress,
      encodedFunctionData: AaveIncentivesController.encodeFunctionData(
        "getRewardsBalance",
        [
          [asset.address], // assets
          walletAddress, // to
        ]
      ),
      encodedFunctionResult: AaveIncentivesController.encodeFunctionResult(
        "getRewardsBalance",
        [MAGIC_REPLACERS[0]]
      ),
      storeOperations: [
        {
          storeOpType: StoreOpType.RetrieveResultAddStore,
          storeNumber: storeNumberReward,
          offsetReplacer: { replacer: MAGIC_REPLACERS[0], occurrence: 0 },
          fraction: FRACTION_MULTIPLIER,
        },
      ],
    });

    routerOperation.addStep({
      stepAddress: incentivesControllerAddress,
      encodedFunctionData: AaveIncentivesController.encodeFunctionData(
        "claimRewards",
        [
          [asset.address], // assets
          MAGIC_REPLACERS[0], // amount
          walletAddress, // to
        ]
      ),
      encodedFunctionResult: AaveIncentivesController.encodeFunctionResult(
        "claimRewards",
        [MAGIC_REPLACERS[0]]
      ),
      storeOperations: [
        {
          storeOpType: StoreOpType.RetrieveStoreAssignCall,
          storeNumber: storeNumberReward,
          offsetReplacer: { replacer: MAGIC_REPLACERS[0], occurrence: 0 },
          fraction: FRACTION_MULTIPLIER,
        },
      ],
    });
  }

  return routerOperation;
}

Add assets

See adding assets

Last updated