diff --git a/.github/workflows/run-ci-polygon.yml b/.github/workflows/run-ci-polygon.yml deleted file mode 100644 index 83d9f1f41..000000000 --- a/.github/workflows/run-ci-polygon.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Compile & Run Smart Contract Unit Tests - -on: [pull_request] - -jobs: - test: - name: Build Project - Polygon - runs-on: ubuntu-latest - environment: testing-keys - env: - MNEMONIC_KEY: ${{ secrets.MNEMONIC_KEY }} - ALCHEMY_MAINNET_KEY: ${{ secrets.ALCHEMY_MAINNET_KEY }} - ALCHEMY_RINKEBY_KEY: ${{ secrets.ALCHEMY_RINKEBY_KEY }} - ALCHEMY_ROPSTEN_KEY: ${{ secrets.ALCHEMY_ROPSTEN_KEY }} - ALCHEMY_KOVAN_KEY: ${{ secrets.ALCHEMY_KOVAN_KEY }} - MATIC_MAINNET_KEY: ${{ secrets.MATIC_MAINNET_KEY }} - MATIC_MUMBAI_KEY: ${{ secrets.MATIC_MUMBAI_KEY }} - INFURA_KEY: ${{ secrets.INFURA_KEY }} - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} - CMC_KEY: ${{ secrets.CMC_KEY }} - GAS_PRICE_GWEI_KEY: 20 - GAS_WEI_KEY: 2500000 - ADDRESS_COUNT_KEY: 20 - DEFAULT_ADDRESS_INDEX_KEY: 0 - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '14' - registry-url: 'https://registry.npmjs.org/' - - name: Install dependencies - Polygon - run: | - yarn --network-concurrency 1 - - name: Compiling and Executing Tests - Polygon - run: | - yarn test polygon diff --git a/config/markets.ts b/config/markets.ts index df889a26b..98c1cc53a 100644 --- a/config/markets.ts +++ b/config/markets.ts @@ -11,7 +11,7 @@ const parseCompoundWeth = (sym: string): string => { } const compoundStrategy = (tokenSym: string): MarketStrategy => ({ - name: 'TTokenCompoundStrategy_1', + name: 'TTokenCompoundStrategy_2', initArgs: [ { type: 'TokenSymbol', diff --git a/contracts/lending/ttoken/strategies/compound/TTokenCompoundStrategy_1.sol b/contracts/lending/ttoken/strategies/compound/TTokenCompoundStrategy_1.sol index c5ee13975..cf647b761 100644 --- a/contracts/lending/ttoken/strategies/compound/TTokenCompoundStrategy_1.sol +++ b/contracts/lending/ttoken/strategies/compound/TTokenCompoundStrategy_1.sol @@ -30,15 +30,15 @@ contract TTokenCompoundStrategy_1 is RolesMods, TTokenStrategy { * @dev it creates a reference to the TToken storage */ function() pure returns (TokenStorage.Store storage) - private constant tokenStore = TokenStorage.store; + internal constant tokenStore = TokenStorage.store; /** * @dev it creates a reference to the Compound storage */ function() pure returns (CompoundStorage.Store storage) - private constant compoundStore = CompoundStorage.store; + internal constant compoundStore = CompoundStorage.store; - string public constant NAME = "CompoundStrategy_1"; + string public NAME; /* External Functions */ @@ -46,7 +46,7 @@ contract TTokenCompoundStrategy_1 is RolesMods, TTokenStrategy { * @notice it returns the total supply of an underlying asset in a Teller token. * @return uint256 the underlying supply */ - function totalUnderlyingSupply() external override returns (uint256) { + function totalUnderlyingSupply() public virtual override returns (uint256) { return tokenStore().underlying.balanceOf(address(this)) + compoundStore().cToken.balanceOfUnderlying(address(this)); @@ -170,7 +170,7 @@ contract TTokenCompoundStrategy_1 is RolesMods, TTokenStrategy { address cTokenAddress, uint16 balanceRatioMin, uint16 balanceRatioMax - ) external { + ) public virtual { require( balanceRatioMax > balanceRatioMin, "Teller: Max ratio balance should be greater than Min ratio balance" @@ -178,6 +178,7 @@ contract TTokenCompoundStrategy_1 is RolesMods, TTokenStrategy { compoundStore().cToken = ICErc20(cTokenAddress); compoundStore().balanceRatioMin = balanceRatioMin; compoundStore().balanceRatioMax = balanceRatioMax; + NAME = "CompoundStrategy_1"; emit StrategyInitialized(NAME, cTokenAddress, LibMeta.msgSender()); } } diff --git a/contracts/lending/ttoken/strategies/compound/TTokenCompoundStrategy_2.sol b/contracts/lending/ttoken/strategies/compound/TTokenCompoundStrategy_2.sol new file mode 100644 index 000000000..76e81efaa --- /dev/null +++ b/contracts/lending/ttoken/strategies/compound/TTokenCompoundStrategy_2.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Contracts +import { TTokenCompoundStrategy_1 } from "./TTokenCompoundStrategy_1.sol"; + +// Interfaces +import { ICErc20 } from "../../../../shared/interfaces/ICErc20.sol"; +import { LibMeta } from "../../../../shared/libraries/LibMeta.sol"; + +contract TTokenCompoundStrategy_2 is TTokenCompoundStrategy_1 { + address immutable bonusGnosisSafe; + + /* External Functions */ + + function totalUnderlyingSupply() public override returns (uint256) { + // Get current supply + uint256 currentSupply = super.totalUnderlyingSupply(); + + // Calculate bonus interest due + uint256 interestTimeperiod = block.timestamp - + compoundStore().lastBonusIntTimestamp; + uint256 dailyInterest = ((currentSupply / 10) / 365 days) * 1 days; + uint256 bonusInterest = dailyInterest * (interestTimeperiod / 1 days); + + if (bonusInterest > 0) { + uint256 gnosisBalance = tokenStore().underlying.balanceOf( + bonusGnosisSafe + ); + if (gnosisBalance < bonusInterest) { + bonusInterest = gnosisBalance; + } + + // Deposit into + tokenStore().underlying.transferFrom( + bonusGnosisSafe, + address(this), + bonusInterest + ); + } + + return currentSupply + bonusInterest; + } + + /** + * @notice Sets the Compound token that should be used to manage the underlying Teller Token asset. + * @param cTokenAddress Address of the Compound token that has the same underlying asset as the TToken. + * @param balanceRatioMin Percentage indicating the _ limit of underlying token balance should remain on the TToken + * @param balanceRatioMax Percentage indicating the _ limit of underlying token balance should remain on the TToken + * @dev Note that the balanceRatio percentages have to be scaled by ONE_HUNDRED_PERCENT + */ + function init( + address cTokenAddress, + uint16 balanceRatioMin, + uint16 balanceRatioMax + ) public override { + super.init(cTokenAddress, balanceRatioMin, balanceRatioMax); + compoundStore().lastBonusIntTimestamp = block.timestamp; + NAME = "CompoundStrategy_2"; + } + + /** + * @notice Sets the address of the bonus gnosis safe for the bonus interest disbursment + * @param _bonusGnosisSafe The address of the gnosisSafe to pull funds from for bonus interest + */ + constructor(address _bonusGnosisSafe) { + bonusGnosisSafe = _bonusGnosisSafe; + } +} diff --git a/contracts/lending/ttoken/strategies/compound/compound-storage.sol b/contracts/lending/ttoken/strategies/compound/compound-storage.sol index 2cb50d6d5..08e991e14 100644 --- a/contracts/lending/ttoken/strategies/compound/compound-storage.sol +++ b/contracts/lending/ttoken/strategies/compound/compound-storage.sol @@ -7,6 +7,8 @@ struct Store { ICErc20 cToken; uint16 balanceRatioMax; uint16 balanceRatioMin; + address bonusGnosisSafe; + uint256 lastBonusIntTimestamp; } bytes32 constant POSITION = keccak256( diff --git a/deploy/markets.ts b/deploy/markets.ts index b9a599aaf..177bc43db 100644 --- a/deploy/markets.ts +++ b/deploy/markets.ts @@ -1,10 +1,11 @@ import colors from 'colors' -import { ContractTransaction } from 'ethers' +import { Contract, ContractTransaction } from 'ethers' +import { toBN } from 'hardhat' import { HardhatRuntimeEnvironment } from 'hardhat/types' import { DeployFunction } from 'hardhat-deploy/types' import { getDappAddresses, getMarkets, getSigners, getTokens } from '../config' -import { ITellerDiamond, ITToken } from '../types/typechain' +import { IERC20, ITellerDiamond, ITToken } from '../types/typechain' import { NULL_ADDRESS } from '../utils/consts' import { deploy } from '../utils/deploy-helpers' @@ -102,15 +103,42 @@ const initializeMarkets: DeployFunction = async (hre) => { }) } - const tTokenStrategy = await deploy({ - hre, - contract: market.strategy.name, - indent: 4, - }) + let tTokenStrategy: Contract + + if (market.strategy.name == 'TTokenCompoundStrategy_2') { + tTokenStrategy = await deploy({ + hre, + contract: market.strategy.name, + indent: 4, + args: [await (await getNamedSigner('gnosisSafe')).getAddress()], + }) + } else { + tTokenStrategy = await deploy({ + hre, + contract: market.strategy.name, + indent: 4, + }) + } const tToken = await contracts.get('ITToken', { at: tTokenAddress, }) + // Setting approval for bonus int from deployer account + if (market.strategy.name == 'TTokenCompoundStrategy_2') { + const lendingToken = await contracts.get('IERC20', { + at: lendingTokenAddress, + }) + const currentAllowance = await lendingToken.allowance( + await (await getNamedSigner('gnosisSafe')).getAddress(), + tTokenAddress + ) + + if (currentAllowance.eq('0')) { + await lendingToken + .connect(await getNamedSigner('gnosisSafe')) + .approve(tTokenAddress, toBN('1000000', '18')) + } + } const currentStrategy = await tToken.getStrategy() if (currentStrategy !== tTokenStrategy.address) { log(`Setting new TToken strategy...:`, { diff --git a/hardhat.config.ts b/hardhat.config.ts index d65a19179..6c1736c76 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -171,6 +171,10 @@ export default { hardhat: 11, localhost: 11, }, + gnosisSafe: { + hardhat: 13, + localhost: 13, + }, }, networks: { kovan: networkConfig({ diff --git a/test/helpers/set-test-env.ts b/test/helpers/set-test-env.ts index e6694cb50..2a86bc362 100644 --- a/test/helpers/set-test-env.ts +++ b/test/helpers/set-test-env.ts @@ -3,6 +3,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Signer } from 'ethers' import hre, { toBN } from 'hardhat' +import { HardhatRuntimeEnvironment } from 'hardhat/types' import { getPlatformSetting, @@ -29,20 +30,24 @@ export interface TestEnv { deployer: Signer lender: Signer borrower: Signer + gnosisSafe: Signer tellerDiamond: ITellerDiamond priceAggregator: PriceAggregator nft: PolyTellerNFTMock | MainnetTellerNFT tokens: TokenData[] + hre: HardhatRuntimeEnvironment } const testEnv: TestEnv = { deployer: {} as Signer, lender: {} as Signer, borrower: {} as Signer, + gnosisSafe: {} as Signer, tellerDiamond: {} as ITellerDiamond, priceAggregator: {} as PriceAggregator, nft: {} as PolyTellerNFTMock | MainnetTellerNFT, tokens: [] as TokenData[], + hre: {} as HardhatRuntimeEnvironment, } as TestEnv const { contracts, deployments, getNamedSigner, tokens } = hre @@ -52,10 +57,12 @@ export async function initTestEnv() { testEnv.deployer = await getNamedSigner('deployer') testEnv.lender = await getNamedSigner('lender') testEnv.borrower = await getNamedSigner('borrower') + testEnv.gnosisSafe = await getNamedSigner('gnosisSafe') + testEnv.hre = hre // Get a fresh market await deployments.fixture('markets', { - keepExistingDeployments: true, + keepExistingDeployments: false, }) testEnv.tellerDiamond = await contracts.get('TellerDiamond') @@ -128,7 +135,7 @@ export const revertHead = async () => { } const fundMarket = async (testEnv: TestEnv) => { - const { tellerDiamond, lender } = testEnv + const { tellerDiamond, lender, deployer, gnosisSafe } = testEnv // Get list of tokens const dai = await tokens.get('DAI') const collateralTokens = Array.from( @@ -158,6 +165,15 @@ const fundMarket = async (testEnv: TestEnv) => { amount: bnedAmount, hre, }) + + // Get funds for deployer for bonus int + await getFunds({ + to: gnosisSafe, + tokenSym: await token.symbol(), + amount: bnedAmount, + hre, + }) + // Approve protocol await token .connect(lender) diff --git a/test/lending/bonusIntStrategy.ts b/test/lending/bonusIntStrategy.ts new file mode 100644 index 000000000..0c17b9409 --- /dev/null +++ b/test/lending/bonusIntStrategy.ts @@ -0,0 +1,66 @@ +import chai, { expect } from 'chai' +import { solidity } from 'ethereum-waffle' +import { contracts, ethers, network, toBN } from 'hardhat' +import moment from 'moment' + +import { ERC20, TTokenV3 } from '../../types/typechain' +import { getFunds } from '../helpers/get-funds' +import { advanceBlock } from '../helpers/misc' +import { setTestEnv, TestEnv } from '../helpers/set-test-env' + +chai.should() +chai.use(solidity) + +setTestEnv('Lending - TToken bonus interest', (testEnv: TestEnv) => { + it.only('Should accurately dispense the bonus interest', async () => { + // Load tToken + const { tokens, tellerDiamond, lender, hre } = testEnv + + const dai: ERC20 = tokens.find((o) => o.name === 'DAI')!.token + + const tDai = await contracts.get('ITToken', { + at: await tellerDiamond.getTTokenFor(dai.address), + }) + + const bnedAmount = toBN(1000, 18) + + // Get funds for lender + await getFunds({ + to: lender, + tokenSym: await dai.symbol(), + amount: bnedAmount, + hre, + }) + + // Deposit as lender + const exchangeRateBefore = await tDai.callStatic.exchangeRate() + const totalUnderlyingBefore = await tDai.callStatic.totalUnderlyingSupply() + + // Approve protocol + await dai + .connect(lender) + .approve(tDai.address, bnedAmount) + .then(({ wait }) => wait()) + // Deposit funds + await tDai + .connect(lender) + .mint(bnedAmount) + .then(({ wait }) => wait()) + + // Advance time + await hre.ethers.provider.send('evm_increaseTime', [ + moment.duration(365, 'days').asSeconds(), + ]) + await hre.ethers.provider.send('evm_mine', []) + + // Check tToken total underlying + const totalUnderlyingAfter = await tDai.callStatic.totalUnderlyingSupply() + + // Check tToken exchange rate + const exchangeRateAfter = await tDai.callStatic.exchangeRate() + + // Assertions + expect(totalUnderlyingAfter).to.be.gt(totalUnderlyingBefore.add(bnedAmount)) + expect(exchangeRateAfter).to.be.gt(exchangeRateBefore) + }) +})