diff --git a/contracts/mainnet/connectors/ubiquity/events.sol b/contracts/mainnet/connectors/ubiquity/events.sol new file mode 100644 index 00000000..acdc679b --- /dev/null +++ b/contracts/mainnet/connectors/ubiquity/events.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; + +contract Events { + event Deposit( + address indexed userAddress, + address indexed token, + uint256 amount, + uint256 lpAmount, + uint256 durationWeeks, + uint256 indexed bondingShareId, + uint256 getId, + uint256 setId + ); +} diff --git a/contracts/mainnet/connectors/ubiquity/helpers.sol b/contracts/mainnet/connectors/ubiquity/helpers.sol new file mode 100644 index 00000000..c1fb4e34 --- /dev/null +++ b/contracts/mainnet/connectors/ubiquity/helpers.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; + +import {TokenInterface} from "../../common/interfaces.sol"; +import {DSMath} from "../../common/math.sol"; +import {Basic} from "../../common/basic.sol"; + +abstract contract Helpers is DSMath, Basic { + /** + * @dev Ubiquity Algorithmic Dollar Manager Address + */ + address internal constant UbiquityAlgorithmicDollarManager = + 0x4DA97a8b831C345dBe6d16FF7432DF2b7b776d98; + + /** + * @dev DAI Address + */ + address internal constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + + /** + * @dev USDC Address + */ + address internal constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + + /** + * @dev USDT Address + */ + address internal constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + + /** + * @dev Curve 3CRV Token Address + */ + address internal constant CRV3 = 0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490; + + /** + * @dev Curve 3Pool Address + */ + address internal constant Pool3 = + 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; +} diff --git a/contracts/mainnet/connectors/ubiquity/interfaces.sol b/contracts/mainnet/connectors/ubiquity/interfaces.sol new file mode 100644 index 00000000..dad4f1ea --- /dev/null +++ b/contracts/mainnet/connectors/ubiquity/interfaces.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; + +import {TokenInterface} from "../../common/interfaces.sol"; + +interface IUbiquityBondingV2 { + function deposit(uint256 lpAmount, uint256 durationWeeks) + external + returns (uint256 bondingShareId); +} + +interface IUbiquityMetaPool { + function add_liquidity(uint256[2] memory _amounts, uint256 _min_mint_amount) + external + returns (uint256); +} + +interface IUbiquity3Pool { + function add_liquidity( + uint256[3] calldata _amounts, + uint256 _min_mint_amount + ) external; +} + +interface IUbiquityAlgorithmicDollarManager { + function dollarTokenAddress() external returns (address); + + function stableSwapMetaPoolAddress() external returns (address); + + function bondingContractAddress() external returns (address); +} diff --git a/contracts/mainnet/connectors/ubiquity/main.sol b/contracts/mainnet/connectors/ubiquity/main.sol new file mode 100644 index 00000000..6b2840a3 --- /dev/null +++ b/contracts/mainnet/connectors/ubiquity/main.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.6; +pragma abicoder v2; + +/** + * @title Ubiquity. + * @dev Ubiquity Dollar (uAD). + */ + +import {TokenInterface, MemoryInterface} from "../../common/interfaces.sol"; +import {Stores} from "../../common/stores.sol"; +import {SafeMath} from "../../common/math.sol"; +import {IUbiquityBondingV2, IUbiquityMetaPool, IUbiquity3Pool, IUbiquityAlgorithmicDollarManager} from "./interfaces.sol"; +import {Helpers} from "./helpers.sol"; +import {Events} from "./events.sol"; + +contract ConnectV2Ubiquity is Helpers, Events { + string public constant name = "Ubiquity-v1"; + + /** + * @dev Deposit into Ubiquity protocol + * @notice 3POOL (DAI / USDC / USDT) => METAPOOL (3CRV / uAD) => uAD3CRV-f => Ubiquity BondingShare + * @notice STEP 1 : 3POOL (DAI / USDC / USDT) => 3CRV + * @notice STEP 2 : METAPOOL(3CRV / UAD) => uAD3CRV-f + * @notice STEP 3 : uAD3CRV-f => Ubiquity BondingShare + * @param token Token deposited : DAI, USDC, USDT, 3CRV, uAD or uAD3CRV-f + * @param amount Amount of tokens to deposit (For max: `uint256(-1)`) + * @param durationWeeks Duration in weeks tokens will be locked (4-208) + * @param getId ID to retrieve amt. + * @param setId ID stores the amount of tokens deposited. + */ + function deposit( + address token, + uint256 amount, + uint256 durationWeeks, + uint256 getId, + uint256 setId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + address UAD = IUbiquityAlgorithmicDollarManager( + UbiquityAlgorithmicDollarManager + ).dollarTokenAddress(); + address UAD3CRVf = IUbiquityAlgorithmicDollarManager( + UbiquityAlgorithmicDollarManager + ).stableSwapMetaPoolAddress(); + + require( + token == DAI || + token == USDC || + token == USDT || + token == UAD || + token == CRV3 || + token == UAD3CRVf, + "Invalid token: must be DAI, USDC, USDT, uAD, 3CRV or uAD3CRV-f" + ); + + uint256 _amount = getUint(getId, amount); + uint256 _lpAmount; + + // Full balance if amount = -1 + if (_amount == uint256(-1)) { + _amount = getTokenBal(TokenInterface(token)); + } + + // STEP 1 : SwapTo3CRV : Deposit DAI, USDC or USDT into 3Pool to get 3Crv LPs + if (token == DAI || token == USDC || token == USDT) { + uint256[3] memory amounts1; + + if (token == DAI) amounts1[0] = _amount; + else if (token == USDC) amounts1[1] = _amount; + else if (token == USDT) amounts1[2] = _amount; + + approve(TokenInterface(token), Pool3, _amount); + IUbiquity3Pool(Pool3).add_liquidity(amounts1, 0); + } + + // STEP 2 : ProvideLiquidityToMetapool : Deposit in uAD3CRV pool to get uAD3CRV-f LPs + if ( + token == DAI || + token == USDC || + token == USDT || + token == UAD || + token == CRV3 + ) { + uint256[2] memory amounts2; + address token2 = token; + uint256 _amount2; + + if (token == UAD) { + _amount2 = _amount; + amounts2[0] = _amount2; + } else { + if (token == CRV3) { + _amount2 = _amount; + } else { + token2 = CRV3; + _amount2 = getTokenBal(TokenInterface(token2)); + } + amounts2[1] = _amount2; + } + + approve(TokenInterface(token2), UAD3CRVf, _amount2); + _lpAmount = IUbiquityMetaPool(UAD3CRVf).add_liquidity(amounts2, 0); + } + + // STEP 3 : Farm/ApeIn : Deposit uAD3CRV-f LPs into UbiquityBondingV2 and get Ubiquity Bonding Shares + if (token == UAD3CRVf) { + _lpAmount = _amount; + } + + address Bonding = IUbiquityAlgorithmicDollarManager( + UbiquityAlgorithmicDollarManager + ).bondingContractAddress(); + approve(TokenInterface(UAD3CRVf), Bonding, _lpAmount); + uint256 bondingShareId = IUbiquityBondingV2(Bonding).deposit( + _lpAmount, + durationWeeks + ); + + setUint(setId, bondingShareId); + + _eventName = "Deposit(address,address,uint256,uint256,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode( + address(this), + token, + amount, + _lpAmount, + durationWeeks, + bondingShareId, + getId, + setId + ); + } +} diff --git a/hardhat.config.js b/hardhat.config.js index 04300869..9c2b5b86 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -65,6 +65,10 @@ module.exports = { blockNumber: 12696000, }, blockGasLimit: 12000000, + gasPrice: parseInt(utils.parseUnits("300", "gwei")), + }, + local: { + url: "http://127.0.0.1:8545", }, matic: { url: "https://rpc-mainnet.maticvigil.com/", diff --git a/test/ubiquity/ubiquity.test.js b/test/ubiquity/ubiquity.test.js new file mode 100644 index 00000000..e4b661ee --- /dev/null +++ b/test/ubiquity/ubiquity.test.js @@ -0,0 +1,316 @@ +const { expect } = require("chai"); +const hre = require("hardhat"); +const { waffle, ethers } = hre; +const { provider } = waffle; +const { BigNumber } = ethers; + +const deployAndEnableConnector = require("../../scripts/deployAndEnableConnector.js"); +const buildDSAv2 = require("../../scripts/buildDSAv2"); +const encodeSpells = require("../../scripts/encodeSpells"); +const addresses = require("../../scripts/constant/addresses"); +const abis = require("../../scripts/constant/abis"); +const impersonate = require("../../scripts/impersonate"); +const { forkReset, sendEth } = require("./utils"); + +const connectV2UbiquityArtifacts = require("../../artifacts/contracts/mainnet/connectors/ubiquity/main.sol/ConnectV2Ubiquity.json"); + +const { abi: implementationsABI } = require("../../scripts/constant/abi/core/InstaImplementations.json") +const implementationsMappingAddr = "0xCBA828153d3a85b30B5b912e1f2daCac5816aE9D" + +describe.only("Ubiquity", function () { + const ubiquityTest = "UBIQUITY-TEST-A"; + + const BOND = "0x2dA07859613C14F6f05c97eFE37B9B4F212b5eF5"; + const UAD = "0x0F644658510c95CB46955e55D7BA9DDa9E9fBEc6"; + const DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; + const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + const USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; + const CRV3 = "0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490"; + const POOL3 = "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7"; + const UAD3CRVF = "0x20955CB69Ae1515962177D164dfC9522feef567E"; + + const ethWhaleAddress = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; + const uadWhaleAddress = "0xefC0e701A824943b469a694aC564Aa1efF7Ab7dd"; + + const blockFork = 13097100; + const one = BigNumber.from(10).pow(18); + const onep = BigNumber.from(10).pow(6); + const ABI = [ + "function balanceOf(address owner) view returns (uint256)", + "function allowance(address owner, address spender) external view returns (uint256)", + "function transfer(address to, uint amount) returns (boolean)", + "function remove_liquidity_one_coin(uint256 _burn_amount, int128 i, uint256 _min_received) external returns (uint256)", + "function add_liquidity(uint256[3],uint256) returns (uint256)", + "function approve(address, uint256) external", + "function holderTokens(address) view returns (uint256[])", + "function getBond(uint256) view returns (tuple(address,uint256,uint256,uint256,uint256,uint256))", + ]; + let dsa; + let POOL3Contract; + let CRV3Contract; + let uAD3CRVfContract; + let uADContract; + let DAIContract; + let USDCContract; + let USDTContract; + let BONDContract; + let instaIndex; + let instaConnectorsV2; + let connector; + let instaImplementationsMapping; + let InstaAccountV2DefaultImpl; + + let uadWhale; + + const bondingShareLpAmount = async function (address) { + let LP = 0; + const bondId = await BONDContract.holderTokens(address); + if (bondId.length){ + const bond = await BONDContract.getBond(bondId[0]); + LP = bond[5]; + } + // console.log("LP", ethers.utils.formatEther(LP.toString())); + return LP; + } + + beforeEach(async () => { + await forkReset(blockFork); + + [uadWhale] = await impersonate([uadWhaleAddress]); + const [ethWhale] = await impersonate([ethWhaleAddress]); + + await sendEth(ethWhale, uadWhaleAddress, 100); + POOL3Contract = new ethers.Contract(POOL3, ABI, uadWhale); + CRV3Contract = new ethers.Contract(CRV3, ABI, uadWhale); + uAD3CRVfContract = new ethers.Contract(UAD3CRVF, ABI, uadWhale); + uADContract = new ethers.Contract(UAD, ABI, uadWhale); + DAIContract = new ethers.Contract(DAI, ABI, uadWhale); + USDCContract = new ethers.Contract(USDC, ABI, uadWhale); + USDTContract = new ethers.Contract(USDT, ABI, uadWhale); + BONDContract = new ethers.Contract(BOND, ABI, uadWhale); + dsa = (await buildDSAv2(uadWhaleAddress)).connect(uadWhale); + await sendEth(ethWhale, dsa.address, 100); + + instaIndex = new ethers.Contract(addresses.core.instaIndex, abis.core.instaIndex, ethWhale); + + const masterAddress = await instaIndex.master(); + const [master] = await impersonate([masterAddress]); + await sendEth(ethWhale, masterAddress, 100); + + instaConnectorsV2 = new ethers.Contract(addresses.core.connectorsV2, abis.core.connectorsV2); + + instaImplementationsMapping = await ethers.getContractAt(implementationsABI, implementationsMappingAddr); + InstaAccountV2DefaultImpl = await ethers.getContractFactory("InstaDefaultImplementation") + instaAccountV2DefaultImpl = await InstaAccountV2DefaultImpl.deploy(addresses.core.instaIndex); + await instaAccountV2DefaultImpl.deployed() + await (await instaImplementationsMapping.connect(master).setDefaultImplementation(instaAccountV2DefaultImpl.address)).wait(); + + connector = await deployAndEnableConnector({ + connectorName: ubiquityTest, + contractArtifact: connectV2UbiquityArtifacts, + signer: master, + connectors: instaConnectorsV2, + }); + }); + + const dsaDepositUAD3CRVf = async (amount) => { + await uAD3CRVfContract.transfer(dsa.address, one.mul(amount)); + }; + + const dsaDepositUAD = async (amount) => { + await uAD3CRVfContract.remove_liquidity_one_coin(one.mul(amount).mul(110).div(100), 0, one.mul(amount)); + await uADContract.transfer(dsa.address, one.mul(amount)); + }; + + const dsaDepositCRV3 = async (amount) => { + await uAD3CRVfContract.remove_liquidity_one_coin(one.mul(amount).mul(110).div(100), 1, one.mul(amount)); + await CRV3Contract.transfer(dsa.address, one.mul(amount)); + }; + + const dsaDepositDAI = async (amount) => { + await uAD3CRVfContract.remove_liquidity_one_coin(one.mul(amount).mul(120).div(100), 1, one.mul(amount).mul(110).div(100)); + await POOL3Contract.remove_liquidity_one_coin(one.mul(amount).mul(110).div(100), 0, one.mul(amount)); + await DAIContract.transfer(dsa.address, one.mul(amount)); + }; + const dsaDepositUSDC = async (amount) => { + await uAD3CRVfContract.remove_liquidity_one_coin(one.mul(amount).mul(120).div(100), 1, one.mul(amount).mul(110).div(100)); + await POOL3Contract.remove_liquidity_one_coin(one.mul(amount).mul(110).div(100), 1, onep.mul(amount)); + await USDCContract.transfer(dsa.address, onep.mul(amount)); + }; + const dsaDepositUSDT = async (amount) => { + await uAD3CRVfContract.remove_liquidity_one_coin(one.mul(amount).mul(120).div(100), 1, one.mul(amount).mul(110).div(100)); + await POOL3Contract.remove_liquidity_one_coin(one.mul(amount).mul(110).div(100), 2, onep.mul(amount)); + await USDTContract.transfer(dsa.address, onep.mul(amount)); + }; + + describe("DSA wallet setup", function () { + it("Should have contracts deployed.", async function () { + expect(POOL3Contract.address).to.be.properAddress; + expect(CRV3Contract.address).to.be.properAddress; + expect(uADContract.address).to.be.properAddress; + expect(uAD3CRVfContract.address).to.be.properAddress; + expect(DAIContract.address).to.be.properAddress; + expect(USDCContract.address).to.be.properAddress; + expect(USDTContract.address).to.be.properAddress; + expect(BONDContract.address).to.be.properAddress; + expect(instaIndex.address).to.be.properAddress; + expect(instaConnectorsV2.address).to.be.properAddress; + expect(connector.address).to.be.properAddress; + expect(dsa.address).to.be.properAddress; + }); + it("Should deposit uAD3CRVf into DSA wallet", async function () { + await dsaDepositUAD3CRVf(100); + expect(await uAD3CRVfContract.balanceOf(dsa.address)).to.be.gte(one.mul(100)); + }); + it("Should deposit uAD into DSA wallet", async function () { + await dsaDepositUAD(100); + expect(await uADContract.balanceOf(dsa.address)).to.be.gte(one.mul(100)); + }); + it("Should deposit 3CRV into DSA wallet", async function () { + await dsaDepositCRV3(100); + expect(await CRV3Contract.balanceOf(dsa.address)).to.be.gte(one.mul(100)); + }); + it("Should deposit DAI into DSA wallet", async function () { + await dsaDepositDAI(100); + expect(await DAIContract.balanceOf(dsa.address)).to.be.gte(one.mul(100)); + }); + it("Should deposit USDC into DSA wallet", async function () { + await dsaDepositUSDC(100); + expect(await USDCContract.balanceOf(dsa.address)).to.be.gte(onep.mul(100)); + }); + it("Should deposit USDT into DSA wallet", async function () { + await dsaDepositUSDT(100); + expect(await USDTContract.balanceOf(dsa.address)).to.be.gte(onep.mul(100)); + }); + }); + + describe("Main", function () { + it("should deposit uAD3CRVf to get Ubiquity Bonding Shares", async function () { + await dsaDepositUAD3CRVf(100); + expect(await bondingShareLpAmount(dsa.address)).to.be.equal(0); + await expect( + dsa.cast( + ...encodeSpells([ + { + connector: ubiquityTest, + method: "deposit", + args: [UAD3CRVF, one, 4, 0, 0], + }, + ]), + uadWhaleAddress + ) + ).to.be.not.reverted; + expect(await bondingShareLpAmount(dsa.address)).to.be.gt(0); + }); + + it("should deposit uAD to get Ubiquity Bonding Shares", async function () { + await dsaDepositUAD(100); + expect(await bondingShareLpAmount(dsa.address)).to.be.equal(0); + await expect( + dsa.cast( + ...encodeSpells([ + { + connector: ubiquityTest, + method: "deposit", + args: [UAD, one, 4, 0, 0], + }, + ]), + uadWhaleAddress + ) + ).to.be.not.reverted; + expect(await bondingShareLpAmount(dsa.address)).to.be.gt(0); + }); + + it("should deposit 3CRV to get Ubiquity Bonding Shares", async function () { + await dsaDepositCRV3(100); + expect(await bondingShareLpAmount(dsa.address)).to.be.equal(0); + await expect( + dsa.cast( + ...encodeSpells([ + { + connector: ubiquityTest, + method: "deposit", + args: [CRV3, one, 4, 0, 0], + }, + ]), + uadWhaleAddress + ) + ).to.be.not.reverted; + expect(await bondingShareLpAmount(dsa.address)).to.be.gt(0); + }); + + it("should deposit DAI to get Ubiquity Bonding Shares", async function () { + await dsaDepositDAI(100); + expect(await bondingShareLpAmount(dsa.address)).to.be.equal(0); + await expect( + dsa.cast( + ...encodeSpells([ + { + connector: ubiquityTest, + method: "deposit", + args: [DAI, one.mul(100), 4, 0, 0], + }, + ]), + uadWhaleAddress + ) + ).to.be.not.reverted; + expect(await bondingShareLpAmount(dsa.address)).to.be.gt(0); + }); + + it("should deposit USDC to get Ubiquity Bonding Shares", async function () { + await dsaDepositUSDC(100); + expect(await bondingShareLpAmount(dsa.address)).to.be.equal(0); + await expect( + dsa.cast( + ...encodeSpells([ + { + connector: ubiquityTest, + method: "deposit", + args: [USDC, onep.mul(100), 4, 0, 0], + }, + ]), + uadWhaleAddress + ) + ).to.be.not.reverted; + expect(await bondingShareLpAmount(dsa.address)).to.be.gt(0); + }); + + it("should deposit USDT to get Ubiquity Bonding Shares", async function () { + await dsaDepositUSDT(100); + expect(await bondingShareLpAmount(dsa.address)).to.be.equal(0); + await expect( + dsa.cast( + ...encodeSpells([ + { + connector: ubiquityTest, + method: "deposit", + args: [USDT, onep.mul(100), 4, 0, 0], + }, + ]), + uadWhaleAddress + ) + ).to.be.not.reverted; + expect(await bondingShareLpAmount(dsa.address)).to.be.gt(0); + }); + }); + + describe("3Pool test", function () { + it("Should add DAI liquidity to 3Pool", async function () { + const n = 100; + await dsaDepositDAI(n); + const amount = one.mul(n); + const [dsaSigner] = await impersonate([dsa.address]); + + expect(await DAIContract.balanceOf(dsa.address)).to.be.equal(amount); + expect(await CRV3Contract.balanceOf(dsa.address)).to.be.equal(0); + + await (await DAIContract.connect(dsaSigner).approve(POOL3, amount)).wait(); + await (await POOL3Contract.connect(dsaSigner).add_liquidity([amount, 0, 0], amount.mul(98).div(100))).wait(); + + expect(await DAIContract.balanceOf(dsa.address)).to.be.equal(0); + expect(await CRV3Contract.balanceOf(dsa.address)) + .to.be.gte(amount.mul(98).div(100)) + .to.be.lte(amount.mul(102).div(100)); + }); + }); +}); diff --git a/test/ubiquity/utils.js b/test/ubiquity/utils.js new file mode 100644 index 00000000..0b79a4ae --- /dev/null +++ b/test/ubiquity/utils.js @@ -0,0 +1,24 @@ +const hre = require("hardhat"); +const hardhatConfig = require("../../hardhat.config"); + +async function forkReset(blockNumber) { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: hardhatConfig.networks.hardhat.forking.url, + blockNumber, + }, + }, + ], + }); +} +async function sendEth(from, to, amount) { + await from.sendTransaction({ + to: to, + value: ethers.BigNumber.from(10).pow(18).mul(amount), + }); +} + +module.exports = { forkReset, sendEth };