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 integrationWriting 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:
provider: Provider
: an Ethers.js providerassetStore: AssetStore
: as described on architectural overviewasset: Asset
: as described on architectural overview
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 itsbalance
function)supply
from the pool (invoked using itstotalSupply
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