diff --git a/contracts/interfaces/external/IComptroller.sol b/contracts/interfaces/external/IComptroller.sol index 2f5b623c8..fa81f1cda 100644 --- a/contracts/interfaces/external/IComptroller.sol +++ b/contracts/interfaces/external/IComptroller.sol @@ -46,4 +46,8 @@ interface IComptroller { function getAllMarkets() external view returns (ICErc20[] memory); function claimComp(address holder) external; + + function compAccrued(address holder) external view returns (uint); + + function getCompAddress() external view returns (address); } \ No newline at end of file diff --git a/contracts/mocks/StandardTokenMock.sol b/contracts/mocks/StandardTokenMock.sol index d4f6fc6b8..e17f8447e 100644 --- a/contracts/mocks/StandardTokenMock.sol +++ b/contracts/mocks/StandardTokenMock.sol @@ -35,4 +35,8 @@ contract StandardTokenMock is ERC20 { _mint(_initialAccount, _initialBalance); _setupDecimals(_decimals); } + + function mint(address to, uint amount) external { + _mint(to, amount); + } } diff --git a/contracts/mocks/external/ComptrollerMock.sol b/contracts/mocks/external/ComptrollerMock.sol index 664fe9150..bc9f17874 100644 --- a/contracts/mocks/external/ComptrollerMock.sol +++ b/contracts/mocks/external/ComptrollerMock.sol @@ -22,10 +22,11 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { ICErc20 } from "../../interfaces/external/ICErc20.sol"; contract ComptrollerMock { - address comp; - uint256 compAmount; - address setToken; + address public comp; + uint256 public compAmount; + address public setToken; ICErc20[] public allMarkets; + mapping(address => uint) public compAccrued; constructor(address _comp, uint256 _compAmount, address _collateralCToken) public { comp = _comp; @@ -53,9 +54,13 @@ contract ComptrollerMock { } function claimComp(address _holder) public { - require(ERC20(comp).transfer(setToken, compAmount), "ERC20 transfer failed"); + require(ERC20(comp).transfer(_holder, compAccrued[_holder]), "ERC20 transfer failed"); // Used to silence compiler warnings _holder; } + + function setCompAccrued(address _holder, uint _compAmount) external { + compAccrued[_holder] = _compAmount; + } } \ No newline at end of file diff --git a/contracts/protocol/integration/claim/CompClaimAdapter.sol b/contracts/protocol/integration/claim/CompClaimAdapter.sol new file mode 100644 index 000000000..14460cd2d --- /dev/null +++ b/contracts/protocol/integration/claim/CompClaimAdapter.sol @@ -0,0 +1,84 @@ +/* + Copyright 2021 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache License, Version 2.0 +*/ + +pragma solidity 0.6.10; +pragma experimental "ABIEncoderV2"; + +import { IComptroller } from "../../../interfaces/external/IComptroller.sol"; + +/** + * @title CompClaimAdapter + * @author bronco.eth + * + * Claim adapter that allows managers to claim COMP from assets deposited on Compound. + */ +contract CompClaimAdapter { + + /* ============ State Variables ============ */ + + // Compound Comptroller contract has a claimComp function + // https://compound.finance/docs/comptroller#claim-comp + IComptroller public immutable comptroller; + + /* ============ Constructor ============ */ + + /** + * Set state variables + * + * @param _comptroller Address of the Compound Comptroller contract with a claimComp function + */ + constructor(IComptroller _comptroller) public { + comptroller = _comptroller; + } + + /* ============ External Getter Functions ============ */ + + /** + * Generates the calldata for claiming all COMP tokens for the SetToken. + * https://compound.finance/docs/comptroller#claim-comp + * + * @param _setToken Set token address + * + * @return address Comptroller holding claimable COMP (aka RewardPool) + * @return uint256 Unused, since it claims total claimable balance + * @return bytes Claim calldata + */ + function getClaimCallData(address _setToken, address /* _rewardPool */) external view returns (address, uint256, bytes memory) { + bytes memory callData = abi.encodeWithSignature("claimComp(address)", _setToken); + + return (address(comptroller), 0, callData); + } + + /** + * Returns balance of COMP for SetToken + * + * @return uint256 Claimable COMP balance + */ + function getRewards(address _setToken, address /* _rewardPool */) external view returns(uint256) { + return comptroller.compAccrued(_setToken); + } + + /** + * Returns COMP token address + * + * @return address COMP token address + */ + function getTokenAddress(address /* _rewardPool */) external view returns(address) { + return comptroller.getCompAddress(); + } +} diff --git a/test/integration/claim/compClaimAdapter.spec.ts b/test/integration/claim/compClaimAdapter.spec.ts new file mode 100644 index 000000000..72c660f7c --- /dev/null +++ b/test/integration/claim/compClaimAdapter.spec.ts @@ -0,0 +1,183 @@ +import "module-alias/register"; +import { waffle } from "hardhat"; +import { Contract, BigNumber } from "ethers"; +import { Address } from "@utils/types"; +import { Account } from "@utils/test/types"; +import { ADDRESS_ZERO } from "@utils/constants"; +import { + CompClaimAdapter, + ClaimModule, + SetToken, + StandardTokenMock, +} from "@utils/contracts"; +import DeployHelper from "@utils/deploys"; +import { ether } from "@utils/index"; +import { + addSnapshotBeforeRestoreAfterEach, + getAccounts, + getSystemFixture, + getWaffleExpect, +} from "@utils/test/index"; +import { SystemFixture } from "@utils/fixtures"; + +const ComptrollerArtifact = require("../../../external/abi/compound/Comptroller.json"); +const expect = getWaffleExpect(); +const { deployMockContract } = waffle; + +describe("CompClaimAdapter", function() { + let owner: Account; + let compoundAdmin: Account; + let deployer: DeployHelper; + let comptroller: Contract; + let compClaimAdapter: CompClaimAdapter; + + before(async function() { + [ + owner, + compoundAdmin, + ] = await getAccounts(); + + deployer = new DeployHelper(owner.wallet); + }); + + context("unit tests", async function() { + + before(async function() { + comptroller = await deployMockContract(owner.wallet, ComptrollerArtifact.abi); + compClaimAdapter = await deployer.adapters.deployCompClaimAdapter(comptroller.address); + }); + + describe("#getClaimCallData", async function() { + let claimCallData: string; + + before(function() { + claimCallData = comptroller.interface.encodeFunctionData("claimComp(address)", [ADDRESS_ZERO]); + }); + + function subject(): Promise<[Address, BigNumber, string]> { + return compClaimAdapter.connect(owner.wallet).getClaimCallData(ADDRESS_ZERO, ADDRESS_ZERO); + } + + it("should return claim callData", async function() { + const callData = await subject(); + + expect(callData[0]).to.eq(comptroller.address); + expect(callData[1]).to.eq(ether(0)); + expect(callData[2]).to.eq(claimCallData); + }); + }); + + describe("#getRewards", async function() { + const rewards: BigNumber = ether(1); + + before(async function() { + await comptroller.mock.compAccrued.returns(rewards); + }); + + function subject(): Promise { + return compClaimAdapter.connect(owner.wallet).getRewards(ADDRESS_ZERO, ADDRESS_ZERO); + } + + it("should return rewards", async function() { + expect(await subject()).to.eq(rewards); + }); + }); + + describe("#getTokenAddress", async function() { + before(async function() { + await comptroller.mock.getCompAddress.returns(ADDRESS_ZERO); + }); + + function subject(): Promise
{ + return compClaimAdapter.connect(owner.wallet).getTokenAddress(ADDRESS_ZERO); + } + + it("should return comp address", async function() { + const address = await subject(); + + expect(address).to.eq(ADDRESS_ZERO); + }); + }); + }); + + context("integration with ClaimModule", async function() { + let comp: StandardTokenMock, cToken: StandardTokenMock; + let claimModule: ClaimModule; + let setToken: SetToken; + let setup: SystemFixture; + + const amount: BigNumber = ether(10); + const anyoneClaim: boolean = true; + const compClaimAdapterIntegrationName: string = "COMP_CLAIM"; + const integrations: string[] = [compClaimAdapterIntegrationName]; + + before(async function() { + comp = await deployer.mocks.deployTokenMock(compoundAdmin.address, amount, 18); + cToken = await deployer.mocks.deployTokenMock(compoundAdmin.address, amount, 18); + comptroller = await deployer.mocks.deployComptrollerMock(comp.address, amount, cToken.address); + compClaimAdapter = await deployer.adapters.deployCompClaimAdapter(comptroller.address); + + setup = getSystemFixture(owner.address); + await setup.initialize(); + + claimModule = await deployer.modules.deployClaimModule(setup.controller.address); + await setup.controller.addModule(claimModule.address); + await setup.integrationRegistry.addIntegration(claimModule.address, compClaimAdapterIntegrationName, compClaimAdapter.address); + + setToken = await setup.createSetToken( + [setup.weth.address], + [ether(1)], + [claimModule.address] + ); + + await claimModule.connect(owner.wallet).initialize(setToken.address, anyoneClaim, [comptroller.address], integrations); + }); + + addSnapshotBeforeRestoreAfterEach(); + + describe("ClaimModule#getRewards", async function() { + const amount: BigNumber = ether(0.1); + + before(async () => { + await comptroller.setCompAccrued(setToken.address, amount); + }); + + async function subject(): Promise { + return claimModule.connect(owner.wallet).getRewards(setToken.address, comptroller.address, compClaimAdapterIntegrationName); + } + + it("should return accrued amount", async () => { + const result: number = await subject(); + + expect(result).to.eq(amount); + }); + }); + + describe("ClaimModule#claim", async function() { + const amount: BigNumber = ether(0.1); + + before(async function() { + await comp.mint(comptroller.address, amount); + await comptroller.setCompAccrued(setToken.address, amount); + }); + + function subject(): Promise { + return claimModule.connect(owner.wallet).claim(setToken.address, comptroller.address, compClaimAdapterIntegrationName); + } + + it("should dispatch RewardClaimed event", async function() { + const claim = await subject(); + const receipt = await claim.wait(); + + // Get RewardClaimed event dispatched in a ClaimModule#_claim call + const rewardClaimed: any = receipt.events.find((e: any): any => e.event == "RewardClaimed"); + + expect(rewardClaimed.args![3]).to.eq(amount); + }); + + it("should claim accrued amount", async function() { + await expect(subject).to.changeTokenBalance(comp, setToken, amount); + }); + }); + }); +}); diff --git a/utils/contracts/index.ts b/utils/contracts/index.ts index 3757a2cb7..3c9a07838 100644 --- a/utils/contracts/index.ts +++ b/utils/contracts/index.ts @@ -20,6 +20,7 @@ export { CompoundLikeGovernanceAdapter } from "../../typechain/CompoundLikeGover export { CompoundLeverageModule } from "../../typechain/CompoundLeverageModule"; export { CompoundMock } from "../../typechain/CompoundMock"; export { CompoundWrapAdapter } from "../../typechain/CompoundWrapAdapter"; +export { CompClaimAdapter } from "../../typechain/CompClaimAdapter"; export { ComptrollerMock } from "../../typechain/ComptrollerMock"; export { ContractCallerMock } from "../../typechain/ContractCallerMock"; export { Controller } from "../../typechain/Controller"; diff --git a/utils/deploys/deployAdapters.ts b/utils/deploys/deployAdapters.ts index 26a645695..359348a7b 100644 --- a/utils/deploys/deployAdapters.ts +++ b/utils/deploys/deployAdapters.ts @@ -22,7 +22,8 @@ import { ZeroExApiAdapter, SnapshotGovernanceAdapter, SynthetixExchangeAdapter, - CompoundBravoGovernanceAdapter + CompoundBravoGovernanceAdapter, + CompClaimAdapter, } from "../contracts"; import { convertLibraryNameToLinkId } from "../common"; import { Address, Bytes } from "./../types"; @@ -49,6 +50,7 @@ import { UniswapV2IndexExchangeAdapter__factory } from "../../typechain/factorie import { SnapshotGovernanceAdapter__factory } from "../../typechain/factories/SnapshotGovernanceAdapter__factory"; import { SynthetixExchangeAdapter__factory } from "../../typechain/factories/SynthetixExchangeAdapter__factory"; import { CompoundBravoGovernanceAdapter__factory } from "../../typechain/factories/CompoundBravoGovernanceAdapter__factory"; +import { CompClaimAdapter__factory } from "../../typechain"; export default class DeployAdapters { private _deployerSigner: Signer; @@ -135,6 +137,10 @@ export default class DeployAdapters { ).deploy(); } + public async deployCompClaimAdapter(comptrollerAddress: Address): Promise { + return await new CompClaimAdapter__factory(this._deployerSigner).deploy(comptrollerAddress); + } + public async deployYearnWrapAdapter(): Promise { return await new YearnWrapAdapter__factory(this._deployerSigner).deploy(); }