Gelato-automations/test/1_mv-DAI-DSR-Compound.test.js

397 lines
14 KiB
JavaScript

// running `npx hardhat test` automatically makes use of hardhat-waffle plugin
// => only dependency we need is "chai"
const {expect} = require("chai");
const hre = require("hardhat");
const {deployments, ethers} = hre;
const GelatoCoreLib = require("@gelatonetwork/core");
//const { sleep } = GelatoCoreLib;
// Constants
const DAI_100 = ethers.utils.parseUnits("100", 18);
// Contracts
const InstaIndex = require("../pre-compiles/InstaIndex.json");
const InstaList = require("../pre-compiles/InstaList.json");
const InstaAccount = require("../pre-compiles/InstaAccount.json");
const ConnectAuth = require("../pre-compiles/ConnectAuth.json");
const ConnectGelato_ABI = require("../pre-compiles/ConnectGelato_ABI.json");
const ConnectMaker = require("../pre-compiles/ConnectMaker.json");
const ConnectCompound = require("../pre-compiles/ConnectCompound.json");
const IERC20 = require("../pre-compiles/IERC20.json");
const IUniswapExchange = require("../pre-compiles/IUniswapExchange.json");
describe("Move DAI lending from DSR to Compound", function () {
this.timeout(0);
if (hre.network.name !== "hardhat") {
console.error("Test Suite is meant to be run on hardhat only");
process.exit(1);
}
// Wallet to use for local testing
let userWallet;
let userAddress;
let dsaAddress;
// Deployed instances
let connectMaker;
let connectCompound;
let gelatoCore;
let dai;
// Contracts to deploy and use for local testing
let dsa;
let mockDSR;
let mockCDAI;
let conditionCompareUints;
before(async function () {
// Reset back to a fresh forked state during runtime
await deployments.fixture();
// Get Test Wallet for local testnet
[userWallet] = await ethers.getSigners();
userAddress = await userWallet.getAddress();
// Hardhat default accounts prefilled with 100 ETH
expect(await userWallet.getBalance()).to.be.gt(
ethers.utils.parseEther("10")
);
// ===== DSA SETUP ==================
const instaIndex = await ethers.getContractAt(
InstaIndex.abi,
hre.network.config.InstaIndex
);
const instaList = await ethers.getContractAt(
InstaList.abi,
hre.network.config.InstaList
);
connectMaker = await ethers.getContractAt(
ConnectMaker.abi,
hre.network.config.ConnectMaker
);
connectCompound = await ethers.getContractAt(
ConnectCompound.abi,
hre.network.config.ConnectCompound
);
// Deploy DSA and get and verify ID of newly deployed DSA
const dsaIDPrevious = await instaList.accounts();
await expect(instaIndex.build(userAddress, 1, userAddress)).to.emit(
instaIndex,
"LogAccountCreated"
);
const dsaID = dsaIDPrevious.add(1);
await expect(await instaList.accounts()).to.be.equal(dsaID);
// Instantiate the DSA
dsaAddress = await instaList.accountAddr(dsaID);
dsa = await ethers.getContractAt(InstaAccount.abi, dsaAddress);
// ===== GELATO SETUP ==================
gelatoCore = await ethers.getContractAt(
GelatoCoreLib.GelatoCore.abi,
hre.network.config.GelatoCore
);
// Add GelatoCore as auth on DSA
const addAuthData = await hre.run("abi-encode-withselector", {
abi: ConnectAuth.abi,
functionname: "add",
inputs: [gelatoCore.address],
});
await dsa.cast(
[hre.network.config.ConnectAuth],
[addAuthData],
userAddress
);
expect(await dsa.isAuth(gelatoCore.address)).to.be.true;
// Deployed Mocks for Testing
mockCDAI = await ethers.getContract("MockCDAI");
mockDSR = await ethers.getContract("MockDSR");
// Deployed Gelato Conditions for Testing
conditionCompareUints = await ethers.getContract(
"ConditionCompareUintsFromTwoSources"
);
// ===== Dapp Dependencies SETUP ==================
// This test assumes our user has 100 DAI deposited in Maker DSR
dai = await ethers.getContractAt(IERC20.abi, hre.network.config.DAI);
expect(await dai.balanceOf(userAddress)).to.be.equal(0);
// Let's get the test user 100 DAI++ from Kyber
const daiUniswapExchange = await ethers.getContractAt(
IUniswapExchange.abi,
hre.network.config.DAI_UNISWAP
);
await daiUniswapExchange.ethToTokenTransferInput(
1,
2525644800, // random timestamp in the future (year 2050)
userAddress,
{
value: ethers.utils.parseEther("2"),
}
);
expect(await dai.balanceOf(userAddress)).to.be.gte(DAI_100);
// Next we transfer the 100 DAI into our DSA
await dai.transfer(dsa.address, DAI_100);
expect(await dai.balanceOf(dsa.address)).to.be.eq(DAI_100);
// Next we deposit the 100 DAI into the DSR
const depositDai = await hre.run("abi-encode-withselector", {
abi: ConnectMaker.abi,
functionname: "depositDai",
inputs: [DAI_100, 0, 0],
});
await expect(
dsa.cast([hre.network.config.ConnectMaker], [depositDai], userAddress)
)
.to.emit(dsa, "LogCast")
.withArgs(userAddress, userAddress, 0);
expect(await dai.balanceOf(dsa.address)).to.be.eq(0);
});
it("#1: Gelato refinances DAI from DSR=>Compound, if better rate", async function () {
// ======= Condition setup ======
// We instantiate the Rebalance Condition:
// Compound APY needs to be 10000000 per second points higher than DSR
const MIN_SPREAD = "10000000";
const rebalanceCondition = new GelatoCoreLib.Condition({
inst: conditionCompareUints.address,
data: await conditionCompareUints.getConditionData(
mockCDAI.address, // We are in DSR so we compare against CDAI => SourceA=CDAI
mockDSR.address, // SourceB=DSR
await hre.run("abi-encode-withselector", {
abi: (await hre.artifacts.readArtifact("MockCDAI")).abi,
functionname: "supplyRatePerSecond",
}), // CDAI data feed first (sourceAData)
await hre.run("abi-encode-withselector", {
abi: (await hre.artifacts.readArtifact("MockDSR")).abi,
functionname: "dsr",
}), // DSR data feed second (sourceBData)
MIN_SPREAD
),
});
// ======= Action/Spells setup ======
// To assimilate to DSA SDK
const spells = [];
// We instantiate target1: Withdraw DAI from DSR and setId 1 for
// target2 Compound deposit to fetch DAI amount.
const connectorWithdrawFromDSR = new GelatoCoreLib.Action({
addr: connectMaker.address,
data: await hre.run("abi-encode-withselector", {
abi: ConnectMaker.abi,
functionname: "withdrawDai",
inputs: [ethers.constants.MaxUint256, 0, 1],
}),
operation: GelatoCoreLib.Operation.Delegatecall,
});
spells.push(connectorWithdrawFromDSR);
// We instantiate target2: Deposit DAI to CDAI and getId 1
const connectorDepositCompound = new GelatoCoreLib.Action({
addr: connectCompound.address,
data: await hre.run("abi-encode-withselector", {
abi: ConnectCompound.abi,
functionname: "deposit",
inputs: [dai.address, 0, 1, 0],
}),
operation: GelatoCoreLib.Operation.Delegatecall,
});
spells.push(connectorDepositCompound);
// ======= Gelato Task Setup =========
// A Gelato Task just combines Conditions with Actions
// You also specify how much GAS a Task consumes at max and the ceiling
// gas price under which you are willing to auto-transact. There is only
// one gas price in the current Gelato system: fast gwei read from Chainlink.
const GAS_LIMIT = "4000000";
const GAS_PRICE_CEIL = ethers.utils.parseUnits("1000", "gwei");
const taskRebalanceDSRToCDAIifBetter = new GelatoCoreLib.Task({
conditions: [rebalanceCondition],
actions: spells,
selfProviderGasLimit: GAS_LIMIT,
selfProviderGasPriceCeil: GAS_PRICE_CEIL,
});
// ======= Gelato Provider setup ======
// Someone needs to pay for gas for automatic Task execution on Gelato.
// Gelato has the concept of a "Provider" to denote who is providing (depositing)
// ETH on Gelato in order to pay for automation gas. In our case, the User
// is paying for his own automation gas. Therefore, the User is a "Self-Provider".
// But since Gelato only talks to smart contract accounts, the User's DSA proxy
// plays the part of the "Self-Provider" on behalf of the User behind the DSA.
// A GelatoProvider is an object with the address of the provider - in our case
// the DSA address - and the address of the "ProviderModule". This module
// fulfills certain functions like encoding the execution payload for the Gelato
// protocol. Check out ./contracts/ProviderModuleDsa.sol to see what it does.
const gelatoSelfProvider = new GelatoCoreLib.GelatoProvider({
addr: dsa.address,
module: hre.network.config.ProviderModuleDsa,
});
// ======= Executor Setup =========
// For local Testing purposes our test User account will play the role of the Gelato
// Executor network because this logic is non-trivial to fork into a local instance
await gelatoCore.stakeExecutor({
value: await gelatoCore.minExecutorStake(),
});
expect(await gelatoCore.isExecutorMinStaked(userAddress)).to.be.true;
// ======= Gelato Task Provision =========
// Gelato requires some initial setup via its multiProvide API
// We must 1) provide ETH to pay for future automation gas, 2) we must
// assign an Executor network to the Task, 3) we must tell Gelato what
// "ProviderModule" we want to use for our Task.
// Since our DSA proxy is the one through which we interact with Gelato,
// we must do this setup via the DSA proxy by using ConnectGelato
const TASK_AUTOMATION_FUNDS = await gelatoCore.minExecProviderFunds(
GAS_LIMIT,
GAS_PRICE_CEIL
);
await dsa.cast(
[hre.network.config.ConnectGelato], // targets
[
await hre.run("abi-encode-withselector", {
abi: ConnectGelato_ABI,
functionname: "multiProvide",
inputs: [
userAddress,
[],
[hre.network.config.ProviderModuleDsa],
TASK_AUTOMATION_FUNDS,
0, // _getId
0, // _setId
],
}),
], // datas
userAddress, // origin
{
value: TASK_AUTOMATION_FUNDS,
gasLimit: 5000000,
}
);
expect(await gelatoCore.providerFunds(dsa.address)).to.be.gte(
TASK_AUTOMATION_FUNDS
);
expect(
await gelatoCore.isProviderLiquid(dsa.address, GAS_LIMIT, GAS_PRICE_CEIL)
);
expect(await gelatoCore.executorByProvider(dsa.address)).to.be.equal(
userAddress
);
expect(
await gelatoCore.isModuleProvided(
dsa.address,
hre.network.config.ProviderModuleDsa
)
).to.be.true;
// ======= 📣 TASK SUBMISSION 📣 =========
// In Gelato world our DSA is the User. So we must submit the Task
// to Gelato via our DSA and hence use ConnectGelato again.
const expiryDate = 0;
await expect(
dsa.cast(
[hre.network.config.ConnectGelato], // targets
[
await hre.run("abi-encode-withselector", {
abi: ConnectGelato_ABI,
functionname: "submitTask",
inputs: [
gelatoSelfProvider,
taskRebalanceDSRToCDAIifBetter,
expiryDate,
],
}),
], // datas
userAddress, // origin
{
gasLimit: 5000000,
}
)
).to.emit(gelatoCore, "LogTaskSubmitted");
// Task Receipt: a successfully submitted Task in Gelato
// is wrapped in a TaskReceipt. For testing we instantiate the TaskReceipt
// for our to be submitted Task.
const taskReceiptId = await gelatoCore.currentTaskReceiptId();
const taskReceipt = new GelatoCoreLib.TaskReceipt({
id: taskReceiptId,
userProxy: dsa.address,
provider: gelatoSelfProvider,
tasks: [taskRebalanceDSRToCDAIifBetter],
expiryDate,
});
// ======= 📣 TASK EXECUTION 📣 =========
// This stuff is normally automated by the Gelato Network and Dapp Developers
// and their Users don't have to take care of it. However, for local testing
// we simulate the Gelato Execution logic.
// First we fetch the gelatoGasPrice as fed by ChainLink oracle. Gelato
// allows Users to specify a maximum fast gwei gas price for their Tasks
// to remain executable up until.
const gelatoGasPrice = await hre.run("fetchGelatoGasPrice");
expect(gelatoGasPrice).to.be.lte(
taskRebalanceDSRToCDAIifBetter.selfProviderGasPriceCeil
);
// Let's first check if our Task is executable. Since both MockDSR and MockCDAI
// are deployed with a normalized per second rate of APY_2_PERCENT_IN_SECONDS
// (1000000000627937192491029810 in 10**27 precision) in both of them, we
// expect ConditionNotOk because ANotGreaterOrEqualToBbyMinspread.
// Check out contracts/ConditionCompareUintsFromTwoSources.sol to see how
// how the comparison of MockDSR and MockCDAI is implemented in Condition code.
expect(
await gelatoCore.canExec(
taskReceipt,
taskRebalanceDSRToCDAIifBetter.selfProviderGasLimit,
gelatoGasPrice
)
).to.be.equal("ConditionNotOk:ANotGreaterOrEqualToBbyMinspread");
// We defined a MIN_SPREAD of 10000000 points in the per second rate
// for our ConditionCompareUintsFromTwoSources. So we now
// set the CDAI.supplyRatePerSecond to be 10000000 higher than MockDSR.dsr
// and expect it to mean that our Task becomes executable.
await mockCDAI.setSupplyRatePerSecond(
(await mockDSR.dsr()).add(MIN_SPREAD)
);
expect(
await gelatoCore.canExec(
taskReceipt,
taskRebalanceDSRToCDAIifBetter.selfProviderGasLimit,
gelatoGasPrice
)
).to.be.equal("OK");
// To verify whether the execution of DSR=>CDAI has been successful in this Testing
// we look at changes in the CDAI balance of the DSA
const cDAI = await ethers.getContractAt(
IERC20.abi,
hre.network.config.CDAI
);
const dsaCDAIBefore = await cDAI.balanceOf(dsa.address);
// For testing we now simulate automatic Task Execution ❗
await expect(
gelatoCore.exec(taskReceipt, {
gasPrice: gelatoGasPrice, // Exectutor must use gelatoGasPrice (Chainlink fast gwei)
gasLimit: taskRebalanceDSRToCDAIifBetter.selfProviderGasLimit,
})
).to.emit(gelatoCore, "LogExecSuccess");
// Since the Execution was successful, we now expect our DSA to hold more
// CDAI then before. This concludes our testing.
expect(await cDAI.balanceOf(dsa.address)).to.be.gt(dsaCDAIBefore);
});
});