Skip to content

Commit

Permalink
Post-audit fixes, more scenarios, and auxiliary improvements fixes (#63)
Browse files Browse the repository at this point in the history
* Compare boolean values numerically to 1, not string 0x01

* Remove the short-circuits for market NEVER initialized (#64)

These were intended to be short-circuits if not a COMP market, but in fact only take effect if *never* a COMP market. Removing the short-circuit avoids this confusion, and enables us to poke all borrowers once, instead of after every time a market becomes a COMP market for the first time.

* add checkIsComped, add isComped to markets storageAt getter (#65)

* Ensure updating COMP indices only takes 1 SSTORE (#66)

* Set claim threshold; scen code for forks (#69)

* Add events for changing comp rate and adding/dropping comped markets (#70)

* Rename dripper -> reservoir; add fauceteer (#72)

* Rename dripper -> reservoir

* Add Fauceteer and Add Timelock Harness Features (#71)

This patch adds the Fauceteer, which is a simple contract that can hold Erc20 tokens and will give away a small percentage every time you poke it. The idea is that it replaces the need for "FaucetToken" as we now just make Fauceteer own some amount of tokens and you can poke _it_ if you need test tokens. This gives us the ability to use real Dai, etc, on testnets *and* have a faucet available to users. As the Fauceteer only gives away 0.01% of what it has, it will effectively never run out, but instead just give away less and less each time you poke it. The raw number of tokens isn't that important for test-nets, so this is acceptable.

We also add some functions to the TimelockTest harness. We allow it to change admin without going through timelocking, and we add the ability for it to call `_acceptAdmin` without timelocking. This significantly makes the flow of setting up a test-net easier.

Co-authored-by: Max Wolff <[email protected]>
Co-authored-by: Geoff Hayes <[email protected]>
  • Loading branch information
3 people committed May 27, 2020
1 parent afbca63 commit e0a16ae
Show file tree
Hide file tree
Showing 19 changed files with 305 additions and 151 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ scenario/build/webpack.js
.solcache
.solcachecov
scenario/.tscache
script/certora
tests/scenarios/
junit.xml
.build
Expand Down
62 changes: 36 additions & 26 deletions contracts/Comptroller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerE
/// @notice Emitted when an action is paused on a market
event ActionPaused(CToken cToken, string action, bool pauseState);

/// @notice Emitted when market comped status is changed
event MarketComped(CToken cToken, bool isComped);

/// @notice Emitted when COMP rate is changed
event NewCompRate(uint oldCompRateMantissa, uint newCompRateMantissa);

/// @notice Emitted when a new COMP speed is calculated for a market
event CompSpeedUpdated(CToken indexed cToken, uint newSpeed);

Expand All @@ -56,8 +62,8 @@ contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerE
/// @notice Emitted when COMP is distributed to a borrower
event DistributedBorrowerComp(CToken indexed cToken, address indexed borrower, uint compDelta, uint compBorrowIndex);

/// @notice The threshold at which the flywheel starts to distribute COMP, in wei
uint public constant compClaimThreshold = 0.01e18;
/// @notice The threshold above which the flywheel transfers COMP, in wei
uint public constant compClaimThreshold = 0.001e18;

/// @notice The initial COMP index for a market
uint224 public constant compInitialIndex = 1e36;
Expand Down Expand Up @@ -1152,9 +1158,12 @@ contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerE
if (deltaBlocks > 0 && supplySpeed > 0) {
uint supplyTokens = CToken(cToken).totalSupply();
uint compAccrued = mul_(deltaBlocks, supplySpeed);
Double memory index = add_(Double({mantissa: supplyState.index}), fraction(compAccrued, supplyTokens));
supplyState.index = safe224(index.mantissa, "new index exceeds 224 bits");
supplyState.block = safe32(blockNumber, "block number exceeds 32 bits");
Double memory ratio = supplyTokens > 0 ? fraction(compAccrued, supplyTokens) : Double({mantissa: 0});
Double memory index = add_(Double({mantissa: supplyState.index}), ratio);
compSupplyState[cToken] = CompMarketState({
index: safe224(index.mantissa, "new index exceeds 224 bits"),
block: safe32(blockNumber, "block number exceeds 32 bits")
});
} else if (deltaBlocks > 0) {
supplyState.block = safe32(blockNumber, "block number exceeds 32 bits");
}
Expand All @@ -1172,9 +1181,12 @@ contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerE
if (deltaBlocks > 0 && borrowSpeed > 0) {
uint borrowAmount = div_(CToken(cToken).totalBorrows(), marketBorrowIndex);
uint compAccrued = mul_(deltaBlocks, borrowSpeed);
Double memory index = add_(Double({mantissa: borrowState.index}), fraction(compAccrued, borrowAmount));
borrowState.index = safe224(index.mantissa, "new index exceeds 224 bits");
borrowState.block = safe32(blockNumber, "block number exceeds 32 bits");
Double memory ratio = borrowAmount > 0 ? fraction(compAccrued, borrowAmount) : Double({mantissa: 0});
Double memory index = add_(Double({mantissa: borrowState.index}), ratio);
compBorrowState[cToken] = CompMarketState({
index: safe224(index.mantissa, "new index exceeds 224 bits"),
block: safe32(blockNumber, "block number exceeds 32 bits")
});
} else if (deltaBlocks > 0) {
borrowState.block = safe32(blockNumber, "block number exceeds 32 bits");
}
Expand All @@ -1188,21 +1200,17 @@ contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerE
function distributeSupplierComp(address cToken, address supplier) internal {
CompMarketState storage supplyState = compSupplyState[cToken];
Double memory supplyIndex = Double({mantissa: supplyState.index});

/* Short-circuit if this is not a COMP market */
if (supplyIndex.mantissa == 0) {
return;
}

Double memory supplierIndex = Double({mantissa: compSupplierIndex[cToken][supplier]});
if (supplierIndex.mantissa == 0) {
compSupplierIndex[cToken][supplier] = supplyIndex.mantissa;

if (supplierIndex.mantissa == 0 && supplyIndex.mantissa > 0) {
supplierIndex.mantissa = compInitialIndex;
}

Double memory deltaIndex = sub_(supplyIndex, supplierIndex);
uint supplierTokens = CToken(cToken).balanceOf(supplier);
uint supplierDelta = mul_(supplierTokens, deltaIndex);
uint supplierAccrued = add_(compAccrued[supplier], supplierDelta);
compSupplierIndex[cToken][supplier] = supplyIndex.mantissa;
compAccrued[supplier] = transferComp(supplier, supplierAccrued, compClaimThreshold);
emit DistributedSupplierComp(CToken(cToken), supplier, supplierDelta, supplyIndex.mantissa);
}
Expand All @@ -1216,12 +1224,6 @@ contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerE
function distributeBorrowerComp(address cToken, address borrower, Exp memory marketBorrowIndex) internal {
CompMarketState storage borrowState = compBorrowState[cToken];
Double memory borrowIndex = Double({mantissa: borrowState.index});

/* Short-circuit if this is not a COMP market */
if (borrowIndex.mantissa == 0) {
return;
}

Double memory borrowerIndex = Double({mantissa: compBorrowerIndex[cToken][borrower]});
compBorrowerIndex[cToken][borrower] = borrowIndex.mantissa;

Expand Down Expand Up @@ -1281,7 +1283,9 @@ contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerE
function _setCompRate(uint compRate_) public {
require(adminOrInitializing(), "only admin can change comp rate");

uint oldRate = compRate;
compRate = compRate_;
emit NewCompRate(oldRate, compRate_);

refreshCompSpeeds();
}
Expand All @@ -1306,15 +1310,20 @@ contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerE
require(market.isComped == false, "comp market already added");

market.isComped = true;
emit MarketComped(CToken(cToken), true);

if (compSupplyState[cToken].index == 0 && compSupplyState[cToken].block == 0) {
compSupplyState[cToken].index = compInitialIndex;
compSupplyState[cToken].block = safe32(getBlockNumber(), "block number exceeds 32 bits");
compSupplyState[cToken] = CompMarketState({
index: compInitialIndex,
block: safe32(getBlockNumber(), "block number exceeds 32 bits")
});
}

if (compBorrowState[cToken].index == 0 && compBorrowState[cToken].block == 0) {
compBorrowState[cToken].index = compInitialIndex;
compBorrowState[cToken].block = safe32(getBlockNumber(), "block number exceeds 32 bits");
compBorrowState[cToken] = CompMarketState({
index: compInitialIndex,
block: safe32(getBlockNumber(), "block number exceeds 32 bits")
});
}
}

Expand All @@ -1329,6 +1338,7 @@ contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerE
require(market.isComped == true, "market is not a comp market");

market.isComped = false;
emit MarketComped(CToken(cToken), false);

refreshCompSpeeds();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
pragma solidity ^0.5.16;

/**
* @title Dripper Contract
* @title Reservoir Contract
* @notice Distributes a token to a different contract at a fixed rate.
* @dev This contract must be poked via the `drip()` function every so often.
* @author Compound
*/
contract Dripper {
contract Reservoir {

/// @notice The block number when the Dripper started (immutable)
/// @notice The block number when the Reservoir started (immutable)
uint public dripStart;

/// @notice Tokens per block that to drip to target (immutable)
Expand All @@ -24,7 +24,7 @@ contract Dripper {
uint public dripped;

/**
* @notice Constructs a Dripper
* @notice Constructs a Reservoir
* @param dripRate_ Numer of tokens per block to drip
* @param token_ The token to drip
* @param target_ The recipient of dripped tokens
Expand All @@ -45,7 +45,7 @@ contract Dripper {
function drip() public returns (uint) {
// First, read storage into memory
EIP20Interface token_ = token;
uint dripperBalance_ = token_.balanceOf(address(this)); // TODO: Verify this is a static call
uint reservoirBalance_ = token_.balanceOf(address(this)); // TODO: Verify this is a static call
uint dripRate_ = dripRate;
uint dripStart_ = dripStart;
uint dripped_ = dripped;
Expand All @@ -55,7 +55,7 @@ contract Dripper {
// Next, calculate intermediate values
uint dripTotal_ = mul(dripRate_, blockNumber_ - dripStart_, "dripTotal overflow");
uint deltaDrip_ = sub(dripTotal_, dripped_, "deltaDrip underflow");
uint toDrip_ = min(dripperBalance_, deltaDrip_);
uint toDrip_ = min(reservoirBalance_, deltaDrip_);
uint drippedNext_ = add(dripped_, toDrip_, "tautological");

// Finally, write new `dripped` value and transfer tokens to target
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
},
"resolutions": {
"scrypt.js": "https://registry.npmjs.org/@compound-finance/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz",
"**/ganache-core": "https://github.com/compound-finance/ganache-core.git#compound"
"**/ganache-core": "https://github.com/compound-finance/ganache-core.git#jflatow/unbreak-fork"
}
}
4 changes: 2 additions & 2 deletions scenario/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "compound-money-market",
"name": "compound-protocol-alpha",
"version": "0.2.1",
"description": "The Compound Money Market",
"main": "index.js",
Expand Down Expand Up @@ -29,6 +29,6 @@
},
"resolutions": {
"scrypt.js": "https://registry.npmjs.org/@compound-finance/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz",
"**/ganache-core": "https://github.com/compound-finance/ganache-core.git#compound"
"**/ganache-core": "https://github.com/compound-finance/ganache-core.git#jflatow/unbreak-fork"
}
}
4 changes: 2 additions & 2 deletions scenario/src/Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {mustArray} from './Utils';
import {NothingV} from './Value';

interface ArgOpts<T> {
default?: T
default?: T | T[]
implicit?: boolean
variadic?: boolean
mapped?: boolean
Expand All @@ -16,7 +16,7 @@ export class Arg<T> {
name: string
type: any
getter: (World, Event?) => Promise<T>
defaultValue: T | undefined
defaultValue: T | T[] | undefined
implicit: boolean
variadic: boolean
mapped: boolean
Expand Down
3 changes: 2 additions & 1 deletion scenario/src/Contract/Comptroller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface ComptrollerMethods {
closeFactorMantissa(): Callable<number>
blockNumber(): Callable<number>
collateralFactor(string): Callable<string>
markets(string): Callable<{0: boolean, 1: number}>
markets(string): Callable<{0: boolean, 1: number, 2?: boolean}>
_setMintPaused(bool): Sendable<number>
_setMaxAssets(encodedNumber): Sendable<number>
_setLiquidationIncentive(encodedNumber): Sendable<number>
Expand Down Expand Up @@ -49,6 +49,7 @@ interface ComptrollerMethods {
_dropCompMarket(market: string): Sendable<void>
getCompMarkets(): Callable<string[]>
refreshCompSpeeds(): Sendable<void>
compRate(): Callable<number>
compSupplyState(string): Callable<string>
compBorrowState(string): Callable<string>
compAccrued(string): Callable<string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Contract } from '../Contract';
import { encodedNumber } from '../Encoding';
import { Callable, Sendable } from '../Invokation';

export interface DripperMethods {
export interface ReservoirMethods {
drip(): Sendable<void>;
dripped(): Callable<number>;
dripStart(): Callable<number>;
Expand All @@ -11,7 +11,7 @@ export interface DripperMethods {
target(): Callable<string>;
}

export interface Dripper extends Contract {
methods: DripperMethods;
export interface Reservoir extends Contract {
methods: ReservoirMethods;
name: string;
}
6 changes: 3 additions & 3 deletions scenario/src/CoreEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { fork } from './Hypothetical';
import { buildContractEvent } from './EventBuilder';
import { Counter } from './Contract/Counter';
import { CompoundLens } from './Contract/CompoundLens';
import { Dripper } from './Contract/Dripper';
import { Reservoir } from './Contract/Reservoir';
import Web3 from 'web3';

export class EventProcessingError extends Error {
Expand Down Expand Up @@ -270,7 +270,7 @@ export const commands: (View<any> | ((world: World) => Promise<View<any>>))[] =
'Web3Fork',
[
new Arg('url', getStringV),
new Arg('unlockedAccounts', getAddressV, { mapped: true })
new Arg('unlockedAccounts', getAddressV, { default: [], mapped: true })
],
async (world, { url, unlockedAccounts }) => fork(world, url.val, unlockedAccounts.map(v => v.val))
),
Expand Down Expand Up @@ -812,7 +812,7 @@ export const commands: (View<any> | ((world: World) => Promise<View<any>>))[] =

buildContractEvent<Counter>("Counter", false),
buildContractEvent<CompoundLens>("CompoundLens", false),
buildContractEvent<Dripper>("Dripper", true),
buildContractEvent<Reservoir>("Reservoir", true),

new View<{ event: EventV }>(
`
Expand Down
15 changes: 10 additions & 5 deletions scenario/src/CoreValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,27 +453,32 @@ const fetchers = [
new Arg('valType', getStringV)
],
async (world, { addr, slot, key, nestedKey, valType }) => {
const areEqual = (v, x) => toBN(v).eq(toBN(x));
let paddedSlot = slot.toNumber().toString(16).padStart(64, '0');
let paddedKey = padLeft(key.val, 64);
let newKey = sha3(paddedKey + paddedSlot);
let val = await world.web3.eth.getStorageAt(addr.val, newKey);

switch (valType.val) {
case 'marketStruct':
let isListed = val == '0x01';
let isListed = areEqual(val, 1);
let collateralFactorKey = '0x' + toBN(newKey).add(toBN(1)).toString(16);
let collateralFactorStr = await world.web3.eth.getStorageAt(addr.val, collateralFactorKey);
let collateralFactor = toBN(collateralFactorStr);
let userMarketBaseKey = padLeft(toBN(newKey).add(toBN(2)).toString(16), 64);
let paddedSlot = padLeft(userMarketBaseKey, 64);
let paddedKey = padLeft(nestedKey.val, 64);
let newKeyToo = sha3(paddedKey + paddedSlot);
let userInMarket = await world.web3.eth.getStorageAt(addr.val, newKeyToo);
let newKeyTwo = sha3(paddedKey + paddedSlot);
let userInMarket = await world.web3.eth.getStorageAt(addr.val, newKeyTwo);

let isCompKey = '0x' + toBN(newKey).add(toBN(3)).toString(16);
let isCompStr = await world.web3.eth.getStorageAt(addr.val, isCompKey);

return new ListV([
new BoolV(isListed),
new ExpNumberV(collateralFactor.toString(), 1e18),
new BoolV(userInMarket == '0x01')
new BoolV(areEqual(userInMarket, 1)),
new BoolV(areEqual(isCompStr, 1))
]);
default:
return new NothingV();
Expand Down Expand Up @@ -965,7 +970,7 @@ const fetchers = [
let contractFetchers = [
{ contract: "Counter", implicit: false },
{ contract: "CompoundLens", implicit: false },
{ contract: "Dripper", implicit: true }
{ contract: "Reservoir", implicit: true }
];

export async function getFetchers(world: World) {
Expand Down
Loading

0 comments on commit e0a16ae

Please sign in to comment.