Skip to content
Open
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
13 changes: 11 additions & 2 deletions buidler.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {deployContract} from "ethereum-waffle";

import DiamondFactoryArtifact from './artifacts/DiamondFactoryContract.json';
import {DiamondFactoryContract} from "./typechain/DiamondFactoryContract";
import { BasketFacet, CallFacet, Diamond, DiamondCutFacet, DiamondFactoryContractFactory, DiamondLoupeFacet, Erc20Facet, LendingRegistry, OwnershipFacet, PieFactoryContract, PieFactoryContractFactory, StakingLogicSushiFactory } from "./typechain";
import { BasketFacet, CallFacet, Diamond, DiamondCutFacet, DiamondFactoryContractFactory, DiamondLoupeFacet, Erc20Facet, LendingRegistry, OwnershipFacet, PieFactoryContract, PieFactoryContractFactory, StakingLogicSushiFactory, TokenListUpdater, TokenListUpdaterFactory } from "./typechain";
import BasketFacetArtifact from "./artifacts/BasketFacet.json";
import Erc20FacetArtifact from "./artifacts/ERC20Facet.json";
import CallFacetArtifact from "./artifacts/CallFacet.json";
Expand Down Expand Up @@ -51,7 +51,7 @@ const config = {
mainnet: {
url: `https://mainnet.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
accounts: [process.env.PRIVATE_KEY],
gasPrice: 70000000000
gasPrice: 200000000000
},
kovan: {
url: `https://kovan.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
Expand Down Expand Up @@ -335,4 +335,13 @@ task("deploy-lending-logic-aave")
console.log(`Deployed lendingLogicAave at: ${lendingLogicAave.address}`);
});

task("deploy-token-list-updater")
.setAction(async(taskArgs, {ethers}) => {
const signers = await ethers.getSigners();

const tokenListUpdater = await (new TokenListUpdaterFactory(signers[0]).deploy());

console.log(`Deployed tokenListUpdater: ${tokenListUpdater.address}`);
});

export default config;
37 changes: 37 additions & 0 deletions contracts/callManagers/TokenListUpdater.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma experimental ABIEncoderV2;
pragma solidity ^0.7.1;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "../interfaces/IExperiPie.sol";

contract TokenListUpdater is Ownable, ReentrancyGuard {

modifier ownerOrPie(address _pie) {
require(msg.sender == owner() ||
msg.sender == _pie, "Not allowed");
_;
}

uint256 public constant MIN_AMOUNT = 10**6;

function update(address _pie, address[] calldata _tokens) ownerOrPie(_pie) nonReentrant external {
IExperiPie pie = IExperiPie(_pie);

for(uint256 i = 0; i < _tokens.length; i ++) {
uint256 tokenBalance = pie.balance(_tokens[i]);

if(tokenBalance >= MIN_AMOUNT && !pie.getTokenInPool(_tokens[i])) {
//if min amount reached and not already in pool
bytes memory data = abi.encodeWithSelector(pie.addToken.selector, _tokens[i]);
pie.singleCall(address(pie), data, 0);
} else if(tokenBalance < MIN_AMOUNT && pie.getTokenInPool(_tokens[i])) {
// if smaller than min amount and in pool
bytes memory data = abi.encodeWithSelector(pie.removeToken.selector, _tokens[i]);
pie.singleCall(address(pie), data, 0);
}
}
}

}
1 change: 1 addition & 0 deletions contracts/facets/shared/Access/CallProtection.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ contract CallProtection {
require(
msg.sender == LibDiamond.diamondStorage().contractOwner ||
msg.sender == address(this), "NOT_ALLOWED"
// TODO consider allowing whitelisted callers from the callFacet
);
_;
}
Expand Down
176 changes: 176 additions & 0 deletions test/TokenListUpdater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import chai, {expect} from "chai";
import { deployContract, solidity} from "ethereum-waffle";
import { ethers, run, ethereum, network } from "@nomiclabs/buidler";
import { Signer, constants, Contract, BytesLike, utils } from "ethers";
import TimeTraveler from "../utils/TimeTraveler";
import { IExperiPie } from "../typechain/IExperiPie";
import { MockToken } from "../typechain/MockToken";
import { BasketFacet, CallFacet, DiamondFactoryContract, Erc20Facet, TokenListUpdater } from "../typechain";
import BasketFacetArtifact from "../artifacts/BasketFacet.json";
import Erc20FacetArtifact from "../artifacts/ERC20Facet.json";
import TokenListUpdaterArtifact from "../artifacts/TokenListUpdater.json";
import CallFacetArtifact from "../artifacts/CallFacet.json";
import { IExperiPieFactory } from "../typechain/IExperiPieFactory";
import MockTokenArtifact from "../artifacts/MockToken.json";
import { parseEther } from "ethers/lib/utils";

chai.use(solidity);

const FacetCutAction = {
Add: 0,
Replace: 1,
Remove: 2,
};

