diff --git a/contracts/mainnet/connectors/yearn_v2/events.sol b/contracts/mainnet/connectors/yearn_v2/events.sol new file mode 100644 index 00000000..f289044b --- /dev/null +++ b/contracts/mainnet/connectors/yearn_v2/events.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.7.0; + +contract Events { + event LogDeposit(address indexed vault, uint256 shareAmt, uint256 depositAmt, uint256 getId, uint256 setId); + event LogWithdraw(address indexed recipient, uint256 shareAmt, uint256 withdrawAmt, uint256 getId, uint256 setId); +} \ No newline at end of file diff --git a/contracts/mainnet/connectors/yearn_v2/interface.sol b/contracts/mainnet/connectors/yearn_v2/interface.sol new file mode 100644 index 00000000..b72e6181 --- /dev/null +++ b/contracts/mainnet/connectors/yearn_v2/interface.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.7.0; + +interface YearnV2Interface { + function deposit(uint256 amount, address recipient) external returns (uint256); + + function withdraw(uint256 maxShares, address recipient) external returns (uint256); + + function token() external view returns (address); + + function balanceOf(address owner) external view returns (uint256); +} + diff --git a/contracts/mainnet/connectors/yearn_v2/main.sol b/contracts/mainnet/connectors/yearn_v2/main.sol new file mode 100644 index 00000000..c8a238f3 --- /dev/null +++ b/contracts/mainnet/connectors/yearn_v2/main.sol @@ -0,0 +1,86 @@ +pragma solidity ^0.7.0; + +/** + * @title Yearn V2. + * @dev Vaults & yield. + */ + +import { TokenInterface } from "../../common/interfaces.sol"; +import { Basic } from "../../common/basic.sol"; +import { Events } from "./events.sol"; +import { YearnV2Interface } from "./interface.sol"; + +abstract contract YearnResolver is Events, Basic { + /** + * @dev Deposit funds in the vault, issuing shares to recipient. + * @notice This will deposit funds to a specific Yearn Vault. + * @param vault The address of the vault to deposit funds into. + * @param amt The amount of tokens to deposit. + * @param getId ID to retrieve amt. + * @param setId ID stores the amount of shares received. + */ + function deposit( + address vault, + uint256 amt, + uint256 getId, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amt); + + YearnV2Interface yearn = YearnV2Interface(vault); + + address want = yearn.token(); + bool iswETH = want == wethAddr; + TokenInterface tokenContract = TokenInterface(want); + + if (iswETH) { + _amt = _amt == uint(-1) ? address(this).balance : _amt; + convertEthToWeth(iswETH, tokenContract, _amt); + } else { + _amt = _amt == uint(-1) ? tokenContract.balanceOf(address(this)) : _amt; + } + + approve(tokenContract, vault, _amt); + + uint256 _shares = yearn.deposit(_amt, address(this)); + setUint(setId, _shares); + + _eventName = "LogDeposit(address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(vault, _shares, _amt, getId, setId); + } + + /** + * @dev Withdraw shares from the vault. + * @notice This will withdraw the share from a specific Yearn Vault. + * @param vault The address of the vault to withdraw shares from. + * @param amt The amount of shares to withdraw. + * @param getId ID to retrieve amt. + * @param setId ID stores the amount want token redeemed. + */ + function withdraw( + address vault, + uint256 amt, + uint256 getId, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amt); + + YearnV2Interface vault = YearnV2Interface(vault); + + + _amt = _amt == uint(-1) ? vault.balanceOf(address(this)) : _amt; + uint256 _wantRedeemed = vault.withdraw(_amt, address(this)); + setUint(setId, _wantRedeemed); + + TokenInterface tokenContract = TokenInterface(vault.token()); + bool isWEth = vault.token() == wethAddr; + convertWethToEth(isWEth, tokenContract, _amt); + + _eventName = "LogWithdraw(address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(vault, _amt, _wantRedeemed, getId, setId); + } +} + +contract ConnectV2YearnV2 is YearnResolver { + string public constant name = "YearnV2-v1.0"; +} diff --git a/hardhat.config.js b/hardhat.config.js index 44456a48..c691b521 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -57,7 +57,7 @@ module.exports = { hardhat: { forking: { url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_ID}`, - blockNumber: 13180514, + blockNumber: 12796965, }, blockGasLimit: 12000000, gasPrice: parseInt(utils.parseUnits("300", "gwei")) @@ -79,4 +79,4 @@ module.exports = { mocha: { timeout: 100 * 1000, }, -}; +}; \ No newline at end of file diff --git a/test/yearn/yearn.test.js b/test/yearn/yearn.test.js new file mode 100644 index 00000000..409161b0 --- /dev/null +++ b/test/yearn/yearn.test.js @@ -0,0 +1,142 @@ +const { expect } = require("chai"); +const hre = require("hardhat"); +const { waffle, ethers } = hre; +const { provider } = waffle + +const deployAndEnableConnector = require("../../scripts/deployAndEnableConnector.js") +const buildDSAv2 = require("../../scripts/buildDSAv2") +const encodeSpells = require("../../scripts/encodeSpells.js") +const getMasterSigner = require("../../scripts/getMasterSigner") + +const tokens = require("../../scripts/constant/tokens"); +const addresses = require("../../scripts/constant/addresses"); +const abis = require("../../scripts/constant/abis"); +const connectV2YearnArtifacts = require("../../artifacts/contracts/mainnet/connectors/yearn_v2/main.sol/ConnectV2YearnV2.json") + +const toBytes32 = (bn) => { + return ethers.utils.hexlify(ethers.utils.zeroPad(bn.toHexString(), 32)); +}; +const setStorageAt = async (address, index, value) => { + await ethers.provider.send("hardhat_setStorageAt", [address, index, value]); + await ethers.provider.send("evm_mine", []); // Just mines to the next block +}; + +describe("Yearn", function () { + const connectorName = "YEARN-TEST-A" + + let dsaWallet0 + let masterSigner; + let instaConnectorsV2; + let connector; + + const wallets = provider.getWallets() + const [wallet0, wallet1, wallet2, wallet3] = wallets + before(async () => { + masterSigner = await getMasterSigner(wallet3) + instaConnectorsV2 = await ethers.getContractAt(abis.core.connectorsV2, addresses.core.connectorsV2); + connector = await deployAndEnableConnector({ + connectorName, + contractArtifact: connectV2YearnArtifacts, + signer: masterSigner, + connectors: instaConnectorsV2 + }) + console.log("Connector address", connector.address) + }) + + it("Should have contracts deployed.", async function () { + expect(!!instaConnectorsV2.address).to.be.true; + expect(!!connector.address).to.be.true; + expect(!!masterSigner.address).to.be.true; + }); + + describe("DSA wallet setup", function () { + it("Should build DSA v2", async function () { + dsaWallet0 = await buildDSAv2(wallet0.address) + expect(!!dsaWallet0.address).to.be.true; + }); + + it("Deposit ETH into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("10")); + }); + }); + + describe("Main", function () { + + it("Should increase the DAI balance to 100 DAI", async function () { + const DAI = new ethers.Contract(tokens.dai.address, abis.basic.erc20, ethers.provider); + const DAI_SLOT = 2; + const locallyManipulatedBalance = ethers.utils.parseEther("100"); + + // Get storage slot index + const index = ethers.utils.solidityKeccak256( + ["uint256", "uint256"], + [dsaWallet0.address, DAI_SLOT] + ); + // Manipulate local balance (needs to be bytes32 string) + await setStorageAt( + tokens.dai.address, + index.toString(), + toBytes32(locallyManipulatedBalance).toString() + ); + + // Get DAI balance + const balance = await DAI.balanceOf(dsaWallet0.address); + expect(await ethers.BigNumber.from(balance).eq(ethers.utils.parseEther("100"))); + }); + + it("Should deposit and withdraw 50 DAI in/out the Yearn Vault", async function () { + const DAI = new ethers.Contract(tokens.dai.address, abis.basic.erc20, ethers.provider); + const DAI_VAULT = '0xdA816459F1AB5631232FE5e97a05BBBb94970c95'; + const amount = ethers.utils.parseEther("50") // 50 DAI + const setId = "132456"; + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [DAI_VAULT, amount, 0, setId] + }, + { + connector: connectorName, + method: "withdraw", + args: [DAI_VAULT, amount, setId, 0] + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet0.address); + await tx.wait(); + + // Get DAI balance + const balance = await DAI.balanceOf(dsaWallet0.address); + expect(await ethers.BigNumber.from(balance).eq(ethers.utils.parseEther("100"))); + }); + + it("Should deposit 70 DAI in the Yearn Vault", async function () { + const DAI_VAULT = '0xdA816459F1AB5631232FE5e97a05BBBb94970c95'; + const DAI = new ethers.Contract(tokens.dai.address, abis.basic.erc20, ethers.provider); + const YVDAI = new ethers.Contract(DAI_VAULT, abis.basic.erc20, ethers.provider); + const amount = ethers.utils.parseEther("70") // 70 DAI + const setId = "568445"; + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [DAI_VAULT, amount, 0, setId] + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet0.address); + await tx.wait(); + + // Get DAI balance + const yvDAIBalance = await YVDAI.balanceOf(dsaWallet0.address); + const daiBalance = await DAI.balanceOf(dsaWallet0.address); + const correctDaiBalance = await ethers.BigNumber.from(daiBalance).eq(ethers.utils.parseEther("30")); + const correctYVDaiBalance = await ethers.BigNumber.from(yvDAIBalance).lte(ethers.utils.parseEther("70")); + expect(correctDaiBalance && correctYVDaiBalance); + }); + }) +})