Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions contracts/interfaces/external/IComptroller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
4 changes: 4 additions & 0 deletions contracts/mocks/StandardTokenMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ contract StandardTokenMock is ERC20 {
_mint(_initialAccount, _initialBalance);
_setupDecimals(_decimals);
}

function mint(address to, uint amount) external {
_mint(to, amount);
}
}
13 changes: 9 additions & 4 deletions contracts/mocks/external/ComptrollerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
84 changes: 84 additions & 0 deletions contracts/protocol/integration/claim/CompClaimAdapter.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
183 changes: 183 additions & 0 deletions test/integration/claim/compClaimAdapter.spec.ts
Original file line number Diff line number Diff line change
@@ -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<BigNumber> {
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<Address> {
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<any> {
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<any> {
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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the actual claimComp doesn't transfer any tokens? Ideally it would be a mock to transfer reward token to the SetToken and check the balance of:
https://github.com/SetProtocol/set-protocol-v2/blob/master/test/protocol/modules/claimModule.spec.ts#L970

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I used ComptrollerMock that was already in the repo. It's claiming function is triggering a transfer on a mocked Comp ERC20 token. New test is added below and it's checking how the balance changes after the #claim is executed.


expect(rewardClaimed.args![3]).to.eq(amount);
});

it("should claim accrued amount", async function() {
await expect(subject).to.changeTokenBalance(comp, setToken, amount);
});
});
});
});
1 change: 1 addition & 0 deletions utils/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
8 changes: 7 additions & 1 deletion utils/deploys/deployAdapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
ZeroExApiAdapter,
SnapshotGovernanceAdapter,
SynthetixExchangeAdapter,
CompoundBravoGovernanceAdapter
CompoundBravoGovernanceAdapter,
CompClaimAdapter,
} from "../contracts";
import { convertLibraryNameToLinkId } from "../common";
import { Address, Bytes } from "./../types";
Expand All @@ -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;
Expand Down Expand Up @@ -135,6 +137,10 @@ export default class DeployAdapters {
).deploy();
}

public async deployCompClaimAdapter(comptrollerAddress: Address): Promise<CompClaimAdapter> {
return await new CompClaimAdapter__factory(this._deployerSigner).deploy(comptrollerAddress);
}

public async deployYearnWrapAdapter(): Promise<YearnWrapAdapter> {
return await new YearnWrapAdapter__factory(this._deployerSigner).deploy();
}
Expand Down