function getSelectors(contract: Contract) {
const signatures: BytesLike[] = [];
for(const key of Object.keys(contract.functions)) {
signatures.push(utils.keccak256(utils.toUtf8Bytes(key)).substr(0, 10));
}

return signatures;
}

describe("TokenListUpdater", function() {
this.timeout(300000000);

let experiPie: IExperiPie;

let account: string;
let account2: string;
let signers: Signer[];
let timeTraveler: TimeTraveler;
let tokenListUpdater: TokenListUpdater;
const testTokens: MockToken[] = [];
const testTokenAddresses: string[] = [];
let extraToken: MockToken;

before(async() => {
signers = await ethers.getSigners();
account = await signers[0].getAddress();
account2 = await signers[1].getAddress();
timeTraveler = new TimeTraveler(ethereum);

const diamondFactory = (await run("deploy-diamond-factory")) as DiamondFactoryContract;

const basketFacet = (await deployContract(signers[0], BasketFacetArtifact)) as BasketFacet;
const erc20Facet = (await deployContract(signers[0], Erc20FacetArtifact)) as Erc20Facet;
const callFacet = (await deployContract(signers[0], CallFacetArtifact)) as CallFacet;


await diamondFactory.deployNewDiamond(
account,
[
{
action: FacetCutAction.Add,
facetAddress: basketFacet.address,
functionSelectors: getSelectors(basketFacet)
},
{
action: FacetCutAction.Add,
facetAddress: erc20Facet.address,
functionSelectors: getSelectors(erc20Facet)
},
{
action: FacetCutAction.Add,
facetAddress: callFacet.address,
functionSelectors: getSelectors(callFacet)
}
]
)


const experiPieAddress = await diamondFactory.diamonds(0);
experiPie = IExperiPieFactory.connect(experiPieAddress, signers[0]);

tokenListUpdater = (await deployContract(signers[0], TokenListUpdaterArtifact)) as TokenListUpdater

for(let i = 0; i < 3; i ++) {
const token = await (deployContract(signers[0], MockTokenArtifact, ["Mock", "Mock"])) as MockToken;
await token.mint(parseEther("1000000"), experiPie.address);
await experiPie.addToken(token.address);
testTokens.push(token);
testTokenAddresses.push(token.address);
}

extraToken = await (deployContract(signers[0], MockTokenArtifact, ["Mock", "Mock"])) as MockToken;
await extraToken.mint(parseEther("1000000"), account);

await experiPie.addCaller(tokenListUpdater.address);

await timeTraveler.snapshot();
});

beforeEach(async() => {
await timeTraveler.revertSnapshot();
});

it("Calling from non owner should fail", async() => {
await tokenListUpdater.renounceOwnership();
await expect(tokenListUpdater.update(experiPie.address, testTokenAddresses)).to.be.revertedWith("Not allowed");
});


it("Removing a token when the balance is too low should work", async() => {
const token = testTokens[testTokens.length - 1]
const transferAmount = (await token.balanceOf(experiPie.address)).sub(1);

// Send out tokens
const tx = await token.populateTransaction.transfer(account2, transferAmount);
await experiPie.singleCall(tx.to, tx.data, 0);

await tokenListUpdater.update(experiPie.address, [token.address]);

const tokens = await experiPie.getTokens();
const tokenCount = tokens.length;

expect(tokenCount).to.eq(testTokens.length - 1);
expect(tokens).to.eql(testTokenAddresses.slice(0, -1));
});

it("Adding a token when it was not added before but the balance is sufficient should work", async() => {
await extraToken.transfer(experiPie.address, parseEther("1"));

await tokenListUpdater.update(experiPie.address, [extraToken.address]);

const tokens = await experiPie.getTokens();
const tokenCount = tokens.length;

expect(tokenCount).to.eq(testTokens.length + 1);
expect(tokens).to.eql([...testTokenAddresses, extraToken.address]);
});

it("Updating a token which is not in the list w/o sufficient balance", async() => {
await extraToken.transfer(experiPie.address, "420");

await tokenListUpdater.update(experiPie.address, [extraToken.address]);

const tokens = await experiPie.getTokens();
const tokenCount = tokens.length;

expect(tokenCount).to.eq(testTokens.length);
expect(tokens).to.eql(testTokenAddresses);
});

it("Updating a token which is in the list with sufficient balance should do nothing", async() => {
await tokenListUpdater.update(experiPie.address, [testTokenAddresses[0]]);

const tokens = await experiPie.getTokens();
const tokenCount = tokens.length;

expect(tokenCount).to.eq(testTokens.length);
expect(tokens).to.eql(testTokenAddresses);
});

it("Updating from the pie itself should work", async() => {
const tx = await tokenListUpdater.populateTransaction.update(experiPie.address, [testTokenAddresses[0]]);

await experiPie.singleCall(tx.to, tx.data, 0);

const tokens = await experiPie.getTokens();
const tokenCount = tokens.length;

expect(tokenCount).to.eq(testTokens.length);
expect(tokens).to.eql(testTokenAddresses);
});
});